putlfAbsent()の限界と変更検知の改善

putlfAbsent()の限界と変更検知の改善

1. はじめに

リアルタイム監視システムでは、特定の値に対する計算式のメタデータを複数のサービス間で同期する必要があるケースが多くあります。今回のプロジェクトでも、計算式が生成または変更された際にイベントを発行し、他のサービスへ変更内容を伝達する機能が必要でした。

初期実装では、同一の計算式が重複して生成されないように ConcurrentHashMapputIfAbsent()を使用しました。条件文に putIfAbsent()を組み込み、処理を通過した場合にのみ計算式伝達イベントを発行するよう実装しました。計算式は通常、一度生成されると変更されないと判断していたため、最初の登録だけを処理すれば十分だと考えていました。

しかし、さまざまな運用シナリオを検討する過程で、予期していなかった問題を発見しました。一部の計算式は、初期設定値の構成だけでなく、利用可能なセンサー信号の構成に応じて計算式自体が変更される可能性があり、従来の構造ではこのような変更を検知できませんでした。

たとえば、特定の計算式はAという信号だけが存在する場合、次のように生成されます。

 Value = A + 1.01

しかし、運用中にBという信号の true/falseに応じて、計算式は次のように変更される可能性があります。

 Value = A - 1.01

つまり、同一の計算式であっても、利用可能な信号の構成に応じて計算式自体が変わる可能性があるということです。

問題は、putIfAbsent()が、すでに登録されている keyに対しては何も処理を行わない点です。そのため、計算式が変更されても既存の値はそのまま維持され、変更イベントも発行されなくなります。

結果として、他のサービスは最新の計算式を受け取れなくなり、サービス間でメタデータの不一致が発生する可能性があります。

この記事では、初期設計で putIfAbsent()を選択した理由とその限界、そして replace()を活用して変更検知とイベント同期を同時に処理できるよう改善した過程を整理します。

2. 初期設計 - putIfAbsent()を活用した重複生成の防止

まず、設計を行う前に、プロジェクトで使用されている計算式を確認しました。初期設計時に計算式を調べたところ、すべて初期設定値を条件とする計算式でした。また、このように一度生成した計算式は、初期設定値が変わらない限り運用中に変更されないと判断しました。そのため、計算式は最初に一度だけ保存すればよいと考えました。

計算式が初めて生成される時点でメモリに保存し、同時に他のサービスへ計算式情報を伝達するためのイベントを発行する構造を設計しました。

また、この機能は複数のスレッドから同時にアクセスされる環境で動作する必要があったため、計算式情報を保存するキャッシュとして ConcurrentHashMapを使用しました。初期実装は非常に単純でした。

image1.png

putIfAbsent()は、指定した Keyが存在しない場合にのみ値を保存し、すでに存在する場合は既存の値を返します。そのため、最初に計算式が生成された場合にのみイベントを発行でき、同一の計算式に対する重複イベントの発生も自然に防止できました。

当時は、計算式は一度生成されると変更されないと判断していました。なぜなら、初期設定値はほとんど変更されることのない固定値だったからです。仮に変更されたとしても、サービスを再起動してメモリを初期化すれば、新しい計算式を再生成できると考えていました。

3. 新しい計算式要件の検討過程で発見した問題

初期実装を終えた後、別の計算式機能に関する要件を検討する会議に参加することになりました。この機能は今回の開発案件とは別の計算式機能でしたが、要件を検討する中で、既存の設計にも影響を与える可能性のある部分を発見しました。

会議で議論された一部の計算式は、単に初期設定値だけを使用するのではなく、センサー信号の有無や信号の値に応じて、計算式自体が変わる可能性がありました。

たとえば、特定のセンサー信号が存在しない場合はAだけを使用して計算を行いますが、そのセンサーが正常に収集され始めると、AとBを併用する計算式に変更する必要がありました。あるいは、Bという信号の値がtrue/efalseのどちらであるかに応じて、計算式を変更する必要がありました。

この要件を検討する中で、現在実装されている putIfAbsent()ベースの構造を改めて見直すことになりました。そして、既存の構造では、すでに生成された計算式が変更されても、それを検知できないという事実を発見しました。

4. 計算式の変更検知に向けた要件の再定義

従来の構造は、計算式の初回生成時にイベントを発行するという要件だけを満たしていました。しかし、新しい要件に対応するためには、これだけでは不十分でした。計算式が変更される可能性があるなら、初回生成だけでなく、変更も検知できなければなりません。

整理すると、次の条件を満たす新しい構造が必要でした。

1. 計算式の初回生成時にイベントを発行

2. 計算式の変更時にイベントを発行

3. 計算式が同一の場合はイベントを発行しない 

4. マルチスレッド環境でも安全に動作

putIfAbsent()は最初の生成のみ処理できました。すでに登録されている Keyについては、新しい計算式が渡されても比較したり、変更の有無を確認したりすることができませんでした。そのため、計算式の変更を検知できる別の構造が必要でした。

5. ConcurrentHashMapのreplace()を活用した変更検知

当初は、単純に既存の値を取得して比較し、変更の有無を判断する方法も検討しました。

image2.png

しかし、この方法はマルチスレッド環境では安全ではありませんでした。たとえば、2つのスレッドが同時に同じ計算式情報を変更しようとした場合、両方のスレッドが既存の値を取得した後、変更されたと判断する可能性があります。この場合、同じ計算式の変更に対してイベントが重複して発行される可能性があります。

そこで、この問題を解決するために、ConcurrentHashMapが提供する replace() メソッドを活用しました。

image3.png

replace(key, oldValue, newValue)は、現在の Mapに保存されている値が oldValueと同一の場合にのみ、newValueに置き換えます。つまり、次のように動作します。

image4.png

別のスレッドが先に値を変更した場合、現在の Mapの値はすでに oldValueではなくなっています。この場合、replace()falseを返し、値の置き換えを実行しません。したがって、実際に計算式の変更に成功したスレッドだけがイベントを発行できるようになります。最終的には、replace()の戻り値を利用して、イベントを発行するかどうかを決定しました。

これにより、計算式の変更が発生した場合にのみイベントを発行できるようになり、同時にマルチスレッド環境で発生する可能性のある重複イベントの問題も防止できました。

6. おわりに

今回この機能を開発する際、当初は単純に計算式の重複生成を防ぐことだけに焦点を当てていました。初期要件だけを考慮した場合、putIfAbsent()を利用した構造だけでも十分に見えました。実際、同じ計算式の重複保存と重複イベント発行を効果的に防止できていたためです。

しかし、新しい計算式の要件を検討する過程で、運用中に計算式が変更される可能性があることに気付き、既存の設計ではこの状況に対応できないことを確認しました。これを解決するため、計算式の最初の生成だけでなく、変更の有無まで検知できるように構造を改善し、ConcurrentHashMapreplace()を活用して、マルチスレッド環境でも安全に動作するよう実装しました。

今回の経験を通じて、現在の要件を満たすだけの設計ではなく、今後発生する可能性のあるシナリオまで考慮することが重要だと改めて感じました。また、単に機能を実装して終わりにするのではなく、さまざまなシナリオを検討し、設計の限界を見つけて改善するプロセスも開発における重要な部分であることを学びました。

Yang

Site footer