Class DatabaseLockFile
Owns the on-disk format of a database's cooperative locking protocol: {name}.typhon/db.lock and its companion db.lock.request.
public static class DatabaseLockFile
- Inheritance
-
DatabaseLockFile
- Inherited Members
Remarks
What the lock actually is. Two layers, and only one is soft. db.lock is purely cooperative — it records who holds the database so a
refused open can name them, and stale entries (dead PID) are cleaned up by the next opener. The real, OS-enforced exclusion is the data file's
FileShare.Read handle. There is no way to hold a mapped, write-open file "softly", which is why a holder that wants to step aside must actually
dispose its engine rather than downgrade a lock mode.
The handoff protocol (#621). A holder may advertise itself as yieldable — the Workbench does; every normal engine does not. A claimant that
finds a live yieldable lock writes a RequestFileName and waits briefly instead of failing. The holder sees the request, releases the
database, and the claimant acquires it.
The trigger is the holder's advertisement, not the claimant's configuration, so two ordinary application instances contend exactly as they always
did: the incumbent wrote yieldable: false, and the claimant throws.
Why files rather than a named mutex or pipe. The two parties may be different users or sessions; the bundle directory is the one thing they provably
share; the semantics are identical on Windows and Linux; and the state is inspectable after the fact. Windows named mutexes are session-scoped
(Local\ vs Global\), which breaks service/desktop combinations.
Why this type exists. Both the engine (which enforces the lock) and out-of-process observers such as the Workbench (which must decide whether a
database is merely busy, and by whom) read and write these files. Left as string literals joined at each call site, the names and their fields drift between
assemblies with nothing to catch it — the same failure that silently broke typhon ui --open-latest when captures moved into the bundle, and
that let an instant-shaped trace event be decoded as a span.
Fields
FileName
File name of the advisory lock inside the database bundle.
public const string FileName = "db.lock"
Field Value
RequestFileName
File name of a claim on a yieldable database — "somebody wants this, please release it".
public const string RequestFileName = "db.lock.request"
Field Value
RequestTimeToLive
How long a claim stays credible before a holder treats it as abandoned.
public static readonly TimeSpan RequestTimeToLive
Field Value
Remarks
Bounds the case where a claimant dies, or the user cancels a launch, between writing its request and acquiring: the request file would otherwise pin the holder out of its own database forever. Generous relative to the claimant's own wait, so a slow-but-alive claimant is never declared dead out from under itself.
Methods
DeleteRequest(string)
Removes a claim. Called by the claimant once it has acquired, and by a holder retiring an orphan.
public static void DeleteRequest(string bundleDirectory)
Parameters
bundleDirectorystringThe database's
{name}.typhondirectory.
Exists(string)
Whether a database bundle currently has an advisory lock file present. Says nothing about whether its owner is alive.
public static bool Exists(string bundleDirectory)
Parameters
bundleDirectorystringThe database's
{name}.typhondirectory.
Returns
HasLiveRequest(string, DateTimeOffset)
Whether a claim exists that a holder must still honour — i.e. one whose claimant is alive and not past its TTL.
public static bool HasLiveRequest(string bundleDirectory, DateTimeOffset utcNow)
Parameters
bundleDirectorystringThe database's
{name}.typhondirectory.utcNowDateTimeOffsetCurrent time, injectable so TTL expiry is testable without waiting.
Returns
Remarks
An orphaned request is deleted here rather than merely reported, because the only party that ever notices one is the holder it is blocking, and leaving it in place would keep that holder out of its own database.
IsHeldByLiveProcess(string)
Whether the database is held by a process that is still running — the question "can an open plausibly succeed right now?".
public static bool IsHeldByLiveProcess(string bundleDirectory)
Parameters
bundleDirectorystringThe database's
{name}.typhondirectory.
Returns
Remarks
Mirrors the engine's own acceptance rule so an observer polling for its turn reaches the same verdict the next open will. Three cases return
false, i.e. "worth trying": no lock file; a lock whose PID has exited (the engine deletes such a lock and proceeds); an unreadable lock (the
engine treats a corrupt lock as removable). A lock from a different machine returns true — its PID means nothing locally, so the engine
treats it as live, and a poller that disagreed would spin forever attempting opens that always fail.
Inherently racy, and safely so: it is a "should I bother?" gate in front of an operation that re-checks under the real lock. A false positive costs one delayed poll; a false negative costs one failed open.
IsOwnerLive(int, string)
Whether the recorded owner of a lock or claim is still running. A different machine is always treated as live.
public static bool IsOwnerLive(int pid, string machineName)
Parameters
Returns
PathFor(string)
The advisory lock path for a database bundle. Does not check for existence.
public static string PathFor(string bundleDirectory)
Parameters
bundleDirectorystringThe database's
{name}.typhondirectory.
Returns
RequestPathFor(string)
The claim-request path for a database bundle. Does not check for existence.
public static string RequestPathFor(string bundleDirectory)
Parameters
bundleDirectorystringThe database's
{name}.typhondirectory.
Returns
SerializeLock(int, DateTimeOffset, string, bool)
Serialises a lock file's contents. The single writer of this format.
public static string SerializeLock(int pid, DateTimeOffset startedAt, string machineName, bool yieldable)
Parameters
pidintProcess id of the holder.
startedAtDateTimeOffsetWhen the holder acquired the database.
machineNamestringMachine the holder runs on.
yieldableboolWhether this holder will release the database on request.
Returns
TryReadHolder(string, out int, out string, out DateTimeOffset)
Reads the lock file's recorded holder, ignoring whether it is yieldable.
public static bool TryReadHolder(string bundleDirectory, out int pid, out string machineName, out DateTimeOffset startedAt)
Parameters
bundleDirectorystringThe database's
{name}.typhondirectory.pidintReceives the holder's process id.
machineNamestringReceives the holder's machine name.
startedAtDateTimeOffsetReceives when the holder acquired the database.
Returns
TryReadLock(string, out LockInfo)
Reads the lock file. Returns false for absent, empty, unparseable or truncated files.
public static bool TryReadLock(string bundleDirectory, out DatabaseLockFile.LockInfo info)
Parameters
bundleDirectorystringThe database's
{name}.typhondirectory.infoDatabaseLockFile.LockInfoReceives the recorded holder.
Returns
Remarks
A lock file is written non-atomically, so a reader can catch it mid-write; the honest answer to "who holds this?" in that instant is "cannot tell", not a fabricated identity. Callers that must act should re-read rather than treat an unreadable lock as absent.
TryReadRequest(string, out ClaimRequest?)
Whether a claim is in flight, and who by.
public static bool TryReadRequest(string bundleDirectory, out DatabaseLockFile.ClaimRequest? request)
Parameters
bundleDirectorystringThe database's
{name}.typhondirectory.requestDatabaseLockFile.ClaimRequest?Receives the claimant's identity, or
nullwhen the file is present but could not be read. Nullable rather than a sentinel value: any in-band marker (a zero pid, say) collides with a legitimately-recorded one, and the two cases lead to opposite decisions — an unreadable claim is honoured, a claim from a dead pid is retired.
Returns
Remarks
Fail toward yielding. A request file that exists but cannot be parsed still returns true with a default request: the
file's mere presence is the signal, and reading it is only ever to decide whether the claimant is still alive. Treating a half-written request as
"no request" would let a holder ignore a claim it was about to be asked for — the one outcome this protocol must not produce.
WriteRequest(string)
Publishes a claim on a yieldable database. Best-effort: a failed write costs the wait, not correctness.
public static void WriteRequest(string bundleDirectory)
Parameters
bundleDirectorystringThe database's
{name}.typhondirectory.