コンテンツにスキップ

設計

chobo は .book を構文解析し、名前と単位を解決して、型の付いた帳簿を検査します。見るのは、単位、境界、キー、仮押さえと、勘定のあいだで何がどう動くかです。その帳簿を参照インタプリタが動かし、どのデータベースも、参照インタプリタが流すシナリオで結果を突き合わせます。11 のジェネレーターが帳簿をビルドし、chobo doc と chobo api が、帳簿が何をするものかを人とツールに伝えます。

一つの勘定の境界

帳簿に書ける条件は、一つの勘定の下限か上限だけです。ほかの決めごとは、どれもこれを前提にしています。境界が見るのは一つの勘定だけなので、その勘定を書き換える書き込みの中で確かめられ、あいだにほかの読み取りが入りません。PostgreSQL では勘定の行をロックする関数が、TigerBeetle では勘定のフラグと、全部通るか一つも通らないかのどちらかになる振替のチェーンが、MySQL の系統では勘定の行をロックしながら読むトランザクションが、それを受け持ちます。条件を付けて行を書き換えることと、何行かをまとめて書くことの二つができるデータベースなら、これに要るものはそろっています。いま試しているのは、PostgreSQL、TigerBeetle、MySQL、TiDB、Vitess です。

二つの勘定にまたがる条件は、このやり方では守れません。二つの返金が同時に来ると、どちらも相手が書き込む前の売上と返金を読み、両方とも通ってしまいます。そこで、そうした条件は、勘定を一つ足して、その勘定の境界にします。

条件 直した先の勘定
返金は売上を超えない 注文ごとの返金できる残り(at least 0)。売上で入れ、返金で取る
どの SKU の在庫も倉庫に収まる 倉庫の空き(at least 0)。入荷のたびに、同じ振替の 2 つ目の移動で取る
返金は月に三回まで 月ごとの、残りの返金の回数(at least 0。単位は回数)
会員ごとに違う与信枠 まだ使える与信の勘定。振替で満たす
手数料は代金の 10% 境界にはしない。手数料は規則が計算し、振替はそれを受け取る

工夫するのは、どの量を勘定にするかのほうで、条件の形は変えません。rulec が表に抜けが無いことを証明できるのも、書けるものを同じように絞っているからです。rulec はセルが見るものを自分の列に限り、chobo は条件が見るものを一つの勘定に限ります。そのおかげで、どの条件も、chobo がビルドするどのデータベースでも守れます。

原則

  • P1. 条件は、一つの勘定の境界だけ。
  • P2. 残高は、入った量から出た量を引いたもの。 借方と貸方を読み分けることはありません。帳簿は量を境界の中に保つためのもので、総勘定元帳ではありません。どの勘定も同じように数えるので、at least 0 はいつも「0 を下回らない」という意味です。帳簿の外(取引の相手など)は、外の勘定(outside)で表します。外の勘定はマイナスになってよく、境界はありません。
  • P3. 額は、いちばん小さい単位で数えた整数で、一つの移動は一つの単位で動く。 chobo は換算をしません。
  • P4. どの振替も、帳簿に書いたキーで呼ぶ。 同じキーで中身も同じなら、二度目は何もしません。中身が違えば拒否します。キーは呼ぶ側が作って持っておく ID ではなく、振替の引数から作る、帳簿のものです。そのおかげで、検査は二度目の呼び出しが何を受け取るかを言え、経理や運用の人は「注文と SKU の組ごとに一度」と帳簿の中で読めます。一つのワークフローを二回走らせても、二度は動きません。
  • P5. 帳簿の意味を決めるのは参照インタプリタ一つ。 PostgreSQL、TigerBeetle、MySQL、TiDB、Vitess は、シナリオごとに、それと突き合わせます。
  • P6. chobo は計算しない。 額は引数かリテラルです。振替は、額を決めるために残高を読むことも、足し合わせることもしません。そのおかげで、結果はいつも「通る」か「境界の理由で拒否される」かのどちらかになり、理由を走らせる前に並べられます。
  • P7. 仮押さえは、確定、取消、期限切れのどれかで終わる。 確定と取消は呼ぶ側がします。期限切れは、呼ぶ側とは関係なく、時計で起きます。

