/// <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(); } }