還らぬ客
客が一人、還らなかった。湯に浸かったまま、上がってもいない。なのに、洗い場にも、番台にも、どこにもいない。——ただ、消えた。
俺は、夜通し開く湯屋「宵霧の湯」のあるじだ。炉に棲む火霊と、地の底から引いた水脈。その二つの術で、大きな湯船と洗い場を、夜っぴて温め続ける。湯気と石と、客の笑い声で満ちた、下町の一軒だ。客の背中を流すのが、俺の性分でね。訪ねてきた客は、一人残らず、いい湯加減で、気持ちよく帰す。それだけが、俺の自慢だった。
夜が明ければ、いつものように仕舞う。火を落とし、湯を抜き、戸を閉める。長年、抜かりなくやってきた、その手順を「仕舞いの術」と呼んでいる。難しいことは、何もない。火と水に「もう休め」と命じる、それだけの術だ。
なのに、近頃、客が消える。
きっかけは、街の夜盗騒ぎだった。物騒だってんで、見回りが夜じゅう巡って、怪しいと見れば早鐘を打つ。鐘が鳴ったら、どの店も、すぐ戸締まりしろ——そういうお達しだ。鐘が鳴れば、俺も慌てて仕舞いの術を唱える。火を落とし、湯を抜く。急いで、閉じる。
その、急いで閉じた夜に限って——湯に浸かったままの客が、一人、還らなくなる。
「あの魔物使いは、終い方を識る者だと聞いた」。客の一人が、いつか番台で言っていた。手がけたものを、何一つ虚無に落とさない。終わったものを、ちゃんと還す人だと。獣を、力でなく契約で鎮める、変わった人がいる、と。——終いでしくじって、客を失くした俺が、頼るあてなんざ、その人しか思いつかなかった。
カイナ、と名乗ったその人は、夜明け前の暖簾を、音もなくくぐってきた。俺が湯気の向こうから事情を話そうとすると、それを、軽く手で止めた。だが、俺の話を聞こうともしない代わりに、こう言った。
「先に、いつもの仕舞いを、見せてくれ。客のいない、今の湯屋で」
俺は、面食らった。話を聞かずに、まず実演しろ、と言う。急ぐふうは、まるでなかった。見る側に、徹するつもりらしい。回りくどいな、と思いながら、俺は、言われた通りにした。
空の湯屋では、何も起きない
俺は、客の引けた湯屋で、仕舞いの術を唱えてみせた。火霊に「休め」と命じ、炉の火を落とす。水脈の栓を開けて、湯を抜く。湯が、排水口へ吸い込まれて、消えていく。戸を閉める。——何も、起きない。
「ほら、この通りだ」と俺は言った。つい、自負が滲んだ。「滞りない。俺の仕舞いに、抜かりはねえんだ」
カイナは、頷かなかった。否定も、しなかった。「手順そのものは、間違っていない」。そう言ったきり、排水口へ消えていく湯を、見たまま、動かない。
俺は、本当の悩みを、ぶちまけた。
「分からねえんだ。近頃、街に夜盗が出る。見回りが早鐘を鳴らして、急な戸締まりを命じてくる。鐘が鳴りゃ、俺は急いで仕舞いの術を唱える。——すると、たまに、湯に浸かったままの客が、一人、還らねえ。出てもいねえ、中にもいねえ。火霊は燻ったまま、水脈の栓は開いたまま。術を、何度検めても、欠陥は一つもねえ。なのに、空で試すと、今みたいに、何ともねえ」
声が、掠れた。
「同じ術だ。同じ手順だ。なのに、客がいる夜の早鐘の時だけ、客が……消える。俺は、何を、し損なってる」
直し方を訊いたんじゃない。なぜ、を、差し出していた。俺には、それが分からなかった。
客のいる湯屋は、空とは違う
カイナが、初めて口を開いた。俺の実演を、否定しなかった。
「お前の手順は、壊れていない。空の湯屋なら、お前の言う通り、何の滞りもなく閉じる」
だが——と、声が続いた。
「空の湯屋を閉じるのと、客のいる湯屋を閉じるのは、同じじゃない。お前が今、見せたのは、空のほうだ。獣は、空では、出ない」
「獣……?」。俺は、訊き返した。「客がいると、何が変わるってんだ」
カイナは、湯船を指した。
「客が、湯に浸かっている。まだ、上がっていない。その最中に、早鐘が鳴る。お前は、急いで仕舞う。——その時、何が起きるかを、ここで起こす。客のいない実演では、永遠に掴めない」
俺は、戸惑った。「客を……模擬の湯に、入れるってのか」。半信半疑だった。だが、従うしかなかった。客が消える理由が、空の湯屋で何度実演しても掴めないことだけは、俺自身、嫌というほど分かっていたからだ。
仕舞いの術が、獣になる
カイナは、この暴走の正体を、一頭の獣に見立てて語った。
「お前の『仕舞いの術』。あれそのものが、獣だ」と言う。「普段——客のいない夜明け——は、おとなしい。命じられた通り、静かに火を落とし、湯を抜く。だが、客を抱えたまま発動すると、性質が変わる。客も、火も、水も、掴んだまま、一息に崩れ落ちる。崩れ際に、掴んでいたものを、還る先のない場所へ、道連れにする」
俺の、仕舞いの術が。客を、道連れに。背筋というより、湯に浸けた手のひらが、ふいに冷えるような心地がした。
「俺の術が……客を、消してたのか。俺が、自分の手で?」
「術が悪いんじゃない」とカイナは言った。声は、静かだった。「『速く閉じよ』と命じられた通りに、閉じているだけだ。咎があるとすれば——『客がまだ居る』のと、『もう閉じる』を、同じ一息にやらせたことだ。獣は、その一息の中で、暴れる」
そこから先を、カイナは、まだ言わなかった。客が倒れるのを、俺の目の前で、まず見せるつもりらしかった。
客がいるのに、幕が下りる
俺は、まず湯屋の造りを、板に写して見せた。難しいことは、何もしていない。
その土台に、下ごしらえの道具が、二つある。一つは、約束(Promise)の解決を、外から握る deferred。resolve を呼ぶまで、約束は宙に浮いたままだ——客が「いつ上がるか」を、こちらの手で決めるための握りになる。もう一つは、還した順を刻む通し番号の Seq だ。
| |
この二つを土台に、湯屋の造りは、こうだ。
| |
「水脈と火霊は、客のためにある」と俺は言った。「Conduit ってのが、それだ。release——還す——と、栓を閉じ、火を落とす。湯屋——Bathhouse——は、客を迎え入れて(admit)、戸を立てる(stopAdmitting)。今、湯に浸かってる客の数が、bathersInside だ」
そして、客一人の入浴を写した。
| |
「客は、湯に浸かってる間は、ただ浸かってる。bath.promise ってやつが、その『浸かってる時間』だ。やがて、上がる。上がり際に、最後の一杯——上がり湯——を、水脈から汲む。それで、さっぱりして帰る。それが、良い湯でした だ」
カイナは、その serveGuest の中の、一行を指した。await bath.promise の、次の行。客が上がってから、水脈に触れる、その一行を。
「ここだ。客は、湯に浸かっている間は、水脈に触れない。触れるのは、上がる、その瞬間だ。——もし、その瞬間に、水脈がもう還されていたら?」
俺は、まだ、ぴんと来なかった。
上がり湯のない湯船で、客が立ち往生する
「鐘は、俺が鳴らす」とカイナは言った。「客も、俺が湯に入れる。肝は、その客が、いつ上がるかを、こちらの手で握ることだ。獣が露れるのは、客が湯にいる、その最中の早鐘だけ。なら、夜盗の鐘が鳴る夜を、いつまでも張り込む要はない。その巡り合わせのほうを、こちらで組んで、狙って起こせばいい」
握りの道具は、さっき見せた deferred だ。約束を外から保留しておき、好きな刻に「客が上がった」と決める。壁の時計も、夜回りの鐘も、現のものは使わない。客の上がりも、鐘の合図も、ぜんぶ手で起こすから、何度走らせても、一度として違わず同じことが起きる。試すたびに結果がぶれる、ということがない。
カイナは、まず、客のいない湯屋で、鐘を鳴らしてみせた。仕舞いの術——shutdownNow——が走り、火と水が、すんなり還る。何も起きない。
「空では、獣は出ない。だから、お前は今まで、気づけなかった」とカイナは言った。「獣がいないんじゃない。呼び出す客が、いなかっただけだ」
そして、湯に、客を一人、入れた。
| |
その早鐘——shutdownNow——の中身は、こうだった。
| |
戸を立てて、すぐ、火と水を還す。客が、まだ湯にいようとお構いなしだ。檻の中で、それが、はっきり目に見えた。
| |
客は、まだ湯にいる。bathersInside は、一。なのに、水脈は、もう還された。water.released が、真になっている。そこで、客が上がろうとした。上がり湯を、汲もうとした。——だが、栓は、もう閉じられている。湯は、一滴も出ない。客は、良い湯でした と言えないまま、倒れた。
俺は、これを「まだ間に合うかも」と、半分、思っていた。客が、もう少し早く上がっていれば、と。だが、カイナは、それを潰した。
「これは『まだ上がり湯が出ていないだけ』じゃない。水脈は、もう還された。還した栓からは、二度と、湯は出ない。客がいつ上がろうと、結果は同じだ。早かろうと、遅かろうと、出ないものは、出ない。——だから、この客は『まだ送れていない』んじゃない。『もう、決して送れない』んだ」
その「決して」が、腹に、ずしりと来た。
急いで仕舞おうとする俺の焦りが、客の逃げ道を塞ぐ。その残酷なすれ違いが、カイナの指し示す盤面に、冷酷な流れとして浮かび上がっていた。

