Slide 1

Slide 1 text

なぜThrottleではなく Debounceだったのか? 700並列リクエストと戦うサーバーサイド実装のすべて

Slide 2

Slide 2 text

@yoshiori 2

Slide 3

Slide 3 text

3 写真もSNSもOK

Slide 4

Slide 4 text

宣伝(残念ながら採択されなかった) 4 急な大規模データリクエストに応える 〜現実的なスケールアップ戦略〜 ◦ こっちは超デカイデータの話 ◦ 社内で聞いたときめっちゃ面白かった ◦ カンファレンスクオリティの資料で聞きたいので是非

Slide 5

Slide 5 text

課題背景 技術が必要になった背景を知る 5

Slide 6

Slide 6 text

6 私たちはテスト結果を収集している

Slide 7

Slide 7 text

7 テスト結果を集めてAIを使って様々な処理をしている ◦ 失敗したテストの原因が同じかどうかを判断 ▫ 例えばDBへのコネクションが原因で複数のテストがコケた時に 原因は同じなので1つにまとめる ◦ 同じ原因で失敗したテストが過去にあったか判断 ▫ 新しく発生した失敗なのかよくある失敗なのか ◦ リトライして成功しているかどうか ▫ 不安定なテストは複数回実行して成功していたら成功扱いに ◦ SlackやGithubなど各種通知 ◦ などなど

Slide 8

Slide 8 text

8 分散テスト実行 ◦ CIでのテスト実行に時間がかかるので複数台で実行する ▫ テストをある程度のまとまりごとにわけて実行 ◦ 分散するということはテストが多いということ ▫ つまり多いテストを早く終わらしたい ◦ 早く終わらせるためには平均的な時間で終わるように揃える ▫ 合計10分かかるテストを2分割するとして9分で終わるテストの 集まりと1分で終わる集まりに分散しても意味があまりない。 ▫ 上記なら5分で終わるテストの集まりを2個作るのが良い ◦ ほぼ同時に終わる = ほぼ同時に結果が送られる ▫ つまり、同時に大量のテスト結果が送られてくる

Slide 9

Slide 9 text

700並列でテストを実行しているお客 さんが顧客になる 700個のテストとかではなく、一回テスト実行すると700台で分散 実行されるということ。 それ自体は大口のお客さんなので嬉しい!! 2024 May, 9

Slide 10

Slide 10 text

10 700並列の分散テスト実行結果 ◦ 非同期にしたりジョブキューに入れてもあまり効果ない ▫ 700個のキューが貯まるだけ ◦ Close処理はまとめられる ▫ 700並列のテスト結果データは保存必須 ▫ その後のClose処理はある程度まとめて実行して良い ◦ 良い感じにデータ溜まったあとにClose処理したい ▫ ホントは全部終わったよってAPIリクエスト貰うのが一番楽なん だけど色々運用してそれは不可能だとなった ▫ なので良い感じに終わったと判断しなくてはいけない ◦ つまりClose処理部分を間引く必要がある

Slide 11

Slide 11 text

どうやって処理を間引くのか? ThrottleとDebounceの概要 11

Slide 12

Slide 12 text

処理を間引く方法としてThrottleとDebounceの2つが特に有名 ◦ Throttle ▫ 待ち時間内に何回イベントが発生しても待ち時間後に1回だけ処 理を実行する。 ▫ 5秒に1回とか...わかりやすい ◦ Debounce ▫ 待ち時間内に次のイベントが発生すると、タイマーがリセット され、処理の実行が遅延する。 ▫ もうちょっと説明 12

Slide 13

Slide 13 text

Debounce(デバウンス)って何 特定のイベントが短期間に連続して発生した際に、タ イマーによる入力遅延を利用して都度実行されないよ うにする仕組み 13

Slide 14

Slide 14 text

Debounce(デバウンス)って何 14 例:GUIのWindowリサイズ サイズ変更時に逐次再描画し てしまうと負荷が高いのでマ ウスドラッグが止まったとき に描画。 Throttleと組み合わせて0.1 秒単位で低解像度で描画して ドラッグが止まったら本格的 に描画したり。

Slide 15

Slide 15 text

