Optimistic Locking
TL;DR - Avoid Last Updates - insure row not altered since read
You can optionally ensure your API rejects update attempts for rows that have been altered by other users since read. It works as follows:
-
The system delivers response rows with a
CheckSum
attribute (the as-read CheckSum) -
Client updates return the
CheckSum
-
The system will compute the
Checksum
of the current value of the row -
Exceptions are raised if the as-read/current
CheckSums
mismatch
-
Problem Statement
Optimistic locking is a valuable feature for interactive systems, to avoid update overwrites and maximize concurrency, without requiring special database columns.
Constraints
Most systems operate under the following constraints:
- Maximize concurrency by be eliminating long-duration locks * Rows cannot be locked (pessimistically) on read, in case they are updated
- No special columns, such as
VersionNumber
* Database design is often constrained by other applications, or by internal standards - Minimize network traffic and keep client coding simple * E.g., unwieldy to send all "old" values back
Avoid Update Overwrites
Within these constraints, the key objective is avoid overwriting updates. Consider the following scenario:
Time | User | Action |
---|---|---|
T0 | U1 | Reads Row.Column with value V1 |
T1 | U2 | Reads same row |
T2 | U1 | Updates row with value V2 |
T3 | U2 | Updates row with value V3 - V2 value overwritten, U1 not happy |
The objective, then, is to avoid overwriting U1's update.
Optimistic Locking
A widely accepted solution is optimistic locking:
- On reads, no database locks are acquired
- On update, ensure the row has not changed since the user read it
Approach: virtual CheckSum
to detect changes
Before summarizing the approach, we note some key elements provided by architectural components.
Background: Key Architectural Elements
1. SAFRS @jsonapi_attr
SAFRS API provides adding derived virtual (non-stored) attributes: @jsonapi_attr
:
- This enables the server to compute unstored values, here,
S_CheckSum
- SAFRS supports sending such values on client
patch
operations, so it is visible in logic
2. SQLAlchemy loaded_as_persistent
SQLAlchemy provides the loaded_as_persistent
event, enabling us to compute the CheckSum
, store it in the row, so we can later check it on update.
3. The rules engine supports generic before_logic
This enables us to check the row compare CheckSum
values before updates; see `logic/declare_logic. Note such logic has access to the about-to-be-updated row, and the old-row.
Creation options
You can configure optimistic locking when you create projects, with the following 2 CLI arguments:
-
--opt_locking_attr
- this is the name of the attribute that contains the CheckSum. It defaults toS_CheckSum
-
--opt_locking
- select one of the following (default is optional):
Option | Included on Get |
Checked on Patch |
---|---|---|
ignored | Never | Never |
optional | Always | Yes - but no error if omitted |
required | Always | Yes - error if omitted |
Configuration options
You can override the created opt_locking
on server startup:
- by updating the Config file, and
- by using the
OPT_LOCKING
Env variable.
The options are the same as shown in the table above.
Note the env variables can be set on your IDE Run Configurations.
Processing Overview
The approach is summarized in the table below. See the the code in api/system/opt_locking/opt_locking.py
for details.
Phase | Responsibility | Action | Notes |
---|---|---|---|
Design Time | System | Declare <opt_locking_attr > as a @jsonapi_attr |
Project creation (CLI) builds models.py with @json_attr |
Runtime - Read | System | Compute Checksum | opt_locking#loaded_as (setup from from api_logic_server_run.py) |
Runtime - Call Patch | User App Code, Admin App |
Return as-read-Checksum | See examples below |
Runtime - Process Patch | System | Compare CheckSums: as-read vs. current | opt_locking#opt_locking_patch , via logic/declare_logic.py : generic before event |
Exploring Optimistic Locking
You can explore this using the sample database with the the Admin App, or with the cURL commands below.
Use the No Security
run config.
Category Patch
- Missing S_Checksum passes
This should bypass optlock check and report "can't be x"
curl -X 'PATCH' \
'http://localhost:5656/api/Category/1/' \
-H 'accept: application/vnd.api+json' \
-H 'Content-Type: application/json' \
-d '{
"data": {
"attributes": {
"Description": "x"
},
"type": "Category",
"id": "1"
}
}'
Category Patch
- Invalid S_Checksum raises exception
This should fail "Sorry, row altered by another user..."
curl -X 'PATCH' \
'http://localhost:5656/api/Category/1/' \
-H 'accept: application/vnd.api+json' \
-H 'Content-Type: application/json' \
-d '{
"data": {
"attributes": {
"Description": "x",
"S_CheckSum": "Invalid S_Checksum raises exception"
},
"type": "Category",
"id": "1"
}
}'
Category 9 Patch
valid S_CheckSum passes
This should bypass optlock check and report "can't be x"
curl -X 'PATCH' \
'http://localhost:5656/api/Category/9/' \
-H 'accept: application/vnd.api+json' \
-H 'Content-Type: application/json' \
-d '{
"data": {
"attributes": {
"Description": "x",
"S_CheckSum": "83926768455664603"
},
"type": "Category",
"id": "9"
}
}'
Order 10643 Set Shipped (from null)
This case tests different attribute ordering (per alias attribute), resulting in different checksums.
Be sure to replace the db.sqlite after the test, since this changes it.
curl -X 'PATCH' \
'http://localhost:5656/api/Order/10643/' \
-H 'accept: application/vnd.api+json' \
-H 'Content-Type: application/json' \
-d '{
"data": {
"attributes": {
"RequiredDate": "2013-10-13",
"Id": 10643
},
"type": "Order",
"id": 10643
}
}'
curl -X 'GET' \
'http://localhost:5656/api/Order/10643/?include=parent%2COrderDetailList%2CCustomer%2CLocation%2CEmployee%2COrderList&fields%5BOrder%5D=ShipZip%2CId%2CCustomerId%2CEmployeeId%2COrderDate%2CRequiredDate%2CShippedDate%2CShipVia%2CFreight%2CShipName%2CShipAddress%2CShipCity%2CShipRegion%2CShipCountry%2CAmountTotal%2CCountry%2CCity%2CReady%2COrderDetailCount%2CCloneFromOrder%2C_check_sum_%2CCheckSum' \
-H 'accept: application/vnd.api+json' \
-H 'Content-Type: application/vnd.api+json'
Testing and PYTHONHASHSEED
You may want to build tests that require you to supply checksums. Checksum values differ from run to run (as they should), which can interfere with tests.
For test runs, you can set an environment variable for predictable checksum values: