{"record":{"id":"bafe1da948e6ec6d","repo":"GopeedLab/gopeed","slug":"engine-loop-terminated","errorCode":null,"errorMessage":"engine loop terminated","messagePattern":"engine loop terminated","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/download/engine/engine.go","lineNumber":126,"sourceCode":"\t\t\t\treturn\n\t\t\t}\n\t\t\tonFulfilled := runtime.ToValue(func(call goja.FunctionCall) goja.Value {\n\t\t\t\tsendResult(result{value: exportJSValue(call.Argument(0))})\n\t\t\t\treturn goja.Undefined()\n\t\t\t})\n\t\t\tonRejected := runtime.ToValue(func(call goja.FunctionCall) goja.Value {\n\t\t\t\tsendResult(result{err: exportJSError(call.Argument(0))})\n\t\t\t\treturn goja.Undefined()\n\t\t\t})\n\t\t\tif _, err := thenFn(promiseObj, onFulfilled, onRejected); err != nil {\n\t\t\t\tsendResult(result{err: err})\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tsendResult(result{value: exportJSValue(value)})\n\t})\n\tif !ok {\n\t\treturn nil, errors.New(\"engine loop terminated\")\n\t}\n\tres := <-ch\n\tif res.err != nil {\n\t\treturn nil, res.err\n\t}\n\treturn res.value, nil\n}\n\nfunc (e *Engine) Close() {\n\te.loop.Terminate()\n}\n\ntype Config struct {\n\tProxyConfig  *base.DownloaderProxyConfig\n\tStreamConfig *stream.Config\n}\n\nfunc NewEngine(cfg *Config) *Engine {","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/GopeedLab/gopeed/blob/7b7327ffb30816273a74b142cccc0bc10c5a4c67/pkg/download/engine/engine.go#L108-L144","documentation":"In Engine.runOnLoop (pkg/download/engine/engine.go:125-127), eventloop.RunOnLoop returns false when the JS event loop is no longer running — i.e. the loop was terminated. The engine then reports 'engine loop terminated'. Every engine API that marshals work onto the loop (RunString, and everything built on runOnLoop) fails with this once Engine.Close() (engine.go:135-137, loop.Terminate) has been called or the loop exited on its own.","triggerScenarios":"Calling engine.RunString / any engine API after Engine.Close(); a concurrent Close from another goroutine (e.g. shutdown or error cleanup) while a script call is in flight; the underlying goja eventloop terminating due to unhandled termination inside script execution; using an engine stored in a pool that was drained and closed.","commonSituations":"Shutdown races: extension finishes and closes its engine while a stats/progress poller still queries it; reusing a closed engine because the close error was swallowed; per-request engines in servers being closed by a timeout path but still referenced by in-flight handlers.","solutions":["Ensure no callers use the engine after Close: close it last, after all goroutines that reference it have stopped","Track a closed flag (atomic.Bool) set before calling Close and check it at API entry points","If the error surfaces, recreate the engine (NewEngine) and re-run the script — the old one is unrecoverable","Order operations: stop producers/pollers -> drain -> Engine.Close()"],"exampleFix":"// before\ne := engine.NewEngine(cfg)\n// ... later, concurrently:\ngo e.Close()\n_, err := e.RunString(\"1+1\") // \"engine loop terminated\"\n\n// after\ntype safeEngine struct { *engine.Engine; closed atomic.Bool }\nfunc (s *safeEngine) Run(script string) (any, error) {\n    if s.closed.Load() { return nil, errors.New(\"engine closed\") }\n    return s.Engine.RunString(script)\n}\nfunc (s *safeEngine) Close() { s.closed.Store(true); s.Engine.Close() }","handlingStrategy":"try-catch","validationCode":"var engineClosed atomic.Bool\nrun := func(script string) (any, error) {\n    if engineClosed.Load() { return nil, errors.New(\"engine closed\") }\n    return engine.RunString(script)\n}\ncloseEngine := func() { engineClosed.Store(true); engine.Close() }","typeGuard":null,"tryCatchPattern":"v, err := run(script)\nif err != nil && strings.Contains(err.Error(), \"engine loop terminated\") {\n    // engine is dead: recreate with NewEngine and re-run once\n}","preventionTips":["Check a closed flag before every engine API call","Close the engine only after all users have stopped (join goroutines first)","One owner goroutine for engine lifecycle; never close from arbitrary callbacks"],"tags":["engine","lifecycle","concurrency","shutdown"],"backgroundTag":null,"analyzedSha":"7b7327ffb30816273a74b142cccc0bc10c5a4c67","analyzedAt":"2026-08-16T02:51:03.250Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}