Skip to content

Commit bda8828

Browse files
committed
Bump version to 2.1.0 and add pessimistic transactions guide
- Update version to 2.1.0 in app.src - Add comprehensive pessimistic transactions guide - Update CHANGELOG with all new features - Add guide to hex docs configuration
1 parent bc9adfa commit bda8828

File tree

4 files changed

+349
-1
lines changed

4 files changed

+349
-1
lines changed

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
1+
## erlang-rocksdb 2.1.0, released on 2025/12/28
2+
3+
- add Pessimistic Transaction support for high-contention workloads:
4+
- `open_pessimistic_transaction_db/2,3`: open a TransactionDB with row-level locking
5+
- `pessimistic_transaction/2,3`: begin a new transaction
6+
- `pessimistic_transaction_put/3,4`: put with lock acquisition
7+
- `pessimistic_transaction_get/3,4`: read without lock
8+
- `pessimistic_transaction_get_for_update/3,4`: read with exclusive lock
9+
- `pessimistic_transaction_delete/2,3`: delete with lock
10+
- `pessimistic_transaction_iterator/2,3`: create transaction iterator
11+
- `pessimistic_transaction_commit/1`: commit transaction
12+
- `pessimistic_transaction_rollback/1`: rollback transaction
13+
- `release_pessimistic_transaction/1`: release resources
14+
- add Pessimistic Transaction savepoint support:
15+
- `pessimistic_transaction_set_savepoint/1`: mark a savepoint
16+
- `pessimistic_transaction_rollback_to_savepoint/1`: rollback to last savepoint
17+
- `pessimistic_transaction_pop_savepoint/1`: discard savepoint without rollback
18+
- add Pessimistic Transaction introspection:
19+
- `pessimistic_transaction_get_id/1`: get unique transaction ID
20+
- `pessimistic_transaction_get_waiting_txns/1`: get lock contention info
21+
- TransactionDB options: lock_timeout, deadlock_detect, max_num_locks, num_stripes
22+
- Transaction options: set_snapshot, deadlock_detect, lock_timeout
23+
- all pessimistic transaction operations use dirty NIFs to prevent blocking the Erlang scheduler
24+
125
## erlang-rocksdb 2.0.0, released on 2025/12/28
226

