Slide 1

Slide 1 text

クライアントサイドでよく使われる Debounce処理 をサーバサイドで3回実装した話

Slide 2

Slide 2 text

@yoshiori 2

Slide 3

Slide 3 text

3 写真もSNSもOK

Slide 4

Slide 4 text

Debounceとは? あんまり聞き馴染みのない言葉かもしれな いので簡単に説明します 4

Slide 5

Slide 5 text

Debounce(デバウンス)って何 特定のイベントが短期間に連続して発生した際に、タ イマーによる入力遅延を利用して一度だけ処理が実行 されるようにする仕組み ◦ 高頻度で発火するイベントを制御する仕組み ▫ よく使われるのはWindowのリサイズやキー、マウスの入力など ▫ 過剰な関数実行を防いだり、ネットワーク通信を減らしたりす る ◦ 基本的にデータは逐次保存してその後の処理だけ遅延する ◦ 似たような処理にThrottle(スロットル)がある 5

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

“ どれが優れているとかではなく要件によって使い分ける レスポンスタイミングがシビアなら逐次実行とか 実行回数そこまで減らさなくて良いならThrottleとか 10

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

なぜサーバサイドで Debounceが必要に? なぜ今回サーバサイドで必要になったのか 説明します 12

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

14 Launchableはテスト結果を収集してい る

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

19 翌日だした Proposal

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

28 実際のコード v1

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

Race Conditionがあった 30

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

削除時、Keyだけではなく値も一致してる時だけ消す Lua スクリプト実行するだけ 38 @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 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

50 V2 案

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

53 V2 案 相反する2個の課題に対して1つで対応するのではなく それぞれ別の呼び出しで対応する ◦ 速報値と確定値のようなイメージ

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

68 🤔

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

実装はどうなるか delay決定部分 86 @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 87

Slide 87 text

実装はどうなるか debounce 本体実装 87 @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()); <- time.plus(delay) を返してるだけ }

Slide 88

Slide 88 text

実装はどうなるか 88 @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 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

今度こそ...... . 91

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

おしまい! 97