Debounce(デバウンス)って何 15 例:入力サジェスト 1文字づつデータ送って処理 をしていると負荷も高くなる ので500ms以上入力がなかっ たら送信するとか。 * ちなみに今のGoogleの検索サジェストは逐次 送ってる。凄い。

Slide 16

Slide 16 text

例: YAPCという入力を受け取って何かしら実行する ユーザーの文字入力と同時にサーバから情報を出すよ うな処理のとき ◦ キー入力毎にリクエストを送ると負荷が高い 16

Slide 17

Slide 17 text

Debounceの場合 500msのDelayをもつDebounce ◦ 最初の"Y"でDebounce開始 ◦ 100msでまた"A"が入力されたのでまた500ms待機 ◦ "P","C"の入力後500ms何も入力されなかったので処理を実行 17

Slide 18

Slide 18 text

Throttleの場合 500msのDelayをもつThrottle ◦ 最初の"Y"でThrottle開始 ◦ 500ms後にアクション ◦ 次の500ms後に(データに更新があるので)またアクション 18

Slide 19

Slide 19 text

それぞれの処理のタイミングと回数の違い 19

Slide 20

Slide 20 text

20 ちなみにDebounceの 元ネタはハードウェアの ボタンスイッチらしいです

Slide 21

Slide 21 text

700並列リクエストの対応 実際に発生したこと 21

Slide 22

Slide 22 text

想定してなかった量の負荷 22 悲鳴をあげるサーバ&DB ◦ 秒間10以上のclose処理が走る ◦ 普通のendpoindなら平気だけどcloseは推論が走ったり重い処理が大 量にある

Slide 23

Slide 23 text

一旦初期対応終わったあと どうするのが良いのか? ◦ ❌ APIにRate Limitつける ▫ お客さんのテストデータを受け入れないのはダメ ◦ ❌ 処理を夜間バッチとかにする ▫ テスト結果はすぐに見たい(当たり前) ◦ ❌ スケールアップ/スケールアウト ▫ スパイクなのでauto scale的なものでは間に合わない ◦ ❌ ジョブキューなどにして非同期にする ▫ 700並列で非同期処理が走るようになるだけ 23

Slide 24

Slide 24 text

リクエストと処理の特性は最初に説明したもの 24

Slide 25

Slide 25 text

Debounceでは!? ほぼ同時とはいえ送られてくるタイミング にはある程度の振り幅があるのでThrottle よりDebouceのほうがより処理をまとめられ る 25

Slide 26

Slide 26 text

Debounceでは!? 26 ほぼ同時とはいえ送られてくるタイミングにはある程 度の振り幅があるのでThrottleよりDebounceのほうが より処理をまとめられる Throttle Debounce

Slide 27

Slide 27 text

しかしDebounceは基本的にクライアントサイドの技術 クライアントサイドのDebounceはサーバへの負荷軽減 が主な目的です ◦ サーバへのリクエストを減らす ▫ なのでクライアントサイドでDebounceする ◦ DoSアタックのようにならないように ▫ サーバ側はrate limitなどで多すぎるリクエストを弾く ◦ JavaScriptなどでは良くライブラリになってる ▫ 古くはjQueryとかLodashとかUnderscoreにもあった 27

Slide 28

Slide 28 text

Debounce処理 こういった1本のラインで処理するイメージ ◦ クライアントサイドの技術なので ◦ もちろんマルチスレッドなどはあるけど 28

Slide 29

Slide 29 text

サーバサイドDebounceの場合 クライアント多数(今回は700個) 29

Slide 30

Slide 30 text

サーバサイドDebounceの場合 サーバも複数 30

Slide 31

Slide 31 text

サーバサイドDebounceの場合 リクエストはそれぞれ 31

Slide 32

Slide 32 text

サーバサイドDebounceの場合 これを1つのDebounce処理にする 32

Slide 33

Slide 33 text

33 🤔

Slide 34

Slide 34 text

34 障害の翌日にだした Proposal

Slide 35

Slide 35 text

Server-Side Debounce v1 とりあえず現実の問題を解決する ために作った仕組み 35

Slide 36

Slide 36 text

