[readonly] markdown buffer
Most Race Conditions Are Ownership Problems
A user types car, then quickly types carbon.
The search for carbon returns first. A moment later, the slower search for car replaces it.
Nothing ran at the same instant. No memory was corrupted. The application simply allowed an operation that no longer owned the screen to update it.
Many race conditions become easier to reason about once ownership is explicit.
Completion order is not authority
The first request began earlier. The second request represents the user's newer intent.
Whichever promise resolves last should not automatically win.
One solution is to assign each search an identity and allow only the current owner to commit:
let currentSearch = 0;
async function search(query: string): Promise<void> {
const searchId = ++currentSearch;
const results = await loadResults(query);
if (searchId !== currentSearch) {
return;
}
renderResults(results);
}
Cancellation is better when the underlying work supports it, but the ownership check still protects against results that arrive after cancellation or cannot be cancelled.
The important rule is not “latest response wins”. It is “only the operation representing current intent may update this state”.
The car response arrives last and replaces the carbon results.
Comparing the response query with the current input is not enough either.
The user can type car, change it to carbon, then return to car. The first car response arrives first. Its query matches the box again, so a text-only guard lets it render. The carbon response arrives next and is rejected because its text no longer matches. The final car response arrives last and replaces the interim results.
That is not necessarily wrong. Matching the current query is a useful policy and much better than allowing an unrelated response to render. If request 03 takes a long time, showing the still-relevant results from request 01 gives the user something useful while they wait. The final results replace them when they arrive.
There is no universal answer. Name both policies instead of letting promise timing choose them accidentally.
There are really two choices.
First, when the input returns to car, should the application reuse the in-flight request 01 or launch request 03? Reusing avoids duplicate work and may return sooner. Launching again uses the newest surrounding context, filters and permissions.
Second, if request 03 is launched, should request 01 be allowed to render while it is still loading? Showing it gives lower-latency interim results. Suppressing it means only the final expression of intent can update the page.
Neither policy is universally correct. The mistake is allowing completion order to choose one accidentally.
Name the owner and the lease
For any contested state, ask:
- which operation owns the right to change it?
- how is that owner identified?
- when does its ownership end?
- can another operation take over?
- how does the old owner discover that it lost?
In a UI, ownership may belong to the latest request.
In a background job, it may belong to a worker holding a time-limited lease.
In a database update, it may belong to the caller that read the current record version.
Different mechanisms implement the same idea.
Locks are one ownership mechanism
A mutex gives one execution exclusive ownership of a critical section. It is useful when shared mutable state genuinely must be changed atomically.
It is not the only option.
Optimistic concurrency lets an update proceed only when the version has not changed:
UPDATE documents
SET body = ?, version = version + 1
WHERE id = ? AND version = ?;
If no row is updated, the caller lost ownership because somebody else changed the document first.
Queues can assign one message to one consumer. Idempotency keys can make several deliveries share one business operation. Database constraints can ensure that only one contender successfully creates a resource.
These designs avoid asking every participant to “be careful at the same time”.
Ownership must match the business rule
“Last write wins” is an ownership policy, but often an accidental one.
It may be correct for a disposable search result. It is dangerous for inventory, balances or collaborative editing because the final writer can erase a valid earlier change.
The business rule might instead require:
- rejecting stale updates
- merging independent changes
- reserving a resource temporarily
- processing commands in order
- allowing an operation only once
Choose that rule before choosing a lock, queue or version column.
Make stale work harmless
Concurrent work is difficult to prevent completely. Networks retry, users click twice, jobs outlive their leases and responses arrive late.
The safer design assumes stale work will eventually appear and prevents it from committing an invalid result.
Give operations identities. Give ownership an expiry or version. Check ownership at the point where state changes, not only when work begins.
A race condition is not merely two things happening close together.
It is two things believing they still have the right to decide what happens next.