そこから決まること

  • 移動は、書いた順に一つずつ確かめる。 TigerBeetle はチェーンを振替ごとに確かめ、PostgreSQL と MySQL の系統も同じにしているので、どれも同じ結果を返し、帳簿の意味を帳簿から読み取れます。全部の移動のあとでだけ残高を確かめるなら、読む人の知らないところでチェーンを並べ替えることになります。その代わり、移動を合わせれば足りるのに、順序のせいで拒否されることがあり、検査がそれを警告します(W103)。
  • 仮押さえは、最悪の場合を考えて数える。 押さえて出ていく分は下限に対して、押さえて入ってくる分は上限に対して数え、逆向きにはどちらも数えません。TigerBeetle の数え方と同じです。通った仮押さえが、確定のときに境界で拒否されることはありません。
  • 拒否は、失敗ではなく結果の一つ。 在庫が無いのは業務として起こりうることで、クライアントはそれを結果として返します。境界で拒否されたキーは、拒否されたままになります。TigerBeetle が一度拒否した ID を二度と通さないのと同じで、リトライはいつも同じ結果を受け取ります。
  • chobo はほかの言語を呼ばない。 ワークフローは ritsu を通して帳簿を読み、ほかのツールは chobo api を読みます。chobo はそのどちらも知りません。
  • データベースごと、言語ごとのクライアント。 どのデータベースのクライアントも、名前も引数も結果も同じです。データベースを替えても、変わるのは帳簿の値の作り方だけで、言語ごとに一つのランナーで、テストを全部のデータベースに通せます。Rust のクライアントは、TigerBeetle の公式の Rust のクライアントと sqlx に合わせて非同期で、呼ぶ相手を、ドライバのプールや接続やトランザクションが実装するトレイトとして受け取ります。
  • MySQL の系統では、クライアントが SQL 文を送る。 TiDB にはストアドプロシージャが無く、Vitess を動かすサービスには、ストアドルーチンもデータベースを作ることも許さないものがあるので、呼ぶための関数を置きません。クライアントは、chobo が書いた SQL 文を、呼び出しごとに一つのトランザクションの中で送ります。境界の確かめはその SQL 文の中にあり、クライアントは値を入れるだけです。
  • ビルドは、そのデータベースが受け付けないところで止まる。 chobo のテストで使えるレプリカの一つのリクエストに入らない長さのチェーン(253 件)、PostgreSQL の 63 バイトより長い名前、MySQL が持てない名前は、そのデータベース向けのビルドだけを止め、ほかのビルドは止めません。Rust の識別子に使えない字を持つ名前は、Rust 向けのビルドを止めます。

採らなかったもの

  • 残高を読んで額を決める振替:結果が「通るか、拒否されるか」の二つでなくなります。呼ぶ側が残高を読んで額を決め、そのあいだに残高が変わっていれば、振替が拒否されます。
  • 借方と貸方:P2 のとおりです。
  • 呼ぶ側が作るキー:二度目の呼び出しが何を受け取るかを、検査が言えなくなります。
  • 勘定ごとに違う境界:その枠を一つの勘定にし、振替で満たします。
  • データベースの中で帳簿を読むインタプリタ(帳簿をテーブルに置いて読むもの):操作ごとの関数なら、帳簿の移動を一つずつそのまま書くので、帳簿と並べて読めます。
  • データベースの前に置く chobo のサーバー:書き込みをまとめて守ることは、データベースがすでにしています。サーバーを置いても、あいだに挟まるものが一つ増えるだけです。
  • 期限が来たら自分で確定する仮押さえ:TigerBeetle の期限切れは、仮押さえを戻すだけです。「30 日たったら確定する」は、ワークフローの待ちで書きます。

詳しい記録

DESIGN.md には、ここに挙げたものを含むすべての決めごとについて、その理由を書いています。設計がそれに頼る前に確かめた TigerBeetle、PostgreSQL、MySQL、TiDB、Vitess の振る舞い、採らなかった設計、残っていることも、そこにあります。