俺は、ようやく、自分の言葉で、像を結んだ。
「客が、まだ湯にいるのに……俺は、水を抜いて、火を落としてた。客が上がる前に、客が要るものを、先に片付けてたんだ。だから客は——上がり湯のない湯船で、立ち往生した」
「これが、獣の正体だ」とカイナは言った。「獣は、お前の術の『中』にはいない。『客がまだ居る』のに『もう還す』——その、順番の中にいる」
触らず抜けても、客は宙に残る
俺は、つい、手を出した。素人なりの、思いつきだ。
「なら——客がいる時は、何も片付けずに、そのまま放って出りゃいい。水も火も、触らなけりゃ、客は、湯ん中で倒れたりしねえ。違うか」
カイナは、頷かなかった。代わりに、俺の言った通りの仕舞いを、もう一つ、檻に流して見せた。何も還さず、ただ、その場を抜ける——justExit という、使い捨ての反証だ。
| |
| |
確かに、客は、倒れなかった。水脈は還されていないから、上がり湯は、出る。良い湯でした も、ちゃんと返る。
「だが、これは『無事』とは違う」とカイナは言った。「誰も上がりを待たず、誰も送り出さないまま、湯屋ごと、その場を捨てる。客は、湯に浸かったまま——bathersInside は、一のままだ——上がる合図も、見送りもないまま、宙吊りになる。果たされも、捨てられもしない。火霊は燻ったまま、fire.released は偽。水脈の栓は開いたまま、water.released も偽。火は燃え続け、水は流れ続け、誰一人、その客を迎えに来ない」
カイナは、続けた。
「倒れないことを、無事と呼ぶな。客は、倒れぬ代わりに、どこにも還れず、湯の中に取り残される。『倒さなかった』だけで、『送り出した』わけじゃない。……お前が一番恐れていた、『還ってこない』が、これだ。早く片付ければ客が倒れ、片付けずに抜ければ客が宙吊る。どちらも、客を送り切る前に、幕を下ろしている。——それが、同じ一つの病だ」
俺は、足元が、ぐらりとした。
「早く片付けても、駄目。片付けずに抜けても、駄目。客がいる、その間は——どうすりゃ、いいんだ」
「待つんだ」とカイナは言った。「客が、自分の足で上がるのを。それから、片付ける。順が、要る。それだけだ」
待つ。それだけは、掴めた。だが、どう待つのか、二度鐘が鳴ったらどうするのか、いつまで待つのか——そこまでは、まだ、何も見えていなかった。
客を送り切る前に、幕を下ろした
俺は、ここまでを、頭の中で、ひとまとめにした。
害は、仕舞いの術そのものじゃない。「客がまだ居る」と「もう閉じる」を、同じ一息にやらせたこと。順序と、待ちの、欠落だ。早く還しても客が倒れ、還さず抜ければ客が宙に残る。どっちも、客を送り切る前に、幕を下ろしたから起きる。
カイナが、その仕舞い方に、名を与えた。「客が皆、自分の足で上がるのを待ってから、火と水を還して閉じる。この行儀のいい幕引きには、ちゃんとした名前がある」と言う。
その作法を——Graceful Shutdown、美しき幕引き——という。新規の受付を止め、処理中の仕事の完了を待ち、資源を正しい順序で還してから終わる、その一連の手順のことだ、とカイナは言った。対して、客を抱えたまま一息に崩れ、進行中の仕事も握った資源も顧みず、できる限り速く閉じてしまうのが、行儀の悪い終端——あの、強制終了だ。
俺は、それを、湯屋の言葉に、言い直した。
「……客を、送り切ってから、戸を閉める。それだけのことが、名前を、持ってるのか」
「ああ」とカイナは言った。「名前を持つほど、皆がし損なう、ということでもある」
皆を還してから、己を閉じる
止めてから、待つ。待ってから、還す
カイナが、契約を施した。仕舞いの式には、四つの作法がある、と言う。素のままの言葉で、一つずつ。
「一つ、二度目の早鐘でも、仕舞いを始め直さない。二つ、新規の客を断つ——戸を立てる。三つ、中の客が、皆上がるのを待つ。四つ、それから、順に、水脈と火を還す。——肝は、三つ目だ。客が皆、上がってから、初めて、鍵を返す」
俺は、引っかかった。
「止めてから、待つ。待ってから、還す。……なんで、その順でなきゃ、駄目なんだ。戸を立てた時点で、もう新しい客は来ねえ。なら、待たずに、さっさと還したって、同じじゃねえのか」
「違う」とカイナは言った。「戸を立てるのは、『これから来る客』を断つだけだ。新規の受け入れを、止めること。だが、『もう中にいる客』は、それで上がるわけじゃない。皆の上がりを待つ、は、いま手の中にある仕事が終わるのを、見届けること。この二つを、混ぜるな。止めてから、待つ。待ってから、還す」
俺は、湯屋の言葉で、受けた。
「……暖簾を下ろすのと、中の客が上がるのは、別の話か。暖簾を下ろしても、中の客は、まだ湯にいる」
「そこだ」とカイナの声が、低くなった。「客が今、湯に浸かっているかどうかは、その時々の巡りだ。術の形では、決められん。形をいくら正しく書いても、『客がいる最中の早鐘』は、いつか巡ってくる。守るのは、形じゃない。『還す前に、上がりを待つ』——その一手で、客の上がりと、鍵の還しが、時の上で、交わらなくなる。交わらなければ、倒れようがない」
形では決められない。だから、形の外で——待つ、その一手で守る。俺には、まだ半分しか掴めなかったが、その半分は、確かに、腑に落ちた。
同じ早鐘で、客が「良い湯でした」と帰る
カイナが刻んだ、仕舞いの式は、これだった。
| |
「挟んだのは、一手だけだ」とカイナは言った。「前の早鐘——shutdownNow——は、戸を立てて、すぐ火と水を還した。house.stopAdmitting() の、すぐ次が、release のひと回しだった。今度の式は、その二つの間に、drain()——皆の上がりを待つ——を、一つ、挟んだ。客の入浴も、上がり湯も、湯屋も、何一つ、変えていない。挟んだのは、待ち、一つきりだ」
カイナは、Before と寸分同じ「客が湯中+早鐘」で、この式を流した。
| |
ここで、式は、止まっていた。run() は走り出している。戸も、立てた。だが、火も水も、まだ還っていない。water.released も fire.released も、偽のまま。客は、まだ中にいる。
途中で Promise.resolve() を三度まわしているのは、宙に浮いた仕事を、念のため掃いておくためだ。だが——三度だろうと、十度だろうと、結果は変わらない。客が上がらない限り、還しへ進む道は、一寸も開かないからだ。だからこの「未還」は、掃き足りないだけの途中経過じゃない。客が湯にいる間は、決して還されない、という確定した像だ。
俺は、その「止まり方」を、目で追った。
「水脈は、まだ開いてる。火も、まだ燻ってねえ。仕舞いが……止まってる。客が、上がるのを、待ってるのか。客が、まだ何もしてねえのに」
「drain() の、await のところで、止まっている」とカイナは言った。「中の客が皆、上がるまで、その先——還しのひと回し——へは、一歩も進めない。進む道が、塞がれている。だから『まだ還していない』んじゃない。『客が上がるまで、還す処理へ進む道が、どこにもない』んだ」
そして、客を、上がらせた。
| |
客が、上がる。bath が、解ける。その瞬間に、何が、どの順で動くのか——カイナは、そのひと続きを、ほどいて見せた。
「客が上がると、まず、湯に浸かっていた serveGuest が、続きを動かす。上がり湯を、汲もうとする。このとき、水脈は、まだ還されていない——released は、偽だ。だから、上がり湯は、出る。客は『良い湯でした』と言って、帰る。送り切れた。それを見届けてから、初めて、drain() の待ちが、解ける。Promise.race が、決着する。そこでようやく、還しのひと回しが、動き出す。水脈、火——順に、還る。客の上がりが、先。鍵の還しが、後。この順は、もう、決して入れ替わらない」
カイナが示した盤面には、今度は美しく噛み合った歯車のような、整然とした順序が描かれていた。早鐘が鳴っても、仕舞いの式は慌てず、客を見送るための時を刻む。

