Programming & Compilers
File locking and replacement: keeping one coordination identity
File locking and replacement can split a document between two locked objects. Trace lost updates and a stable lock that protects the current-state read.

Two cooperating programs can both hold an exclusive lock and still edit what they call the same document. That happens when the lock belongs to an opened file object while a save replaces the object reached by the document's pathname. The first program keeps its lock on the old object. The next program opens the replacement and acquires a lock on a different object, so the locking system sees no conflict.
Understanding file locking and replacement starts with three identities: the logical document, the pathname used to find it, and the object returned by an open operation. A protocol that treats them as interchangeable can lose its exclusion guarantee during an otherwise ordinary save. The repair is to define a coordination identity that remains stable while the data object changes, then make every participant use it for the same critical section.
State the lock object identity contract first
Use a stipulated local locking contract for the following example. Opening a pathname yields a handle to its current file object. An exclusive advisory lock attaches to that object, and another cooperating process requesting the conflicting lock on the same object must wait. A lock does not stop an application that ignores the convention. Locks on different objects do not conflict, even when those objects have occupied the same pathname at different times.
Also stipulate that replacement changes a directory binding atomically for later pathname lookups. Existing open handles continue to refer to their previously opened objects. This describes the identity behavior needed for the example; actual APIs vary in lock ownership, handle inheritance, replacement permissions, and filesystem support. A production design must verify those details for its chosen interface instead of transferring this model unchanged to every operating system.
Linux documents a concrete version of these boundaries. Its flock interface associates locks with an open file description; descriptors duplicated by fork or dup refer to the same lock, which is released by an explicit unlock or when all those descriptors close. The rename interface can atomically replace an existing destination binding, and open descriptors continue to refer to their opened objects. The unlink rules explain why a removed object remains available through open descriptors until the last referring descriptor closes. These local semantics support the example, but lock ownership and filesystem behavior must be checked together. Linux also documents different NFS and SMB behavior, including situations where a network lock is mandatory.
Call the logical document D, its pathname document.json, and its original object O1. Process A opens document.json and gets a handle to O1. It acquires O1's exclusive lock, reads the current content, and constructs a replacement object O2 under a temporary name. At this point, the document pathname still names O1, and A's lock has the intended relationship to that binding.
Next A replaces the document pathname with O2. New opens of document.json now reach O2. A's existing handle still refers to O1, and the lock object identity is still O1. Nothing in the stipulated contract transfers the lock to O2 or to the abstract document D. A can continue to believe it is protecting the document while the pathname leads elsewhere.
Trace the second opener after a save
Process B now opens document.json and receives a handle to O2. It requests an exclusive lock on O2. That request succeeds because A's lock is on O1. The processes hold exclusive locks on different objects, and each may believe its locked handle represents D. Their belief does not create a conflict in the lock manager.
Put numbers on the competing updates. O1 initially contains 0, and A's first replacement publishes O2 containing 1. A still holds its lock on O1 and retains the value 1 it just saved. It proposes another update adding 10, for a result of 11. B opens and locks O2, reads 1, and proposes adding 100, for a result of 101. If B publishes 101 and A then publishes 11, the current document ends at 11 and B's increment has been lost. Both calculations used the value 1; the failure comes from concurrent logical updates protected by different locks.
The exact effect depends on what the processes do after replacement. If A's protected operation truly ends at the first replacement and does nothing else requiring exclusion, the timeline alone does not prove a lost update. It proves that A's still-held data-object lock no longer excludes a later opener of the logical document. Assessing the application requires identifying the remaining protected actions and their assumptions.
Opening earlier does not solve the problem by itself. B might open O1 before A replaces it, wait for O1's lock, then acquire that old object's lock after A releases. B's handle remains O1 even though the current pathname names O2. If B expects the latest document, it must resolve and read the document at the right point in the agreed protocol. Waiting on a lock cannot refresh an already-open handle's identity.
The runtime code replacement guide gives a useful comparison for lock object identity. Redirecting a public entry affects later calls, while an old frame or retained callback can still refer to an earlier version. A pathname lookup and an existing file handle likewise need separate identities in this example. The code guide explains retained references; the file-lock conclusion here depends on the declared file and lock contract.
Use stable lock files for the logical document
Introduce a separate coordination object L, reached through document.lock. Participants lock L while operating on D. Saving a new version replaces the data object at document.json but leaves L intact. A and B therefore request locks on the same object before opening and updating the current data. The logical mapping D to L is part of the protocol.
For stable lock files, the underlying coordination object must remain the same across saves. Recreating document.lock at the same pathname can reproduce the original defect: an old participant keeps a lock on L1 while a new participant opens and locks L2. The coordination object must survive normal saves, releases, and cleanup. Deleting it after each unlock is a dangerous release convention under this model.
One possible initialization convention creates the coordination object if absent, then opens the persistent object for locking. Its creation procedure must safely handle simultaneous initializers and must not replace an existing object. Once initialized, participants agree not to unlink and recreate, rename over, or otherwise substitute it. Truncating an existing object alone need not change its identity, although modifying the coordination object may still violate the protocol. Permissions and directory ownership must support that agreement; an uncontrolled participant able to replace L can break the protocol.
Every way of naming D must resolve to the same L. An editor using an absolute path and another using an alias cannot choose unrelated lock objects for the same intended resource. Conversely, separate documents should not accidentally share a coordination identity unless serializing them together is deliberate. The application needs a documented mapping, including how renames of the logical document change that mapping without allowing two active identities.
The lock ordering guide provides a second boundary for stable lock files: a proof must identify the resources and dependencies it actually covers. Giving D one surviving coordination object makes its participating writers contend on the same lock. It supplies neither a rank policy for several resources nor a proof that callbacks cannot create other waits. A stable identity also cannot make multiple filesystem writes one atomic transaction.
Put the whole update decision inside the critical section
Return to the value 1 after A's first save. If the next +10 and +100 updates both acquire L before reading the current data, one writer must wait. With A first, it reads 1 and publishes 11; B then reads 11 and publishes 111. With B first, it publishes 101; A subsequently reads 101 and publishes 111. Each writer reads after acquiring the common lock. Merely locking L around two replacements prepared earlier from 1 would still allow the results 11 and 101 to overwrite each other.
A participating updater first opens the stable coordination object and acquires its exclusive lock. Only after acquisition does it open the current document, read the data needed for the update, and validate the proposed change against that state. It constructs the replacement, completes the checks required by the application, and replaces the document binding while still holding L's lock.
Actions after replacement can also belong inside the section. If the application updates a related version record, checks the current pathname, or records a result that later participants rely on, it must define how those actions relate to the protected state. Keeping a lock while performing them prevents cooperating processes from entering early, but it does not automatically make several distinct writes one indivisible filesystem transaction.
Temporary-file cleanup needs an ownership rule. A failed writer can remove its own unpublished temporary object before releasing the lock, provided it can identify it safely. It should not delete another process's temporary object based merely on a similar name. If stale-file cleanup is part of the shared update procedure, its discovery and deletion conditions must respect the same coordination contract. Cleanup must leave L in place.
Expensive preparation may sometimes occur outside the lock. A process can calculate a proposed result from an earlier snapshot, then acquire L and re-read the current version before committing. It must reject, recompute, or merge the proposal if its assumptions no longer hold. Acquiring the lock after the calculation does not make the old snapshot current. A version check is useful only when the checked state and resulting replacement remain protected together.
Participating readers need their own declared boundary. A reader that requires exclusion from saves may take an agreed compatible or exclusive lock on L before opening the document. A reader outside the convention may open an old or new object according to its lookup timing. An already-open reader may continue reading the old object after replacement. Those observations can be acceptable snapshot behavior, but they do not constitute participation in the writer's exclusion protocol.
Test exit, replacement, and nonparticipation separately
For the process-exit test, stipulate that the selected lock API releases A's ownership when A exits and no other owner retains it. B waiting on L can then proceed. B still needs to inspect the current document because A may have exited before or after replacement, and an unpublished temporary file may remain. Automatic lock release establishes availability of the coordination object; it does not tell B which update actions finished.
Now deliberately replace L while A holds its lock. A remains attached to L1. B opens the new binding L2 and can acquire its lock without conflicting with A. This test should fail the protocol's stable-identity condition. Prevent the coordination-object replacement or change the mechanism, then verify that every participant resolves one surviving object. Renaming the replacement back to the same familiar filename would leave the split unresolved.
Finally introduce process C, which directly rewrites or replaces document.json without locking L. C can interfere with A and B because the locks are advisory in the stipulated contract. A successful cooperative-lock test with A and B says nothing about C's behavior. If the application needs to exclude such actors, it requires an enforceable access boundary or another mechanism suited to that requirement.
None of these checks proves power-loss durability, distributed ownership fencing, or deadlock prevention across several resources. Their result is narrower: under the declared local API and participation assumptions, updates to D contend on one stable coordination object and protect a specified sequence of actions. file locking and replacement is sound only when those identity and participation assumptions match the running application.
Inspect the protocol on a concrete development system
On the ASUS Vivobook 14-inch FHD laptop, the listed 14-inch display gives a concrete workspace for keeping the document pathname beside a handle-and-lock table. Its catalogue description specifies 16GB DDR4 and a Core 5 120U processor. For this investigation, record which object each process opened before and after replacement, then mark which object owns each lock. Establish the filesystem and selected locking API from the running environment before interpreting that record.
For the Lenovo IdeaPad 15.6-inch FHD touch laptop, the catalogue identifies a 512GB PCIe SSD alongside 16GB DDR4 and an i5-1334U processor. A saved event log on that development workspace can preserve the O1-to-O2 binding change alongside the two nonconflicting lock requests. Run the stable-L comparison with the same logical updates and record the second writer's reread. The decisive evidence is the split between O1 and O2 in the first protocol, followed by common-L contention and a current-state read in the corrected protocol.