{"record":{"id":"f5d6c045bea3d4a2","repo":"stride3d/stride","slug":"cannot-dispose-while-a-job-is-in-flight","errorCode":null,"errorMessage":"Cannot dispose while a job is in flight","messagePattern":"Cannot dispose while a job is in flight","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"sources/engine/Stride.BepuPhysics/Stride.BepuPhysics/BepuSimulation.cs","lineNumber":1186,"sourceCode":"                _workerType = WorkerType.Managed;\n                _managedWorker = workerBody;\n                SignalThreads(maximumWorkerCount);\n                _managedWorker = null;\n            }\n            else if (maximumWorkerCount == 1)\n            {\n                workerBody(0);\n            }\n            _unmanagedContext = null;\n            _managedContext = null;\n        }\n\n        public void Dispose()\n        {\n            if (!_disposed)\n            {\n                if (_managedContext is not null || _unmanagedContext is not null)\n                    throw new InvalidOperationException(\"Cannot dispose while a job is in flight\");\n\n                _disposed = true;\n                WorkerPools.Dispose();\n            }\n        }\n\n        private enum WorkerType\n        {\n            Managed,\n            Unmanaged,\n        }\n\n        private readonly struct Job(DispatcherWrapper wrapper) : Dispatcher.IBatchJob\n        {\n            public void Process(int start, int endExclusive)\n            {\n                for (int i = start; i < endExclusive; i++)\n                    wrapper.DispatchThread(i);","sourceCodeStart":1168,"sourceCodeEnd":1204,"githubUrl":"https://github.com/stride3d/stride/blob/96fad776d210c221682aac1ccdf4c79dc046fc38/sources/engine/Stride.BepuPhysics/Stride.BepuPhysics/BepuSimulation.cs#L1168-L1204","documentation":"This Dispose implementation refuses to release the worker buffer pools while a dispatch job is still in flight: _managedContext or _unmanagedContext is non-null, meaning a dispatch is between begin and completion. Disposing then would free memory the running job still uses, so it throws InvalidOperationException instead of corrupting state.","triggerScenarios":"Calling DispatcherWrapper.Dispose() from another thread while an outstanding dispatch (Begin/End job) has not finished, or tearing down the simulation while worker threads still reference the dispatcher.","commonSituations":"App shutdown racing with a physics step; disposing the simulation in a scene unload while a job started on another thread is mid-execution.","solutions":["Wait for all in-flight jobs to complete (join the dispatch / end the job) before calling Dispose().","Ensure shutdown ordering: stop the simulation loop, then dispose the dispatcher.","Synchronize disposal with the code path that starts jobs (lock or task completion await)."],"exampleFix":"// before\ndispatcher.Dispose(); // while job running\n// after\nawait taskUsingDispatcher; // or dispatcher.EndDispatch(...)\ndispatcher.Dispose();","handlingStrategy":"try-catch","validationCode":"bool isIdle = dispatcher.HasNoInFlightJobs; // expose/check before disposing\ncanDispose = isIdle || _disposed;","typeGuard":null,"tryCatchPattern":"try { dispatcher.Dispose(); } catch (InvalidOperationException) { await inFlightTask; dispatcher.Dispose(); }","preventionTips":["Stop the simulation loop before tearing down the dispatcher","Track in-flight jobs and await their completion in shutdown code","Serialize disposal and dispatch on the same lock or owner thread"],"tags":["physics","threading","disposal","race-condition"],"backgroundTag":"invalid-state-transition","analyzedSha":"96fad776d210c221682aac1ccdf4c79dc046fc38","analyzedAt":"2026-09-14T02:59:31.279Z","contentChangedAt":"2026-09-14T02:59:31.279Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}