テレビのアニメはどこから来る? / 第79話

第79話:イトは、最後に届いた返事を採用しなかった|順番が逆?

ピコルート第79話。CLOSEのあとに古いOPENが遅れて届いた。イトは到着順で上書きせず、同じsessionのsequenceで新しいstateを守ります。

リアルタイム通信往復遅延イベント順序確認応答10分更新

第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 state lockのtwo-notch amber CLOSED eventを左手で保持し、あとから届いたone-notch cyan OPEN eventを右手でgray stale trayへ分け、到着順による巻き戻しを止めている
last arrivalを採用せず、同じsessionでnewerなabsolute stateを保持する。

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のまま話せる?

この物語を、解説で深める

第79話で生まれた疑問を、解説記事で少し深く見ていきます。物語で感じた不思議さを残したまま、言葉と仕組みを順番に整理しましょう。

リアルタイム通信とは|今の状態を短い遅れでそろえる通信を読む