ex async evals can't load/use GLMakie — GLFW requires thread 1, but Threads.@spawn lands on default pool #21
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
kahliburke/Kaimon.jl#21
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The
exMCP tool routes evals to:eval_async, which usesThreads.@spawn(default pool). GLFW'srequire_main_thread()then trips on any GLMakie code:This affects both module init (
using GLMakie) and every plot call (display(fig), screen creation), so plotting is unreachable fromexeven after pre-loading GLMakie.This is a follow-up to #11 with a deterministic repro and root cause.
Repro
Then via MCP
ex:Or, if GLMakie is already loaded:
Threads.threadid()from insideexconfirms it lands on the default pool (e.g. thread 22).Root cause
gate.jl:1565usesThreads.@spawnfor:eval_async, which schedules onto the default thread pool — never guaranteed to be thread 1. GLFW's main-thread guard (GLFW.jlsrc/GLFW.jl:26) checksThreads.threadid() == 1on every callback registration.Workaround (verified working)
Kaimon's REPL backend task lives on thread 1 and is exposed via
Base.active_repl.backendref.REPL.eval_on_backend(ast, backend)sends an AST through the REPL channel, whichrepl_backend_loopprocesses viatoplevel_eval_with_hooks(proper world-age handling). I define this once per session:Then anything thread-1-sensitive is wrapped:
Works end-to-end —
using GLMakie, figure creation,display(fig), all on thread 1. Variables persist inMainacrossexandmain_evalcalls in the same session.Note:
REPL.call_on_backend(f, backend)(the function-call path) doesn't work for closures defined post-startup —repl_backend_loopcallsf()directly withoutinvokelatest, so any closure defined after the REPL backend started hits a world-age error. Only the AST path (eval_on_backend) is usable fromex.Interesting observation
gate_evalalready does the right thing for sync:eval(gate.jl:906–908): it detectshas_repl && on_interactiveand routes throughREPL.call_on_backend(() -> _eval_with_capture(expr), backend). That closure is "old" because Kaimon was loaded before the REPL backend started, so it dodges the world-age issue. But theexMCP tool always uses:eval_async(presumably so long-running evals can be promoted to background jobs), losing this routing.Suggested fixes
A few options, ordered by my guess at impact-vs-effort:
exlikemain_thread=truethat routes througheval_on_backend(AST path). Cheap, explicit, doesn't break the default async-promotion flow. Probably the right call for plotting workflows.:eval_asynctasks withThreads.@spawn :defaultand a sticky pin to thread 1 when an active REPL backend exists. More invasive, may interact with promotion/cancellation.main_evalworkaround in the README under "Plotting" — at minimum a paste-able snippet so users hitting this don't have to spelunk gate.jl.Happy to PR (1) or (4) if useful.
Environment
~/.julia/packages/Kaimon/RY8VZ--threads=auto,2 --gcthreads=1(default kaimon launch flags)@prittjam Funny that you mention this, because I've been running into exactly the same thing with some recent stuff I've been trying to do with GLMakie!
Got a couple of workarounds I've been using, but they're a little kludgy. So I would certainly entertain a PR if you want to take a stab at it. Otherwise, know that it's something on my mind and also near and dear to my heart?
Fixed in v1.2.2 (commit
d95bfd5). Theextool now acceptsmt=true(main-thread mode) which routes evals through the REPL backend on thread 1.Thanks for the excellent write-up @prittjam!