第78話で見つけた細いreturn railから、viewerのevent cardが二枚届いた。
viewerはlive labのshutterへ、まずSET OPENを送った。
40 msあと、すぐに考え直してSET CLOSEDを送る。
ところがserverへ着いた順番は逆だった。
viewer intent order
event 41 SET OPEN sent +0 ms
event 42 SET CLOSED sent +40 ms
server arrival order
event 42 SET CLOSED arrived +120 ms
event 41 SET OPEN retry arrived +210 ms
CLOSEDが先に届き、shutterが閉じる。
その90 msあと、古いOPENがretry railから届いた。
serverが最後の到着をそのまま採用し、shutterはもう一度開いた。
イトはarrival-last-wins switchへ手を伸ばした。
「最後に届いたなら、これがいちばん新しい返事なんじゃない?」
次のlive sceneが始まる前に、shutterのfinal stateはCLOSEDでなければならない。
到着が逆になっても、viewerが最後に選んだstateを一件だけ残す必要がある。
速く届いても、意図した順番とは限らない
ユイは同じtwo-event traceを一件だけ再生した。
二枚とも短い遅れで届いている。
それでもarrival orderで上書きすると、final stateはOPENになった。
expected final state CLOSED
arrival-last result OPEN
state errors 1
realtimeは、すべてが同時になる魔法ではない。
短いdelayで届いても、別route、retry、queueの差で到着順が入れ替わるfixtureは作れる。
イト
速く届いたかと、あとから選んだかは、別の順番なんだ
ユイ
同じsessionで送った二件なら、sender側の順番をeventに残せます
マコト
何を最新と呼ぶかを、到着時刻だけに任せないんだね
Picoはshutterにもserver switchにも触れない。
二枚のevent cardにある一つと二つのnotchを、cyan lightで照らしただけだった。
三つの直し方
A:最後にarrivalしたeventを採用する
実装は単純になる。
けれど今回のようにold eventがretryで遅着すると、newer intentを巻き戻す。
B:すべてのeventを200 ms待ってから並べる
今回の二件は並べられるかもしれない。
しかしdelayを一律に増やし、200 msより遅いretryやmissing eventをどう扱うかは残る。
C:同じsessionのabsolute state eventへsequenceを付け、accepted versionよりnewerだけを適用する
senderが選んだ順番をevent cardへ持たせる。
serverはaccepted sequenceをstateと一緒に保持し、old / duplicate eventのeffectを0にする。
イトはarrival-last-wins switchをOFFへ戻した。
fixed wait railも外す。
「C。届いた順ではなく、このviewerが選んだ順をstateへ残す」
one-way delayとround tripを混ぜない
viewerからserverへ一件が進む時間は、片道のdelayだ。
serverからviewerへackを返し、往復して戻るまでがround trip。
W3C WebRTC Statistics APIには、candidate pairのlatest round tripを示すcurrentRoundTripTimeや、sent / receivedの観測項目がある。
ただしround-trip timeが短くても、application stateの順番が自動で正しくなるわけではない。
今回の二eventは、どちらも一秒未満で着いた。
問題は速さだけでなく、どちらをcurrent stateとして受け入れるかだ。
「realtimeなら何ms以下」という一つの数字も、この話では決めない。
chat、game input、共同編集、通知では、許せるdelayと守るべきstateが違う。
sequenceは、同じsession内の順番
イトはviewer consoleへ、shutter session専用のcounterを置いた。
壁時計の時刻ではない。
同じauthorized viewerが、このsessionで送ったeventの順番だ。
session shutter-7
sender viewer-A
accepted seq 40
accepted state CLOSED
server側の規則を一つに絞る。
if incoming.seq > accepted.seq
apply absolute state
store incoming.seq
else
effect 0
return ack(current seq, current state)
event 42が先に着く。
42 > 40なので、CLOSEDを適用してaccepted seqを42へ進める。
あとからevent 41が着く。
41 <= 42なので、OPENへ巻き戻さずeffect 0にする。
イトがnewer stateを自分の手で守った
イトはsource event二枚とarrival logをread-onlyで固定した。
two-notchのevent 42 CLOSEDをstate lockへ残す。
one-notchのevent 41 OPENがlate railから着くと、自分のright handでgray stale trayへ分けた。
left handは、accepted version 42のlockから離さない。

serverはviewerへackを返した。
ack session shutter-7
current seq 42
current state CLOSED
viewerは「自分のCLOSEDがcurrent stateになった」と確認できる。
old eventを黙って捨てるだけでなく、現在受け入れているversionを返す。
absolute stateだから、old eventを無効にできた
今回のcommandはSET OPENとSET CLOSEDだ。
event単体に、目標stateが全部入っている。
そのためnewerなabsolute state 42を受け入れた後、older 41を適用しなくてもCLOSEDを保てる。
しかしTOGGLE、+1、一文字を挿入のようなrelative operationは同じではない。
old operationを一件飛ばすと、最終結果そのものが変わる場合がある。
複数senderが同時に編集する場合も、viewer-Aのcounter一つだけでは全体順序を決められない。
その場合はmissing eventの回復、server order、merge、conflict policyなど、対象に合う別設計が必要だ。
一つのclosed fixtureから、すべてのrealtime appへlatest-winsを広げない。
遅着したOPENは、shutterを動かさなかった
イトはarrival順を三通りに変え、同じabsolute state eventを流した。
final state CLOSED
accepted sequence 42
stale events ignored 1
duplicate effects 0
state errors 0
ack current 42 / CLOSED
late event 41が最後に届いても、shutterは閉じたままだ。
同じevent 42をduplicateで再送しても、二度目のeffectは0。
イトはserver state、physical shutter、viewer ackを自分で一件ずつ照合した。
そこで初めて、return railのhandoffを許可する。
newer stateは、到着の遅いold cardに押し戻されなかった。
「今に近い」だけでなく、何をそろえるか決める
今回の最初の一手は、最後に届いたeventをcurrentにすることではない。
1. session / sender / stateのscopeを決める
2. arrival orderとintent orderを分けて記録する
3. absolute stateへmonotonic sequenceを付ける
4. accepted version以下をeffect 0にする
5. ackでcurrent version / stateをsenderへ返す
relative operation、複数sender、missing sequenceを扱うなら、ここで止まり、同じ規則を流用しない。
realtimeの短さと、stateの一貫性を別々に試験する。
二枚のeventごとに、railを作り直していた
shutterのfinal stateはCLOSEDでそろった。
viewerが次のcyan buttonを押す。
return eventを送る前に、lab machineはcontact routeを作る。
一件送る。
ackを受け取る。
routeを閉じる。
次のeventで、また最初からcontact routeを作る。
イトは、二枚のeventよりroute準備の方が大きいことに気づいた。
最後の到着ではなくsessionの順を守ったから、今度はmessageの中身とconnectionの作り方を分けて見られた。
「毎回routeを閉じず、つながったまま両方から送れないの?」
次回、ピコルート第80話。
WebSocketは、どうしてopenのまま話せる?

