届かぬ報せ
魔物使いが来たのは、嵐が抜けた翌朝だった。
俺は辺境の中継伝達所をひとりで預かっている。仕事は単純だ。麓の村々から上がってくる報せを、雷鳥(伝令の獣)に結んで、稜線の向こうの本塔へ継ぐ。本塔がそれを受けて、王都へ流す。止まり木に並んだ雷鳥を一羽放ち、向こうが受け取れば、それで一件。何年もそうやってきた。
厄介なのは、魔力嵐だ。嵐が稜線を覆うと、本塔は黙り込む。雷鳥を放っても、結界に弾かれて戻ってくる。そういうときの俺のやり方は、決まっていた。もう一羽放つ。それでも戻れば、また放つ。届くまで放つ。リトライ——失敗した送りを、もう一度試みること。そんな上等な名前があると知ったのは、あの女が来てからだ。俺にとっては、ただの「数撃ちゃ当たる」だった。
先の大嵐までは、それで通っていた。
呼んだ覚えはない。嵐の夜、狼煙は雨に死に、伝書の鳩も飛べず、残ったのは止まり木の雷鳥だけだった。俺は最後の一羽に「腕利きの魔物使いを寄越してくれ」と短く結んで、行き先も決めず、闇雲に空へ放った。当てなんてなかった。
その一羽が、たまたまこの女のいる方角へ落ちたらしい。カイナと名乗った。稜線を越えて、嵐の晴れ間に、ひとりで歩いてきた。
「悪い報せほど、早く飛ぶ」とカイナは止まり木を見て言った。「お前があの夜に放った雷鳥は、何十羽だ。その中で、ちゃんと行き先に届いたのは——一羽だけだったろう」
虚を突かれた。確かに、当てずっぽうに放った最後の一羽だけが、こうして使い手を連れてきた。連発した残りは、どこへ消えたのか俺も知らない。だがその皮肉に俺が気づくのは、もっと後だ。
数撃てば、当たるはずだった
「本塔が嵐で黙る。雷鳥が弾かれる。なら、もっと放つ」俺は止まり木の雷鳥を顎で示した。「届くまで放てば、いつか一羽は通る。今までずっと、そうやって嵐をやり過ごしてきた」
カイナは何も言わず、俺の術式(コード)を写した巻物へ目を落とした。俺は続けた。半分は言い訳だったかもしれない。
「だが、先の大嵐は違った。放っても放っても弾かれる。だから手数を増やした。間を詰めて、立て続けに放った。なのに——本塔は、よけいに深く黙り込んだ。最後は一羽も通らねえ。報せが丸ごと落ちた」声が掠れた。「数撃ちゃ当たるはずだ。なのに、撃つほど当たらねえ。どういうことなんだ、それは」
巻物に書いてあるのは、こういう術式だった。失敗したら、すぐ次を放つ。当たり前のことしか書いていない。
| |
カイナは巻物を端まで追ってから、止まり木の雷鳥に手をやり、稜線の向こうの本塔の影へ目をやった。すぐには名を下さなかった。
「お前の雷鳥は、速い。速すぎる」と、低く言った。「嵐は、お前の雷鳥より、ずっと長く吹く」
意味が掴めなかった。速いのは美徳だろう。一刻も早く届けるのが、伝達所の仕事だ。
カイナは空の一点を指した。嵐が抜けかけて、本塔の影がかすかに輪郭を取り戻そうとしている、その方角だ。「あの塔は今、ようやく立ち上がろうとしている。嵐に打たれて、膝をついて、それでも起きようとしている。——そこへお前は、何十羽を一度に叩きつけた。立ちかけた者の肩を、もう一度押し倒すように」
俺は黙った。答えは、まだ返ってこなかった。
嵐を、檻に閉じ込める
カイナは伝達所の卓に、奇妙な仕掛けを組みはじめた。本人いわく「模擬戦」——嵐は、半日も吹く。その半日を、卓の上の数行へ畳み込む檻だ。
仕掛けの作りを、俺にも分かるように説明してくれた。本塔は、ある刻まで嵐で黙り、その刻を過ぎれば応える。その「嵐が去る刻」を、こちらで決められるようにする。雷鳥を放つ術(attempt)も、待つ間(sleep)も、外から握れる作り物に差し替える。
肝はここだった。待つ間(sleep)は、模擬戦では実際には待たない。代わりに、待つはずだった時間を一つの帳面に足し込んでいって、その合計を「今、何刻まで来たか」とみなす。待ち時間の累計が、そのまま時計の針になる。放った雷鳥の数も数える。これで、嵐が吹く前と、過ぎた後を、思いのままに行き来できる。
| |
「待たずに時計だけ進めるなら、嵐の半日も、瞬きの間に通り抜けられる」とカイナは言った。本塔がいつ口を開くかは stormEndsAt の一語で決まり、針がそこを越えたかどうかで、雷鳥が弾かれるか通るかが決まる。針が必ず「進んでから」次の試行で読まれるのは、send が待ち(sleep)と試行(attempt)を一つずつ順に await していくからだ。同じ刻に二羽が並んで判定を奪い合うことはない——だから、待ちの累計をそのまま時計と呼べる。曖昧なところは、どこにもない。
そして、俺の術式を——即時連射のやつを——その檻に載せた。嵐は、針にして千の刻まで吹くことにした。
| |
走らせると、結果は素っ気なかった。雷鳥は間髪入れず連射され、六羽——手持ちの全部——が一息に飛び、一息に弾かれた。時計の針は、0 から一刻も動いていない。
嵐が去る前に、羽は尽きる
「待て」と俺は言った。「なんで一羽も通らねえんだ。嵐だって、いつかは止むんだろう」
カイナは答えなかった。ただ、時計の針を——elapsed を指した。「数えてみろ」
俺は見た。雷鳥が六羽すべて飛び終わってもなお、針は 0。嵐が去る刻は千だというのに、針はそこへ一歩も近づいていない。カイナはそれ以上、何も言わなかった。その沈黙が、俺に続きを考えさせた。針が動いていない。ということは——待っていない。一秒も。
「……嵐が去る前に、手持ちを全部使い切ったのか」声が低くなった。「嵐が明けた後に放てる雷鳥が、もう一羽も残っちゃいねえ。明ける頃には、俺はとっくに諦めてる」
掴めなかった逆説が、誰に教えられたわけでもなく、自分の口から像を結んだ。連発は、当たる確率を上げるどころか、嵐の後に残るはずの最後の手を、自分で先に潰していた。
カイナは本塔の影を見たまま、刻むように続けた。
「お前の雷鳥は、自分が届くことしか考えていない。嵐で弱った塔に、群がって覆いかぶさる」
「で、つまり?」俺は急かした。
「塔が息を継ごうとするたび、お前の鳥がまた口をふさぐ。応えようにも、応える隙がない」
「……じゃあ、俺の連発が、本塔の立ち直りを、よけいに遅らせてたってのか」
「待つというのは」とカイナは俺を見た。「お前の雷鳥のためじゃない。叩かれる塔のためだ。塔に息を継がせる間を空ける。それだけが、嵐を早く終わらせる」
リトライは利己的なのだ、とカイナは言った。自分が通りたいがために、向こうに負荷を押しつける。待つのは譲歩じゃない。弱った相手を立ち直らせて、結果として自分の報せを通すための、唯一の筋道だった。
カイナは止まり木へ顎をしゃくった。「お前が飼っているのは、ただの伝令じゃない。焦りの雷鳥だ。急くほどに羽を散らして、嵐の前に使い果たす」
俺は、どうにも納得のいかないことを口にした。「だったら、放つ前に分かりゃいい。今は嵐だから待て、今は晴れたから放て、って。先に分かりゃ、無駄撃ちせずに済むだろう」
「『今、向こうが嵐か』は」とカイナ。「放って、弾かれて、初めて分かる。放つ前には決まっていない。だから、撃つ前に止めるんじゃない——弾かれた後に、どう退くかを、術式に持たせるしかない」
書いた時点では、向こうの空模様は誰にも見えない。見えないものを先回りで防ぐことはできない。できるのは、弾かれてから、どう身を引くかだ。
「……間を空けりゃいいのか」俺は唸った。「だが、どれだけ空ける? やみくもに長く待ったら、今度は嵐が明けてるのに気づかねえ。それじゃ遅すぎる」
問いが、自分から次へ転がっていた。
間を置く、それだけの契約
カイナの直しは、呆気ないものだった。術式を組み替えるでも、新しい鎖を足すでもない。俺の catch——失敗を受け止めるあの場所に、待つ一行を、ただ一つ挿んだだけだった。
| |
「変えたのは、待つか待たないか。それだけだ」とカイナは言った。雷鳥を放つ、通れば返す、上限まで試す、尽きれば諦める——術のすることは何も変わっていない。変わったのは、失敗と次の試行の間に、間ができたこと。たったそれだけだった。
どれだけ待つかを決める
「待つ、と決めた。あとは——どれだけ待つかだ」
カイナは backoff を示した。待ち時間を決める、短い手だ。
| |
「一度目の失敗は、短く退く。二度目はその倍。三度目はさらに倍」。失敗のたびに、次までの待ちを倍々に伸ばす。これを指数バックオフ——失敗のたびに再試行までの待機を指数的に(倍々に)伸ばすやり方、という。「嵐が長引くほど、深く退く。立ち上がろうとする塔に、たっぷり間を与えるためだ」
ただし、際限なく伸ばしはしない。ある上限で頭打ちにする。これをcap——待ち時間の天井、と呼ぶ。「退きすぎれば、嵐が明けても気づくのが遅れる」。退きそのものが塔のための譲歩なら、この天井だけは、こちら側の都合だ。報せを抱えたまま無限に沈み込まない、その歯止め。「深く退くにも、限度を決めておく」
俺は、もう一つ目に留まったものを指した。待ち時間に、何か乱数のようなものが掛かっている。
「これか」とカイナは this.random() の一語を指したが、深くは語らなかった。「これは、お前一羽のための仕掛けじゃない。意味は、後で分かる」。それ以上は説明せず、術式はそのまま——最初から、その乱数を含んだ形——で置かれた。
同じ嵐で、直した術式を走らせた。今度は針が進む。
| |
一羽放って、弾かれる。百の刻、退く。また放つ。弾かれる。二百、退く。三度退いて累計は七百、まだ嵐の中だ。四度目に八百退いて、累計は千五百——ここで初めて嵐の千を越え、本塔はもう口を開いていた。次の一羽が、通った。
ここで random を () => 1.0 に固定しているのには、わけがある。揺らぎを最大の 1.0 に張りつければ、待ち時間は指数の値そのものになり、毎回きっかり 100, 200, 400, 800 と積み上がる。乱数を使う術式でも、乱数の出方をこちらで握ってしまえば、結果は一通りに定まる。模擬戦が、まぐれでも気まぐれでもなく、いつ走らせても同じ針を刻む——だから「待ったから届いた」と、迷いなく言い切れる。
待ちが倍々に伸び、天井で頭打ちになることも、針を覗けば確かめられた。
| |
100, 200, 400 と倍に伸び、800 で天井に着いて、あとは 800 のまま。退きは深くなり、しかし無限には沈まない。「待ったから、嵐の後まで雷鳥が生き残った」とカイナは言った。「お前の前のやり方は、嵐の前に羽を使い果たしていた。それだけの違いだ」
焦りの雷鳥は、嵐の前に羽を使い果たす。直したのは、それを使い果たさないように、間を置いただけ。
群れは、同じ刻に飛ぶ
これで終わりか、と肩の力が抜けかけたとき、カイナが横から新しい事実を置いた。
「辺境の伝達所は、お前のところだけじゃない」
そうだ。稜線沿いには、同じような中継所がいくつもある。どこも同じ本塔へ報せを継いでいる。
「嵐が来れば、どの伝達所も、お前と同じように雷鳥を退かせる。皆が同じだけ退き、皆が同じ刻に待ち終え——」カイナは言葉を切った。「皆が、同じ刻に、一斉に飛び立つ」
背筋が冷えた。嵐が明けた、まさにその瞬間。立ち直りかけた本塔へ、稜線じゅうの伝達所の雷鳥が、揃って殺到する。せっかく退いて与えた間が、明けた途端に帳消しになる。群れごと覆いかぶさって、塔をまた焼く。これをThundering Herd——殺到する群れ、と呼ぶらしい。俺がひとりで間を詰めて自滅するのが連射の暴走なら、こっちは、退きを覚えた伝達所どうしが、皆そろって同じ拍子で戻ってきてぶつかる暴走だ。原因は逆を向いているのに、潰れるのは同じ本塔だ。皆が同じ拍子で退くから、同じ拍子で戻ってくる。
「待てよ」俺は当然の反論をした。「俺一羽が退く刻をずらしたって、他の連中が揃って飛んだら、波は立つだろう。俺だけがバラけて、何になる」
「波は立つ」とカイナは認めた。「だが、その波を作る側を降りる第一歩は、お前の術式だ。それに——」カイナは止まり木の雷鳥へ目をやった。「辺境の伝達所は、どれも同じ古い術式の写しを使っている。お前が直した一枚が、巡って、皆に配られる」
連発が利己なら、退きをずらすのは、譲り合いだ。自分が波を立てない側に回る。それが巡って、本塔を守り、結局は自分の報せを通す。
俺は、さっきの乱数を思い出した。
「……待つ刻が、皆同じだからだ。同じだけ待ちゃ、同じ刻に飛ぶ。なら——待つ刻を、一羽ずつ、わざとバラけさせりゃいい」
「それが、さっきの random() だ」とカイナ。「最初から、そこにあった」
待ち時間を、0 から、さっき決めた指数の値までの間で、ランダムに選ぶ。これをFull Jitter——待ちを「0から指数値までの一様乱数」で選ぶ揺らぎの入れ方、という。退きの深さは指数で決め、その範囲のどこに着地するかは、一羽ずつ運任せにする。群れの飛び立ちが時間に散らばり、本塔は一羽ずつ捌けるようになる。「揃えて待つな。散らして待て」
カイナは、それを数で見せた。同じ術式を、百の伝達所ぶん並べて走らせる。揺らぎを殺した場合と、Full Jitter を効かせた場合で。断っておくと、「揺らぎを殺す」といっても、別の術式に差し替えるわけじゃない。さっきの random() に毎回きっかり 1.0 を返させ、待ちを指数の上限へ張りつけて、全員を同じ値に揃えるだけだ。式は一文字も変えていない。揺らぎを入れるか殺すかは、供給する乱数の出方だけで決まる。
| |
ここで使う物差しが peakInBucket だ。退いた刻を、十刻ごとの窓に振り分けて、一番混んだ窓に何羽詰まったかを数える。これが、群れの殺到の度合いになる。
| |
揺らぎを殺すと、百羽が一つの窓に揃って詰まった。ピークは、きっかり百。全部が同じ刻に飛ぶ。Full Jitter を効かせると、同じ百羽が刻の上に散り、一番混んだ窓でも十五羽までしか重ならなかった。百が十五へ。[0,100) の幅に百羽を撒けば、十刻ごとの窓は十個ぶん——一窓あたり平均すれば十羽前後で、十五はそのばらつきの内に収まる。だからこの十五は窓の幅(bucketMs)しだいで動く目安であって、きっかりの数そのものに意味があるわけじゃない。テストが見るのも「ピークが半分(五十)を割るか」——群れがほどけたか否かの境であって、何羽きっかりか、ではない。これだけ散れば、本塔も一羽ずつ受けられる。
ここでも、揺らぎは運任せに見えて、種(シード)を固定した擬似乱数を使っている。だから「散った」という結果も、まぐれではなく、いつ走らせても同じ十五に落ち着く。乱数を使う仕掛けほど、検める時には乱数の出所をこちらで握る。それで初めて、「揺らぎが群れを解いた」と数で言い切れる。
断っておくと、この百羽の模擬戦で並べたのは、さっき直した術式(RelayAfter)を、ただ百個こしらえただけだ。術式そのものは一文字も変えていない。群れに効かせるからといって、別物に組み替えたわけじゃない。群れの散り方は最初の退きで決まるから、ここでは一度だけ退かせれば足りる(maxRetries: 1)。回数を絞ったのは、測りたいものに合わせて手数を減らしただけで、中身は直したときのままだ。変えたのは、相変わらず、あの待つ一行だけだった。
この「群れがほどける」という現象を、伝達所と本塔のやり取りに引き写して考えてみる。
まずは、揺らぎを殺して(Jitterなし)全員を同じ待ち時間に揃えた場合だ。

