連打の夜
その魔物使いの工房は、街外れの石塀の奥にあった。
紹介してくれたのは、前職で世話になった同僚だ。彼の現場でも一度、術式(コード)が手に負えなくなったことがあって、そのとき呼んだのが「カイナ」という女だったらしい。「力でねじ伏せるんじゃない。術式の生態を、しばらく黙って見てから手を入れる。変わった魔物使いだよ」——半信半疑のまま、私はその名刺の住所を頼りに、夜の石畳を歩いてきた。
私は、街の転送陣の管理人をしている。前任者から引き継いだ古い術式だ。住民が荷を台に載せ、転送印に手をかざせば、荷は遠くの受け取り所へ飛ぶ。日々の暮らしを支える、ごくありふれた魔法だった。先月までは。
私は、この陣を、ささやかに誇っていた。朝いちばんに転送印の魔力を検め、台と受け取り所の符牒を合わせ、その日の荷の流れを整える。派手な術じゃない。だが、誰かの荷を、確かに、向こう側へ渡す。それだけのことを、何年も、抜かりなくやってきた。——その自負が、ある祭りの夜に、崩れた。
重い扉を押すと、低いランタンの灯りと、羊皮紙の匂いがした。壁には魔獣の輪郭を写した契約録がいくつも貼られ、卓には符牒と、細い鎖が幾本か。灯りの届かない壁の上のほうで、契約録の獣たちが、いくつもの目で、こちらを見下ろしているようだった。物音ひとつしない、しんとした奥行きだ。場違いなところへ来てしまった、と思った。奥から現れたカイナは、あいさつもそこそこに、私の手元の巻物(ソースコード)へ目をやった。
「祭りの夜だけ、荷が割れるんです」と私は切り出した。泣きつくつもりはなかった。手は打ってある。それを先に言いたかった。
すでに、鎖はかけてある
ことの起こりは、先々週の収穫祭だった。受け取り所から苦情が殺到した。同じ荷が二度届く。あるいは、片方がどこにも届かず、台帳にだけ二件の転送記録が残って、荷の実体は次元の狭間——どこへ消えたとも知れぬ虚空——へ落ちていた。住民が祭りの高揚で転送印を二度、三度と連打したのが引き金らしい、とそこまでは私にも分かった。
苦情の山の中に、忘れられない一通があった。嫁入りの祝いに贈るはずだった反物が、二重に送られ、片方が次元の狭間へ落ちた、という。受け取り所の台帳には、確かに二件の転送が記されている。なのに、届いた現物は、片方きり。私は、台帳の記録と、手元に残った控えを、何度も照らした。記録は、嘘をついていない。荷だけが、消えていた。記したものと、現にあるものが食い違う——その薄ら寒さを、私はその朝、初めて知った。住民の荷を、虚空へ落とした。管理人になって、初めてのことだった。その重さは、まだ、手のひらに残っている。
だから私は、術式に鎖を一本縫い込んだ。「転送中は、次を受け付けない」。実行中であることを示す札(フラグ)を立て、立っている間は弾く。理屈は通っているはずだった。
| |
「if (this.busy) return で弾いています」私は巻物の一行を指でなぞった。我ながら、不安そうな手つきだったと思う。「なのに、祭りの夜だけ、すり抜ける。平日は何ともない。連打が殺到する日だけ、二頭の荷が走るんです」
カイナは巻物を受け取り、最初から最後まで、ゆっくり目を走らせた。すぐには何も言わなかった。やがて、this.busy = true の行を指の背で軽く叩いて、低い声で言った。
「お前のかけた鎖は、確かに鎖だ。——だが、掛ける"刻"を見ていない」
刻、と私は繰り返した。意味が掴めなかった。札は立てている。弾いてもいる。何が遅いというのか。
「言葉で説明するより、見せたほうが早い」カイナは卓の鎖を一本、手に取った。「俺が、祭りの夜をこの檻の中で起こす」
模擬戦——祭りの夜を、檻の中で起こす
カイナのいう檻とは、模擬戦——つまりテストのことだった。暴走が牙をむくのは、検めの最中に二頭目が割り込む、その一瞬の隙だ。祭りの夜を待たずとも、その隙さえ狙えれば、檻の中で必ず再現できる。彼女は荷を送る術(send)と、荷を検める術(validate)を、外から差し替えられる偽物(モック)に置き換えた。こうすれば、術が「いつ終わるか」を、こちらの手で握れる。
私は、その手際を、横で見ていた。カイナは、私の術式を、一行も書き換えない。ただ、その周りに、検めと送りの偽物を据えていく。暴れる獣そのものには、まだ手を出さず、捕らえる罠のほうを、先に、静かに組み上げていく。同僚の言っていた「しばらく黙って見てから手を入れる」とは、こういうことかと、私は思った。
仕掛けの肝は、一頭目の検め(validate)を途中で凍らせることだった。検めが終わらないうちに、二頭目の転送印を撃つ。そして、荷を送る術が何度走ったかを数える。
| |
カイナが模擬戦を走らせると、結果は無慈悲だった。send——荷を送る術が、二回呼ばれていた。「祭りの夜だけ」「たまに」だった暴走が、檻の中では、狙えば必ず起きる。数えているのは送信の回数だけだ。どちらの首が先に送ったかには依らない——二度走った、その事実だけで競合は確定する。
私は、自分のかけた鎖の前で、立ち尽くした。縫い込んだはずの鎖が、目の前で、こうもあっさり破られる。祭りの朝に覚えた、あの薄ら寒さが、明るい工房の中に、もう一度、立ち上がってきた。
「待ってください。札は縫ってあるんですよ。if (this.busy) return を、ちゃんと最初に置いてある。なのに、なぜ二頭目はすり抜けるんですか」
なぜ、二頭目はすり抜ける
カイナは答える前に、壁の契約録の一枚を外して卓に広げた。二つの首を持つ、黒い猟犬の絵だった。
「これはガルム。双頭の猟犬だ。右の首と左の首が、別々の獲物を追って同時に駆け出すと、首が絡んで、両方が同じ獲物に殺到する」彼女は絵の、右の首を指した。「お前の転送も同じだ。一度目の詠唱が右の首。連打で割り込む二度目が、左の首だ」
そして、私の術式の一行を指でたどった。
「お前は、荷を検め終えてから『転送中』の札を立てた。だが二頭目は、一頭目が検めている最中に入ってくる。その刻、札はまだ立っていない。だから二頭目も『今は空いている』と読む」
——右の首に目隠しをするのが、左の首がもう走り出した後だった。私は絵の二つの首を見て、ようやく、自分の鎖の形が見えた気がした。
カイナは少し間を置いて、こんどは術の理屈そのものを、低く、淡々と続けた。聞き取れた範囲で書き留めておく。
「この術式(JavaScript/TypeScript)を回すテイマーは、一人だ。同時に二箇所は見られない。処理を順に巡回していく——これをイベントループ(テイマーの巡回路)という。一人で回るから、if (this.busy) return; this.busy = true; のように await を挟まずに続く区間は、途中で巡回が他へ逸れない。割り込まれない。この同期の区間は、原子的に通り抜ける」
私は眉を寄せた。原子的、という言葉を頭の中で転がす。割り込まれない、ひとかたまり。
「だが」とカイナは私の札の位置を指した。「await this.validate(cargo) で、巡回は一度、他へ回る。await は、術の手を一旦止めて『巡回路を譲る』合図だ。ここが中断点(サスペンションポイント)——関数の実行が一旦止まり、巡回路が他のコードを動かせるようになる地点だ。その隙に、二頭目の dispatch が入ってくる。そして、まだ立っていない busy を見て、同じ false を読む。——勘違いするな。一人で回るから安全なんじゃない。お前の壊れた術式も、同じ一人で回っている。安全と暴走を分けるのは、確認と札立ての“間”に、この中断点が挟まるかどうか。それだけだ」
結果が、複数の処理の「どちらが先か」というタイミング次第で変わってしまう。これがRace Condition(歩調の乱れ)——処理の結果が、複数の操作の実行順序やタイミングに依存してしまう状態だ、とカイナは言った。
「魔物使いの世界じゃ、歩調の乱れた魔獣ほど厄介なものはない。同じ命令を、同じ刻に、二頭が別々に実行する」
彼女は卓に、刻の流れを線で描いてみせた。後で図に起こしたのが、これだ。

