/// <summary> /// Performs the basic update logic for a lease. This call should always be wrapped in a call that /// performs validation. /// </summary> /// <param name="lease">The lease to update.</param> private async Task UpdateLeaseInternalAsync(Lease lease) { DiskLease realLease = lease is DiskLease x ? x : new DiskLease(lease) { LeaseDuration = LeaseDuration, }; string data = JsonConvert.SerializeObject(realLease); string partitionFile = GetPartitionFile(realLease); await WaitForLockWrite(partitionFile, data).ConfigureAwait(false); }
/// <summary> /// Acquires the provided lease, the easy way or the hard way. /// </summary> /// <remarks> /// Acquiring is only used to steal leases from other owners. This applies exclusively to leases that haven't expired /// yet. Please be aware that "" counts as another owner, so released leases need to be stolen. If the acquire fails, /// the lease will not be available to anyone during the current acquire loop in the PartitionManager. /// </remarks> /// <param name="lease">The lease to acquire.</param> /// <returns>True if the lease was acquired, false if not.</returns> public async Task<bool> AcquireLeaseAsync(Lease lease) { // BUG: Not safe when used across multiple processes or machines. await Task.Delay(1).ConfigureAwait(false); DiskLease staleLease = lease is DiskLease x ? x : new DiskLease(lease) { LeaseDuration = LeaseDuration, }; string key = GetLockKey(staleLease); bool hasLock = false; try { Monitor.Enter(key, ref hasLock); // NOTE: We can't check the token or expiry, because acquiring is used to steal leases. // Synchronous Task execution because we need to remain in the same thread for the lock we're holding. staleLease.Acquire(_owner, LeaseDuration); UpdateLeaseInternalAsync(staleLease).Wait(); return true; } finally { if (hasLock) { Monitor.Exit(key); } } }
/// <summary> /// Gets the lease for the provided partition from the lease store. /// </summary> /// <remarks> /// This call is used during the acquire loop of the PartitionManager to either set up a new partition to listen to or /// during the stealing of leases. It is used to refresh all lease data and replaces the original lease when used. That /// means the returned lease must be complete and up to date to avoid runtime issues. /// </remarks> /// <param name="partitionId">The partition to get the lease for.</param> /// <returns>The lease of the provided partition.</returns> public async Task<Lease> GetLeaseAsync(string partitionId) { await Task.Delay(1).ConfigureAwait(false); string partitionFile = GetPartitionFile(partitionId); string data = await WaitForLockRead(partitionFile).ConfigureAwait(false); DiskLease result = JsonConvert.DeserializeObject<DiskLease>(data); return result; }
/// <summary> /// Creates a new lease if it doesn't exist in the store yet. /// </summary> /// <param name="partitionId">The partition id.</param> /// <returns>The new lease or the existing one.</returns> public async Task<Lease> CreateLeaseIfNotExistsAsync(string partitionId) { string partitionFile = GetPartitionFile(partitionId); // NOTE: Not particularly threadsafe, but this call is only executed on first startup of a consumer ever. if (File.Exists(partitionFile)) { return await GetLeaseAsync(partitionId).ConfigureAwait(false); } DiskLease result = new DiskLease(partitionId) { LeaseDuration = LeaseDuration, }; string data = JsonConvert.SerializeObject(result); await WaitForLockWrite(partitionFile, data).ConfigureAwait(false); return result; }
/// <summary> /// Updates the provided lease, persisting its data to the lease store. /// </summary> /// <remarks> /// This call is not used by the API and is only useful for internal calls like UpdateCheckpointAsync. /// </remarks> /// <param name="lease">The lease to update.</param> /// <returns>True if the lease was updated, false if not.</returns> public async Task<bool> UpdateLeaseAsync(Lease lease) { // BUG: Not safe when used across multiple processes or machines. await Task.Delay(1).ConfigureAwait(false); DiskLease staleLease = lease is DiskLease x ? x : new DiskLease(lease) { LeaseDuration = LeaseDuration, }; DiskLease freshLease = (DiskLease)await GetLeaseAsync(lease.PartitionId).ConfigureAwait(false); string key = GetLockKey(staleLease); bool hasLock = false; try { Monitor.Enter(key, ref hasLock); // We can't modify the lease, it's no longer ours to use. Expiration doesn't matter. If the lease has been // stolen by someone else, but it has already expired, it's still not ours to modify without first acquiring. if (freshLease.Token != staleLease.Token) { return false; } // NOTE: Disabled because there's temporarily only 1 running. //// We can't modify a lease that has already expired. It must first be acquired again. //// Synchronous Task execution because we need to remain in the same thread for the lock we're holding. //if (freshLease.IsExpired().Result) //{ // return false; //} // Synchronous Task execution because we need to remain in the same thread for the lock we're holding. UpdateLeaseInternalAsync(staleLease).Wait(); return true; } finally { if (hasLock) { Monitor.Exit(key); } } }
/// <summary> /// Updates the provided checkpoint for the provided lease, persisting it to the checkpoint store. /// </summary> /// <remarks> /// Checkpoints are protected against out of sync and old data by the PartitionContext, so there is no need to /// perform checks on whether or not it's going backwards. /// </remarks> /// <param name="lease">The lease to associate the checkpoint with.</param> /// <param name="checkpoint">The checkpoint to persist.</param> public async Task UpdateCheckpointAsync(Lease lease, Checkpoint checkpoint) { await Task.Delay(1).ConfigureAwait(false); DiskLease realLease = lease is DiskLease x ? x : new DiskLease(lease) { LeaseDuration = LeaseDuration, }; // TODO: Also renew the lease to ensure it doesn't expire right after checkpointing. Basically anything touching the lease should renew it, creating a sliding expiration. realLease.Offset = checkpoint.Offset; realLease.SequenceNumber = checkpoint.SequenceNumber; bool result = await UpdateLeaseAsync(realLease).ConfigureAwait(false); // Because we don't have a return value, we have no other choice than to throw an exception if the lease failed // to update. If we don't, we would have a checkpoint that was never persisted. if (!result) { throw new InvalidOperationException($"The checkpoint for partition '{checkpoint.PartitionId}' could not be updated, because the lease is not owned."); } }
/// <summary> /// Releases the provided lease, resetting ownership information so that another owner may acquire the lease. /// </summary> /// <remarks> /// Leases are only ever released if a PartitionPump closes gracefully by shutdown. Stealing of leases or crashes /// of a consumer result in 'dirty' leases of which must be stolen before they can be used by another owner. The /// return value is not used, so failure to release a lease in any way will cause the application to continue anyway. /// However, exceptions during releasing will be propagated to the unhandled exception handler in EventProcessorOptions. /// </remarks> /// <param name="lease">The lease to release.</param> /// <returns>True if the lease was released, false if not.</returns> public async Task<bool> ReleaseLeaseAsync(Lease lease) { // BUG: Not safe when used across multiple processes or machines. await Task.Delay(1).ConfigureAwait(false); DiskLease staleLease = lease is DiskLease x ? x : new DiskLease(lease) { LeaseDuration = LeaseDuration, }; DiskLease freshLease = (DiskLease)await GetLeaseAsync(lease.PartitionId).ConfigureAwait(false); string key = GetLockKey(staleLease); bool hasLock = false; try { Monitor.Enter(key, ref hasLock); // We can't release the lease, it's no longer ours to use. Expiration doesn't matter. If the lease has been // stolen by someone else, but it has already expired, it's still not ours to release without first acquiring. if (freshLease.Token != staleLease.Token) { return false; } // Normally we shouldn't release a lease that has expired, but since we already confirmed we were the last owners, // and the caller doesn't use the return value, we have no choice but to release it anyway. // Synchronous Task execution because we need to remain in the same thread for the lock we're holding. staleLease.Release(); UpdateLeaseInternalAsync(staleLease).Wait(); return true; } finally { if (hasLock) { Monitor.Exit(key); } } }