KV / Transactions
Transactions
A KV transaction reads keys, decides, and writes, all or nothing: selling the last item of stock and recording the order together, so that two buyers can never both take it. Writes that need no reads need no transaction: separate calls already share commits.
const stock = store.bucket<number>('stock')
const orders = store.bucket<Order>('orders')
const placed = await store.tx(async tx => {
const left = (await tx.with(stock).get(sku)) ?? 0
if (left < 1) return false
await tx.with(stock).set(sku, left - 1)
await tx.with(orders).set(orderId, order)
return true
})stock = store.bucket("stock", int)
orders = store.bucket("orders", Order)
async def sell(tx) -> bool:
left = await tx.with_(stock).get(sku) or 0
if left < 1:
return False
await tx.with_(stock).set(sku, left - 1)
await tx.with_(orders).set(order_id, order)
return True
placed = await store.tx(sell)stock, err := kv.OpenBucket[int64](store, "stock")
orders, err := kv.OpenBucket[Order](store, "orders")
err = store.Tx(ctx, func(tx *tinystore.Tx) error {
left, _, err := stock.In(tx).Get(ctx, sku)
if err != nil {
return err
}
if left < 1 {
return ErrSoldOut // rolls back
}
if err := stock.In(tx).Set(ctx, sku, left-1); err != nil {
return err
}
return orders.In(tx).Set(ctx, orderID, order)
})let stock = store.bucket::<i64>("stock").open()?;
let orders = store.bucket::<Order>("orders").open()?;
let placed = store.tx(|tx| -> Result<bool, ShopError> {
let left = tx.with(&stock).get(&sku)?.unwrap_or(0);
if left < 1 {
return Ok(false);
}
tx.with(&stock).set(&sku, &(left - 1))?;
tx.with(&orders).set(&order_id, &order)?;
Ok(true)
})?;How it behaves
tx.with(handle)takes a bucket or counters into the transaction (with_in Python,handle.In(tx)in Go). Every call inside goes through it.- The function's return commits; a throw, an error or a panic rolls back (contract DUR-02).
- A call on the handle itself,
stock.get(sku)instead oftx.with(stock).get(sku), isinvalidinside the function: it would read the file without the transaction's own writes. store.txis a transaction of one file. Buckets live inkv.dband queues injobs.db, so a handle of the other file isinvalid. To commit keys and jobs together, open both from a database,db.bucketanddb.queue, and usedb.tx.
Across a connection
In Rust a transaction holds kv.db's writer while it reads and decides. Over
the pipe or the network, a transaction never holds the writer while your
program awaits: its reads go at once, its writes are sent together when the
function returns, with a check that nothing it read has changed. If something
did, the function runs again, five times at most before conflict. So keep
the function free of side effects outside the store: it may run more than
once.
See also
- KV: the engine's overview.
- Versions: one key's read-modify-write without a transaction.
- Two engines in one file: keys and jobs that commit with rows.