|
| 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 | +``` |
0 commit comments