You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
ezmsg's shared-memory segments are created by the GraphServer process and handed to clients by name to attach in their own process (shm.py):
server side: SharedMemory(size=..., create=True) inside the server's SHMInfo,
client side: GraphService.create_shm(...) returns the segment name over TCP, and the client runs SharedMemory(name=name, create=False) (in SHMContext).
This silently assumes that anything that can reach the GraphServer over TCP also shares its shared-memory namespace. That assumption breaks in increasingly common setups where TCP crosses a boundary SHM cannot:
WSL2 <-> Windows: WSL forwards 127.0.0.1 ports both ways, so a Windows client connects happily to a Linux-side GraphServer, but then SharedMemory(name='psm_xxxxxxxx', create=False) raises FileNotFoundError: [WinError 2], because the segment exists only in the Linux kernel.
Containers with host/forwarded networking but no shared /dev/shm.
Two user sessions on one machine: same OS, reachable TCP, but SHM access denied -> PermissionError.
The asymmetry that makes it a bug rather than a limitation
The subscriber side already treats SHM as a capability to be probed: the channel handshake in messagechannel.py attaches inside a try/except (ValueError, OSError) and answers the publisher with Command.SHM_ATTACH_FAILED, falling back to TCP for that channel. So, subscriber-side boundary crossings degrade gracefully by design.
On the other hand, the publisher side has no such path: Publisher setup calls graph_service.create_shm(num_buffers, buf_size) (in pubclient.py) and attaches the returned name unguarded. The FileNotFoundError/PermissionError propagates out of publisher creation and takes the whole pipeline down, with a traceback pointing at multiprocessing/shared_memory.py rather than at the transport decision that caused it.
Manual 'escape hatches' exist (force_tcp=True per OutputStream, EZMSG_FORCE_TCP=1 process-wide, EZMSG_ALLOW_LOCAL). In fact, in test_attach.py, we lean one of these escape hatches:
# Force TCP on ack messages to ensure delivery# and avoid SHM deallocation between processesACK=ez.OutputStream(str, force_tcp=True)
The thing is that these workarounds require the user to already know exactly what went wrong, which is precisely what the error does not tell them.
Steps to reproduce
Inside WSL2: start a GraphServer (ezmsg serve --address 0.0.0.0:25978).
From Windows (same machine): run any pipeline against it, e.g. EZMSG_GRAPHSERVER_ADDR=127.0.0.1:25978 python examples/ezmsg_intro.py.
Publisher creation fails with FileNotFoundError: ... 'psm_xxxxxxxx' from SharedMemory.__init__. (Same-OS variant: run server and client as two different users -> PermissionError.)
Contrast: a subscriber-only client crossing the same boundary degrades to TCP via SHM_ATTACH_FAILED.
Solution criteria
A client that can reach the graph over TCP must never hard-fail because an SHM assumption was wrong. The worst outcome should be TCP transport plus a one-time diagnostic naming the boundary.
SHM eligibility must be established by capability (try to attach), not by address heuristics. For example, 127.0.0.1 does not imply a shared SHM namespace (WSL forwarding, containers) and same-host does not imply permission (user sessions).
No regression for the true same-namespace case: SHM remains the default fast path, decided once per publisher/channel, not per message.
If any case remains unrecoverable, the error must name the transport decision: topic, publisher, graph address, and a pointer to force_tcp / EZMSG_FORCE_TCP, but never a bare shared_memory.py traceback.
Observability: the profiling snapshot already labels channels LOCAL/SHM/TCP. A fallen-back publisher should be visible there the same way.
Possible solutions
Mirror the subscriber's fallback on the publisher path (surgical). Wrap the publisher's create_shm-then-attach in the same capability probe: on OSError, release the lease and continue with force_tcp-equivalent behaviour for that publisher (the TCP path already exists and is exercised wherever force_tcp=True is used today), logging one warning naming the boundary. This restores symmetry with SHM_ATTACH_FAILED and fixes WSL, containers, and permission splits in one move.
Cheap namespace probe at session start. When a client first connects, it attaches a tiny well-known probe segment the server creates at boot (or reads a per-boot token from it). Success -> SHM eligible for the session. Failure -> the client requests TCP-only leases up front. Avoids per-publisher discovery cost and lets the graph server know (and report) which clients are SHM-capable.
Identity handshake heuristic (defense in depth, not sufficient alone). Exchange a machine/boot identity in the client handshake and require equality before offering SHM. Catches the cross-kernel cases (WSL, containers) early with zero SHM syscalls, but misses same-host permission splits, so it complements options 1) and 2) rather than replacing them.
At minimum (if no behaviour change is wanted): catch the attach failure at both creation sites in pubclient.py and re-raise with an actionable message ("publisher for topic X cannot attach SHM created by GraphServer at Y, so you are likely across a WSL/container/user boundary, so set force_tcp=True or EZMSG_FORCE_TCP=1"), and document the boundary limitation alongside force_tcp.
Note: fixing this also de-fangs the companion issue about the test suite attaching to foreign servers. The failures there were this defect surfacing through tests. But the test-isolation problem is worth fixing independently (stale foreign graph state remains harmful even with graceful TCP fallback).
What is the issue?
ezmsg's shared-memory segments are created by the GraphServer process and handed to clients by name to attach in their own process (
shm.py):SharedMemory(size=..., create=True)inside the server'sSHMInfo,GraphService.create_shm(...)returns the segment name over TCP, and the client runsSharedMemory(name=name, create=False)(inSHMContext).This silently assumes that anything that can reach the GraphServer over TCP also shares its shared-memory namespace. That assumption breaks in increasingly common setups where TCP crosses a boundary SHM cannot:
127.0.0.1ports both ways, so a Windows client connects happily to a Linux-side GraphServer, but thenSharedMemory(name='psm_xxxxxxxx', create=False)raisesFileNotFoundError: [WinError 2], because the segment exists only in the Linux kernel./dev/shm.PermissionError.The asymmetry that makes it a bug rather than a limitation
The subscriber side already treats SHM as a capability to be probed: the channel handshake in
messagechannel.pyattaches inside atry/except (ValueError, OSError)and answers the publisher withCommand.SHM_ATTACH_FAILED, falling back to TCP for that channel. So, subscriber-side boundary crossings degrade gracefully by design.On the other hand, the publisher side has no such path:
Publishersetup callsgraph_service.create_shm(num_buffers, buf_size)(inpubclient.py) and attaches the returned name unguarded. TheFileNotFoundError/PermissionErrorpropagates out of publisher creation and takes the whole pipeline down, with a traceback pointing atmultiprocessing/shared_memory.pyrather than at the transport decision that caused it.Manual 'escape hatches' exist (
force_tcp=TrueperOutputStream,EZMSG_FORCE_TCP=1process-wide,EZMSG_ALLOW_LOCAL). In fact, intest_attach.py, we lean one of these escape hatches:The thing is that these workarounds require the user to already know exactly what went wrong, which is precisely what the error does not tell them.
Steps to reproduce
ezmsg serve --address 0.0.0.0:25978).EZMSG_GRAPHSERVER_ADDR=127.0.0.1:25978 python examples/ezmsg_intro.py.FileNotFoundError: ... 'psm_xxxxxxxx'fromSharedMemory.__init__. (Same-OS variant: run server and client as two different users ->PermissionError.)Contrast: a subscriber-only client crossing the same boundary degrades to TCP via
SHM_ATTACH_FAILED.Solution criteria
127.0.0.1does not imply a shared SHM namespace (WSL forwarding, containers) and same-host does not imply permission (user sessions).force_tcp/EZMSG_FORCE_TCP, but never a bareshared_memory.pytraceback.LOCAL/SHM/TCP. A fallen-back publisher should be visible there the same way.Possible solutions
create_shm-then-attach in the same capability probe: onOSError, release the lease and continue withforce_tcp-equivalent behaviour for that publisher (the TCP path already exists and is exercised whereverforce_tcp=Trueis used today), logging one warning naming the boundary. This restores symmetry withSHM_ATTACH_FAILEDand fixes WSL, containers, and permission splits in one move.pubclient.pyand re-raise with an actionable message ("publisher for topic X cannot attach SHM created by GraphServer at Y, so you are likely across a WSL/container/user boundary, so setforce_tcp=TrueorEZMSG_FORCE_TCP=1"), and document the boundary limitation alongsideforce_tcp.Note: fixing this also de-fangs the companion issue about the test suite attaching to foreign servers. The failures there were this defect surfacing through tests. But the test-isolation problem is worth fixing independently (stale foreign graph state remains harmful even with graceful TCP fallback).