in src/Microsoft.ServiceFabric.Actors/Runtime/ActorConcurrencyLock.cs [85:210]
public async Task Acquire(
string incomingCallContext,
ActorDirtyStateHandler handler,
ActorReentrancyMode actorReentrancyMode,
CancellationToken cancellationToken)
{
// acquire the reentrancy lock
await this.reentrantLock.WaitAsync(cancellationToken);
try
{
// A new logical call context is appended to every outgoing method call.
// The received callContext is of form callContext1callContext2callContext3...
// thus to check if incoming call was made from the current calls in progress
// we need to check if the incoming call context starts with the currentCallContext
var startsWith = incomingCallContext.StartsWith(this.currentCallContext);
if (startsWith)
{
// the incoming call is part of the current call chain
// The messaging layer may deliver duplicate messages, therefore if the
// incomingCallContext is same as currentCallContext it is a duplicate message
// this is because every outgoing call from actors has a new callContext appended
// to the currentCallContext
if (incomingCallContext.Length == this.currentCallContext.Length)
{
throw new DuplicateMessageException(string.Format(
CultureInfo.CurrentCulture,
SR.ErrorDuplicateMessage,
this.GetType()));
}
// **
// this is a reentrant call
// **
// if the reentrancy is disallowed, throw and exception
if (actorReentrancyMode == ActorReentrancyMode.Disallowed)
{
throw new ReentrancyModeDisallowedException(string.Format(SR.ReentrancyModeDisallowed, this.GetType()));
}
// if the actor is dirty, do not allow reentrant call to go through
// since its not expected that actor state be dirty in a reentrant call
// throw exception that flows back to caller.
if (this.owner.IsDirty)
{
throw new ReentrantActorInvalidStateException(
string.Format(
CultureInfo.CurrentCulture,
SR.ReentrantActorDirtyState,
this.owner.Id));
}
// the currentCallCount must not be zero here as the startsWith comparison can only
// be true if the incoming call is part of the current call chain
//
// we only allow one cycle in the reentrant call chain, so if this is a second reentrant call
// then reject it. this also ensures that if multiple calls are made from the actor in parallel
// and they reenter the actor only one is allowed
if (this.currentCallCount == 1)
{
this.currentCallCount++;
return;
}
else
{
throw new InvalidReentrantCallException(SR.InvalidReentrantCall);
}
}
}
finally
{
this.reentrantLock.Release();
}
// this is not a reentrant call, which means that the caller needs to wait
// for its turn to execute this call
var timeout = this.GetTurnLockWaitTimeout();
if (!await this.turnLock.WaitAsync(timeout, cancellationToken))
{
throw new ActorConcurrencyLockTimeoutException(
string.Format(
CultureInfo.CurrentCulture,
SR.ConcurrencyLockTimedOut,
this.owner.Id,
timeout));
}
// the caller has the turn lock
try
{
// check if the owner is dirty
if (this.owner.IsDirty && handler != null)
{
// call dirty state handler to handle it
await handler(this.owner);
}
// get the reentrancy lock and initialize it with the current call information
// so that if this call were to generate reentrant calls they are allowed
await this.reentrantLock.WaitAsync(cancellationToken);
try
{
this.currentCallContext = incomingCallContext;
this.currentCallCount = 1;
}
finally
{
this.reentrantLock.Release();
}
}
catch
{
// dirty handler threw and exception, release the turn lock
// and throw the exception back
this.turnLock.Release();
throw;
}
// release the reentrancy lock but continue to hold the turn lock and release
// it through ReleaseContext method after this call invocation on the actor is completed
// indicate that the turn based concurrency lock is acquired and proceed to
// call the method on the actor
}