Optimistic locking
Maya and Ken open the same post. Maya saves her edit first. When Ken saves his older copy, the application should notice rather than silently replacing Maya's work.
Optimistic locking gives each saved version a number. An edit succeeds only if that number still matches the copy the editor read.
Give the post a version
Add one version field to the model:
version UInt @rent.versionSave the copy you read
Pass the loaded post to the update builder. Rent checks its version and increments it when the edit succeeds:
let updated = client
.update_post_one(&post)
.body("new text")
.save_one()
.await?;
assert_eq!(updated.version, post.version + 1);The generated SQL includes both the ID and old version in its predicate, and increments the version in the same update. A stale write returns EntityError::OptimisticLock rather than an affected-row count of zero.
Handle conflicts in one place
You do not need a catch-and-retry block around every save. Handlers can keep using
.await? and pass errors to a shared application error type. In an Axum API, for
example, implement From<EntityError> once and map OptimisticLock to an HTTP
409 Conflict response. The Axum example uses this
shared error-conversion pattern.
The frontend can also share one save flow. Keep the editor's draft when a conflict arrives, then offer to load the latest version or merge the changes. Each screen can use that flow without implementing its own error handling.
Suppose Maya and Ken both open version 7. Maya saves first, creating version 8. Ken's save still names version 7, so Rent rejects it. Sending the same request again will fail again. Fetching version 8 and blindly applying Ken's old text could overwrite Maya's work.
Rent detects the conflict. The application decides which changes to keep. A
shared helper can reload and recompute automated changes when the operation is
safe to repeat, with a bounded number of attempts. Replacing the expected version
or using overwrite() is not a safe default for an editor's stale draft.
Use a version from an API request
When only an ID and version are available:
client
.update_post()
.one(post_id)
.expect_version(read_version)
.body("new text")
.save()
.await?;Choose the right kind of retry
| Situation | How to handle it |
|---|---|
| Ken submits an older copy of a post | Return a conflict through the shared error handler and preserve his draft. |
| A short transaction hits a deadlock or serialization failure | Use Rent's transaction retry helper to rerun the complete operation. |
| Several requests increment a counter | Use an atomic update so each request adds to the current value. |
transaction_retry
retries transient database conflicts. It does not retry EntityError::OptimisticLock.
Reads and calculations that need to be repeated belong inside the transaction
closure, so each attempt can work from the data visible to that transaction.
Deletes, overwrites, and counters
delete_post_one(&post) checks the version the same way, so a stale copy cannot delete a row that changed
after it was read. overwrite() on a versioned update or delete drops the comparison for a deliberate
last-write-wins. An update still advances the version. Filtered updates such as update_post().id_eq(...) never compare
a version, because they did not read one, but they do advance it.
The version is per model: declare it where concurrent editors are real, and leave it off rows with one writer. When concurrent writers only add to a list or adjust a counter, an atomic operation avoids the conflict entirely. See atomic updates.
