Table of Contents

Class LockTable

Namespace
DynaSharp.Coordination
Assembly
DynaSharp.Coordination.dll
public sealed class LockTable
Inheritance
LockTable
Inherited Members
Extension Methods

Examples

var locks = new LockTable(client.Table("Shop", "pk", "sk"));

var lease = await locks.TryAcquireAsync("cache-refresh", TimeSpan.FromSeconds(30), ct);
if (lease is null)
{
    return; // Another process holds it; this one tries again on its next round.
}

await using (lease)
{
    await client.TransactWrite().IsHeld(lease).Put(orders, order).SendAsync(ct);
}

Remarks

Semantics. They are those of the DynamoDB Lock Client: a lease has a relative duration and a record version number, its holder renews it, and a contender takes it over only after it has seen the same record version for a whole lease on its own monotonic clock. No two clocks are ever compared, so the clocks of the machines may be set apart by any offset; what it assumes is that they run at about the same rate. A host that is suspended, or a virtual machine that freezes, can still hold a lease that has ended without knowing it, which is what IsHeld<TChain>(TChain, Lease) and FencingToken are for.

The item. A lock is one item in the table the caller passes, so it fits a single-table design. Its partition key is LOCK# and the name of the lock, its sort key, if the table has one, is LOCK, and both key attributes must be strings. Its attributes are _type (LOCK), owner, rvn, fence and leaseMs, with holder and a time to live attribute if the LockTableOptions ask for them; the names are fixed. An item at that key with an owner whose attributes are not those, such as an entity whose key happens to match, makes an acquire throw InvalidOperationException naming the lock and the table, rather than guess how long it is held. A release keeps the item, so that the fencing token never goes back: without TimeToLiveAttribute the item has no time to live, and deleting it starts the token at 1 again, which a system that checks the token refuses.

Clock, logs and retries. It uses the TimeProvider of the client of the table for every wait and for the monotonic clock, and its LoggerFactory for the log category DynaSharp.Coordination.LockTable. The requests go through the client with its retries. It is thread-safe; hold one per table, because it remembers which holders it has seen, which a takeover depends on.

One region. The lock table must have a single region that writes. A global table that takes writes in more than one region can grant a lock twice, with the same fencing token, because each region applies its conditional write before the other one's arrives.

Constructors

LockTable(Table<Item>, LockTableOptions?)

Initializes a new instance of the LockTable class over a table without a model.

public LockTable(Table<Item> table, LockTableOptions? options = null)

Parameters

table Table<Item>

The table, opened with Table(string, string, string?). Its key attributes must be strings.

options LockTableOptions

What the items of the locks carry beyond the lock, or null for nothing.

Exceptions

ArgumentNullException

table is null.

ArgumentException

The options are not valid: see LockTableOptions.

Fields

MaxLeaseDuration

The longest lease, a day.

public static readonly TimeSpan MaxLeaseDuration

Field Value

TimeSpan

MinLeaseDuration

The shortest lease: below a second a lease cannot outlast the round trips and retries of its renewals.

public static readonly TimeSpan MinLeaseDuration

Field Value

TimeSpan

Properties

Options

Gets what the items of the locks carry beyond the lock.

public LockTableOptions Options { get; }

Property Value

LockTableOptions

Table

Gets the table that holds the locks, without a model.

public Table<Item> Table { get; }

Property Value

Table<Item>

Methods

AcquireAsync(string, TimeSpan, CancellationToken)

Acquires a lock, waiting as long as it takes: cancel the token to give up. It takes a lock over once its holder has not renewed it for a whole lease.

public Task<Lease> AcquireAsync(string name, TimeSpan leaseDuration, CancellationToken cancellationToken = default)

Parameters

name string

The name of the lock.

leaseDuration TimeSpan

How long the lease lasts without a renewal, from MinLeaseDuration to MaxLeaseDuration.

cancellationToken CancellationToken

Ends the wait with an OperationCanceledException.

Returns

Task<Lease>

The lease. An acquire whose answer came only after a whole lease hands out a lease that has ended already, see Ended.

Remarks

Polling. While another holder has the lock it reads the item every third of that holder's lease, or every 10 seconds if that is sooner, and takes the lock over once the holder has not renewed it for a whole lease, measured on the monotonic clock of the client from the first time this saw that record version. The wait sends one strongly consistent GetItem per poll and no write while the holder renews; it writes when it finds the lock free or the holder due, see TryAcquireAsync(string, TimeSpan, CancellationToken).

Giving up. A process that must not wait for a live holder passes a token that cancels after a little more than one lease: that is long enough to take over the lock of a holder that crashed, which TryAcquireAsync(string, TimeSpan, CancellationToken) of a short-lived process cannot.

Exceptions

ArgumentException

name is null or empty.

ArgumentOutOfRangeException

leaseDuration is below MinLeaseDuration or above MaxLeaseDuration.

OperationCanceledException

cancellationToken was cancelled.

ResourceNotFoundException

The table does not exist.

DynamoDbException

DynamoDB failed a request, after the retries of the client.

InvalidOperationException

The item at the key of the lock has an owner but is not a lock item written by LockTable.

For<T>(Table<T>, LockTableOptions?)

Makes a lock table over any table, typed or not, for LockTable.For(client.Table<Order>("Shop")): it needs only the name of the table, the names of its keys and its client, so the locks share a table with entities without restating its keys.

public static LockTable For<T>(Table<T> table, LockTableOptions? options = null) where T : IDynamoDbDocument<T>

Parameters

table Table<T>

The table. Its key attributes must be strings.

options LockTableOptions

What the items of the locks carry beyond the lock, or null for nothing.

Returns

LockTable

The lock table, over the same table opened without a model.

Type Parameters

T

