Run scenarios
The reference interpreter says what a book means. chobo run runs a scenario in it: a list of
operations, with the clock moving on between them and callers coming at the same time, and prints
what each operation answered and the accounts and holds at the end. chobo scenarios writes the
scenarios that try a book's bounds, keys and holds, and every database chobo builds for is held to
the reference interpreter on all of them.
A scenario
The refunds example has one written by hand beside it, examples/refunds/refunds.more.json: a sale of 30.00, two refunds of 20.00 at the same time, and then one of 10.00.
[
{
"name": "more: two refunds of 20.00 at the same time on a sale of 30.00, then one of 10.00",
"book": "refunds",
"steps": [
{"op": "do", "kind": "sale", "args": {"order": "A-100", "amount": 3000}},
{"op": "together", "callers": [
[{"op": "hold", "kind": "refund", "args": {"request": "R-1", "order": "A-100", "amount": 2000}}],
[{"op": "hold", "kind": "refund", "args": {"request": "R-2", "order": "A-100", "amount": 2000}}]
]},
{"op": "hold", "kind": "refund", "args": {"request": "R-3", "order": "A-100", "amount": 1000}}
]
}
]
- A step is an operation (
do,hold,postorvoid) of a transfer (kind), with its arguments by name:doandholdtake every parameter,postandvoidthe key alone, andposttakesamountstoo, to post less than the hold holds. - An amount is a whole number in the smallest step of its unit: 3000 is 30.00 in
USD scale 2. {"op": "pass", "duration": "31 minutes"}moves the clock on.{"op": "together", "callers": [[…], […]]}holds the operations of each caller, in order: eight at most, and nopassortogetheramong them.
A file can hold one scenario, or a list of them, as chobo scenarios prints them.
Callers at the same time
Each operation is dealt with whole, one at a time, so two callers at the same moment come out as one order or the other. The reference interpreter tries every order the callers' operations can interleave in, keeping each caller's own order, and gives every distinct way the scenario can come out:
$ chobo run examples/refunds/refunds.book --scenario examples/refunds/refunds.more.json
examples/refunds/refunds.book (more: two refunds of 20.00 at the same time on a sale of 30.00, then one of 10.00): 3 steps, 2 possible outcomes
outcome 1:
1 sale.do(order: A-100, amount: 3000) done
2 together
caller 1: refund.hold(request: R-1, order: A-100, amount: 2000) done
caller 2: refund.hold(request: R-2, order: A-100, amount: 2000) refused: refund_exceeds_sale
3 refund.hold(request: R-3, order: A-100, amount: 1000) done
accounts:
refundable(A-100) posted 3000, held out 3000, held in 0
sales posted -3000, held out 0, held in 0
refunded posted 0, held out 0, held in 3000
holds:
refund(R-1) held
refund(R-3) held
outcome 2:
1 sale.do(order: A-100, amount: 3000) done
2 together
caller 1: refund.hold(request: R-1, order: A-100, amount: 2000) refused: refund_exceeds_sale
caller 2: refund.hold(request: R-2, order: A-100, amount: 2000) done
3 refund.hold(request: R-3, order: A-100, amount: 1000) done
accounts:
refundable(A-100) posted 3000, held out 3000, held in 0
sales posted -3000, held out 0, held in 0
refunded posted 0, held out 0, held in 3000
holds:
refund(R-2) held
refund(R-3) held
Whichever comes first is held, and the other is refused; the refund of the 10.00 left goes through
either way. The balances are three numbers: what is posted, what holds take out (held out) and what
they put in (held in). What a refund holds stays held out of refundable(A-100) until the hold
ends, so the next refund is checked with it counted. When a database runs this scenario, with the two callers on two connections at once,
its answers must be one of the two outcomes.
Time going by
The interpreter's clock is in seconds from the start of the scenario, and only pass moves it.
When it reaches a hold's expiry, the hold expires then, and what it held goes back. A hold of the
English test book stock_reservation lasts 30 minutes, the same as the inventory example's:
$ chobo run tests/books/stock_reservation.book --scenario tests/books/stock_reservation.more.json
…
tests/books/stock_reservation.book (more: reserve expired, then posted for more than it held): 4 steps
1 receive.do(delivery: delivery-1, sku: sku-2, qty: 5) done
2 reserve.hold(order: order-3, sku: sku-2, qty: 2) done
3 pass 31 minutes reserve(order-3, sku-2) expired
4 reserve.post(order: order-3, sku: sku-2, qty: 3) refused: expired
accounts:
stock(sku-2) posted 5, held out 0, held in 0
suppliers posted -5, held out 0, held in 0
customers posted 0, held out 0, held in 0
holds:
reserve(order-3, sku-2) expired
…
A post after the expiry is refused with expired, whatever it asks for. On TigerBeetle a hold
expires on the cluster's clock; PostgreSQL and the MySQL family have none that acts by itself, so
a hold past its expiry answers expired at once, and what it holds goes back when expire() is
next called (Build for a database).
The scenarios chobo writes
chobo scenarios writes the scenarios of a book, as one list or, with --out, a file each
(001.json, …):
$ chobo scenarios examples/refunds/refunds.book
[
{
"name": "bound: refund.hold takes refundable(order) to 0.01, one above `at least 0.00`",
…
"name": "key: refund.hold refused with refund_exceeds_sale, then again, and again once refundable(order) has enough",
…
"name": "hold: refund posted before it is held, then held and posted",
…
"name": "pass: refund held again with the same key after it expired",
…
"name": "together: two callers take the last 0.01 of refundable(order) with refund.hold",
…
$ chobo scenarios examples/refunds/refunds.book --out scenarios
$ chobo run examples/refunds/refunds.book --scenario examples/refunds/refunds.more.json --format json
What each tries is the start of its name:
bound:each bound, with the account just before it, at it and past it.key:each key twice with the same arguments and with others, and a key a bound refused, called again, and again once the account has enough.hold:every way a hold ends: posted in full and in part, voided, posted then voided and voided then posted, posted for more than it holds, posted before it is held, posted twice.pass:a hold that expires, and is then posted and voided; and held again with its key.moves:a transfer of two moves or more, refused partway through, with nothing moved.together:two callers after the last of something, at the same moment.same_account:a move whose two accounts come out the same, refused withsame_account.
Each scenario is built by running it, and kept only when it comes out as its name says. The string
values are a parameter's name and a number, numbered as they first appear in the scenario, so that no two
meet by chance; the numbers in a name are written in the unit's decimal places. The page
chobo doc writes shows every one of them, step by step (Pages for people).
What a database is sent
--show postgres, --show tigerbeetle and --show mysql print, in place of the answers, what the
clients chobo build writes send each target for the scenario: the SQL and its arguments for
PostgreSQL; for TigerBeetle the accounts made, the opening transfers and the chain of each
operation, field by field; and for the MySQL family the statements of each operation, with the
values in place (Build for a database).
$ chobo run examples/refunds/refunds.book --scenario examples/refunds/refunds.more.json --show postgres
examples/refunds/refunds.book (more: two refunds of 20.00 at the same time on a sale of 30.00, then one of 10.00): 3 steps, as sent to PostgreSQL (tenant "")
1 sale.do(order: A-100, amount: 3000)
select * from "refunds"."sale_do"('', 'A-100', 3000)
2 together, caller 1: refund.hold(request: R-1, order: A-100, amount: 2000)
select * from "refunds"."refund_hold"('', 'R-1', 'A-100', 2000)
2 together, caller 2: refund.hold(request: R-2, order: A-100, amount: 2000)
select * from "refunds"."refund_hold"('', 'R-2', 'A-100', 2000)
3 refund.hold(request: R-3, order: A-100, amount: 1000)
select * from "refunds"."refund_hold"('', 'R-3', 'A-100', 1000)
The tests hold the clients of all four languages to it on every scenario, so the four send the
same calls, and on TigerBeetle the same IDs, which are made from the keys. Formats has
the JSON of a scenario, of its result (--format json) and of what is sent.