簡単に言うとRedisをロックオブジェクトにしたDebounce処理 36 * 最近のRedis界隈のゴタゴタでRedisと言うのは正確ではないけど この発表ではそこは本質ではないのでRedisで統一します @Async public waitAndClose(int testSessionID){ UUID myId = UUID.create(); cache.set(testSessionID, myId); Thread.sleep(5 * 100); UUID latest = cache.get(testSessionID); if(myId == latest){ testSessionsService.close(testSessionID); cache.del(testSessionID); } } ベースはProposalに書いた疑似コード

Slide 37

Slide 37 text

例: 動きの説明 呼び出されたらUUIDを生成してRedisに保存し500ms待つ ◦ 呼び出しは@Asyncを使い非同期に呼び出す ◦ KeyはTestSessionのID ◦ UUIDは他のリクエストとの区別のために使うのでユニークな値であれ ば何でも良い 37

Slide 38

Slide 38 text

例: 動きの説明 500ms後にRedisから値を取得し照合する ◦ 値が同じなら処理実行 ◦ 値が書き換わっていない=他の呼び出しはない 38

Slide 39

Slide 39 text

例: 動きの説明 500ms内にもう一度呼ばれていたら 39

Slide 40

Slide 40 text

例: 動きの説明 後から呼ばれたほうが実行 40

Slide 41

Slide 41 text

41 コードはシンプル @Async public waitAndClose(int testSessionID){ UUID myId = UUID.create(); <- 呼ばれたら UUID作成 cache.set(testSessionID, myId); <- Redisに保存 Thread.sleep(5 * 100); <- 500ms待つ UUID latest = cache.get(testSessionID); <- Redisから取得 if(myId == latest){ <- 値の照合 testSessionsService.close(testSessionID); <- 実行 cache.del(testSessionID); <- Redisから値消す } }

Slide 42

Slide 42 text

ここまでのまとめ Server-Side Debounce ◦ サーバ跨いで処理するのでsynchronizedは使えない ▫ というか使っても同期できない ◦ なのでRedisをロック機構に使う ▫ Redisは基本処理がAtomicなので便利 ◦ サーバ再起動の時に処理が失われる可能性がある ▫ 処理自体のシリアライズなどの仕組みが必要 ▫ 99%はこれで上手く行くのでもっと余裕が出来たら考える 42

Slide 43

Slide 43 text

43 実際のコード v1

Slide 44

Slide 44 text

実際のコード v1 44 疑似コードとの変更点 ◦ 処理を引数にLambdaで渡せるように ▫ 非同期で値は返せないのでRunnable ◦ delayも引数で渡せる ▫ ユーザーや処理によって変更できるように ◦ 疑似コードからほとんど変化なし ▫ コード自体は数行でシンプルなもの ◦ エラー処理の追加

Slide 45

Slide 45 text

Race Conditionがあった 45

Slide 46

Slide 46 text

Race Conditionがあった 処理の最後にRedisから値を消しているところ ◦ 本質的にはその削除処理はいらない ▫ 次のアクセスがなければRedisのexpireで揮発する ◦ 何故消す処理を入れたのか? ▫ 消すことによって実行されたかどうかの判断が出来る ▫ 残っていたらまだ実行されていないと判断できる ■ 将来的にサーバ再起動遅延させたり、再起動後に残ってた 実行を復活させたりの判断が出来きる 46

Slide 47

Slide 47 text

Race Conditionがあった 終了処理的なお掃除として Redis から削除 47

Slide 48

Slide 48 text

Race Conditionがあった このタイミングで別のリクエストがあるとUUID消されているので 何もアクションしなくなる 48

Slide 49

Slide 49 text

Race Conditionがあった 対応案 ◦ 処理実行中はロックする ▫ Single Threaded Execution パターン的なやつ ▫ サーバ跨いでロックするのは大変 ◦ 削除時Keyだけではなく値も一致してる時だけ消す ▫ 処理実行前に値が同じことを確認しているので終わった後、消 すタイミングでも確認してから消す ▫ こっちが良さそう 49

Slide 50

Slide 50 text

削除時、Keyだけではなく 値も一致してる時だけ消す Redisのデータ削除時に条件付きでしかもAtomicに実行したい 50

Slide 51

Slide 51 text