図の上から下へ、刻が流れている。二頭が並んで busy? → false を読んでいる二行。私はそこを指でたどって、声を落として言い直した。
「……つまり、悪いのは札そのものじゃない。札を掛ける位置か」
カイナは、わずかに頷いたように見えた。
「型(コンパイル時の検め)は、この競合からはお前を守れない。型は『ありえない状態の同居』なら弾ける。だが『二頭が同じ刻に入ってくる』という実行順序の競合は、型には見えない。時間の競合は、実行時の設計——契約で締め出すしかない」
型では守れない領域がある。これを聞いたとき、私は初めて、自分の鎖がなぜ夜だけ破られたのかを、自分の言葉で説明できる気がした。
契約——札を、入口で立てる
カイナの手当ては、拍子抜けするほど静かだった。鎖を増やすでも、術を組み直すでもない。彼女は札を立てる一行——this.busy = true——を、検める前へ、つまり入口へ移しただけだった。
| |
差分は、たった一つ。this.busy = true の位置が、await this.validate() の後から前へ動いた。それだけだ。検めて、送って、札を下ろす——術のすることは何も変わっていない。
私は、その一行を、しばらく見つめた。あれほど受け取り所を騒がせ、誰かの祝いを虚空へ落とした暴走の根が、たった一行の、置き場所だった。鎖の数でも、術の出来でもない。札を、いつ立てるか——その一点に、すべてが懸かっていた。あまりに小さなその差に、私は、すぐには言葉が出なかった。
「if (this.busy) return; this.busy = true; の間には、await がない」カイナはその二行を指でくくった。「だから、ここは割り込まれない原子的な区間だ。一頭目がこの入口を通り抜けた瞬間に、札は立つ。最初の await より前に立つ。二頭目がやってきたときには、もう札が立っている。だから、検めにすらたどり着けずに弾かれる」
処理中の再呼び出しを、入口の早期 return で締め出す。この仕掛けを再入ガードという、とカイナは言い添えた。鍵をかけるのは、走り出す前。それが契約の肝だった。
カイナは炭を取り、卓の羊皮紙に、新しく整えられた刻の絵を描き加えた。

