/// <summary> /// Perform the actual package queuing and wait for it to be committed. /// </summary> /// <remarks>This must be done within the message queue lock. This method may return a null envelope if called /// on a thread which must not block and the packet had to be discarded due to an overflow condition.</remarks> /// <param name="packet">The packet to be queued</param> /// <param name="writeThrough">True if the call should block the current thread until the packet has been committed, /// false otherwise.</param> /// <returns>The packet envelope for the packet that was queued, or null if the packet was discarded.</returns> private PacketEnvelope QueuePacket(IMessengerPacket packet, bool writeThrough) { //even though the packet might already have a timestamp that's preferable to ours, we're deciding we're the judge of order to ensure it aligns with sequence. packet.Timestamp = DateTimeOffset.Now; //we convert to UTC during serialization, we want local time. //wrap it in a packet envelope and indicate we're in write through mode. PacketEnvelope packetEnvelope = new PacketEnvelope(packet, writeThrough); //But what queue do we put the packet in? if ((m_MessageOverflowQueue.Count > 0) || (m_MessageQueue.Count > m_MessageQueueMaxLength)) { // We are currently using the overflow queue, so we'll put it there. // However, if we were called by a must-not-block thread, we want to discard overflow packets... // unless it's a command packet, which is too important to discard (it just won't wait on pending). if (t_ThreadMustNotBlock && !packetEnvelope.IsCommand) { packetEnvelope = null; // We won't queue this packet, so there's no envelope to hang onto. } else { m_MessageOverflowQueue.Enqueue(packetEnvelope); //and set that it's pending so our caller knows they need to wait for it. packetEnvelope.IsPending = true; } } else { //just queue the packet, we don't want to wait. m_MessageQueue.Enqueue(packetEnvelope); } return(packetEnvelope); }
/// <summary> /// Suspends the calling thread until the provided packet is no longer pending. /// </summary> /// <remarks>This method performs its own synchronization and should not be done within a lock.</remarks> /// <param name="packetEnvelope">The packet that must be submitted</param> private static void WaitOnPending(PacketEnvelope packetEnvelope) { //we are monitoring for pending by using object locking, so get the lock... lock (packetEnvelope) { //and now we wait for it to be submitted... while (packetEnvelope.IsPending) { // This releases the envelope lock, only reacquiring it after being woken up by a call to Pulse System.Threading.Monitor.Wait(packetEnvelope); } //as we exit, we need to pulse the packet envelope in case there is another thread waiting //on it as well. System.Threading.Monitor.PulseAll(packetEnvelope); } }
/// <summary> /// Publish the provided batch of packets. /// </summary> /// <param name="packetArray">An array of packets to publish as a batch.</param> /// <param name="writeThrough">True if the information contained in packet should be committed synchronously, /// false if the publisher can use write caching (when available).</param> public void Publish(IMessengerPacket[] packetArray, bool writeThrough) { // Sanity-check the most likely no-op cases before we bother with the lock if (packetArray == null) { return; } // Check for nulls from the end to find the last valid packet. int count = packetArray.Length; int lastIndex = count - 1; while (lastIndex >= 0 && packetArray[lastIndex] == null) { lastIndex--; } if (lastIndex < 0) { return; // An array of only null packets (or empty), just quick bail. Don't bother with the lock. } //resolve users... var resolver = m_PrincipalResolver; IPrincipal principal = null; if (resolver != null) { //and set that user to each packet that wants to track the current user (and doesn't have one manually set) foreach (var packet in packetArray.AsEnumerable().OfType <IUserPacket>().Where(p => p.Principal == null)) { //we only want to resolve the principal once per block, even if there are multiple messages. if (principal == null) { //before we resolve the principal make sure our thread isn't *currently* trying to resolve a principal. if (!t_ThreadMustNotResolvePrincipal) { try { t_ThreadMustNotResolvePrincipal = true; var resolved = resolver.TryResolveCurrentPrincipal(out principal); if (resolved == false) { principal = null; //in case they broke the contract.. } } catch (Exception ex) { Log.DebugBreak(); GC.KeepAlive(ex); } finally { t_ThreadMustNotResolvePrincipal = false; } } if (principal == null) { break; //no point in keeping trying if we filed to resolve the principal.. } } packet.Principal = principal; } } PacketEnvelope lastPacketEnvelope = null; bool effectiveWriteThrough; bool isPending; int queuedCount = 0; // Get the queue lock. lock (m_MessageQueueLock) { if (m_Shutdown) // If we're already shut down, just bail. We'll never process it anyway. { return; } // Check to see if either the overall force write through or the local write through are set... // or if we are in ExitingMode. In those cases, we'll want to block until the packet is committed. effectiveWriteThrough = (m_ForceWriteThrough || writeThrough || m_ExitingMode); for (int i = 0; i < count; i++) { IMessengerPacket packet = packetArray[i]; // We have to double-check each element for null, or QueuePacket() would barf on it. if (packet != null) { // We have a real packet, so queue it. Only WriteThrough for the last packet, to flush the rest. PacketEnvelope packetEnvelope = QueuePacket(packet, effectiveWriteThrough && i >= lastIndex); // If a null is returned, the packet wasn't queued, so don't overwrite lastPacketEnvelope. if (packetEnvelope != null) { queuedCount++; lastPacketEnvelope = packetEnvelope; // Keep track of the last one queued. if (!m_ExitMode && packetEnvelope.IsCommand) { CommandPacket commandPacket = (CommandPacket)packet; if (commandPacket.Command == MessagingCommand.ExitMode) { // Once we *receive* an ExitMode command, all subsequent messages queued // need to block, to make sure the process stays alive for any final logging // foreground threads might have. We will be switching the Publisher to a // background thread when we process the ExitMode command so we don't hold // up the process beyond its own foreground threads. m_ExitingMode = true; // Force writeThrough blocking from now on. // Set the ending status, if it needs to be (probably won't). SessionStatus endingStatus = (SessionStatus)commandPacket.State; if (m_SessionSummary.Status < endingStatus) { m_SessionSummary.Status = endingStatus; } } } } } } if (effectiveWriteThrough && t_ThreadMustNotBlock == false && queuedCount > 0 && (lastPacketEnvelope == null || ReferenceEquals(lastPacketEnvelope.Packet, packetArray[lastIndex]) == false)) { // The expected WriteThrough packet got dropped because of overflow? But we still need to block until // those queued have completed, so issue a specific Flush command packet, which should not get dropped. CommandPacket flushPacket = new CommandPacket(MessagingCommand.Flush); PacketEnvelope flushEnvelope = QueuePacket(flushPacket, true); if (flushEnvelope != null) { lastPacketEnvelope = flushEnvelope; } } // Grab the pending flag before we release the lock so we know we have a consistent view. // If we didn't queue any packets then lastPacketEnvelope will be null and there's nothing to be pending. isPending = (lastPacketEnvelope == null) ? false : lastPacketEnvelope.IsPending; // Now signal our next thread that might be waiting that the lock will be released. System.Threading.Monitor.PulseAll(m_MessageQueueLock); } // Make sure our dispatch thread is still going. This has its own independent locking (when necessary), // so we don't need to hold up other threads that are publishing. EnsureMessageDispatchThreadIsValid(); if (lastPacketEnvelope == null || t_ThreadMustNotBlock) { // If we had no actual packets queued (e.g. shutdown, or no packets to queue), there's nothing to wait on. // Also, special case for must-not-block threads. Once it's on the queue (or not), don't wait further. // We need the thread to get back to processing stuff off the queue or we're deadlocked! return; } // See if we need to wait because we've degraded to synchronous message handling due to a backlog of messages if (isPending) { // This routine does its own locking so we don't need to interfere with the nominal case of // not needing to pend. WaitOnPending(lastPacketEnvelope); } // Finally, if we need to wait on the write to complete now we want to stall. We had to do this outside of // the message queue lock to ensure we don't block other threads. if (effectiveWriteThrough) { WaitOnPacket(lastPacketEnvelope); } }
/// <summary> /// Send the packet to every current messenger and add it to the packet cache if it's cachable /// </summary> /// <param name="envelope"></param> private void DispatchPacket(PacketEnvelope envelope) { IMessengerPacket packet; lock (envelope) { packet = envelope.Packet; // rather than dig it out each time bool writeThrough = envelope.WriteThrough; // Any special handling for this packet? if (envelope.IsCommand) { //this is a command packet, we process it as a command instead of just a data message CommandPacket commandPacket = (CommandPacket)packet; // Is this our exit or shutdown packet? We need to handle those here. if (commandPacket.Command == MessagingCommand.ExitMode) { m_ExitMode = true; // Mark us in ExitMode. We will be by the time this method returns. // Make sure we block until each messenger flushes, even if we weren't already in writeThrough mode. writeThrough = true; } else if (commandPacket.Command == MessagingCommand.CloseMessenger) { m_Shutdown = true; // Mark us as shut down. We will be by the time this method returns. // Make sure we block until each messenger closes, even if we weren't already in writeThrough mode. writeThrough = true; } } else { // Not a command, so it must be a Gibraltar data packet of some type. //stamp the packet, and all of its dependent packets (this sets the sequence number) StampPacket(packet, packet.Timestamp); GibraltarPacket gibraltarPacket = packet as GibraltarPacket; if (gibraltarPacket != null) { //this is a gibraltar packet so lets go ahead and fix the data in place now that we're on the background thread. gibraltarPacket.FixData(); } //resolve the application user if feasible.. if (packet is IUserPacket userPacket && userPacket.Principal != null) { var userResolver = m_ApplicationUserProvider; if (userResolver != null) { ResolveApplicationUser(userResolver, userPacket); } } //and finally run it through our filters.. var cancel = false; var filters = m_Filters; if (filters != null) { foreach (var filter in filters) { try { filter.Process(packet, ref cancel); if (cancel) { break; } } catch (Exception) { Log.DebugBreak(); // Catch this in the debugger, but otherwise swallow any errors. } } } //if a filter canceled then we can't write out this packet. if (cancel) { envelope.IsCommitted = true; //so people waiting on us don't stall.. return; } } //If this is a header packet we want to put it in the header list now - that way //if any messenger recycles while we are writing to the messengers it will be there. //(Better to pull the packet forward than to risk having it in an older stream but not a newer stream) if (envelope.IsHeader) { lock (m_HeaderPacketsLock) { m_HeaderPackets.Add((ICachedMessengerPacket)packet); System.Threading.Monitor.PulseAll(m_HeaderPacketsLock); } } // Data message or Command packet - either way, send it on to each messenger. foreach (IMessenger messenger in m_Messengers) { //we don't want an exception with one messenger to cause us a problem, so each gets its own try/catch try { messenger.Write(packet, writeThrough); } catch (Exception) { Log.DebugBreak(); // Stop in debugger, ignore in production. } } //if this was a write through packet we need to let the caller know that it was committed. envelope.IsCommitted = true; //under the covers this does a pulse on the threads waiting on this envelope. } // Now that it's committed, finally send it to any Notifiers that may be subscribed. QueueToNotifier(packet); //we only need to do this here if the session file writer is disabled; otherwise it's doing it at the best boundary. if ((m_Configuration.SessionFile.Enabled == false) && (packet.Sequence % 8192 == 0)) { StringReference.Pack(); } }
/// <summary> /// The main method of the message dispatch thread. /// </summary> private void MessageDispatchMain() { try { // Before initialization... We must never allow this thread (which processes the queue!) to block // when adding items to our queue, or we would deadlock. (Does not need the lock to set this.) ThreadMustNotBlock(); bool backgroundThread; // Now we need to make sure we're initialized. lock (m_MessageDispatchThreadLock) { //are we initialized? EnsureInitialized(); backgroundThread = m_MessageDispatchThread.IsBackground; // distinguish which we are. System.Threading.Monitor.PulseAll(m_MessageDispatchThreadLock); } // Enter our main loop - dequeue packets and write them to all of the messengers. // Foreground thread should exit when we process exit, but a background thread should continue. while (m_Shutdown == false && (!m_ExitMode || backgroundThread)) { PacketEnvelope currentPacket = null; lock (m_MessageQueueLock) { // Is this check needed? We check m_ExitMode above outside the lock, so it may not have been // up to date, but now we have the lock it should be. If we're still on the foreground thread // with m_ExitMode set to true, we want to exit the thread and create a background thread to // continue dispatching, which we do upon exiting the while loop. if (m_ExitMode && !backgroundThread) { break; } // If the queue is empty, wait for an item to be added // This is a while loop, as we may be pulsed but not wake up before another thread has come in and // consumed the newly added object or done something to modify the queue. In that case, we'll have to wait for another pulse. while ((m_MessageQueue.Count == 0) && (m_Shutdown == false)) { // This releases the message queue lock, only reacquiring it after being woken up by a call to Pulse System.Threading.Monitor.Wait(m_MessageQueueLock); } if (m_MessageQueue.Count > 0) { //if we got here then there was an item in the queue AND we have the lock. Dequeue the item and then we want to release our lock. currentPacket = m_MessageQueue.Dequeue(); //and are we now below the maximum packet queue? if so we can release the pending items. while ((m_MessageOverflowQueue.Count > 0) && (m_MessageQueue.Count < m_MessageQueueMaxLength)) { //we still have an item in the overflow queue and we have room for it, so lets add it. PacketEnvelope currentOverflowEnvelope = m_MessageOverflowQueue.Dequeue(); m_MessageQueue.Enqueue(currentOverflowEnvelope); //and indicate that we've submitted this queue item. This does a thread pulse under the covers, //and gets its own lock so we should NOT lock the envelope. currentOverflowEnvelope.IsPending = false; } } //now pulse the next waiting thread there are that we've dequeued the packet. System.Threading.Monitor.PulseAll(m_MessageQueueLock); } //We have a packet and have released the lock (so others can queue more packets while we're dispatching items. if (currentPacket != null) { lock (m_ConfigLock) { DispatchPacket(currentPacket); } } } // We only get here if we exited the loop because a foreground thread sees we are in ExitMode, // or if we are completely shut down. // Clear the dispatch thread variable since we're about to exit it and... m_MessageDispatchThread = null; if (m_Shutdown == false) { CreateMessageDispatchThread(); // Recreate one as a background thread, if we aren't shut down. } } catch (Exception ex) { lock (m_MessageDispatchThreadLock) { //clear the dispatch thread variable since we're about to exit. m_MessageDispatchThread = null; //we want to write out that we had a problem and mark that we're failed so we'll get restarted. m_MessageDispatchThreadFailed = true; System.Threading.Monitor.PulseAll(m_MessageDispatchThreadLock); } OnThreadAbort(); GC.KeepAlive(ex); } }