コンテンツにスキップ

シナリオを流す

帳簿の意味を決めるのは参照インタプリタです。chobo run は、シナリオを参照インタプリタで流します。シナリオは操作の並びで、あいだで時計を進めたり、呼び出し元を同時に来させたりできます。出力は、操作ごとの結果と、終わったときの勘定と仮押さえです。chobo scenarios は、帳簿の境界とキーと仮押さえを試すシナリオを作ります。テストは、chobo がビルドするどのデータベースも、そのシナリオの全部で参照インタプリタと同じ結果を返すことを確かめています。

シナリオ

返金の例には、手で書いたシナリオが横に置いてあります(examples/refunds/refunds.ja.more.json)。30000 円の売上に 20000 円の返金が二つ同時に来て、そのあと 10000 円を返金します。

[
  {
    "name": "more: 30000 円の売上に 20000 円の返金が二つ同時に来て、そのあと 10000 円を返金する",
    "book": "返金",
    "steps": [
      {"op": "do", "kind": "売上計上", "args": {"注文": "A-100", "額": 30000}},
      {"op": "together", "callers": [
        [{"op": "hold", "kind": "返金", "args": {"申請": "R-1", "注文": "A-100", "額": 20000}}],
        [{"op": "hold", "kind": "返金", "args": {"申請": "R-2", "注文": "A-100", "額": 20000}}]
      ]},
      {"op": "hold", "kind": "返金", "args": {"申請": "R-3", "注文": "A-100", "額": 10000}}
    ]
  }
]
  • ステップは、振替(kind)の操作(do、hold、post、void)で、引数を名前で渡します。do と hold には全部の引数を、post と void にはキーの引数だけを渡します。押さえた量より少なく確定するときは、post に amounts も渡します。
  • 額は、いちばん小さい単位で数えた整数です。unit 円 の 30000 は 30000 円です。英語の版の unit USD scale 2 なら、3000 が 30.00 ドルです。
  • {"op": "pass", "duration": "31 minutes"} は時計を進めます。
  • {"op": "together", "callers": [[…], […]]} には、呼び出し元ごとの操作を順に書きます。呼び出し元は八つまでで、中に pass と together は書けません。

一つのファイルには、シナリオを一つか、chobo scenarios が出すようなシナリオのリストを書きます。

同時に来る呼び出し元

操作は一つずつ、まるごと処理されるので、同じ瞬間に来た二つの呼び出し元のどちらかが先になります。参照インタプリタは、呼び出し元ごとの操作の順序は保ったまま、二つの呼び出し元の操作がどう前後しうるかを全部試し、シナリオのとりうる結果を、同じものは一つにまとめて全部出します。

$ chobo run examples/refunds/refunds.ja.book --scenario examples/refunds/refunds.ja.more.json --lang ja
examples/refunds/refunds.ja.book (more: 30000 円の売上に 20000 円の返金が二つ同時に来て、そのあと 10000 円を返金する): 3 ステップ、とりうる結果は 2 通り
結果 1:
    1  売上計上.do(注文: A-100, 額: 30000)                           通る
    2  together
         呼び出し元 1: 返金.hold(申請: R-1, 注文: A-100, 額: 20000)  通る
         呼び出し元 2: 返金.hold(申請: R-2, 注文: A-100, 額: 20000)  返金超過 で拒否される
    3  返金.hold(申請: R-3, 注文: A-100, 額: 10000)                  通る
  勘定:
    返金できる残り(A-100)  確定 30000、出ていく仮押さえ 30000、入ってくる仮押さえ 0
    売上                   確定 -30000、出ていく仮押さえ 0、入ってくる仮押さえ 0
    返金済み               確定 0、出ていく仮押さえ 0、入ってくる仮押さえ 30000
  仮押さえ:
    返金(R-1)  押さえ中
    返金(R-3)  押さえ中
結果 2:
    1  売上計上.do(注文: A-100, 額: 30000)                           通る
    2  together
         呼び出し元 1: 返金.hold(申請: R-1, 注文: A-100, 額: 20000)  返金超過 で拒否される
         呼び出し元 2: 返金.hold(申請: R-2, 注文: A-100, 額: 20000)  通る
    3  返金.hold(申請: R-3, 注文: A-100, 額: 10000)                  通る
  勘定:
    返金できる残り(A-100)  確定 30000、出ていく仮押さえ 30000、入ってくる仮押さえ 0
    売上                   確定 -30000、出ていく仮押さえ 0、入ってくる仮押さえ 0
    返金済み               確定 0、出ていく仮押さえ 0、入ってくる仮押さえ 30000
  仮押さえ:
    返金(R-2)  押さえ中
    返金(R-3)  押さえ中

先に処理された方が押さえられ、もう一方は拒否されます。残りの 10000 円の返金は、どちらの結果でも通ります。残高は三つの数で表します。確定した量、仮押さえで出ていく量(出ていく仮押さえ)、仮押さえで入ってくる量(入ってくる仮押さえ)です。返金が押さえた分は、仮押さえが終わるまで 返金できる残り(A-100) の出ていく仮押さえに残るので、次の返金はそれも数えて確かめられます。データベースでこのシナリオを流すときは、二つの呼び出し元を二つの接続から同時に送ります。データベースが返すものは、この二つの結果のどちらかと同じでなければなりません。

時間を進める

参照インタプリタの時計は、シナリオの始まりからの秒数で、pass でだけ進みます。時計が仮押さえの有効期限に届くと、仮押さえはそこで期限切れになり、押さえていた量は元に戻ります。テスト用の帳簿 tests/books/在庫.book の仮押さえは、在庫の引当の例と同じく 30 分です。

