private int DequeueNextRequest() { lock (m_QueueLock) { int dequeueCount = 0; // Make sure we don't thread-abort in the middle of this logic. try { } finally { while (m_CurrentLockTurn == null && m_WaitQueue.Count > 0) { m_CurrentLockTurn = m_WaitQueue.Dequeue(); dequeueCount++; if (m_CurrentLockTurn.IsExpired) { m_CurrentLockTurn.Dispose(); // There's no one waiting on that request, so just discard it. m_CurrentLockTurn = null; // Get the next one (if any) on next loop. } else { m_CurrentLockTurn.Disposed += Lock_Disposed; // Subscribe to their Disposed event. Now we care. } } } return(dequeueCount); } }
/// <summary> /// Check the thread with the current turn for the lock and grant a secondary lock if applicable. /// </summary> /// <param name="candidateLock">An unexpired lock request on the current thread, or null to just check the turn thread.</param> /// <returns>The Thread with the current turn for the lock, or null if there are none holding or waiting.</returns> internal Thread CheckCurrentTurnThread(InterprocessLock candidateLock) { if (candidateLock != null && candidateLock.OwningThread != Thread.CurrentThread) { throw new InvalidOperationException("A lock request may only be waited on by the thread which created it."); } lock (m_QueueLock) { if (m_CurrentLockTurn != null) { Thread currentOwningThread = m_CurrentLockTurn.OwningThread; if (candidateLock != null && Thread.CurrentThread == currentOwningThread) { candidateLock.GrantTheLock(m_CurrentLockTurn); // Set it as a secondary lock on that holder (same thread). if (candidateLock.ActualLock == m_CurrentLockTurn) // Sanity-check that it was successful. { candidateLock.OurLockProxy = this; // So its dispose-on-close setting pass-through can function. } } return(currentOwningThread); // Whether it's a match or some other thread. } return(null); // No thread owns the lock. } }
internal InterprocessLock(object requester, string indexPath, string lockName, int timeoutSeconds) { m_OwningObject = requester; m_OwningThread = Thread.CurrentThread; m_IndexPath = indexPath; m_LockName = lockName; m_ActualLock = null; m_MyTurn = false; m_WaitForLock = (timeoutSeconds > 0); m_WaitTimeout = m_WaitForLock ? DateTimeOffset.Now.AddSeconds(timeoutSeconds) : DateTimeOffset.Now; m_LockFullFileNamePath = GetLockFileName(indexPath, lockName); }
private void Lock_Disposed(object sender, EventArgs e) { InterprocessLock disposingLock = (InterprocessLock)sender; disposingLock.Disposed -= Lock_Disposed; // Unsubscribe. //we need to remove this object from the lock collection lock (m_QueueLock) { // Only remove the lock if the one we're disposing is the original top-level lock for that key. if (m_CurrentLockTurn == null || ReferenceEquals(m_CurrentLockTurn, disposingLock) == false) { return; // Wasn't our current holder, so we don't care about it. } m_CurrentLockTurn = null; // It's disposed, no longer current owner. if (m_Disposed == false) { // We're releasing the lock for this thread. We need to check if any other process has a request pending. // And if so, we need to force this process to wait a minimum delay, even if we don't have one waiting now. if (m_FileLock != null && CheckLockRequest(m_LockFullFileNamePath)) { m_MinTimeNextTurn = DateTimeOffset.Now.AddMilliseconds(BackOffDelay); // Back off for a bit. m_FileLock.Dispose(); // We have to give up the OS lock because other processes need a chance. m_FileLock = null; } StartNextTurn(null); // Find and signal the next turn to go ahead (also handles all-done). } else { // We're already disposed, so we'd better release the lock and request now if we still have them! if (m_LockRequest != null) { m_LockRequest.Dispose(); m_LockRequest = null; } if (m_FileLock != null) { m_FileLock.Dispose(); m_FileLock = null; } } } }
/// <summary> /// Queue a lock request (RepositoryLock instance). Must be followed by a call to AwaitOurTurnOrTimeout (which can block). /// </summary> /// <param name="lockRequest"></param> internal void QueueRequest(InterprocessLock lockRequest) { if (string.Equals(lockRequest.FullName, m_LockFullFileNamePath, StringComparison.OrdinalIgnoreCase) == false) { throw new InvalidOperationException("A lock request may not be queued to a proxy for a different full name."); } if (lockRequest.OwningThread != Thread.CurrentThread) { throw new InvalidOperationException("A lock request may only be queued by the thread which created it."); } lock (m_QueueLock) { m_WaitQueue.Enqueue(lockRequest); } }
/// <summary> /// Query whether a particular lock is available without holding on to it. /// </summary> /// <param name="requester">The object that is querying the lock (useful for debugging purposes)</param> /// <param name="indexPath">The fully qualified path to the directory containing the index file of the repository</param> /// <param name="lockName">The name of the lock to query (locks are a combination of index and this name)</param> /// <returns>True if the lock could have been obtained. False if the lock could not be obtained without waiting.</returns> public static bool QueryLockAvailable(object requester, string indexPath, string lockName) { string fileName = InterprocessLockProxy.GetLockFileName(indexPath, lockName); if (System.IO.File.Exists(fileName) == false) { return(true); // Lock file didn't exist, so we could have obtained it. } bool lockAvailable; using (InterprocessLock attemptedLock = Lock(requester, indexPath, lockName, 0, true)) { lockAvailable = (attemptedLock != null); } return(lockAvailable); }
/// <summary> /// Try to get the actual file lock on behalf of the current request. /// </summary> /// <param name="currentRequest"></param> /// <returns></returns> private bool TryGetLock(InterprocessLock currentRequest) { bool waitForLock = currentRequest.WaitForLock; DateTimeOffset lockTimeout = currentRequest.WaitTimeout; bool validLock = false; while (waitForLock == false || DateTimeOffset.Now < lockTimeout) { if (DateTimeOffset.Now >= m_MinTimeNextTurn) // Make sure we aren't in a back-off delay. { m_FileLock = GetFileLock(m_LockFullFileNamePath); // TODO: DeleteOnClose no longer supported in our file opens. if (m_FileLock != null) { // We have the lock! Close our lock request if we have one so later we can detect if anyone else does. if (m_LockRequest != null) { m_LockRequest.Dispose(); m_LockRequest = null; } validLock = true; // Report that we have the lock now. } } // Otherwise, just pretend we couldn't get the lock in this attempt. if (validLock == false && waitForLock) { // We didn't get the lock and we want to wait for it, so try to open a lock request. if (m_LockRequest == null) { m_LockRequest = GetLockRequest(m_LockFullFileNamePath); // Tell the other process we'd like a turn. } // Then we should allow some real time to pass before trying again because file opens aren't very fast. Thread.Sleep(LockPollingDelay); } else { // We either got the lock or the user doesn't want to keep retrying, so exit the loop. break; } } return(validLock); }
internal void GrantTheLock(InterprocessLock actualLock) { if (actualLock != null && actualLock.IsDisposed == false && actualLock.OwningThread == m_OwningThread && string.Equals(actualLock.FullName, m_LockFullFileNamePath, StringComparison.OrdinalIgnoreCase)) { // We don't need to lock around this because we're bypassing the proxy's queue and staying only on our own thread. m_ActualLock = actualLock; m_WaitTimeout = DateTimeOffset.MaxValue; // We have a lock (sort of), so reset our timeout to forever. } else { // It's an invalid call, so make sure our setting is cleared out. m_ActualLock = null; // Note: Should this case always throw an exception? #if DEBUG throw new InvalidOperationException("Can't set a secondary lock from an invalid actual lock."); #endif } }
/// <summary> /// Find the next request still waiting and signal it to go. Or return true if the current caller may proceed. /// </summary> /// <param name="currentRequest">The request the caller is waiting on, or null for none.</param> /// <returns>True if the caller's supplied request is the next turn, false otherwise.</returns> private bool StartNextTurn(InterprocessLock currentRequest) { lock (m_QueueLock) { int dequeueCount = DequeueNextRequest(); // Find the next turn if there isn't one already underway. if (m_CurrentLockTurn != null) { // If we popped a new turn off the queue make sure it gets started. if (dequeueCount > 0) { m_CurrentLockTurn.SignalMyTurn(); // Signal the thread waiting on that request to proceed. } if (ReferenceEquals(m_CurrentLockTurn, currentRequest)) // Is the current request the next turn? { return(true); // Yes, so skip waiting and just tell our caller they can go ahead (and wait for the lock). } } else { // Otherwise, nothing else is waiting on the lock! Time to shut it down. if (m_LockRequest != null) { m_LockRequest.Dispose(); // Release the lock request (an open read) since we're no longer waiting on it. m_LockRequest = null; } if (m_FileLock != null) { m_FileLock.Dispose(); // Release the OS file lock. m_FileLock = null; } if (m_DisposeOnClose) { Dispose(); } } return(false); } }
private async void RepositoryPublishMain() { //before we get going, lets stall for a few seconds. We aren't a critical operation, and I don't //want to get in the way of the application starting up. if (BackgroundStartupDelay > 0) { if (!Log.SilentMode) { Log.Write(LogMessageSeverity.Information, LogCategory, "Waiting for startup delay to start publisher", "To avoid competing against other startup activities we're going to delay for {0} before we start.", BackgroundStartupDelay); } await Task.Delay(BackgroundStartupDelay).ConfigureAwait(false); } InterprocessLock backgroundLock = null; //we can't do our normal using trick in this situation. try { //we have two totally different modes: Either WE'RE the background processor or someone else is. //if we are then we move on to start publishing. If someone else is then we just poll //the lock to see if whoever owned it has exited. backgroundLock = GetLock(0); while ((backgroundLock == null) && (m_StopRequested == false)) { //we didn't get the lock - so someone else is currently the main background thread. SleepUntilNextCheck(m_MultiprocessLockCheckInterval); backgroundLock = GetLock(0); } //if we got the lock then we want to go ahead and perform background processing. if (backgroundLock != null) { //here is where we want to keep looping - it will return every time the subscription changes. await RepositoryPublishLoop().ConfigureAwait(false); //release the lock; we'll get it on the next round. backgroundLock.Dispose(); backgroundLock = null; } } catch (Exception ex) { GC.KeepAlive(ex); } finally { if (backgroundLock != null) { backgroundLock.Dispose(); } lock (m_SessionPublishThreadLock) { //clear the dispatch thread variable since we're about to exit. m_SessionPublishThread = null; //we want to write out that we had a problem and mark that we're failed so we'll get restarted. m_SessionPublishThreadFailed = true; System.Threading.Monitor.PulseAll(m_SessionPublishThreadLock); } if (!Log.SilentMode) { Log.Write(LogMessageSeverity.Information, LogCategory, "Background session publisher thread has stopped", null); } } }
/// <summary> /// Performs the actual releasing of managed and unmanaged resources. /// Most usage should instead call Dispose(), which will call Dispose(true) for you /// and will suppress redundant finalization. /// </summary> /// <param name="releaseManaged">Indicates whether to release managed resources. /// This should only be called with true, except from the finalizer which should call Dispose(false).</param> private void Dispose(bool releaseManaged) { if (releaseManaged) { // Free managed resources here (normal Dispose() stuff, which should itself call Dispose(true)) // Other objects may be referenced in this case lock (m_QueueLock) { if (!m_Disposed) { m_Disposed = true; // Make sure we don't do it more than once. // Empty our queue (although it should already be empty!). while (m_WaitQueue.Count > 0) { InterprocessLock lockInstance = m_WaitQueue.Dequeue(); //lockInstance.Disposed -= Lock_Disposed; // Suppress the events, don't start new turns! lockInstance.Dispose(); // Tell any threads still waiting that their request has expired. } if (m_CurrentLockTurn == null) { // No thread is currently prepared to do this, so clear them here. if (m_LockRequest != null) { m_LockRequest.Dispose(); m_LockRequest = null; } if (m_FileLock != null) { m_FileLock.Dispose(); m_FileLock = null; } } // We're not fully disposed until the current lock owner gets disposed so we can release the lock. // But fire the event to tell the RepositoryLockManager that we are no longer a valid proxy. OnDispose(); } } } else { // Free native resources here (alloc's, etc) // May be called from within the finalizer, so don't reference other objects here // But we need to make sure the file opens get cleaned up, so we will anyway if we have to.... ??? if (m_LockRequest != null) { m_LockRequest.Dispose(); m_LockRequest = null; } if (m_FileLock != null) { m_FileLock.Dispose(); m_FileLock = null; } } }
/// <summary> /// Wait for our turn to have the lock (and wait for the lock) up to our time limit /// </summary> /// <param name="lockRequest"></param> /// <returns></returns> internal bool AwaitOurTurnOrTimeout(InterprocessLock lockRequest) { if (lockRequest.IsExpired) { throw new InvalidOperationException("Can't wait on an expired lock request."); } if (string.Equals(lockRequest.FullName, m_LockFullFileNamePath, StringComparison.OrdinalIgnoreCase) == false) { throw new InvalidOperationException("A lock request may not be queued to a proxy for a different full name."); } if (lockRequest.OwningThread != Thread.CurrentThread) { throw new InvalidOperationException("A lock request may only be waited on by the thread which created it."); } lockRequest.OurLockProxy = this; // Mark the request as pending with us. // Do NOT clear out current lock owner, this will allow DequeueNextRequest to find one already there, if any. bool ourTurn = StartNextTurn(lockRequest); // Gets its own queue lock. if (ourTurn == false) { // It's not our turn yet, we need to wait our turn. Are we willing to wait? if (lockRequest.WaitForLock && lockRequest.WaitTimeout > DateTimeOffset.Now) { ourTurn = lockRequest.AwaitTurnOrTimeout(); } // Still not our turn? if (ourTurn == false) { if (!CommonCentralLogic.SilentMode) { // Who actually has the lock right now? if (m_CurrentLockTurn != null) { Thread currentOwningThread = m_CurrentLockTurn.OwningThread; int currentOwningThreadId = -1; string currentOwningThreadName = "null"; if (currentOwningThread != null) // To make sure we can't get a null-ref exception from logging this... { currentOwningThreadId = currentOwningThread.ManagedThreadId; currentOwningThreadName = currentOwningThread.Name ?? string.Empty; } m_Logger.LogDebug("{0}\r\nA lock request gave up because it is still being held by another thread.\r\n" + "Lock file: {1}\r\nCurrent holding thread: {2} ({3})", lockRequest.WaitForLock ? "Lock request timed out" : "Lock request couldn't wait", m_LockFullFileNamePath, currentOwningThreadId, currentOwningThreadName); } else { m_Logger.LogError("Lock request turn error\r\nA lock request failed to get its turn but the current lock turn is null. " + "This probably should not happen.\r\nLock file: {0}\r\n", m_LockFullFileNamePath); } } lockRequest.Dispose(); // Expire the request. return(false); // Failed to get the lock. Time to give up. } } // Yay, now it's our turn! Do we already hold the lock? bool validLock; if (m_FileLock != null) { validLock = true; // It's our request's turn and this proxy already holds the lock! } else { validLock = TryGetLock(lockRequest); // Can we get the lock? } // Do we actually have the lock now? if (validLock) { lockRequest.GrantTheLock(lockRequest); // It owns the actual lock itself now. } else { if (!CommonCentralLogic.SilentMode) { m_Logger.LogTrace("{0}\r\nA lock request gave up because it could not obtain the file lock. " + "It is most likely still held by another process.\r\nLock file: {1}", lockRequest.WaitForLock ? "Lock request timed out" : "Lock request couldn't wait", m_LockFullFileNamePath); } lockRequest.Dispose(); // Failed to get the lock. Expire the request and give up. } return(validLock); }
/// <summary> /// Attempt to lock the repository with the provided index path. /// </summary> /// <param name="requester">The object that is requesting the lock (useful for debugging purposes)</param> /// <param name="indexPath">The fully qualified path to the directory containing the index file of the repository</param> /// <param name="lockName">The name of the lock to get (locks are a combination of index and this name)</param> /// <param name="timeoutSeconds">The maximum number of seconds to wait on the lock before giving up.</param> /// <param name="deleteOnClose">Whether the lock file should be deleted on close or left around for reuse.</param> /// <returns>A Repository Lock object if the lock could be obtained or Null if the lock timed out.</returns> public static InterprocessLock Lock(object requester, string indexPath, string lockName, int timeoutSeconds, bool deleteOnClose) { if (requester == null) { throw new ArgumentNullException(nameof(requester)); } if (indexPath == null) { throw new ArgumentNullException(nameof(indexPath)); } if (lockName == null) { throw new ArgumentNullException(nameof(lockName)); } InterprocessLock candidateLock = new InterprocessLock(requester, indexPath, lockName, timeoutSeconds); // Lookup or create the proxy for the requested lock. InterprocessLockProxy lockProxy; lock (g_Lock) { if (g_Proxies.TryGetValue(candidateLock.FullName, out lockProxy) == false) { // Didn't exist, need to make one. lockProxy = new InterprocessLockProxy(indexPath, lockName, deleteOnClose); #if DEBUG if (string.Equals(lockProxy.FullName, candidateLock.FullName, StringComparison.OrdinalIgnoreCase) == false) { throw new InvalidOperationException("Proxy generated a different full name than the candidate lock."); } #endif lockProxy.Disposed += LockProxy_Disposed; g_Proxies.Add(lockProxy.FullName, lockProxy); } // Does the current thread already hold the lock? (If it was still waiting on it, we couldn't get here.) Thread currentTurnThread = lockProxy.CheckCurrentTurnThread(candidateLock); if (Thread.CurrentThread == currentTurnThread && candidateLock.ActualLock != null) { return(candidateLock); // It's a secondary lock, so we don't need to queue it or wait. } // Or is the lock currently held by another thread that we don't want to wait for? if (currentTurnThread != null && candidateLock.WaitForLock == false) { candidateLock.Dispose(); // We don't want to wait for it, so don't bother queuing an expired request. return(null); // Just fail out. } lockProxy.QueueRequest(candidateLock); // Otherwise, queue the request inside the lock to keep the proxy around. } // Now we have the proxy and our request is queued. Make sure some thread is trying to get the file lock. bool ourTurn = false; // Assume false. try { ourTurn = lockProxy.AwaitOurTurnOrTimeout(candidateLock); } finally { if (ourTurn == false) { // We have to make sure this gets disposed if we didn't get the lock, even if a ThreadAbortException occurs. candidateLock.Dispose(); // Bummer, we didn't get it. Probably already disposed, but safe to do again. candidateLock = null; // Clear it out to report the failure. } } // Otherwise... yay, we got it! return(candidateLock); }