Gralkor.Python (gralkor_ex v2.1.3)

Copy Markdown View Source

PythonX runtime owner for the embedded Gralkor stack.

Two responsibilities, both in init/1:

  1. Reap redislite orphans. falkordblite (loaded into PythonX in this BEAM) spawns a redis-server grandchild. A hard BEAM SIGKILL leaves it orphaned. SIGKILL anything matching redislite/bin/redis-server before we boot — safe because this runs before our own Python init, so anything matching is by definition not ours-yet, and the path is unique to falkordblite.

  2. Smoke-import graphiti_core through PythonX so any venv / import failure surfaces at boot rather than on the first real call.

Pythonx's interpreter + venv materialisation are configured in config/config.exs via :pythonx, :uv_init and start automatically with the :pythonx OTP application; Gralkor.Python does not duplicate that work.

See ex-python-runtime in gralkor/TEST_TREES.md.

Summary

Functions

Returns a specification to start this module under a supervisor.

Spin up a daemon-thread asyncio event loop and stash it on asyncio as _gralkor_loop plus a _gralkor_run(coro) helper that submits onto it.

SIGKILL every pid the listing function returns. Pure plumbing — accepts injected list/kill functions so the unit test doesn't have to spawn real redis processes.

Try to import graphiti_core via Pythonx; surface any failure as {:error, _}.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

install_async_runtime()

@spec install_async_runtime() :: :ok | {:error, term()}

Spin up a daemon-thread asyncio event loop and stash it on asyncio as _gralkor_loop plus a _gralkor_run(coro) helper that submits onto it.

Must run once per Pythonx interpreter, before any code that calls into graphiti via asyncio._gralkor_run. Idempotent — the second call is a no-op.

Why: Pythonx.eval creates a fresh event loop per asyncio.run call. AsyncFalkorDB (and any redis-async connection) binds its connections to the loop they were created on; reusing them on a different loop raises "Future attached to a different loop". The spike measured the alternative pattern (Step 6 in pythonx-spike/spike.exs) at ~56µs per call vs ~112µs for asyncio.run — and, crucially, it shares one loop across all calls so connection reuse works.

reap_redislite_orphans(list_orphans, kill_pid)

@spec reap_redislite_orphans((-> [integer()]), (integer() -> any())) ::
  :ok | {:error, term()}

SIGKILL every pid the listing function returns. Pure plumbing — accepts injected list/kill functions so the unit test doesn't have to spawn real redis processes.

smoke_import_graphiti()

@spec smoke_import_graphiti() :: :ok | {:error, term()}

Try to import graphiti_core via Pythonx; surface any failure as {:error, _}.

start_link(opts \\ [])