そしてこちらが、揺らぎ(Full Jitter)を効かせて、各自がバラバラの待ち時間を選択した場合だ。

揺らぎがない上の状況では、三所の雷鳥が同じ刻にぶつかって、本塔がまた潰れてしまう(再送衝突)。しかし、揺らぎを効かせた下の状況では、退く刻が一羽ずつずれて、本塔が順に捌いていくことができる。退きを倍々にするだけでは、群れは揃ったまま戻ってくる。揺らぎを足して初めて、群れがほどけるのだ。
待っても、縛れないもの
「これで」と俺は訊いた。「嵐は、もう怖くねえか」
カイナは止まり木の雷鳥を一羽、指の腹で撫でた。羽が落ち着くのを待つように、ひと呼吸おいてから口を開いた。
「この退きが効くのは、嵐がいつか去るときだけだ。一時の不調——それが大前提だ」。一時的に塞がっているだけで、待てば必ず通じる相手。そういう障害を一時的障害と呼ぶ。「本塔が嵐じゃなく、崩れて完全に死んでいるなら、何度退いても応えはしない。待つだけ無駄に雷鳥を捨てることになる。何度退いても無駄な相手には、いっそ放つのをやめ、結界を下ろして傷を広げない見極めが要る。それは別の獣の領分だ」。失敗が続けば送りを自動で遮断して、連鎖の倒れを防ぐ仕組み——Circuit Breaker、封印の自動結界。それはまた、別のいつか名を付けることになる、とカイナは言った。
「それと」とカイナは俺を見た。「同じ報せを二度放っても害がない——それが、退いて放ち直すことの、もう一つの前提だ」。同じことを何度繰り返しても結果が変わらない性質を、冪等という。「お前の雷鳥が運ぶのが、ただの報せなら、二度届いても害はない。だが、もし運ぶのが『荷を一つ送れ』のような、二度効けば二度起きる命令だったら——退いて放ち直した一羽が、向こうにもう一度、荷を送らせる。待つことが、そのまま二重の事故になる」
二度効けば、二度起きる。俺はその図を思い浮かべて、ぞっとした。
「届いたか分からぬまま、もう一羽放つ。両方が効いて、荷が二つ出る。——それは、別の街で双頭の犬を飼っていた、どこかの誰かの話だ」とカイナは言った。誰のことかは知らない。だが、再送という手は、二度効いて困らない命令にだけ許される。それだけは、肝に銘じた。
縛れる相手と、そうでない相手。その境が、ようやく見えた。退きで救えるのは、いつか目を覚ます塔だけだ。死んだ塔にも、二度効けば二度起きる荷にも、退きは届かない——そこには別の手がいる。それでも、嵐に黙るだけの塔が相手なら、俺はもう、焦って羽を撒き散らしはしない。
見送り
檻は、そのまま伝達所に置いていくという。連射する古い術式も、退きを覚えた術式も、掛けたままだ。連射する方には「嵐の前に尽きる」ことを、退く方には「嵐の後に届く」ことを、それぞれ刻みつけて。
| |
これからは、この檻が見張る。次に誰かが——あるいは俺自身が、また気が急いて——間を詰めようとすれば、嵐の前に羽が尽きるその姿が、卓の上で先に捕まる。
その日の夕、嵐は本当に去った。稜線の影が晴れて、本塔の輪郭が戻った。俺は止まり木から雷鳥を一羽だけ取って、空へ放った。そして——放った手を、次の一羽へ伸ばさなかった。待った。
「待つのは、性に合わねえ」と、つい口に出た。
「だが、待てない奴の雷鳥は」とカイナは稜線の方を向いたまま言った。「嵐の前に、羽を使い果たす」
しばらくして、稜線の向こうの本塔に、小さく灯がともった。一羽、届いた。連発しなくても、たった一羽で、届いた。
カイナはもう、伝達所に背を向けて、晴れた稜線の下を歩き出していた。呼び止めようとして、やめた。次に何かが黙り込んでも——今度は、自分で待てる気がした。
📜 カイナの魔獣契約録(Tamer’s Registry)
- 魔獣名(クラス/パターン名): サンダーバード(焦りの雷鳥)/ 即時連射の自滅(単一クライアント)と Thundering Herd(群れの同期再送)(指数バックオフ+Full Jitter で馴致)
- 危険度(難易度/バグの影響度): ★★★★☆(平時は無害。相手が一時障害で弱った刻にだけ牙をむき、即時連射が復旧を妨げ、群れが揃えば回復しかけた相手をもう一度倒す)
- 主な生態(アンチパターンの特徴):
- 失敗するたび、間を空けず次を放つ(
catchで何もせず周回)。一時障害の相手には、嵐が去る前に手持ち(maxRetries+1回)を撃ち尽くし最終失敗する - 弱った相手へ高頻度で叩き込むため、相手の復旧そのものを遅らせる(リトライは利己的)
- 複数のクライアントが同じ拍子で退くと、同じ刻に揃って再送し、回復しかけた相手をまた倒す(クラスタリング)
- 失敗するたび、間を空けず次を放つ(
- 契約のポイント(設計の要点):
- 変えたのは「待つか待たないか」1点。
catch内にawait this.sleep(this.backoff(n))を一つ挿むだけ。for・上限・成功時return・最終throwのロジックは Before と同一 - 待ちの中身は、指数で倍々に伸ばし(Exponential Backoff)、cap で頭打ちにし、Full Jitter(
random() * exp)で散らす。相手に息を継がせ、群れの再送を脱同期する sleepを論理時計に流用し、randomを固定して、実時間ゼロ・決定的に検証。群れの脱同期は同じ術式を N 個並べて定量化(術式自体は不変)
- 変えたのは「待つか待たないか」1点。
- 契約外事項(保証しないこと):
- 嵐がいつか去る(一時障害)前提。完全に死んだ相手には待つだけ無駄=遮断(Circuit Breaker)の領分
- 二度効けば二度起きる命令(非冪等)の再送は二重実行になる。冪等な送りにのみ許される
- 多層で各層が独立に退くと、総試行は層の数だけ掛け算で膨らむ(各層が3回退けば3層で27倍)。退くのは単一の層だけにするのが原則
- 一時障害かどうかの判定(待つ価値のある失敗か)は本話では前提とし、別途見極めが要る
- 現在のステータス: 🟢 焦りの雷鳥を馴致(嵐がいつか去る前提で・群れは揺らぎで散らす)/死んだ塔への見極めと、二度効く荷は別領分
