VX/LCKMGR: OpenVMS Lock Manager
Overview
VX/LCKMGR is Sector7's reimplementation of the OpenVMS lock manager for Linux: the SYS$ENQ, SYS$DEQ, and SYS$GETLKI system-service family together with the arbitration daemon behind them. Migrated OpenVMS applications keep their locking logic unchanged. They call the same SYS$ services, and a local lock-manager daemon arbitrates every request and delivers grants and blocking notifications through ASTs, exactly as the VMS lock manager does.
It ships as a single-node lock manager. One server process owns the resource and lock databases, and client processes reach it over a System V IPC message queue. All six OpenVMS lock modes, the full grant and conversion queues, 16-byte and 64-byte lock value blocks, resource domains, and an audit log with replay are supported. Cluster-wide lock mastering is out of scope for the current build.
<!-- Webflow embed (min). Source: lckmgr-page-technical/lckmgr-tech-body-embed-dark.html → python3 website/embeds/minify_webflow_embed.py … -->
<div class="s7-tech-embed" data-vxlckmgrd-body-embed="1" data-technical-mega-embed="1" lang="en"><h2 id="architecture">Architecture and components</h2><p>
VX/LCKMGR is a client/server pair, not a library the application links its locking logic
into. The application calls the <code>SYS$</code> wrappers on the client side; those marshal
a request packet onto the daemon's well-known IPC queue; the daemon owns the resource and
lock databases, arbitrates the request, and calls back asynchronously through ASTs
delivered as reply messages. The table maps each OpenVMS lock-manager concept to its
VX/LCKMGR counterpart.
</p><div class="table-wrapper"><table><thead><tr><th>OpenVMS concept</th><th>VX/LCKMGR component</th><th>Role</th><th>Status</th></tr></thead><tbody><tr><td>Lock manager (LOCKMGR)</td><td><code>lckmgr</code> daemon</td><td>The arbitration daemon: owns the resource and lock databases and fires grant and blocking ASTs.</td><td><span class="ac-live">live</span></td></tr><tr><td><code>SYS$ENQ</code> / <code>SYS$DEQ</code> / <code>SYS$GETLKI</code></td><td>Client library wrappers</td><td>Marshal request packets and run a per-process AST-dispatch thread that invokes the caller's completion and blocking ASTs.</td><td><span class="ac-live">live</span></td></tr><tr><td>SDA <code>SHOW LOCK</code>, operator control</td><td><code>lckctl</code></td><td>Start, stop, statistics, status, and queue maintenance for the daemon.</td><td><span class="ac-live">live</span></td></tr><tr><td>(no VMS analog)</td><td><code>AuditHistory</code></td><td>Generates a post-run report over a recorded audit log.</td><td><span class="ac-live">live</span></td></tr></tbody></table></div><div class="callout"><strong>One local daemon, no cluster transport.</strong> The resource master is always the
local host and there is no cluster interconnect wired into the shipping build. Clients and
the daemon communicate over a System V IPC message queue on the same node.
</div><h6>In-memory data model</h6><p>
The daemon keeps one <strong>Lock Block (LKB)</strong> per outstanding request and one
<strong>Resource Block (RSB)</strong> per named resource. Each RSB holds the three queues
that drive arbitration: the grant queue, the conversion queue, and the wait queue. A
<strong>Lock Holder</strong> record tracks each client session, and a sparse lock table
indexed by a 24-bit slot plus an 8-bit generation counter turns a lock id back into its
LKB. Named resources live in a <code>ResourceDomain</code> hierarchy so unrelated
applications can share the daemon without colliding.
</p><h6>Request lifecycle</h6><p>Every lock operation follows the same round trip between the client wrappers and the daemon:</p><style>.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-diagram {margin: 1.5rem 0;padding: 16px;border: 1px solid var(--s7t-border);border-radius: 10px;background: rgba(255, 255, 255, 0.01);}.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-peers,.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-clients {display: grid;gap: 10px;}.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-peers {grid-template-columns: repeat(2, minmax(0, 1fr));}.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-clients {grid-template-columns: repeat(3, minmax(0, 1fr));}.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-card,.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-hub {min-width: 0;padding: 14px 16px;border-radius: 8px;background: var(--s7t-surface);}.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-daemon {border: 1px solid var(--s7t-note-bd);background: var(--s7t-note-bg);}.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-card strong,.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-card span,.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-hub strong,.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-hub span {display: block;}.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-card strong,.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-hub strong {margin-bottom: 4px;color: var(--s7t-text);}.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-card span,.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-hub span {color: var(--s7t-muted);font-size: 0.9em;line-height: 1.45;}.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-label,.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-direction {font-family: var(--s7t-mono);color: var(--s7t-accent);font-size: 0.76em;font-weight: 700;letter-spacing: 0.08em;text-transform: uppercase;}.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-transfers {display: grid;gap: 8px;margin-top: 10px;}.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-transfer {display: grid;grid-template-columns: minmax(100px, 0.35fr) minmax(0, 1fr);gap: 12px;align-items: center;padding: 11px 14px;border-radius: 6px;background: var(--s7t-surface2);}.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-transfer span:last-child {color: var(--s7t-muted);font-size: 0.9em;}.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-converge {display: grid;gap: 10px;}.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-hub {text-align: center;}.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-queue {border-top: 3px solid var(--s7t-accent);background: var(--s7t-surface2);}.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-converge-note {text-align: center;font-family: var(--s7t-mono);color: var(--s7t-accent);font-size: 0.8em;font-weight: 700;}@media (max-width: 700px) {.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-peers,.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-clients,.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-transfer {grid-template-columns: 1fr;}.s7-tech-embed[data-vxlckmgrd-body-embed] .ipc-transfer {gap: 4px;}}</style><div class="ipc-diagram" role="group" aria-label="VX/LCKMGR request and AST reply round trip"><div class="ipc-peers"><div class="ipc-card"><span class="ipc-label">Application process</span><strong><code>SYS$ENQ</code>, <code>SYS$DEQ</code>, and <code>SYS$GETLKI</code> wrappers</strong><span>The client marshals requests and its AST-dispatch thread receives asynchronous replies.</span></div><div class="ipc-card ipc-daemon"><span class="ipc-label">lckmgr daemon</span><strong>Resource and lock databases</strong><span>The daemon arbitrates the grant, conversion, and wait queues.</span></div></div><div class="ipc-transfers"><div class="ipc-transfer"><span class="ipc-direction">Client to daemon</span><span>Request packet travels over the System V IPC message queue.</span></div><div class="ipc-transfer"><span class="ipc-direction">Daemon to client</span><span>Grant or blocking AST returns as a reply message to the AST-dispatch thread.</span></div></div></div><p>
A synchronous grant can be collapsed into the acknowledgement packet, so a lock that is
immediately grantable returns without a separate AST round trip. A queued lock is granted
later, when the resource frees up, by a reply message that the client's AST-dispatch thread
turns into the caller's completion AST.
</p><h2 id="lock-modes">Lock modes and compatibility</h2><p>
VX/LCKMGR implements all six OpenVMS lock modes with the same symbol values and the same
grant rules as the VMS lock manager. Modes range from the null placeholder used during
conversions up to the exclusive lock that excludes every other holder.
</p><div class="table-wrapper"><table><thead><tr><th>Symbol</th><th>Value</th><th>Abbr.</th><th>Meaning</th></tr></thead><tbody><tr><td><code>LCK$K_NLMODE</code></td><td>0</td><td>NL</td><td>Null: grants no access. Used as an interest marker or a placeholder for a lock conversion.</td></tr><tr><td><code>LCK$K_CRMODE</code></td><td>1</td><td>CR</td><td>Concurrent read: shared read, other readers and writers allowed.</td></tr><tr><td><code>LCK$K_CWMODE</code></td><td>2</td><td>CW</td><td>Concurrent write: shared, unprotected write.</td></tr><tr><td><code>LCK$K_PRMODE</code></td><td>3</td><td>PR</td><td>Protected read: the traditional share lock, no writers.</td></tr><tr><td><code>LCK$K_PWMODE</code></td><td>4</td><td>PW</td><td>Protected write: the traditional update lock, shares with CR.</td></tr><tr><td><code>LCK$K_EXMODE</code></td><td>5</td><td>EX</td><td>Exclusive: excludes every mode except NL.</td></tr></tbody></table></div><h6>Compatibility matrix</h6><p>
A new request is granted immediately only if its mode is compatible with every lock already
granted on the resource (and no conversion or wait is queued ahead of it). Rows are the mode
of a currently granted lock; columns are the mode of the new request.
</p><div class="table-wrapper"><table><thead><tr><th>Granted \ Requested</th><th>NL</th><th>CR</th><th>CW</th><th>PR</th><th>PW</th><th>EX</th></tr></thead><tbody><tr><td><strong>NL</strong></td><td>Yes</td><td>Yes</td><td>Yes</td><td>Yes</td><td>Yes</td><td>Yes</td></tr><tr><td><strong>CR</strong></td><td>Yes</td><td>Yes</td><td>Yes</td><td>Yes</td><td>Yes</td><td>No</td></tr><tr><td><strong>CW</strong></td><td>Yes</td><td>Yes</td><td>Yes</td><td>No</td><td>No</td><td>No</td></tr><tr><td><strong>PR</strong></td><td>Yes</td><td>Yes</td><td>No</td><td>Yes</td><td>No</td><td>No</td></tr><tr><td><strong>PW</strong></td><td>Yes</td><td>Yes</td><td>No</td><td>No</td><td>No</td><td>No</td></tr><tr><td><strong>EX</strong></td><td>Yes</td><td>No</td><td>No</td><td>No</td><td>No</td><td>No</td></tr></tbody></table></div><div class="callout"><strong>Reading the grid.</strong> NL is compatible with everything, which is why it is safe
as a conversion placeholder. EX is compatible only with NL. CR is the most permissive real
access mode: it coexists with every mode except EX.
</div><h6>Grant and conversion rules</h6><p>
A request on a new resource is granted at once. On an existing resource it is granted only
when the conversion and wait queues are empty and the mode is compatible with all granted
locks; an NL request with the expedite option can jump the wait queue. Otherwise the request
either returns <code>SS$_NOTQUEUED</code> when the no-queue option is set, or is appended to
the wait queue. Conversions to a less restrictive mode are granted synchronously; conversions
to a more restrictive mode are granted synchronously when compatible, and otherwise join the
conversion queue while the lock stays granted at its current mode. When a lock is released or
converted down, the conversion queue is re-evaluated up to the first incompatible entry, and
the wait queue is evaluated once the conversion queue empties.
</p><h2 id="request-flags">Request flags</h2><p>
Lock requests carry the same option flags as OpenVMS. The header defines them in the mask
form (<code>LCK$M_*</code>); older application code often spells the same options in the
bit-position form (<code>LCK$V_*</code>). Both refer to the identical OpenVMS options.
Several bit positions are deliberately overloaded by call context, exactly as on VMS: the
value is the same, but its meaning differs between an enqueue (<code>SYS$ENQ</code>) and a
dequeue (<code>SYS$DEQ</code>).
</p><div class="table-wrapper"><table><thead><tr><th>Flag (mask form)</th><th>Hex</th><th>Meaning</th></tr></thead><tbody><tr><td><code>LCK$M_VALBLK</code></td><td><code>0x00000001</code></td><td>Read or write the lock value block at grant time.</td></tr><tr><td><code>LCK$M_DEQALL</code></td><td><code>0x00000001</code></td><td>On dequeue: release all locks held by the caller.</td></tr><tr><td><code>LCK$M_CONVERT</code></td><td><code>0x00000002</code></td><td>The request converts the mode of an existing lock id.</td></tr><tr><td><code>LCK$M_CANCEL</code></td><td><code>0x00000002</code></td><td>On dequeue: cancel a pending conversion.</td></tr><tr><td><code>LCK$M_NOQUEUE</code></td><td><code>0x00000004</code></td><td>Do not queue if not immediately grantable; return <code>SS$_NOTQUEUED</code>.</td></tr><tr><td><code>LCK$M_SYNCSTS</code></td><td><code>0x00000008</code></td><td>Return <code>SS$_SYNCH</code> on a synchronous grant and skip the completion AST.</td></tr><tr><td><code>LCK$M_SYSTEM</code></td><td><code>0x00000010</code></td><td>System-wide lock (resource group 0).</td></tr><tr><td><code>LCK$M_NOQUOTA</code></td><td><code>0x00000020</code></td><td>Bypass quota checks.</td></tr><tr><td><code>LCK$M_EXPEDITE</code></td><td><code>0x00000800</code></td><td>An NL-mode request jumps the wait queue.</td></tr><tr><td><code>LCK$M_XVALBLK</code></td><td><code>0x00010000</code></td><td>Request the 64-byte extended lock value block.</td></tr><tr><td><code>LCK$M_AUDITED</code></td><td><code>0x00080000</code></td><td><strong>Sector7 extension</strong>: mark the request for audit tracing.</td></tr></tbody></table></div><div class="callout"><strong>Overloaded bits.</strong> <code>LCK$M_CONVERT</code>, <code>LCK$M_CANCEL</code>, and
<code>LCK$M_DEQALL</code> share bit values because VMS assigns them to the same bit in
mutually exclusive service contexts. VX/LCKMGR preserves that encoding so existing code
that sets these options keeps working unchanged.
</div><h2 id="client-api">Client API: the SYS$ routines</h2><p>
Applications call the same system services as on OpenVMS. Each routine has an asynchronous
form and a synchronous form that blocks until the request completes. The synchronous forms
are the ones that end in W.
</p><div class="table-wrapper"><table><thead><tr><th>Routine</th><th>Purpose</th><th>Status</th></tr></thead><tbody><tr><td><code>SYS$ENQ</code></td><td>Asynchronous enqueue or convert; returns a request acknowledgement, with the grant delivered by AST.</td><td><span class="ac-live">live</span></td></tr><tr><td><code>SYS$ENQW</code></td><td>Synchronous enqueue: blocks until the lock is granted or <code>SS$_SYNCH</code> is returned.</td><td><span class="ac-live">live</span></td></tr><tr><td><code>SYS$DEQ</code></td><td>Asynchronous dequeue by lock id, with optional value block and flags.</td><td><span class="ac-live">live</span></td></tr><tr><td><code>SYS$DEQW</code></td><td>Synchronous dequeue.</td><td><span class="ac-live">live</span></td></tr><tr><td><code>SYS$GETLKI</code></td><td>Query lock information by lock id or wildcard, through an item list.</td><td><span class="ac-live">live</span></td></tr><tr><td><code>SYS$GETLKIW</code></td><td>Synchronous GETLKI.</td><td><span class="ac-live">live</span></td></tr><tr><td><code>SYS$SET_RESOURCE_DOMAIN</code></td><td>Join or leave a resource domain.</td><td><span class="ac-live">live</span></td></tr><tr><td><code>SYS$RUNDOWN</code></td><td>Release the locks held by an image at exit.</td><td><span class="ac-live">live</span></td></tr></tbody></table></div><h6>Example: enqueue, convert up, release</h6><p>
This is the common lock lifecycle for a shared resource. The example takes a system-wide
lock on a banking account resource at NL mode, converts it up to EX to make an exclusive
change, then releases it and writes the lock value block back. It checks both the routine
return code and the lock status block after each step.
</p>
<pre><span class="cmt">/* new NL-mode lock on a system-wide resource (async request, then wait) */</span>
rc = <span class="kw">sys$enq</span>(efn, <span class="kw">LCK$K_NLMODE</span>, &lksb, <span class="kw">LCK$M_SYSTEM</span>,
&resnam, 0, ast_granted, ctx, ast_blocked, <span class="kw">PSL$C_USER</span>, 0);
rc = <span class="kw">sys$waitfr</span>(efn);
<span class="kw">if</span> (!(rc & 1)) { request_error++; }
<span class="kw">if</span> (!(lksb.status & 1)) { grant_error++; } <span class="cmt">/* not granted */</span>
<span class="cmt">/* convert the same lock up to EX; LCK$M_CONVERT names the held lock id */</span>
rc = <span class="kw">sys$enqw</span>(efn, <span class="kw">LCK$K_EXMODE</span>, &lksb,
<span class="kw">LCK$M_SYSTEM</span> | <span class="kw">LCK$M_VALBLK</span> | <span class="kw">LCK$M_CONVERT</span>,
0, 0, 0, 0, 0, <span class="kw">PSL$C_USER</span>, 0);
<span class="cmt">/* release; the value block is written back from lksb.valblk */</span>
rc = <span class="kw">sys$deq</span>(lksb.lkid, &lksb.valblk, <span class="kw">PSL$C_USER</span>, 0);</pre>
<p>
The completion AST and the blocking AST are ordinary functions passed by address. The
client-side AST-dispatch thread invokes them when the daemon publishes the matching reply
on the process's AST channel, so the asynchronous behaviour of the VMS lock manager is
preserved without the application changing how it waits.
</p><h2 id="getlki">Lock information and status codes</h2><p><code>SYS$GETLKI</code> returns detailed information about a lock through an item list: the
caller supplies item codes and receives the matching values. It is important to keep three
things separate, because migrated code sometimes conflates them: the lock-state constants,
the GETLKI item codes, and the completion status codes a request returns.
</p><h6>Lock-state constants</h6><p>These describe where a lock currently sits, and are returned for the state item code.</p><div class="table-wrapper"><table><thead><tr><th>Constant</th><th>Value</th><th>Meaning</th></tr></thead><tbody><tr><td><code>LKI$C_WAITING</code></td><td>-1</td><td>Lock is on the wait queue.</td></tr><tr><td><code>LKI$C_CONVERT</code></td><td>0</td><td>Lock is on the conversion queue.</td></tr><tr><td><code>LKI$C_GRANTED</code></td><td>1</td><td>Lock is granted.</td></tr></tbody></table></div><h6>GETLKI item codes</h6><p>
The item codes select which field to return. Per-lock identity and state codes fall in the
256 to 264 range; namespace, resource, value block, and counter fields fall in the 512 to
524 range.
</p><div class="table-wrapper"><table><thead><tr><th>Item code</th><th>Value</th><th>Item code</th><th>Value</th></tr></thead><tbody><tr><td><code>LKI$_PID</code></td><td>256</td><td><code>LKI$_LKID</code></td><td>263</td></tr><tr><td><code>LKI$_STATE</code></td><td>257</td><td><code>LKI$_CSID</code></td><td>264</td></tr><tr><td><code>LKI$_PARENT</code></td><td>258</td><td><code>LKI$_NAMSPACE</code></td><td>512</td></tr><tr><td><code>LKI$_LCKREFCNT</code></td><td>259</td><td><code>LKI$_RESNAM</code></td><td>513</td></tr><tr><td><code>LKI$_LOCKID</code></td><td>260</td><td><code>LKI$_VALBLK</code></td><td>515</td></tr><tr><td><code>LKI$_REMLKID</code></td><td>261</td><td><code>LKI$_LCKCOUNT</code></td><td>517</td></tr><tr><td><code>LKI$_MSTLKID</code></td><td>262</td><td><code>LKI$_BLOCKEDBY</code></td><td>518</td></tr><tr><td><code>LKI$_BLOCKING</code></td><td>519</td><td><code>LKI$_WAITCOUNT</code></td><td>522</td></tr><tr><td><code>LKI$_LOCKS</code></td><td>520</td><td><code>LKI$_GRANTCOUNT</code></td><td>523</td></tr><tr><td><code>LKI$_CVTCOUNT</code></td><td>521</td><td><code>LKI$_MSTCSID</code></td><td>524</td></tr></tbody></table></div><div class="callout"><strong>Single-node degeneracy.</strong> Because there is no cluster mastering, the
cluster-relative fields collapse: the master lock id equals the local lock id
(<code>LKI$_MSTLKID</code> equals <code>LKI$_LKID</code>) and the cluster system ids are
always zero (<code>LKI$_CSID</code> equals <code>LKI$_MSTCSID</code> equals 0). The node is
always the local host.
</div><h6>Return and completion status codes</h6><p>
These are the status values a lock request returns, in the routine return code or in the
lock status block. They are distinct from the GETLKI item codes above.
</p><div class="table-wrapper"><table><thead><tr><th>Status</th><th>Meaning</th></tr></thead><tbody><tr><td><code>SS$_NORMAL</code></td><td>Request completed normally.</td></tr><tr><td><code>SS$_SYNCH</code></td><td>Synchronous grant; no completion AST will be delivered.</td></tr><tr><td><code>SS$_NOTQUEUED</code></td><td>No-queue request could not be granted immediately.</td></tr><tr><td><code>SS$_CANCEL</code></td><td>A pending conversion was cancelled.</td></tr><tr><td><code>SS$_ABORT</code></td><td>The request was aborted.</td></tr><tr><td><code>SS$_VALNOTVALID</code></td><td>The lock value block is marked invalid.</td></tr><tr><td><code>SS$_IVLOCKID</code></td><td>Invalid lock id supplied.</td></tr><tr><td><code>SS$_NOLOCKID</code></td><td>No lock id available.</td></tr><tr><td><code>SS$_PARNOTGRANT</code></td><td>Parent lock is not granted.</td></tr></tbody></table></div><figure style="margin:1.5rem 0;"><img src="https://cdn.prod.website-files.com/6654a06b5910e86da45495b0/678e68036e503584b0427fdc_LOCK_MANAGER.png"
alt="VX/LCKMGR lock-status and return-code reference chart for SYS$ENQ, SYS$DEQ and SYS$GETLKI on Linux"
loading="lazy" style="max-width:100%;height:auto;border-radius:8px;"></figure><h2 id="transport">Single-node transport</h2><p>
The shipping client and server communicate over a <strong>System V IPC message queue</strong>
on the same host. The daemon derives a well-known queue key from its configured system value,
attaches to the queue, and both sides exchange packets with the standard
<code>msgget</code>, <code>msgsnd</code>, and <code>msgrcv</code> calls. Per-process
statistics are read the same single-host way, out of a shared status file in the daemon's
temporary directory.
</p><div class="ipc-diagram ipc-converge" role="group" aria-label="Many client processes converging on one VX/LCKMGR daemon"><div class="ipc-clients"><div class="ipc-card"><span class="ipc-label">Client A</span><strong>Application process</strong><span>Publishes lock requests and receives AST replies.</span></div><div class="ipc-card"><span class="ipc-label">Client B</span><strong>Application process</strong><span>Uses the same host-local transport.</span></div><div class="ipc-card"><span class="ipc-label">Client C</span><strong>Application process</strong><span>Converges on the shared queue and daemon.</span></div></div><div class="ipc-converge-note">All clients converge through one host-local IPC queue</div><div class="ipc-hub ipc-queue"><span class="ipc-label">System V message queue</span><strong>Key = SYSTEM value + 3</strong><span>Request packets flow to the daemon. Grant and blocking AST replies flow back to the originating process.</span></div><div class="ipc-converge-note">One queue converges on one arbitration service</div><div class="ipc-hub ipc-daemon"><span class="ipc-label">lckmgr daemon</span><strong>Resource database, lock database, and AST reply dispatch</strong><span>The single local daemon coordinates all attached client processes.</span></div></div><div class="callout"><strong>Why single-node.</strong> The resource master is always the local host and cluster
system ids are zero, because the transport is a local message queue rather than a cluster
interconnect. This is a deliberate scope boundary: multi-node and cluster-wide lock
mastering are out of scope for the current build. Applications that lock only within a
single node run unchanged; applications that depend on cluster-wide lock coordination
should be reviewed against this boundary during migration planning.
</div><h2 id="operator">Operator control, statistics, and audit</h2><p>
The <code>lckctl</code> utility starts and stops the daemon and reports on its state. It is
the VX/LCKMGR equivalent of using SDA and operator commands to inspect the VMS lock manager.
</p><div class="table-wrapper"><table><thead><tr><th>Command</th><th>Action</th></tr></thead><tbody><tr><td><code>lckctl start</code></td><td>Launch the <code>lckmgr</code> daemon.</td></tr><tr><td><code>lckctl stop</code></td><td>Send a shutdown request to the daemon.</td></tr><tr><td><code>lckctl stats</code> / <code>status</code></td><td>Dump the daemon statistics block.</td></tr><tr><td><code>lckctl reset-stats</code></td><td>Zero and rewrite the statistics block.</td></tr><tr><td><code>lckctl vacuum</code></td><td>Drain orphaned packets from the IPC queue: republish those whose target process is still alive, discard the rest.</td></tr></tbody></table></div><h6>Statistics</h6><p>
The statistics block reports service timestamps and a running set of counters: packets
received, dispatched, and replied; blocking ASTs and grant ASTs fired; enqueue, dequeue,
and query counts; how many requests took the grant, conversion, and wait paths; and outcome
counters for no-queue rejections and deadlock-search cases. It also reports object counts
(lock blocks, resource blocks, resource names, and active sessions) and per-arena memory
use, with a memory-to-request ratio for capacity planning.
</p><h6>Audit log and replay</h6><p>
The daemon can record every lock request to an audit log and later replay it. Each record
captures the operation, direction, timestamp, and the full request message, in a
32-bit-portable format so a recording made by one build can be read by another. Replay can
be driven back through the daemon to reproduce a workload for tuning, or reported offline
with the <code>AuditHistory</code> tool, which tallies enqueue, dequeue, and query volumes
and prints an operations-per-second summary. Because replay reconstructs the request stream
directly, bottlenecks can be isolated without re-running the original application.
</p><h2 id="status">Status and divergences from the OpenVMS lock manager</h2><p>
VX/LCKMGR reproduces the lock-manager surface that migrated applications actually call. The
matrix below is explicit about what is live, what is stubbed, and what is deliberately out of
scope, so migration planning has no surprises.
</p><div class="table-wrapper"><table><thead><tr><th>Capability</th><th>Status</th></tr></thead><tbody><tr><td><code>SYS$ENQ(W)</code> / <code>SYS$DEQ(W)</code> / <code>SYS$GETLKI(W)</code></td><td><span class="ac-live">live</span></td></tr><tr><td>Six lock modes and the full compatibility matrix</td><td><span class="ac-live">live</span></td></tr><tr><td>Grant, conversion, and wait queues; 16-byte and 64-byte value blocks</td><td><span class="ac-live">live</span></td></tr><tr><td>Resource domains (<code>SYS$SET_RESOURCE_DOMAIN</code>)</td><td><span class="ac-live">live</span></td></tr><tr><td>Audit log record, replay, and reporting</td><td><span class="ac-live">live</span></td></tr><tr><td><code>lckctl</code> start, stop, stats, status, vacuum</td><td><span class="ac-live">live</span></td></tr><tr><td>Runtime debug-mask add and remove in <code>lckctl</code></td><td><span class="ac-v1">stubbed</span></td></tr><tr><td>Deadlock detection</td><td><span class="ac-no">not implemented</span></td></tr><tr><td>ENQLM and sublock-depth quota enforcement</td><td><span class="ac-no">out of scope</span></td></tr><tr><td>Cluster and multi-node lock mastering</td><td><span class="ac-no">out of scope</span></td></tr></tbody></table></div><div class="callout warn"><strong>No deadlock detection.</strong> The daemon does not currently run a deadlock search.
The counters and status plumbing exist, but applications that rely on the VMS lock manager
raising a deadlock condition should be reviewed during migration. Where deadlock avoidance
matters, design lock ordering into the application rather than depending on detection.
</div><h6>Deliberate divergences</h6><div class="table-wrapper"><table><thead><tr><th>Divergence</th><th>Reason</th></tr></thead><tbody><tr><td>Single-node only; cluster system ids degenerate to 0</td><td>The transport is a local System V message queue, not a cluster interconnect.</td></tr><tr><td>No deadlock detection</td><td>The deadlock search is not implemented in the current build.</td></tr><tr><td>ENQLM and sublock-depth limits never raised</td><td>These quota limits are not imposed.</td></tr><tr><td>Client-side pre-validation of arguments</td><td>The daemon never touches client memory, so access-violation and bad-parameter checks are the client library's responsibility.</td></tr><tr><td><code>LCK$M_AUDITED</code> option added</td><td>A Sector7 extension for audit tracing, not present in the original lock manager.</td></tr><tr><td>Maximum of 80,000 concurrent lock ids</td><td>The lock table is sized for up to 80,000 active locks per daemon.</td></tr></tbody></table></div><h2 id="quickref">Quick reference</h2><h6>Lock modes</h6><div class="kw-grid"><div>NL = 0</div><div>CR = 1</div><div>CW = 2</div><div>PR = 3</div><div>PW = 4</div><div>EX = 5</div></div><p>EX excludes all but NL; NL is compatible with every mode.</p><h6>Client routines</h6><div class="kw-grid"><div>SYS$ENQ</div><div>SYS$ENQW</div><div>SYS$DEQ</div><div>SYS$DEQW</div><div>SYS$GETLKI</div><div>SYS$GETLKIW</div><div>SYS$SET_RESOURCE_DOMAIN</div><div>SYS$RUNDOWN</div></div><h6>Common request flags</h6><div class="kw-grid"><div>LCK$M_VALBLK</div><div>LCK$M_CONVERT</div><div>LCK$M_NOQUEUE</div><div>LCK$M_SYNCSTS</div><div>LCK$M_SYSTEM</div><div>LCK$M_EXPEDITE</div><div>LCK$M_XVALBLK</div><div>LCK$M_AUDITED</div></div><h6>Lock states</h6><div class="kw-grid"><div>LKI$C_WAITING = -1</div><div>LKI$C_CONVERT = 0</div><div>LKI$C_GRANTED = 1</div></div><h6>Operator commands</h6><div class="kw-grid"><div>lckctl start</div><div>lckctl stop</div><div>lckctl stats</div><div>lckctl status</div><div>lckctl reset-stats</div><div>lckctl vacuum</div></div><h6>Scope</h6><p>
Single node, one <code>lckmgr</code> daemon per node, clients reached over a System V message
queue. Up to 80,000 concurrent lock ids. No cluster mastering and no deadlock detection in
the current build.
</p></div>