長年書類を書き続けて待ち続けた末に、ついに銀行の免許を取得しました。これで正式に、自分の銀行を開く資格を得たことになります。やった!
まず取りかかるのは、ITシステムを動かすことです。1日がんばったおかげで、もう口座の開設と閉鎖、そして引き出しと預け入れまでできるようになりました。
テストを書くのは面倒だったので、友達に声をかけてシステムのテストを手伝ってもらいます。ところが、始めてからわずか5分で、友達の一人がお金がなくなったと言い出しました! 自分のコードにバグはないと確信していますが、念のためログを調べて原因を探ることにします。
ああ、やっぱり。疑ったとおり、悪いのは友達のほうでした! その友達はテスト用の認証情報を別の友達と共有していて、二人で結託して同じ口座から_並行して_預け入れと引き出しを繰り返していたのです。いったい誰がそんなことをするでしょうか?
誰かが同時に自分の口座にアクセスするのは物理的に_不可能_だと主張しますが、友達は得意げに、銀行のルールではそれをサポートすることが_必須_だと告げます。つまり、並行処理をサポートしなければ、サービス開始のゴーサインは出ないということです。ため息をつきながら、明日取りかかろうと心にメモします。これでサービス開始は_少なくとも_あと1日は延びますが、まあ仕方ありません…
この課題では、口座の開設・閉鎖、出金、入金に対応する銀行口座を実装します。
銀行口座には、インターネット、携帯電話、自動引き落としなど、さまざまな方法でアクセスできます。そのため、作成する銀行ソフトウェアは、複数のスレッドやプロセス(呼び方はプログラミング言語によって異なります)から、口座に安全に並行アクセスできるようにしなければなりません。 たとえば、多数の入金と出金が並行して行われることがあります。残高を読み取ってから新しい残高を設定するまでの間に、競合状態が起きないようにする必要があります。
口座は閉鎖できる必要があります。閉鎖された口座に対する操作は、必ず失敗しなければなりません。
Rocのコードでは、並行処理や共有状態はプラットフォームに任せ、私たちはビジネスロジックに集中するのが一般的です。そのため、この演習ではBankAccount.rocの口座操作を実装するだけでよく、並行処理を気にする必要はありません。口座は閉じた状態から始まり、開き直すと残高はリセットされます。入金と出金は正の値でなければなりません。
同梱のbank-account-demo.rocは、この実装を小さなWebサービスでどう使えるかを示しています。エディターで読むことはできますが、テストとは独立しており、解答の一部ではありません。
デモを実行するには、ご自身のマシンにRocをインストールし、Exercism CLIを導入したうえで、ターミナルで次のコマンドを実行します。
exercism download --exercise=bank-account --track=roc
次に、BankAccount.rocを実装し、次のコマンドでテストします(CLIが表示するダウンロード先が下のパスと異なる場合は、その場所に合わせて変更してください)。
cd ~/Exercism/roc/bank-account
roc test bank-account-test.roc
すべてのテストが通ったら、次のコマンドでデモサーバーを起動できます。
roc bank-account-demo.roc
このサーバーはbasic-webserverプラットフォームと、その組み込みのSQLiteサポートを利用しています(Rocは初回実行時にプラットフォームをダウンロードします)。別途データベースサーバーを用意する必要はありません。サーバーはhttp://127.0.0.1:8000で待ち受け、カレントディレクトリにbank-account-demo.dbを作成します(別のデータベースファイルを使うにはDB_PATHを設定してください)。サーバーはCtrl-Cで停止します。データは再起動しても保持されます。
別のターミナルから、curlでWebサービスを呼び出せます(Windowsでは、PowerShellのcurlエイリアスを避けるため、常にcurl.exeを使ってください)。たとえば、口座#123を開き、50を入金し、10を出金して、残高を確認するには次のようにします。
curl -X POST http://127.0.0.1:8000/accounts/123/open
curl -X POST http://127.0.0.1:8000/accounts/123/deposit/50
curl -X POST http://127.0.0.1:8000/accounts/123/withdraw/10
curl http://127.0.0.1:8000/accounts/123/balance
残高は40です。口座IDと金額は符号なし整数でなければなりません。更新が成功するとOKを返し、失敗すると口座の実装からのエラーを返します。口座#123を閉じるには次のようにします。
curl -X POST http://127.0.0.1:8000/accounts/123/close
Linux/macOSで並行リクエストを試すには、新しい口座IDを使い、8つのクライアントから100件の入金を送ります。
curl -X POST http://127.0.0.1:8000/accounts/456/open
seq 100 | xargs -P 8 -I '{}' curl --fail --silent --show-error -X POST http://127.0.0.1:8000/accounts/456/deposit/1
curl http://127.0.0.1:8000/accounts/456/balance
Windowsでは、並列の例にPowerShell 7以降を使ってください。
curl.exe -X POST http://127.0.0.1:8000/accounts/456/open
1..100 | ForEach-Object -Parallel { curl.exe --fail --silent --show-error -X POST http://127.0.0.1:8000/accounts/456/deposit/1 } -ThrottleLimit 8
curl.exe http://127.0.0.1:8000/accounts/456/balance
入金はすべて成功し、残高は100になるはずです。
各リクエストは口座を読み取り、実装したRocの関数を呼び出し、その結果をSQLiteのImmediateトランザクション内で保存します。失敗した操作はロールバックされ、口座は変更されません。SQLiteは同時に1つの書き込みしか許さないため、並行するリクエストが互いの更新を上書きすることはありません(異なる口座間でも同様です)。
注意:この演習ではU64を使いますが、デモでは残高をSQLiteの整数として保存するため、I64の最大値(9223372036854775807)を超える更新は拒否されます。
独立した計算を並行して実行する例については、Parallel Letter Frequencyの演習を試してみてください。
matchを使ってClosedとOpen(balance)を区別します。操作は、渡された口座を書き換えるのではなく、更新後の口座を返します。更新が成功したときはOk(updated_account)、操作が拒否されたときはErr(...)を使います。