今度の絵では、二頭目は門の入口で静かに引き返していた。一頭目が検めに差し掛かるよりも早く、札が立っているからだ。
「これなら……」私は絵の線を指でなぞった。「二頭目が busy? を見るときには、すでに true になっている。だから、検めに入る前に弾き返せるんですね」
カイナは私の手元を見つめたまま、静かに頷いた。
「一度目の詠唱が await の中断点で巡回路を譲る前に、すでに門は閉ざされている。二度目の詠唱が滑り込む隙間は、もうない」
私は try の範囲が、send だけから validate まで広がっているのに気づいた。
「try が、検めも巻き込んでますね。これも直したんですか」
「直したんじゃない。札を入口で立てたら、こうなるだけだ」カイナは finally の行を指した。「札を立てた以上、何があっても下ろさねばならん。下ろし忘れれば、二度と誰も通れなくなる。だから、札を立てた入口から、下ろす finally までが、ひとつながりの臨界区間になる。検めはその中に入った。位置を動かした、その帰結だ。新しい鎖を足したわけじゃない」
直したのは一箇所。札の位置だけ。それ以外は何も足していない——彼女はそこを、二度、念を押した。
もう一度、檻の中で
カイナは、さっきと寸分違わぬ模擬戦を、今度は手懐けた術式へ仕掛けた。一頭目を検めで凍らせ、二頭目を撃つ。
| |
今度は、send は一回。validate も一回。二頭目は検めに届く前に弾かれ、何も実行せず、undefined を返して静かに引き返した。
二頭目が、何もせず、静かに引き返す。その一部始終を、私は息を詰めて見ていた。さっきは二度走った送りが、同じ連打で、今度は一度きり。札の位置を、入口へ動かしただけで。檻の中の小さな出来事だったが、私には、あの祭りの夜が、やり直されたように見えた。
戻り値が Promise<Receipt | undefined> になっているのは、その正直さの表れだ、とカイナは言った。「弾かれた呼び出しは、荷を送らない。送らないなら、受け取り証(Receipt)は無い。だから undefined が返る。『実行されないことがある』という事実が、型に書いてある」——もっとも、呼び出し側で『弾かれた』と『送った』を確実に見分けたいなら、undefined ではなく判別できる戻り値にする手もある。今宵の転送陣に、そこまでは要らなかった。
私は、もう一つ気になっていたことを訊いた。札を下ろし忘れたら——finally がなかったら、どうなるのか。検めの途中で何かが失敗したら。
カイナは、検めをわざと一度だけ失敗させる模擬戦を足した。
| |
検めが失敗しても、finally が札を確実に下ろす。だから次の転送はちゃんと通る。「臨界区間が入口から始まっている——try が検めを含んでいるからこそ、検めの失敗でも札は下りる」とカイナ。もし try が send だけを包んでいて、札を立てた後の検めが try の外で失敗していたら、札は立ったまま、二度と誰も通れなくなっていた。位置を動かしたことが、ここでも効いていた。
この鎖が縛れないもの
「これで——もう二度と、荷は割れないと、言い切れるんですか」私は最後に、それを確かめたかった。
カイナは即答しなかった。鎖をひとつ、卓に置いて、こう言った。
「この鎖が締め出すのは、『次の二度目』だけだ。まだ起きていない連打を、起こさせない。それは確かにやった」彼女は指を一本ずつ立てた。
「だが、すでに走り出した一頭は、この鎖では止まらない。一頭目が荷を送っている最中に『やっぱりやめろ』と言っても、この鎖にその力はない。走っている処理そのものを断ち切りたいなら、それは符牒を断つ別の獣——AbortController(進行中の処理を中断する取り決め)の領分だ」
「古い結果を捨てて、最新だけを採る、というのも別の獣だ。検索の先読みのように、何度も投げて『最後の答えだけ要る』なら、古い詠唱の結果を破棄する手がある。だが、荷を送る術のように副作用がある処理——一度送れば取り消せない処理——に、それは効かない。だから今宵は、この再入の鎖を選んだ」
「そして」カイナは扉の方へ目をやった。「扉を閉じて、開け直して、もう一度転送印を押す住民——画面を再読み込みした者や、別の入口(別タブ)から来た者は、この工房の鎖では縛れない。この鎖は、一つの陣の中でしか効かない。そこまで縛りたいなら、転送先の砦——受け取り側で『同じ依頼は一度しか処理しない』と保証する仕組み(サーバ側の冪等キー)が要る。冪等とは、何度実行しても結果が変わらない性質のことだ。それは、また別の話になる」
縛れるもの。縛れないもの。その線が引かれて、私はようやく、自分が今夜手に入れたものの大きさを、正しく測れた気がした。万能の鎖ではない。だが、祭りの夜の連打は、もう荷を割らない。
試運転
カイナは、Before と After の両方の模擬戦を、まとめて走らせた。暴走する転送陣には「二度走る」ことを、手懐けた転送陣には「一度きり」を、それぞれ記録として残してある。
| |
試運転、合格。暴走の再現も、その封印も、これからはこの模擬戦が見張り続ける。次の祭りの前夜に誰かが術式へ手を入れても、二度目の詠唱が滑り込めば、檻の中で必ず捕まる。
ふと、あの反物のことが、頭をよぎった。次元の狭間へ落ちた、嫁入りの祝い。あれは、もう、戻らない。それは、私が背負っていくものだ。だが——次の祭りの夜、同じ悲しみは、もう生まれない。失くしたものは戻らずとも、これから先、誰の荷も、虚空へは落とさせない。私に残された償いは、その一点だけだった。
私が札の下りる所作を確かめて、ようやく肩の力が抜けたのが、自分でも分かった。カイナは羊皮紙に、今宵の契約を書きつけていた。
「今日縛ったのは、一頭だ」彼女は顔を上げずに言った。「お前の陣には、まだ別の気配が眠っている。走り出した一頭を止める鎖。古い詠唱を捨てる鎖。——いずれ、それぞれに名前を付けることになる」
「……次に何かが暴れたら、また来ます」私がそう返すと、カイナは答えず、ランタンの芯を少し下げた。工房の影が、契約録の猟犬の絵を、静かに覆った。
📜 カイナの魔獣契約録(Tamer’s Registry)
- 魔獣名(クラス/パターン名): ガルム(双頭の猟犬)/ Race Condition・二重送信(再入ガードで馴致)
- 危険度(難易度/バグの影響度): ★★★★☆(平時は無害。連打・高負荷の刻にだけ牙をむき、荷=データの整合を割る)
- 主な生態(アンチパターンの特徴):
- 実行中フラグ(
busy)を、最初のawaitの後に立てている - 一頭目が
await validateの中断点で巡回路を譲った隙に、二頭目が同じbusy = falseを読み、両者がsendを実行する=二重送信 - 平日(連打のない日)は中断点に誰も滑り込まないため再現せず、原因の特定が遅れる
- 実行中フラグ(
- 契約のポイント(設計の要点):
- 札(
busy = true)をif (busy) returnの直後=最初のawaitより前へ移す。この同期区間は割り込まれず原子的に通るため、二頭目は必ず立った札を見て弾かれる - 札を立てた入口から
finallyまでが臨界区間。tryが検めも含むので、検めの失敗でも札は確実に下りる(送還) - 変えたのは札の位置 1点のみ。検め→送る→下ろすのロジックは Before と同一
- 札(
- 契約外事項(保証しないこと):
- すでに走り出した処理は止められない(中断は
AbortControllerの領分) - 古い結果の上書き(last-write-wins)はしない。副作用のある送信には不適
- リロード・別タブ・再接続からの再送は縛れない(受け取り側のサーバ冪等キーが受け持つ領分)
- すでに走り出した処理は止められない(中断は
- 現在のステータス: 🟢 一頭テイム成立(再入を封印)/残る二頭は別の獣として後日