俺は、安堵で、息が漏れた。
「同じ早鐘だ。同じ、湯に浸かった客だ。なのに——客は、ちゃんと『良い湯でした』と言って、帰っていく。火も、水も、客が出てから、還る。誰も、置き去りにならねえ」
だが、俺は、もう一つ、引っかかった。客が先に上がって、それを追って還した——ただ順に並んだ、それだけのことなんじゃないか、と。——カイナは、首を振った。
「並んだんじゃない。並ばせたんだ」とカイナは言った。「『待つ一手』が、客の上がりより先には、鍵を一本も還させない。だから、客の上がり湯が、間に合う。さっき、見たろう。客が、まだ何もしていないのに、還しは、drain の await で止まって、客の上がりを待っていた」
「だが——」と俺は食い下がった。「客が上がった、その一瞬に、客の上がり湯と、あんたの『還し』。どっちが先に動くか、なんて、たまたま、ってことは、ねえのか」
カイナは、コードの一点を、指でなぞった。「それも、運じゃない。順は、結んだ順で決まる。客の serveGuest は、湯に入った時に、もう同じ bath.promise へ『上がったら、続きをやる』と段取りを結んでいる。drain が同じ約束へ『皆、上がったか』と尋ねるのは、その後だ。同じ約束が解けた一瞬、先に結ばれたほう——客の上がり湯——が、先に走る。だから released がまだ偽のうちに、客は通り抜ける。早い者勝ちじゃない。結んだ順だ。だから、何度やっても、客が、先だ」
俺は、半分だけ、頷いた。残りの半分は、後で何度もこのコードを読み返して、ようやく腹に落ちるんだろう。だが、肝は、分かった。待ち中は、まだ還っていない。そして、上がりが、還しより先。その二つは、運じゃない。仕掛けの中に、初めから、織り込まれている——それを、俺は、両方、目で見た。
「獣は、消えていない」とカイナは続けた。「獣は、『客がまだ居る最中の早鐘』——その巡りそのものだ。消せはしない。だが、待つ一手で、その一息を、二息に割った。『客がまだ居る』が終わってから、『もう閉じる』が始まる。一息に重ならなければ、獣は、暴れようがない」
ここで、俺は、ふと、訝った。Before との差は、待ち一つ、と言うが——目で見るかぎり、After の式には、Before になかったものが、いくつか足されている。「二度目を聞き流す」やら、「刻限」やら。一手、と言い切って、いいのか。
カイナは、それを、見透かしたように言った。
「お前が今、訝ったろう。『一手じゃない、いくつも足してる』と。——いい目だ。だが、見てみろ。今のこの模擬戦は、客は一人、早鐘は一度きり、その客はやがて上がる。この道では、『二度目を聞き流す』札は、一度も、出番がない。鐘が、二度鳴っていないからだ。『刻限』も、出番がない。客が、ちゃんと上がるからだ。——この二つは、今の道では、何もしていない。倒れを送り切りへ反転させたのは、ただ一つ、戸を立てた後に drain が在るか、無いか。それだけだ。残りの二つは、別の夜にだけ要る、別の備えだ。二度目の鐘が効く様子も、上がらぬ客の刻限も、後で、別の檻で確かめる」
足したものが、この道では何もしていない。だから、差は、待ち一つ。——その理屈は、確かに、通っていた。
待つにも、刻限が要る
俺には、どうしても、引っかかることがあった。職業柄だ。
「だが——客が、いつまでも上がらなかったら? のぼせて、動けなくなった客がいたら? 朝まで、待つのか。夜が、明けちまう」
「待つにも、刻限が要る」とカイナは言った。「上がらぬ客は、穏やかに促して、退いてもらう。力ずくで、湯から引きずり出すんじゃない。『もう仕舞いだ』と、声をかける。——なぜ、際限なく待てないか。お前の湯屋には、お前より強い時計があるからだ」
「俺より、強い時計?」
「夜明けだ」とカイナは言った。「見回りは、夜が明ければ、戸を壊してでも、閉める。その夜明けより前に、お前は、自分の手で、仕舞いを終えねばならん。だから、待つ刻限は、夜明けより、短く切る。外の強制より、内の刻限を、先に効かせる」
刻限の式は、こうだった。客が上がらなくても、刻限が勝てば、式は、無理にでも仕舞いを終える。
| |
drain() は、永遠に終わらない。客が、上がらないからだ。だが、Promise.race で、刻限のほうが、先に決着した。式は、"forced"——促しての仕舞い——を返した。プロセスは、ハングしない。そして、forced でも、水脈と火は、ちゃんと還る。上がらぬ客を促してでも、鍵だけは、闇に残さない。
なぜ、刻限で打ち切っても、必ず鍵を還す段に辿り着けるのか。俺は、式の中の、release の置き場所を、目で追った。火と水を還すひと回しは、try の囲いの、外にある。皆を送り切った時(clean)も、刻限で打ち切った時(forced)も、必ず通る場所だ。それに——客を待つ drain は、中で個々の客の約束を握り潰している(b.promise.catch(() => undefined))から、それ自身は、しくじって投げ出すことがない。だから、囲いが拾うしくじりは、刻限のひとつきり。仕舞いが、途中で投げ出される道は、塞いである。
ただ、カイナは、forced の代償を、隠さなかった。「刻限で打ち切ったとき、まだ上がらなかった客は、鍵こそ巻き添えにせずに済む。だが、その客自身は、送り切れていない。湯に浸かったまま、約束を果たせず、残る。clean は、皆を送り切った証だ。forced は——鍵は守ったが、最後の一人は、まだ宙にいる、という証だ。刻限は、万能の救いじゃない。間に合わなかった時の、最後の落とし所だ」
その刻限の砂時計に、カイナは、もう一つ、細工をしていた。
| |
「その砂時計が、最後の一人になってまで、湯屋を起こし続けてはいけない」とカイナは言った。「客が皆、上がってしまえば、砂時計は、用済みだ。なのに砂が落ちきるまで、皆を待たせては、本末転倒だろう。だから——待つ者がいなくなれば、砂時計のほうが、先に黙る。unref と呼ぶ。タイマー以外に、することが何もなくなれば、そのタイマーは、もう、終わりを妨げない」
待つ者がなくなれば、砂時計が、先に黙る。湯屋の終いを、急かしも、引き止めもしない。——うまくできている、と俺は思った。
そういえば、と思い当たった。さっきの檻に注いだ刻限は、この本番の砂時計の、模擬戦版だったのだ。清浄系の檻には、永遠に落ちない砂時計(neverDeadline——drain が必ず勝つ)。刻限切れの檻には、即座に落ちる砂時計(instantDeadline——決定的に刻限切れを起こす)。ここでも時の流れは手の内にある。刻限すら手で起こすから、走らせるたび、結末は同じ一点に着く。
| |
そして、最後の作法——二度目の早鐘だ。
「鐘が、二度鳴ったら?」と俺は訊いた。「早鐘は、続けて鳴ることがある。慌てて、二度、仕舞うのか」
「いや」とカイナは言った。「一度始めた仕舞いを、二度目の鐘で、始め直してはいけない。さもないと、まだ湯にいる客を、二度数えたり、もう還した火を、もう一度還そうとして、事故になる。仕舞いは、一度きり。二度目の合図は、ただ、聞き流す」
| |
二度目の run() は、started が真だから、すぐに引き返す。仕舞いを、始め直さない。だから、水脈も火も、還そうとされたのは、一度きり。releaseCount は、一だ。
俺は、引っかかった。「だが——今のは、一度目が済んでから、二度目を鳴らした。早鐘ってのは、そんなに行儀よく、待っちゃくれねえ。一度目の仕舞いが、まだ客を待ってる最中に、二度目が鳴ったら、どうなる」
「それでも、同じだ」とカイナは、その問いも、模擬戦にかけた。一度目の run() が、客を待って drain で止まっている——その最中に、二度目の run() を鳴らす。
| |
「started の札は、await を一つも挟まず、同期で立つ」とカイナは言った。「だから、一度目が drain で止まっている隙に、二度目が割り込んでも、その時にはもう、札は立っている。二度目は、何も始められず、引き返すしかない。——一度目が終わってからの二度目より、待っている最中の二度目こそ、この札の、本当の出番だ」
カイナは、その二度目を聞き流す札について、ぽつりと、一言だけ付け足した。冪等、という、聞き慣れない言葉を、説きながら。
「一度始まったなら、二度目は、入れるな。……俺が一等最初に締め出した獣も、これと同じ、一枚の札だった」
俺は、その「最初の獣」が何のことか、深くは訊かなかった。カイナの来し方に、軽々しく踏み込むものでもない。ただ、この人は、ずっと前から、同じ札を切ってきたんだな、と、それだけ、思った。
還らなかったものが、落ちる場所
俺には、まだ、聞けていないことが、一つ、残っていた。一番、聞くのが、怖かったことだ。
「あんたは、さっき、『還ってこない』と言った。客が、どこにも居ねえ。出てもいねえ、中にもいねえ。……あれは、どこへ、行っちまうんだ。俺の湯屋から、消えた客は」
カイナは、すぐには、答えなかった。湯屋の隅の、排水口——暗がりへ吸い込まれていく、湯——を、見た。手を止めたまま、声が、低くなった。
「行儀の悪い終端が、残すものだ。送り出されなかった客。還されなかった鍵。途中で断たれた、まだ続くはずだった仕事。——それらが落ちて、二度と返らない場所が、ある。何も返さないから、俺たちは、それを、NULL——虚無——と呼ぶ」
NULL。初めて聞く、その名を、カイナは、続けて、一つずつ、実体に結んでいった。還されなかった鍵は、解放され損ねた資源——開いたままの栓、leaked fd。燻ったままの火は、死なずに残る接続——ゾンビ接続。途中で断たれた仕事は、誰も待たなくなった、宙吊りの約束——dangling promise、果たされも、捨てられもしない、宙ぶらりんの Promise だ。NULL とは、その三つが落ちて、混じり合った場所の、名なのだ、とカイナは言った。
そして、カイナは、初めて、港の、あの大結界に、踏み込んで触れた。
「お前が毎朝、この湯屋で、こぼしかけている、その一杯の虚無。あれと、同じ名のものを、港の大結界は、幾重もの封印で、堰き止めている。行儀悪く終わったものが、ことごとく落ちて、積もり積もった——大きな、大きな、NULL を」
湯気の中にいるのに、うなじのあたりが、すうっと冷えた。
たかが、湯屋のあるじだ。十人かそこらの客を、湯に入れて、送り出すだけの男だ。その俺が、毎朝、早鐘のたびにこぼしかけていたもの——あの、還ってこない一杯——が、港のあの大結界が、幾重もの封印で堰き止めている、大きな虚無と、同じ名のものだったのか。
同時に、うまくは言えないが——妙な、静かな心持ちが、湯気の奥から、立ってきた。俺の、この小さな手仕事が、見たこともない、大きなものと、一本の筋で、繋がっていたような。
「お前の仕舞いの式は、小さい」とカイナは言った。「客は、せいぜい十人だ。だが、作法は、同じだ。皆を送り、鍵を還してから、閉じる。——その作法だけが、一杯の虚無も、大きな虚無も、同じように、堰き止める。港の大結界を保つのも、突き詰めれば、お前が今日覚えた、この終い方と、同じものだ」
カイナが、港の大結界のことを口にするのを、俺は、初めて聞いた。前に一度だけ、港の縁で、結界のほうを向いて立つ、後ろ姿を、遠くから見かけたことがある。あの時は、ただ、景色でも見ているのかと思った。——今、思う。あれは、毎朝、これと同じ虚無を、堰き止めている背中だったのかもしれない。
カイナは、それ以上は、語らなかった。排水口の暗がりを、見たまま、しばらく、動かなかった。
すべての模擬戦を、通す
カイナは、組んだ檻を、最初から最後まで、通して走らせた。
| |
六つの模擬戦は、どれも緑だった。一つも、倒れなかった。
Before の早鐘は、客がまだ湯にいるのに、水脈を還して、客を倒した。何も還さず抜ければ、客は倒れぬ代わりに、栓は開き、火は燻り、客は宙に取り残された。After の式は、同じ早鐘を違えず流しても、客が皆上がるのを待ってから還し、客を『良い湯でした』と送り出した。二度目の鐘は聞き流し、上がらぬ客には、夜明けより短い刻限を切って、促してでも、鍵を還した。
唯一、挟んだのは、「客が皆、上がるのを待つ」一手。それが、倒れも、漏れと置き去りも、二度還しも、際限ない待ちも、まとめて防いだ。客の入浴も、上がり湯も、湯屋も、何一つ、変えていない。
仕舞いの式が、引き受けないもの
カイナは、最後に、この式が、引き受けないもの——守らないこと、誤って使えば仇になることを、一つずつ、線引きした。
「まず、刻限を、どれだけにするか。これは、式の中には、書いていない。短く切りすぎれば、まだ上がれた客を、追い出す。長く取りすぎれば、夜明けに、間に合わない。どれだけ待つかは、客の長湯の実績から、逆算するしかない。式は、待ち方を決めるが、待ち時間までは、決めてくれん」
「次に」とカイナは続けた。「お前の湯屋で客を送り切っても、その客が次に向かう先——隣の宿、表の通り——が、行儀悪く閉じれば、そこで、また虚無が出る。終い方は、連なる先まで貫いて、初めて効く。お前一人が綺麗に閉じても、繋がる先が崩れれば、虚無は、残る」
「それから、上がらぬ客を促す、その合図だ」。カイナは、言葉を選んだ。「力ずくで、湯から引きずり出すんじゃない。『もう仕舞いだ』と、合図を送って、自分から上がってもらう。進行中の仕事に、中断の合図を伝える符牒——前に診た、鎖を断つ符牒と、同じものだ。刻限が来たら、ただ待つだけでなく、能動的に、合図を送れる。だが、それは、待ちの代わりじゃない。待ちの、補いだ」
「最後に、二度目を聞き流す札——冪等は、終いだけの話じゃない」とカイナは言った。「二度効いては困る合図、すべてに要る。終いの獣も、始まりの獣も、これで締め出す」
俺は、その線引きを、一つずつ、受け取った。情の語で、思わず、こぼれた。
「客のことだけ考えてりゃいい、ってわけじゃ、ねえんだな」
「ああ」とカイナは言った。「だが、その客を、一人残らず送り切る——それが、根っこだ。根っこを、お前は、もう掴んでいる」
清く、空
カイナは、暖簾を、くぐって出ていった。俺は、見送りもそこそこに、夜明けの仕舞いに、取りかかった。今度は、慌てなかった。
明け方、街に、また早鐘が鳴った。見回りの、戸締まり令だ。だが、もう、俺は、慌てない。
暖簾を下ろし、新規を断つ。湯の客が、一人、また一人、自分の足で上がって、「良い湯でした」と、帰っていく。その背中を、一つずつ、見送る。最後の一人が、暖簾を出ていくのを、待つ。それから——湯を抜き、火を還し、湯札を、番台に、戻す。順に。一つずつ。慌てず、抜かりなく。
湯屋が、空になる。
だが——あの、急いで閉めたあとの、空とは、違った。客は皆、自分の足で帰った。火は還り、栓は閉じ、湯札は、一枚残らず、番台に戻っている。排水口の暗がりへ——何も、落ちていかない。澄んだ湯が、最後に、すうっと流れて、消える。還ってこないものが、一つも、ない。
清く、空だ。
訪れた客を、一人残らず、気持ちよく帰す。それが、俺の信条だった。長年、そう思って、やってきた。だが、本当にそれを全うできたのは——皆の上がりを、待つ作法を、知った、今日が、初めてだった。
肝は、たった一手だった。皆が上がるのを、待つ。それだけで、終いは、こんなにも、静かになる。カイナが言っていた、最初の獣の話が、今は、少しだけ、分かる気がした。
終い方を識る者
暖簾を出ると、夜明けの匂いがした。
あの湯守は、たいしたものだ。十人かそこらの客を湯に入れて、送り出すだけの仕事で、知らぬ間に、いちばん大きな結界の理に触れていた。皆を還してから、閉じる。たったそれだけの作法が、小さな湯屋も、港の大結界も、同じように虚無から守る。人は俺を「終い方を識る者」と呼ぶ。だが本当のことを言えば、俺自身、その終い方の最後の一片を、今朝あの小さな湯屋で、ようやく見届けたのだ。
下町を抜けて、港へ降りた。
潮の匂いの先に、それはある。港を丸ごと囲う、幾重もの封印。俺が護る、古い大結界だ。一枚岩に見えて、近づけば無数の仕切りで、いくつもの区画に分かれている。堅牢なものが堅牢なのは、一枚岩だからではない。区画で、仕切られているからだ——あの船の機関長に、俺が言ったのと、同じことだ。
結界の前に立つと、低い軋みが、足の裏から伝わってきた。
この軋みを、王宮の術師も、迷宮の中継係も、遠くに聞いていた。皆、別の獣を診ている最中に、ふと地の底のほうから聞こえる、と言った。正体を、俺は知っている。この封印の奥で堰き止められているものの、圧だ。行儀悪く終わったものが——送り出されなかった客、還されなかった鍵、途中で断たれた仕事が——ことごとく落ちて、積もりに積もった、大きな、大きな虚無。NULL。それが、押している。
封印の、いちばん古い印に、指を当てた。俺がここを継いだとき、この印を刻んだ手は、もうなかった。継いだ時の俺は、まだ、終い方を知らなかった。だから、旅に出た。
旅で出会った獣を、一頭ずつ、この結界に重ねていく。双頭の犬の首を繋いだ、一本の鎖。二つの釜の睨み合いを解いた、鍵を拾う順。倒れた相手への鎖を断つ、封印の自動結界。魔力の槽を機能ごとに隔てる、区画。——十二の獣、十二の作法。どれ一つ欠けても、この結界は、どこかから漏れる。そして最後まで欠けていた一片が、今朝の、あれだった。皆を還してから、己を閉じる。終い方だ。
NULLは、滅ぼせない。虚無を、剣で斬ることはできん。できるのは、ただ——落ちてくるものを、一つずつ、正しく終わらせ続けることだけだ。行儀よく終わったものは、虚無に落ちない。落ちなければ、積もらない。積もらなければ、この封印は、保つ。
俺は、最後の作法を、古い印に編み込んだ。
軋みが、少し、引いた。消えはしない。消すものではないからだ。今日もまた、落ちかけたものを一つ、正しく還した。明日も、明後日も、そうする。終わらせ続ける巡りを、一つ、整え直したのだ。
——思えば、可笑しなものだ。
俺がいちばん最初に締め出した獣は、二度目の詠唱を撥ねる、一枚の札だった。そして今朝、最後に覚えた作法も、二度目の早鐘を聞き流す、同じ一枚の札だった。入口で締め出すものと、出口で聞き流すもの。違う獣だと思っていた二頭は、裏返しの、同じ札だったのだ。
十二の旅は、これで、一巡した。
夜が明ける。港の大結界は、今日も、誰にも知られず、虚無を堰き止めている。それでいい。——あの湯守が、客を一人残らず還したことを、客自身は、ついぞ知らないように。
📜 カイナの魔獣契約録(Tamer’s Registry)
- 魔獣名(クラス/パターン名): 終いを急ぐ幕引きの獣(行儀の悪い終端=強制切断・リソースリーク)/その残す虚無=NULL。契約=皆を還してから己を閉じる仕舞いの式(Graceful Shutdown=新規受付を止め、処理中の完了を待ち〔drain〕、資源を順に還してから終了する/二度目の合図は聞き流し〔冪等〕、待ちには刻限〔timeout〕を切る)
- 危険度(難易度/バグの影響度): ★★★★★(プロセス終了時のみ露出し、平時には絶対に顕れない=客のいない空の湯屋でいくら試しても再現しない。ログにも例外にも残りにくい。一度の発動で、in-flight の喪失〔客が倒れる〕・資源リーク〔栓が開いたまま〕・ゾンビ接続〔火が燻ったまま〕・未コミット処理の喪失〔途中の仕事が無かったことに〕を同時に起こす。デプロイ・スケールイン・クラッシュなど、外部要因で不可避に発動する。シリーズの深淵 NULL の正体そのもの)
- 主な生態(アンチパターンの特徴):
- 終了シグナル(早鐘)受信時に、新規受付の停止・処理中の完了待ち・資源解放を伴わず、即座にプロセスを終える(
process.exit(0)直接呼び出し相当/シグナル無処理でのデフォルト強制終了) - 「客がまだ居る」(in-flight)のに「もう閉じる」(資源解放)を同じ一息でやる順序違反——処理中ハンドラがクローズ済み資源へ触れて倒れる(上がり湯を断たれた客)
- 何も還さず抜ければ、資源はリークし(開いた栓=leaked fd)、接続は死なず残り(燻る火=ゾンビ接続)、処理は宙吊りのまま誰も待たなくなる(dangling promise)=還ってこない=NULL。なお塞ぐ場所は別レイヤ——前二者は資源ハンドルの解放漏れ(
Conduit.release側)、宙吊りの約束は完了しないハンドラ(serveGuestの未解決bath.promise側=刻限切れで送り切れなかった客そのもの)。落ちる先(NULL)は同じでも、塞ぐ手は別 - 害は終了時のタイミング依存で、客のいない空の状態では決して再現しない=平時のテストをすり抜ける
- 終了シグナル(早鐘)受信時に、新規受付の停止・処理中の完了待ち・資源解放を伴わず、即座にプロセスを終える(
- 契約のポイント(設計の要点):
- 1:1 単一差分=火と水を還す前に「皆の上がりを待つ一手(drain)」を挟む。
stopAdmitting()→releaseに対し、stopAdmitting()→drain()→releaseとするだけ。資源・ハンドラ・サーバは不変。冪等ガード・刻限は同入力経路では no-op(鐘は一度・客は上がる)ゆえ、倒れ→送り切りの反転に効くのは drain 一手のみ - 論理的保証:客の上がり(
bath.promiseの resolve)と鍵の還し(release)の競合を、drain()のawaitが「上がりが先、還しが後」と直列化する。並行のタイミング(どちらが先に走るか)は実行時の値ゆえ型では守れず、実行時設計(待ちの一手)で守る。一方で受付の可否(admit(): Deferred<void> | null)や仕舞いの結末(run(): Promise<"clean" | "forced">)は型で守っている——型で守れないのは「どちらが先か」の一点だけ、という二層構造 - 二度目の合図は聞き流す(冪等ガード
started)——二重実行で「もう還した資源を再度還す」事故を防ぐ - 待ちには刻限(
Promise.raceで drain と競わせる)。上がらぬ客は促して退かせ、"forced"でも資源は還す。刻限は外側の強制(夜明け=SIGKILL/terminationGracePeriodSeconds)より短く切る。刻限タイマーはunrefし、待つ者が無くなれば自然終了を妨げない - 還す順序:新規停止→drain→資源(水脈→火)を逐次。逆順(資源を先に還す)は順序違反を招く
- 1:1 単一差分=火と水を還す前に「皆の上がりを待つ一手(drain)」を挟む。
- 契約外事項(保証しないこと):
- 刻限の見積もり(sizing):短すぎれば上がれる客を追い出し、長すぎれば外の強制に間に合わない。過去の負荷実績からの逆算が要る
- 下流の虚無:自プロセスを綺麗に閉じても、連なる下流(次のサービス・共有資源)が行儀悪く閉じればそこで虚無が出る。終い方は下流まで貫いて初めて効く
- 断つ符牒(AbortController)との併用:刻限到達時、待つだけでなく
AbortSignalで進行中処理へ中断の合図を能動的に送れる(待ちの補完であって代替ではない) - 冪等は終端専用ではない:二度効いては困るあらゆる合図に要る汎用の札
- 現在のステータス: 🟢 火と水を還す前に「皆の上がりを待つ一手(drain)」を挟み、二度目の早鐘は聞き流し(冪等)、待ちには夜明けより短い刻限を切る仕舞いの式の契約成立(客を送り切ってから鍵を還す/倒れ・漏れ・置き去り・二度還しを単一差分で封じる)。刻限の見積もり・下流の虚無・断つ符牒の併用は契約外として残置。——アーク「堅牢な結界」、締めくくり。小さな湯屋の終い方が、大結界の堰き止める深淵 NULL を退ける作法と、同じものだった
