Skip to content

Fix data race in LazyObject::calculate() under thread-safe observer pattern - #2774

Closed
amanyadav2022 wants to merge 1 commit into
lballabio:masterfrom
amanyadav2022:fix/lazyobject-calculate-data-race
Closed

amanyadav2022 wants to merge 1 commit into
lballabio:masterfrom
amanyadav2022:fix/lazyobject-calculate-data-race

Conversation

@amanyadav2022

@amanyadav2022 amanyadav2022 commented Sep 9, 2026

Copy link
Copy Markdown

Summary

Fixes #2633.

QL_ENABLE_THREAD_SAFE_OBSERVER_PATTERN makes Observable/Observer registration thread-safe (via a recursive_mutex guarding their internal sets in observable.hpp/.cpp), but LazyObject::calculate() itself reads and writes calculated_/frozen_/failed_/updating_ with no synchronization of its own. So calling calculate() concurrently on the same LazyObject is a data race, even with the macro enabled -- the macro name is a bit misleading, since it does not make LazyObject itself safe to use from multiple threads.

Confirmed with ThreadSanitizer: a minimal repro (8 threads calling calculate() concurrently on one LazyObject, 500 iterations) reproduces the race 10/10 runs, always at the calculated_ check-and-set in calculate().

Fix

Guard calculate() and the other methods touching this state (update(), recalculate(), freeze(), unfreeze(), forwardFirstNotificationOnly(), alwaysForwardNotifications(), isCalculated(), setCalculated()) with a recursive_mutex, following the same locking pattern already used by Observer in this codebase. The mutex is wrapped in a small helper so LazyObject keeps its existing implicit copy/move semantics (a raw std::recursive_mutex member would otherwise silently delete them). The whole change is compiled out when QL_ENABLE_THREAD_SAFE_OBSERVER_PATTERN is off, so there is no cost or behavior change for the default configuration.

Testing

  • New regression test testConcurrentCalculate in test-suite/lazyobject.cpp, guarded by the same macro (matching the existing convention in test-suite/observable.cpp). Verified with real CI-style TSan settings (TSAN_OPTIONS=halt_on_error=1 exitcode=66): fails deterministically (exit 66) before this fix, 5/5 runs; passes cleanly (exit 0) after, 5/5 runs.
  • Full library + entire test suite built and run with QL_ENABLE_THREAD_SAFE_OBSERVER_PATTERN=ON (Debug, GCC 13.3.0): all tests pass, no regressions.
  • Verified the fix and test also compile cleanly with the macro off (default config), confirming zero impact there.

🤖 Generated with Claude Code

…attern

QL_ENABLE_THREAD_SAFE_OBSERVER_PATTERN makes Observable/Observer
registration thread-safe (via a recursive_mutex on their internal
sets), but LazyObject::calculate() itself reads and writes
calculated_/frozen_/failed_/updating_ with no synchronization at
all, so calling calculate() concurrently on the same LazyObject is
a data race, confirmed under ThreadSanitizer (10/10 runs).

Guard calculate() and the other methods touching this state with a
recursive_mutex, following the same locking pattern already used by
Observer. The mutex is wrapped so LazyObject keeps its existing
implicit copy/move semantics, and the whole change is compiled out
when QL_ENABLE_THREAD_SAFE_OBSERVER_PATTERN is off.

Fixes #2633.

Add testConcurrentCalculate to test-suite/lazyobject.cpp, verified
to fail deterministically under TSan before this fix and pass
cleanly after.
@boring-cyborg

boring-cyborg Bot commented Sep 9, 2026

Copy link
Copy Markdown

Thanks for opening this pull request! It might take a while before we look at it, so don't worry if there seems to be no feedback. We'll get to it.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@lballabio

Copy link
Copy Markdown
Owner

Thanks for the contribution, but as I wrote in the original issue, I don't think this is something that can or should be fixed.

@lballabio lballabio closed this Sep 9, 2026
@amanyadav2022
amanyadav2022 deleted the fix/lazyobject-calculate-data-race branch September 9, 2026 17:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LazyObject::calculate() not thread-safe with QL_ENABLE_THREAD_SAFE_OBSERVER_PATTERN

3 participants