$ chobo run tests/books/在庫.book --scenario tests/books/在庫.more.json --lang ja
…
tests/books/在庫.book (more: 引当 expired, then posted for more than it held): 4 ステップ
  1  入荷.do(納品書: 納品書-1, sku: sku-2, 数: 5)  通る
  2  引当.hold(注文: 注文-3, sku: sku-2, 数: 2)    通る
  3  pass 31 minutes                               引当(注文-3, sku-2) が期限切れ
  4  引当.post(注文: 注文-3, sku: sku-2, 数: 3)    expired で拒否される
勘定:
  在庫(sku-2)  確定 5、出ていく仮押さえ 0、入ってくる仮押さえ 0
  仕入先       確定 -5、出ていく仮押さえ 0、入ってくる仮押さえ 0
  客           確定 0、出ていく仮押さえ 0、入ってくる仮押さえ 0
仮押さえ:
  引当(注文-3, sku-2)  期限切れ
…

期限が切れたあとの確定は、何を頼んでも expired で拒否されます。TigerBeetle では、仮押さえはクラスタの時計で切れます。PostgreSQL と MySQL の系統には、時刻が来たら自分で動く仕組みがありません。期限を過ぎた仮押さえはすぐに expired を返しますが、押さえていた量が戻るのは、次に expire() を呼んだときです(データベース向けのビルド)。

chobo が作るシナリオ

chobo scenarios は、帳簿のシナリオを一つのリストとして出します。--out を付けると、一つずつファイルに書きます(001.json、…)。

$ chobo scenarios examples/refunds/refunds.ja.book
[
  {
    "name": "bound: 返金.hold takes 返金できる残り(注文) to 1, one above `at least 0`",
…
    "name": "key: 返金.hold refused with 返金超過, then again, and again once 返金できる残り(注文) has enough",
…
    "name": "hold: 返金 posted before it is held, then held and posted",
…
    "name": "pass: 返金 held again with the same key after it expired",
…
    "name": "together: two callers take the last 1 of 返金できる残り(注文) with 返金.hold",
…
$ chobo scenarios examples/refunds/refunds.ja.book --out scenarios
$ chobo run examples/refunds/refunds.ja.book --scenario examples/refunds/refunds.ja.more.json --format json

シナリオの名前は、--lang ja を付けても英語で書きます。人が読むページでは、同じシナリオに日本語の見出しが付きます(最後のものなら「同時: 二つの呼び出し元が 返金.hold で 返金できる残り(注文) の最後の 1 を取り合う」)。何を試すシナリオかは、名前の頭で分かります。

  • bound: 境界ごとに、勘定をその手前、ちょうど、超えるところまで動かします。
  • key: キーごとに、同じ引数と違う引数で二度呼びます。境界で拒否されたキーは、すぐにもう一度と、勘定が足りるようになってからもう一度呼びます。
  • hold: 仮押さえの終わり方を全部試します。全額の確定、一部の確定、取消、確定のあとの取消、取消のあとの確定、押さえた量を超える確定、押さえる前の確定、二度の確定です。
  • pass: 仮押さえの期限を切らしてから、確定と取消を試します。同じキーで押さえ直すことも試します。
  • moves: 移動が二つ以上ある振替が途中の移動で拒否され、何も動かないことを試します。
  • together: 最後の一つを、二つの呼び出し元が同時に取り合います。
  • same_account: 移動の元と先が同じ勘定になり、same_account で拒否されます。

どのシナリオも、実際に流して作り、名前のとおりの結果になったものだけを残します。文字列の値は引数の名前に番号を付けたもの(注文-1、申請-2)で、番号はシナリオの中で出てきた順に振るので、二つの値がたまたま同じになることはありません。名前の中の数は、単位の小数の桁で書きます。chobo doc が書くページには、このシナリオが全部、ステップごとに載ります(人が読むページ)。

データベースに送るもの

--show postgres、--show tigerbeetle、--show mysql を付けると、結果の代わりに、chobo build が書くクライアントがそのシナリオで出力先に送るものを出します。PostgreSQL なら SQL とその引数を、TigerBeetle なら作る勘定、最初に流す振替、操作ごとのチェーンを、フィールドまで出します。MySQL の系統なら、操作ごとの SQL 文を、値を入れた形で出します(データベース向けのビルド)。

$ chobo run examples/refunds/refunds.ja.book --scenario examples/refunds/refunds.ja.more.json --show postgres --lang ja
examples/refunds/refunds.ja.book (more: 30000 円の売上に 20000 円の返金が二つ同時に来て、そのあと 10000 円を返金する): 3 ステップ、PostgreSQL に送るもの(テナント "")
  1  売上計上.do(注文: A-100, 額: 30000)
       select * from "返金"."売上計上_do"('', 'A-100', 30000)
  2  together, caller 1: 返金.hold(申請: R-1, 注文: A-100, 額: 20000)
       select * from "返金"."返金_hold"('', 'R-1', 'A-100', 20000)
  2  together, caller 2: 返金.hold(申請: R-2, 注文: A-100, 額: 20000)
       select * from "返金"."返金_hold"('', 'R-2', 'A-100', 20000)
  3  返金.hold(申請: R-3, 注文: A-100, 額: 10000)
       select * from "返金"."返金_hold"('', 'R-3', 'A-100', 10000)

テストは、四つの言語のクライアントがどのシナリオでもこれと同じものを送ることを確かめています。だから四つは同じ呼び出しをし、TigerBeetle にはキーから作る同じ ID を送ります。シナリオ、その結果(--format json)、送るものの JSON の形は、形式(英語)にあります。