JuliusBrussee/caveman · error · MiddlewareError

closed

closed

Error message

closed

What it means

The async runtime's _submit refuses to run work after aclose() has been called: it raises MiddlewareError("closed"). This protects against submitting sync functions onto a shut-down executor and leaking tasks after teardown.

Solutions

  1. Keep all runtime calls inside the `async with runtime:` block
  2. Re-create a new runtime instance if you need to use it after close
  3. Track lifecycle in app code: don't close in one component while another still holds the runtime
  4. Catch MiddlewareError with code "closed" and treat it as a lifecycle bug, not retryable

Example fix

// before
runtime = AsyncRuntime(...)
await runtime.aclose()
await runtime.optimize(...)  # MiddlewareError: closed
// after
async with AsyncRuntime(...) as runtime:
    await runtime.optimize(...)  # all usage inside the context
Defensive patterns

Strategy: try-catch

Validate before calling

if runtime_is_closed:
    raise RuntimeError('runtime already closed; create a new instance')

Type guard

def runtime_open(runtime) -> bool:
    return not getattr(runtime, '_closed', True)

Try / catch

try:
    await runtime.optimize(msg)
except MiddlewareError as e:
    if str(e) == 'closed':
        runtime = AsyncRuntime(...)  # recreate
        await runtime.optimize(msg)
    else:
        raise

Prevention

When it happens

Trigger: Calling any runtime entry point — ready, preflight, optimize, retrieve, observe, delete_session — after the runtime was used as an async context manager and exited, or after an explicit await aclose().

Common situations: Calling runtime methods outside the `async with` block; keeping a reference to the runtime after shutdown; a lifespan/handler that closes the runtime while other code still uses it.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/e96d1dbcd2930df6. Report an issue: GitHub.

Appendix: source

Thrown at packages/sdk/python/caveman_cloud/middleware/async_runtime.py:118

    async def delete_session(self, scope):
        return await self._submit(self._runtime.delete_session, scope)

    async def aclose(self):
        self._closed = True
        if self._owns_runtime:
            self._runtime.close()
        await asyncio.to_thread(self._executor.shutdown, wait=True, cancel_futures=True)

    async def __aenter__(self):
        return self

    async def __aexit__(self, *_):
        await self.aclose()

    async def _submit(self, function, *args, **kwargs):
        if self._closed:
            raise MiddlewareError("closed")
        if not self._slots.acquire(blocking=False):
            raise MiddlewareError("capacity")
        context = contextvars.copy_context()
        try:
            future = self._executor.submit(context.run, functools.partial(function, *args, **kwargs))
        except BaseException:
            self._slots.release()
            raise
        # Release only when the underlying worker actually terminates. Releasing
        # on coroutine cancellation would allow unlimited queued/running work.
        future.add_done_callback(lambda _: self._slots.release())
        return await asyncio.wrap_future(future)

View on GitHub (pinned to 3ee70a1026)