How the table reads and writes items. The locks do not use it.

Exceptions

ArgumentNullException

table is null.

ArgumentException

The options are not valid: see LockTableOptions.

RunAsLeaderAsync(string, TimeSpan, Func<Lease, CancellationToken, Task>, CancellationToken)

Leads on a lock: of all the processes that run this for the same lock, one at a time runs work, and when its lease is lost another takes over.

public Task RunAsLeaderAsync(string name, TimeSpan leaseDuration, Func<Lease, CancellationToken, Task> work, CancellationToken cancellationToken = default)

Parameters

name string

The name of the lock.

leaseDuration TimeSpan

How long a lease lasts without a renewal, from MinLeaseDuration to MaxLeaseDuration. It is also about how long the work of a leader that crashed stays undone.

work Func<Lease, CancellationToken, Task>

The work of the leader. It gets the lease and a token to stop at.

cancellationToken CancellationToken

Ends the leadership: the work is stopped and the lock released.

Returns

Task

A task that completes when the work completed on its own, and fails when the work failed on its own, the caller cancelled or an acquire failed.

Examples

await locks.RunAsLeaderAsync("order-sweeper", TimeSpan.FromSeconds(30), async (lease, stop) =>
{
    while (true)
    {
        await client.TransactWrite().IsHeld(lease).Put(orders, order).SendAsync(stop);
        await Task.Delay(TimeSpan.FromMinutes(1), stop);
    }
}, ct);

Remarks

A term. It acquires the lock with AcquireAsync(string, TimeSpan, CancellationToken), runs the work while the lease is held, and when the lease is lost, waits for the work to stop and contends again. The token the work gets is cancelled when the caller cancels or the lease is lost, so work that honours it stops in time.

How the work ends. The work ends in one of three ways. If the lease was lost, whatever the work returned or threw, this contends again. If the caller cancelled, the lease is released and this throws OperationCanceledException, whatever the work returned or threw. If the work returned or threw on its own, the lease is released and this returns or throws that exception: a leader that is done hands the lock on. A service that should lead for as long as it runs calls this in a loop, as the guide shows for a BackgroundService.

Errors of the election. An acquire that fails with a transient error, a DynamoDbException whose IsTransient is true, a TimeoutException, an HttpRequestException or a CredentialsUnavailableException, after the retries of the client, is counted, and this contends again after a third of the lease. Only the first failure of a row is logged as a warning, and the next try that DynamoDB answers as an information, so an outage of an hour logs two lines and not one per try. Any other error ends it, ResourceNotFoundException included. An acquire whose answer came only after a whole lease starts no term, and this contends again.

Fencing. Work that does not honour the token runs on after the lease is lost, and another process may then be leader at the same time. Writes that must not happen then take IsHeld<TChain>(TChain, Lease), or the FencingToken to a system that checks it.

Current. During a term it is the lease of the term, for the work and what it starts, so code deep in the work fences its writes with IsHeld<TChain>(TChain) without the lease passed down to it.

Exceptions

ArgumentNullException

work is null.

ArgumentException

name is null or empty.

ArgumentOutOfRangeException

leaseDuration is below MinLeaseDuration or above MaxLeaseDuration.

OperationCanceledException

cancellationToken was cancelled, also when the work then threw something else.

InvalidOperationException

The item at the key of the lock is not a lock item written by LockTable; see AcquireAsync(string, TimeSpan, CancellationToken). Or the work threw it.

TryAcquireAsync(string, TimeSpan, CancellationToken)

Tries to acquire a lock once, without waiting: null if another holder has it. It takes over a lock whose holder stopped renewing it, which this LockTable knows only after watching it for a whole lease.

public Task<Lease?> TryAcquireAsync(string name, TimeSpan leaseDuration, CancellationToken cancellationToken = default)

Parameters

name string

The name of the lock. Every process that names it contends for the same item.

leaseDuration TimeSpan

How long the lease lasts without a renewal, from MinLeaseDuration to MaxLeaseDuration. The lease renews itself every third of it.

cancellationToken CancellationToken

Cancels the requests.

Returns

Task<Lease>

The lease, or null if another holder has the lock. An acquire whose answer came only after a whole lease hands out a lease that has ended already, see Ended.

Remarks

Takeover. It succeeds if the lock is free, released, or held by a lease that this LockTable has seen unchanged for that lease's whole duration. A call that finds the lock held remembers the record version of the holder and the time on the monotonic clock, so a later call of the same LockTable takes the lock over once the holder's lease has run out since then without a renewal. Polling this is how a contender waits; AcquireAsync(string, TimeSpan, CancellationToken) does the polling.

Short-lived processes. A takeover needs a LockTable that lives longer than a lease. A process that lives shorter, such as a scheduled job, a function or a command line tool, never takes over a lock whose holder crashed with this, whatever it waits between its runs: it calls AcquireAsync(string, TimeSpan, CancellationToken) with a token that cancels after more than one lease, which waits out a crashed holder and gives up on a live one.

Requests. A LockTable that has not seen the lock held writes at once: one conditional UpdateItem that raises the fencing token with ADD, and which returns the item that failed its condition if the lock is held. Once it has seen a holder, it reads the item with a strongly consistent GetItem first, and writes only if the lock is free or the holder is due for a takeover. So a contender that waits for a live holder sends reads, and a failed conditional write happens only when contenders race for a lock that is free or due.

Exceptions

ArgumentException

name is null or empty.

ArgumentOutOfRangeException

leaseDuration is below MinLeaseDuration or above MaxLeaseDuration.

ResourceNotFoundException

The table does not exist.

DynamoDbException

DynamoDB failed the request, after the retries of the client.

InvalidOperationException

The item at the key of the lock has an owner but is not a lock item written by LockTable.