Repository navigation
fix: make Query !Send/!Sync, add QueryHandle for multithreaded access - #300
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Query<T>hadunsafe impl Send/Syncgated only on its component tuple beingSend. That bound is insufficient: iterating a query reads world storage (tables, query cache) with no synchronization, while the!SendWorldmutates on its owning thread lock-free. Safe code could therefore move a query to another thread and iterate it while the main thread mutated the world, a data race that crashes (STATUS_FAIL_FAST_EXCEPTIONon Windows). This also answered an inconsistency from review:EntityViewwas already correctly!Send, whileQuery, with the same world pointer, was not.Changes
Queryis now!Send/!SyncLike
World. The original repro is now a compile error, locked in by trybuild tests.New:
QueryHandle: the sound path to queries in multithreaded systemsThe legitimate pattern that relied on the old impls (capturing a query in
par_each/par_runcallbacks and iterating viaiter_stage, mirroring upstream flecs) is preserved through a new type:QueryHandleis Send/Sync only when the component tuple is Send, keeps the query alive like a clone, and cannot be iterated on its own; every iteration requires a world/stage handle already valid on the current thread. Off the owning thread such a handle only exists inside flecs-managed staged execution, where the world is read-only and writes are deferred. Its Drop/Clone synchronize with world teardown through a shared lock, so off-thread handle drops can never interleave with ecs_fini.Additional soundness fixes found during review
iter_stage()(on Query and QueryHandle) now asserts the stage belongs to the query's world. The C side does not validate this; a mismatched pair walks foreign tables with the wrong query plan. This was reachable in safe code before this PR.Changelog user facing
Breaking
Query<T>is now!Send/!Sync(previouslySend/Syncwhen its component tuple wasSend). The old impls were unsound: iterating from another thread raced world mutation and could crash. To use a query inside multithreaded (par_*) system callbacks, capture aQueryHandleviaQuery::handle()and iterate with handle.iter_stage(stage); existing iter_stage call sites inside callbacks need no changes, only the capture.Added
QueryHandle: thread-shareable handle to a query forpar_*system callbacks.Send/Syncwhen the component tuple isSend; keeps the query alive;Clonesupported.Fixed
iter_stage()now panics when passed a world/stage from a different world than the query (previously undefined behavior).flecs_force_enable_ecs_assertsfeature now actually keeps flecs C asserts in release builds (macro name typo).Changelog internals
WorldCtxgainsworld_dead: Arc<Mutex<bool>>;World::dropmarks it under lock beforeecs_fini, andQueryHandle::drop/clonehold it across refcount operations so off-thread handle teardown cannot interleave with world destruction. Owning-thread drops duringecs_fini(handles in system closures) still run cleanup; post-teardown drops are detected viais_panicking.WorldCtx::is_panickingchangedCell<bool>→AtomicBool(read off-thread by handle drops).ecs_query_fini(would mutate world allocator state); memory is reclaimed at world fini.Query::handle()requiresecs_os_has_threading()(atomic poly refcounts); asserted at construction.world_ctx.rsupdated: par systems are guarded byTupleType: Sendbounds on registration;Queryitself is!Send.Queryerrors now via auto-traitNonNullfields instead of unsatisfied bounds).