削除時、Keyだけではなく値も一致してる時だけ消す やりたいことは ◦ synchronized して値が一致したら削除する。 ▫ コードで書くとこういう感じ ▫ ただし、サーバを跨いで(ry 51 public boolean deleteIfValueEquals (K key, V value) { synchronized (cache) { if (Objects.equals(cache.get(key), value)) { cache.del(key); return true; } } return false; }

Slide 52

Slide 52 text

RedisはLuaスクリプト実行できる しかもスクリプト実行もatomicなので複数処理をatomicに実行できる。 まさに Single Threaded Execution パターン 52

Slide 53

Slide 53 text

削除時、Keyだけではなく値も一致してる時だけ消す Lua スクリプト実行するだけ 53 @Language("lua") private static final String ATOMIC_CHECK_AND_DELETE_LUA_SCRIPT = """ if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end """; public boolean deleteIfValueEquals(K key, V valueOfKey) { return redis.eval( ATOMIC_CHECK_AND_DELETE_LUA_SCRIPT, ScriptOutputType.BOOLEAN, (K[]) new Object[] {key}, valueOfKey); }

Slide 54

Slide 54 text

出来た!! お客さんにデータ送ってもらうの再開するた めに急いだけど2日くらいで実装できた。 54

Slide 55

Slide 55 text

出来た! サーバが何台あっても、高頻度のリクエストが来ても 一度だけ実行される処理 ◦ コードは10行程度 ▫ 個人的にはこういう短いコードでちょっと頭使うロジック好き ◦ リクエストを跨いだマルチスレッド処理 ▫ Redisの特性活かせばロックとかも簡単 ◦ ボスがロックスターだと便利w ▫ さくっとRace conditionとか発見してくれる 55

Slide 56

Slide 56 text

Server-Side Debounce v2 現実の問題は そんなに単純じゃない 56

Slide 57

Slide 57 text

もう少し間隔が長くてたくさん送って くるテナントも発生してきた ちょっとづつdelayの時間調整とかしていたけど対応が必要になっ た。 57

Slide 58

Slide 58 text

Debounce Logicの強化 運用してたらリクエストが10 秒のdelayの直後に再発生す ることが多く観測された。さ らに調査してみると1分の delayの方が多くのケースで 最適であり、不必要な処理を 大幅に削減できることがわ かった。 58

Slide 59

Slide 59 text

もう少し間隔が長くてたくさん送ってくるテナントも発生してきた このように10秒は超えるが1分以内に呼ばれている ◦ その前とか見るとHOST違っても10秒以内ならちゃん とキャンセルされてる 59

Slide 60

Slide 60 text

Debounceはdelay時間の調整がキモ 60 500 ms 100 ms

Slide 61

Slide 61 text

Debounceはdelay時間の調整がキモ delay時間が・・・ ◦ 長過ぎた場合 ▫ 処理が実行されるまで時間がかかる、即時性が失われる ◦ 短すぎた場合 ▫ delayがあまり意味をなさずに全て実行されてしまう 調整して10秒になってたけど、更に遅延させるの?テスト時間短くした くてサービス使ってもらってるのに?1分が長いか短いか論争に。 (そもそも重い処理なので処理を始めてからも時間かかるので) 61

Slide 62

Slide 62 text

Server-Side Debounce v2 案 Debounceで実行する処理の特性を利用する 62

Slide 63

Slide 63 text

2個の実現したいこと 今回の件では相反する2個の実現したいことがある ◦ 重い処理なのでなるべくまとめて実行したい ▫ OpenAIのAPI呼び出しなどもあるので逐次実行するとお金もかか りすぎる -> 出来ればdelay5分とかにしたい ▫ 負荷対策など主に運営側の問題 ◦ ユーザーにはなるべく早く届けたい ▫ 通知やテスト結果の色々な情報など ▫ これはサービスの価値やクオリティの話 63

Slide 64

Slide 64 text

Debounceは処理を複数回実行しても良い 処理を間引きたいのであって複数回実行しても良い ◦ スパイクや過度な実行を防ぐのが目的 ▫ そもそも10秒以上delayがあるやつは複数回実行されている ◦ 最後の呼び出しが実行されれば良い ▫ これはThrottleも一緒で間引くけど最後の呼び出しは実行する 早く届けつつ処理は間引く・・・ 最初と最後の2回呼べばいいんじゃない!? 64

Slide 65

Slide 65 text

65 V2 案

Slide 66

Slide 66 text

即時実行 最初のリクエストの直後に アクションが実行され、重 要な更新が遅延なく確実に 実行される。 この即時実行は、迅速な応 答が重要な通知などに V2 案 条件付き遅延 その後のリクエストからは 5分のdelayのDebounce処 理。 このDebounce処理は、デー タが全て集まった後、分析 や推論などの重い処理をす る 66

Slide 67

Slide 67 text

67 V2 案 即時実行と遅延実行の合わせ技 ◦ 最初の呼び出しは即実行 ◦ 2回目以降の呼び出しはdelayが長めのDebounce ◦ 最後の実行は最後の呼び出しから5分後になる

Slide 68

Slide 68 text

68 V2 案 相反する2個の課題に対して1つで対応するのではなく それぞれ別の呼び出しで対応する ◦ 速報値と確定値のようなイメージ ちなみにThrottleのライブラリなどではLeading Edge + Trailing Edgeみたいな形でデフォルトの挙動がこうなっているものもある

Slide 69

Slide 69 text

実装はどうなるか すごく単純 ◦ 最初にRedisにUUIDを登録する時にもともと値が入っていなかったら 即時実行する。ただしサーバを跨いで(ry 69 var prev = cache.get(key); if (prev == null) { cache.set(key, DUMMY); runnable.run() ; return; } . . // 通常の(長いDelayの)Debounce処理

Slide 70

Slide 70 text

実装はどうなるか ここもRace conditionになりうる ◦ 最初の想定である1秒間に10回以上のスパイク時には簡単に発生しそう ◦ この処理もRedis内でatomicにする必要がある 70 var prev = cache.get(key); if (prev == null) { <-この処理の間に cache.set(key, DUMMY); <-別のリクエストがあるとダメ runnable.run() ; return; } . . // 通常の(長いDelayの)Debounce処理

Slide 71

Slide 71 text

RedisのSET コマンド 実はGETというパラメータが ある。それを渡すと値をセッ トした時にもともと入ってい た値を返してくれる。 71

Slide 72

Slide 72 text

実際の実装 値をsetしていた場所をちょっと修正するだけ ◦ setGet に変更 ◦ 前の値がなかったら即時実行 72 if (cache.setGet(key, uuid) == null) { runnable.run() ; return; } cache.set(key, uuid);

Slide 73

Slide 73 text

出来た!! 即時実行と遅延実行を組み合わせたDebounce の改良型 73

Slide 74

Slide 74 text

出来た! 今回の修正もシンプル ◦ コードの変更は3行くらい ▫ しかもRedisにも最初からatomicな処理があった ◦ v1 のちょっとしたリファクタリングも ▫ Threadを生で触るのではなくSpringのTaskSchedulerを使う ◦ 今度こそ大丈夫なはず! ▫ と思ったけど...この実装はすぐにダメになった 74

Slide 75

Slide 75 text

Server-Side Debounce v3 現実の問題は やっぱりそんなに単純じゃない 75

Slide 76

Slide 76 text

テストの量が大量なので2つに分割し て送ってるテナントもいた 並列実行とかではなく、件数が多いのでリクエストを2つに分けて いた。(これは自分たちのクライアント側の実装) 76

Slide 77

Slide 77 text

テスト結果の数が膨大なときは分割して送る 私達のサービスにはclientもある ◦ リクエストデータが膨大なときは分割して送る ▫ タイムアウトとかを避けるため ■ CI側も長時間レスポンス無いと失敗にしたりする ▫ そもそもコネクション握りっぱなしも良くないし ◦ ちなみにCI/CDサーバに入れるツールなので ▫ 何をしているか見えるようにOSSに ▫ Githubに公開している 77

Slide 78

Slide 78 text

テスト結果の数が膨大なときは分割して送る 2分割を送信時、v2だとどうなるか ◦ 1個目のリクエストは即時実行、2個目のリクエストは遅延実行 ▫ 2個目はdelay長めのDebounceになる 78

Slide 79

Slide 79 text

テスト結果の数が膨大なときは分割して送る 最終結果がなかなか届かない ◦ 速報はくるが、2個目の実行が5分後になる ▫ このときは2分割してたので半分がなかなか来ない 79

Slide 80

Slide 80 text

このケース、v1だと上手く行く 80 v2 v1

Slide 81

Slide 81 text

Debounce Logicの強化再び v1、v2それぞれ適している ケースと適していないケース が存在している。 それを解決するためにv3が必 要。 81

Slide 82

Slide 82 text

82 今までの問題をまとめると ◦ 短期間に大量に来るスパイクへの対応 ▫ v1 - シンプルなDebounce ◦ 緩やかだが量が多いものをまとめる ▫ v2 - 即時実行と遅延実行をするDebounce ▫ もちろんv1の問題も一緒に解決する ◦ スパイクではないが短期間(2分割で送信など) ▫ v3 - イマココ ▫ もちろんv1、v2の問題も一緒に解決する

Slide 83

Slide 83 text

83 🤔

Slide 84

Slide 84 text

“ JavaなどのListの内部実装のように、初 期割り当てよりも多くのデータを受信し たときに配列を拡張するようなものはど う? 同じようにDelayを時間に基づいて拡張 していく感じで 84

Slide 85

Slide 85 text

それだ!! delay時間をフレキシブルに対応させるような 処理にすればいいんだ! 85

Slide 86

Slide 86 text

DelayをフレキシブルにするDebounce delayをリクエスト間隔に基づいて拡張していく ◦ 最初は短いdelayでDebounce ▫ 初期値は5秒とか短めにしておく ◦ 次のリクエストが来たら拡張 ▫ 一個前のリクエストの時間との差分を取る ▫ 差分が前回のdelayより多かったら差分の方を次のdelayにする ▫ 極端にならないように最小値、最大値は決めておく 86

Slide 87

Slide 87 text

v3 ややこしいので図で説明 初期値&最小値を5秒にします ◦ 1回目のリクエストは5秒のdelayのdebounce開始 87

Slide 88

Slide 88 text

v3 ややこしいので図で説明 5秒以内に次のリクエストがなかったら実行 ◦ ここまでは普通のdebounce 88

Slide 89

Slide 89 text

v3 ややこしいので図で説明 次のリクエストが3分後にきたら 89

Slide 90

Slide 90 text

v3 ややこしいので図で説明 次のリクエストが3分後にきたら ◦ 1個前のリクエストの時間との差分をdelayにするので3分のdelayの decounce開始 90

Slide 91

Slide 91 text

v3 ややこしいので図で説明 さらに次のリクエストが2分後にきたら ◦ 1個前のリクエストの時間との差分を2だが、現在の3分のdelayのほ うが大きいのでそちらを引き続き使う 91

Slide 92

Slide 92 text

v3 ややこしいので図で説明 間隔が長いときはv2に似た動きになる ◦ 即時実行と遅延実行に似ている 92 v2 v3

Slide 93

Slide 93 text

v3 ややこしいので図で説明 間隔が短いときはv1に似た動きになる 93 v1 v3

Slide 94

Slide 94 text

v3 ややこしいので図で説明 2分割送信時のときも問題なし! ◦ v1 のときと同じただのdebounce処理になるだけ 94

Slide 95

Slide 95 text

実装はどうなるのか そもそも動作の理解が難しいので実装も... 95

Slide 96

Slide 96 text

実装はどうなるか 保持しておくべきデータができた ◦ delayの決定方法は次の通り ▫ 一個前のリクエストの時間との差分を取る ▫ 差分が前回のdelayより多かったら差分の方を次のdelayにする ◦ つまり「前回のリクエスト時間」「前回使ったdelay」を保存してお く必要が出来た ▫ しかもサーバ跨いで取得できなくてはいけない ◦ しかもそれもatomicにとか(ry。めんどい... 96

Slide 97

Slide 97 text

実装はどうなるか 伏線回収(v1の動きの説明) 97

Slide 98

Slide 98 text

実装はどうなるか 伏線回収(v1の動きの説明) 98 すでにAtomicに値を保存している場所あった!!

Slide 99

Slide 99 text

実装はどうなるか ここだ!!! ◦ UUIDの変わりにこのデータ入れておけば良い! ▫ リクエスト時間とdelayを保持するValueObjectを作成し、 それをシリアライズしてUUIDの代わりに使って保持すれば良い 99 public record CallTrack(Instant time, Duration delay) implements Serializable {}

Slide 100

Slide 100 text

実装はどうなるか こんな感じでUUIDの変わりに使う 100

Slide 101

Slide 101 text

実装はどうなるか delay決定部分 101 @VisibleForTesting static CallTrack calculateNextCallTrack(@Nullable CallTrack prev, Instant now) { if (prev == null) { return new CallTrack(now, DEFAULT_MINIMUM_DELAY); <- 初回呼び出し時はデフォルト値使う } Duration observedInterval = Duration.between(prev.time(), now); <- 一個前の呼出からの時間 Duration delay = min(max(prev.delay(), observedInterval), MAX_DELAY); ↑ 一個前の呼出からの時間と前回使ったdelayの長い方、長過ぎたらMAX_DELAYの方を使う return new CallTrack(now, delay); }

Slide 102

Slide 102 text

実装はどうなるか debounce 本体実装 102 @Async public void debounce(Key key, Runnable runnable) { String keyString = key.toString(); CallTrack prev = cache.get(keyString); CallTrack track = calculateNextCallTrack(prev, Instant.now()); cache.put(keyString, track); taskScheduler.schedule( () -> { if (!track.equals(cache.get(keyString))) { return; } runnable.run(); }, track.scheduledAt()); <- time.plus(delay) を返してるだけ }

Slide 103

Slide 103 text

実装はどうなるか 103 @Async public void debounce(Key key, Runnable runnable) { String keyString = key.toString(); CallTrack prev = synchronizationMap .get(keyString); CallTrack track = calculateNextCallTrack(prev, Instant. now()); synchronizationMap .put(keyString, track); taskScheduler .schedule( () -> { if (!track.equals(synchronizationMap .get(keyString))) { return; } runnable.run(); }, track.scheduledAt()); } @VisibleForTesting static CallTrack calculateNextCallTrack (@Nullable CallTrack prev, Instant now) { if (prev == null) { return new CallTrack(now, DEFAULT_MINIMUM_DELAY); } Duration observedInterval = Duration. between(prev.time(), now); Duration delay = min(max(prev.delay(), observedInterval), MAX_DELAY); return new CallTrack(now, delay); } public record CallTrack(Instant time, Duration delay) implements Serializable { Instant scheduledAt () { return time.plus(delay); } } 全体のコード量 全体でも30行以下。 言葉で説明するより コードのほうがシンプ ルでしょ? でも理解するの難しい よね><

Slide 104

Slide 104 text

実装はどうなるか 104 全体のコード量 事実コミットしてある のコードもコメントの ほうが3倍位量があ る。

Slide 105

Slide 105 text

実装はどうなるか 105 書いてくれたのも... でもボスも簡単な処理がどう動くのか理解に 時間かかったって言ってるから大丈夫!(何 が?)

Slide 106

Slide 106 text

今度こそ...... . 106

Slide 107

Slide 107 text

出来た!! 3回作り直した結果、上手く行くDebounceが出 来た 107

Slide 108

Slide 108 text

まとめ Server-Side Debounceの実装につ いて3回やってきた感想 108

Slide 109

Slide 109 text

109 Server-Side debounce まとめ ◦ サーバを跨いだマルチスレッドプログラミング ▫ atomicな処理が必要だったり楽しい ◦ 現実は複雑だ... ▫ 3回書き直すとは...でも解決できてよかった!! ◦ コード自体は至極シンプルに ▫ 30行以下で実装出来てるの自分でもビビる ▫ やっぱりシンプルなコードで複雑な問題に対応できるの楽しい ◦ やっぱりボスがロックスターだと便利w

Slide 110

Slide 110 text

v4 作るなら... やりたいことはまだある 110

Slide 111

Slide 111 text

111 v4 作るなら ◦ workspace毎にdelayのデータを貯めたい ▫ 今はロックとしても使っているのですぐに上書きされるし、一 日で揮発する ▫ データを元に外れ値弾いたり平均だしたりして初回debounceか ら適切な時間を使えるように... ◦ JobQueueにする ▫ 間引く仕組みは出来たのでAWS SQSとか使ってJobQueueにして完 全に処理を移譲したい ▫ ただし今のようにlambdaでさくっと書けたりしなくなるので悩 ましい

Slide 112

Slide 112 text

おしまい! 112