327
- bump to rocksdb version [10.7.5](https://github.com/facebook/rocksdb/releases/tag/v10.7.5)

guides/pessimistic_transactions.md

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
# Pessimistic Transactions
2+
3+
Pessimistic Transactions provide strict ACID guarantees with row-level locking, deadlock detection, and lock timeouts. They are ideal for high-contention workloads where multiple transactions frequently attempt to update the same keys.
4+
5+
## Pessimistic vs Optimistic Transactions
6+
7+
| Feature | Optimistic | Pessimistic |
8+
|---------|------------|-------------|
9+
| Locking | Validation at commit | Lock on write/GetForUpdate |
10+
| Conflict handling | Retry on commit failure | Block/timeout on lock acquisition |
11+
| Best for | Low contention workloads | High contention workloads |
12+
| Deadlock | N/A | Detection & timeout |
13+
14+
## Opening a Pessimistic Transaction Database
15+
16+
```erlang
17+
%% Basic open
18+
{ok, Db, [DefaultCF]} = rocksdb:open_pessimistic_transaction_db(
19+
"my_db",
20+
[{create_if_missing, true}],
21+
[{"default", []}]
22+
).
23+
24+
%% With transaction database options
25+
Options = [
26+
{create_if_missing, true},
27+
{lock_timeout, 5000}, %% Lock wait timeout in ms (default 1000)
28+
{deadlock_detect, true}, %% Enable deadlock detection
29+
{max_num_locks, -1}, %% Max locks per CF (-1 = unlimited)
30+
{num_stripes, 16} %% Lock table concurrency
31+
],
32+
{ok, Db, CFs} = rocksdb:open_pessimistic_transaction_db("my_db", Options, [{"default", []}]).
33+
```
34+
35+
## Basic Operations
36+
37+
### Creating a Transaction
38+
39+
```erlang
40+
%% Create a transaction with default options
41+
{ok, Txn} = rocksdb:pessimistic_transaction(Db, []).
42+
43+
%% Create with transaction-specific options
44+
TxnOpts = [
45+
{set_snapshot, true}, %% Use snapshot for consistent reads
46+
{deadlock_detect, true}, %% Enable deadlock detection for this txn
47+
{lock_timeout, 2000} %% Override default lock timeout (ms)
48+
],
49+
{ok, Txn} = rocksdb:pessimistic_transaction(Db, [], TxnOpts).
50+
```
51+
52+
### Put, Get, Delete
53+
54+
```erlang
55+
{ok, Txn} = rocksdb:pessimistic_transaction(Db, []),
56+
57+
%% Put acquires a lock on the key
58+
ok = rocksdb:pessimistic_transaction_put(Txn, <<"key1">>, <<"value1">>),
59+
60+
%% Get reads without acquiring a lock
61+
{ok, Value} = rocksdb:pessimistic_transaction_get(Txn, <<"key1">>, []),
62+
63+
%% Delete acquires a lock on the key
64+
ok = rocksdb:pessimistic_transaction_delete(Txn, <<"key1">>),
65+
66+
%% Commit the transaction
67+
ok = rocksdb:pessimistic_transaction_commit(Txn),
68+
69+
%% Release resources
70+
ok = rocksdb:release_pessimistic_transaction(Txn).
71+
```
72+
73+
### Column Family Support
74+
75+
All operations support column families:
76+
77+
```erlang
78+
{ok, Txn} = rocksdb:pessimistic_transaction(Db, []),
79+
80+
%% Operations with column family
81+
ok = rocksdb:pessimistic_transaction_put(Txn, CfHandle, <<"key">>, <<"value">>),
82+
{ok, Value} = rocksdb:pessimistic_transaction_get(Txn, CfHandle, <<"key">>, []),
83+
ok = rocksdb:pessimistic_transaction_delete(Txn, CfHandle, <<"key">>),
84+
85+
ok = rocksdb:pessimistic_transaction_commit(Txn).
86+
```
87+
88+
## GetForUpdate - Exclusive Lock on Read
89+
90+
Use `get_for_update` to acquire an exclusive lock when reading a key. This prevents other transactions from modifying the key until your transaction commits or rolls back.
91+
92+
```erlang
93+
{ok, Txn} = rocksdb:pessimistic_transaction(Db, []),
94+
95+
%% Read and lock the key
96+
{ok, Value} = rocksdb:pessimistic_transaction_get_for_update(Txn, <<"key">>, []),
97+
98+
%% Now we have an exclusive lock - other transactions will block/timeout
99+
%% if they try to write or get_for_update on this key
100+
101+
%% Optionally modify the value
102+
ok = rocksdb:pessimistic_transaction_put(Txn, <<"key">>, <<"new_value">>),
103+
104+
ok = rocksdb:pessimistic_transaction_commit(Txn).
105+
```
106+
107+
## Rollback
108+
109+
Discard all changes made in the transaction:
110+
111+
```erlang
112+
{ok, Txn} = rocksdb:pessimistic_transaction(Db, []),
113+
114+
ok = rocksdb:pessimistic_transaction_put(Txn, <<"key">>, <<"value">>),
115+
116+
%% Changed our mind - rollback
117+
ok = rocksdb:pessimistic_transaction_rollback(Txn),
118+
119+
%% Always release the transaction
120+
ok = rocksdb:release_pessimistic_transaction(Txn).
121+
```
122+
123+
## Savepoints
124+
125+
Savepoints allow you to mark a point in the transaction that you can roll back to without rolling back the entire transaction.
126+
127+
```erlang
128+
{ok, Txn} = rocksdb:pessimistic_transaction(Db, []),
129+
130+
%% First operation
131+
ok = rocksdb:pessimistic_transaction_put(Txn, <<"a">>, <<"v1">>),
132+
133+
%% Set a savepoint
134+
ok = rocksdb:pessimistic_transaction_set_savepoint(Txn),
135+
136+
%% More operations after savepoint
137+
ok = rocksdb:pessimistic_transaction_put(Txn, <<"b">>, <<"v2">>),
138+
ok = rocksdb:pessimistic_transaction_put(Txn, <<"c">>, <<"v3">>),
139+
140+
%% Rollback to savepoint - undoes b and c, keeps a
141+
ok = rocksdb:pessimistic_transaction_rollback_to_savepoint(Txn),
142+
143+
%% Commit - only 'a' will be saved
144+
ok = rocksdb:pessimistic_transaction_commit(Txn).
145+
```
146+
147+
### Nested Savepoints
148+
149+
Multiple savepoints can be nested:
150+
151+
```erlang
152+
ok = rocksdb:pessimistic_transaction_put(Txn, <<"a">>, <<"v1">>),
153+
ok = rocksdb:pessimistic_transaction_set_savepoint(Txn), %% Savepoint 1
154+
155+
ok = rocksdb:pessimistic_transaction_put(Txn, <<"b">>, <<"v2">>),
156+
ok = rocksdb:pessimistic_transaction_set_savepoint(Txn), %% Savepoint 2
157+
158+
ok = rocksdb:pessimistic_transaction_put(Txn, <<"c">>, <<"v3">>),
159+
160+
%% Rollback to savepoint 2 - undoes 'c'
161+
ok = rocksdb:pessimistic_transaction_rollback_to_savepoint(Txn),
162+
163+
%% Rollback to savepoint 1 - undoes 'b'
164+
ok = rocksdb:pessimistic_transaction_rollback_to_savepoint(Txn).
165+
```
166+
167+
### Pop Savepoint
168+
169+
Discard a savepoint without rolling back:
170+
171+
```erlang
172+
ok = rocksdb:pessimistic_transaction_set_savepoint(Txn),
173+
ok = rocksdb:pessimistic_transaction_put(Txn, <<"key">>, <<"value">>),
174+
175+
%% Discard the savepoint - changes are kept
176+
ok = rocksdb:pessimistic_transaction_pop_savepoint(Txn).
177+
```
178+
179+
## Iterators
180+
181+
Create an iterator that sees uncommitted changes in the transaction:
182+
183+
```erlang
184+
{ok, Txn} = rocksdb:pessimistic_transaction(Db, []),
185+
186+
%% Add uncommitted data
187+
ok = rocksdb:pessimistic_transaction_put(Txn, <<"c">>, <<"v3">>),
188+
189+
%% Create iterator - sees both committed and uncommitted data
190+
{ok, Iter} = rocksdb:pessimistic_transaction_iterator(Txn, []),
191+
192+
%% Use standard iterator operations
193+
{ok, Key, Value} = rocksdb:iterator_move(Iter, first),
194+
{ok, NextKey, NextValue} = rocksdb:iterator_move(Iter, next),
195+
196+
ok = rocksdb:iterator_close(Iter),
197+
ok = rocksdb:pessimistic_transaction_commit(Txn).
198+
```
199+
200+
With column family:
201+
202+
```erlang
203+
{ok, Iter} = rocksdb:pessimistic_transaction_iterator(Txn, CfHandle, []).
204+
```
205+
206+
## Transaction Introspection
207+
208+
### Get Transaction ID
209+
210+
Each transaction has a unique ID:
211+
212+
```erlang
213+
{ok, Txn} = rocksdb:pessimistic_transaction(Db, []),
214+
{ok, TxnId} = rocksdb:pessimistic_transaction_get_id(Txn).
215+
%% TxnId is a non-negative integer
216+
```
217+
218+
### Get Waiting Transactions
219+
220+
Find out what transactions are blocking the current transaction:
221+
222+
```erlang
223+
{ok, WaitInfo} = rocksdb:pessimistic_transaction_get_waiting_txns(Txn).
224+
%% Returns:
225+
%% #{column_family_id => 0,
226+
%% key => <<"locked_key">>,
227+
%% waiting_txns => [TxnId1, TxnId2, ...]}
228+
```
229+
230+
This is useful for debugging lock contention or implementing custom monitoring.
231+
232+
## Lock Timeout and Deadlock Detection
233+
234+
### Lock Timeout
235+
236+
When a transaction tries to acquire a lock held by another transaction, it will wait up to the lock timeout:
237+
238+
```erlang
239+
%% Transaction 1 acquires a lock
240+
{ok, Txn1} = rocksdb:pessimistic_transaction(Db, []),
241+
{ok, _} = rocksdb:pessimistic_transaction_get_for_update(Txn1, <<"key">>, []),
242+
243+
%% Transaction 2 tries to lock the same key with short timeout
244+
{ok, Txn2} = rocksdb:pessimistic_transaction(Db, [], [{lock_timeout, 100}]),
245+
Result = rocksdb:pessimistic_transaction_get_for_update(Txn2, <<"key">>, []),
246+
%% Result will be {error, {timed_out, _}} after 100ms
247+
```
248+
249+
### Deadlock Detection
250+
251+
Enable deadlock detection to automatically detect and break deadlocks:
252+
253+
```erlang
254+
Options = [{deadlock_detect, true}],
255+
{ok, Db, _} = rocksdb:open_pessimistic_transaction_db("db", Options, [{"default", []}]).
256+
```
257+
258+
When a deadlock is detected, one of the transactions will receive a `busy` or `timed_out` error.
259+
260+
## Error Handling
261+
262+
Pessimistic transactions can return specific errors:
263+
264+
- `{error, {busy, Reason}}` - Write conflict or lock contention
265+
- `{error, {timed_out, Reason}}` - Lock acquisition timed out
266+
- `{error, {expired, Reason}}` - Transaction expired
267+
- `{error, {try_again, Reason}}` - Transient error, retry the operation
268+
269+
Example error handling:
270+
271+
```erlang
272+
case rocksdb:pessimistic_transaction_get_for_update(Txn, Key, []) of
273+
{ok, Value} ->
274+
%% Success
275+
process_value(Value);
276+
not_found ->
277+
%% Key doesn't exist
278+
handle_not_found();
279+
{error, {timed_out, _}} ->
280+
%% Lock timeout - another transaction holds the lock
281+
handle_timeout();
282+
{error, {busy, _}} ->
283+
%% Write conflict
284+
handle_conflict()
285+
end.
286+
```
287+
288+
## Complete Example
289+
290+
```erlang
291+
transfer_funds(Db, FromAccount, ToAccount, Amount) ->
292+
{ok, Txn} = rocksdb:pessimistic_transaction(Db, [], [{deadlock_detect, true}]),
293+
try
294+
%% Lock both accounts
295+
{ok, FromBalance} = rocksdb:pessimistic_transaction_get_for_update(
296+
Txn, FromAccount, []),
297+
{ok, ToBalance} = rocksdb:pessimistic_transaction_get_for_update(
298+
Txn, ToAccount, []),
299+
300+
FromBalanceInt = binary_to_integer(FromBalance),
301+
ToBalanceInt = binary_to_integer(ToBalance),
302+
303+
case FromBalanceInt >= Amount of
304+
true ->
305+
NewFrom = integer_to_binary(FromBalanceInt - Amount),
306+
NewTo = integer_to_binary(ToBalanceInt + Amount),
307+
308+
ok = rocksdb:pessimistic_transaction_put(Txn, FromAccount, NewFrom),
309+
ok = rocksdb:pessimistic_transaction_put(Txn, ToAccount, NewTo),
310+
ok = rocksdb:pessimistic_transaction_commit(Txn),
311+
ok;
312+
false ->
313+
rocksdb:pessimistic_transaction_rollback(Txn),
314+
{error, insufficient_funds}
315+
end
316+
catch
317+
_:Error ->
318+
rocksdb:pessimistic_transaction_rollback(Txn),
319+
{error, Error}
320+
after
321+
rocksdb:release_pessimistic_transaction(Txn)
322+
end.
323+
```

rebar.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
{"guides/wide_column_entities.md", #{title => "Wide-Column Entities"}},
4747
{"guides/prefix_seek.md", #{title => "Prefix Seek"}},
4848
{"guides/transactions.md", #{title => "Transactions"}},
49+
{"guides/pessimistic_transactions.md", #{title => "Pessimistic Transactions"}},
4950
{"guides/how_to_backup_rocksdb.md", #{title => "How to backup rocksdb"}},
5051
{"CUSTOMIZED_BUILDS.md", #{title => "Customize erlang-rocksdb builds"}},
5152
{"CHANGELOG.md", #{title => "Changelog"}},

src/rocksdb.app.src

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
{application, rocksdb,
1818
[
1919
{description, "RocksDB for Erlang"},
20-
{vsn, "2.0.0"},
20+
{vsn, "2.1.0"},
2121
{registered, []},
2222
{applications, [
2323
kernel,

0 commit comments

Comments
 (0)