concurrent-c-node 0.7.0__tar.gz → 0.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {concurrent_c_node-0.7.0 → concurrent_c_node-0.10.0}/PKG-INFO +25 -15
- {concurrent_c_node-0.7.0 → concurrent_c_node-0.10.0}/README.md +24 -14
- {concurrent_c_node-0.7.0 → concurrent_c_node-0.10.0}/cc_node/__init__.py +66 -22
- {concurrent_c_node-0.7.0 → concurrent_c_node-0.10.0}/cc_node/broker.cjs +23 -9
- {concurrent_c_node-0.7.0 → concurrent_c_node-0.10.0}/concurrent_c_node.egg-info/PKG-INFO +25 -15
- {concurrent_c_node-0.7.0 → concurrent_c_node-0.10.0}/pyproject.toml +1 -1
- {concurrent_c_node-0.7.0 → concurrent_c_node-0.10.0}/cc_node/examples/__init__.py +0 -0
- {concurrent_c_node-0.7.0 → concurrent_c_node-0.10.0}/cc_node/examples/bench_wire.py +0 -0
- {concurrent_c_node-0.7.0 → concurrent_c_node-0.10.0}/cc_node/examples/use_node.py +0 -0
- {concurrent_c_node-0.7.0 → concurrent_c_node-0.10.0}/concurrent_c_node.egg-info/SOURCES.txt +0 -0
- {concurrent_c_node-0.7.0 → concurrent_c_node-0.10.0}/concurrent_c_node.egg-info/dependency_links.txt +0 -0
- {concurrent_c_node-0.7.0 → concurrent_c_node-0.10.0}/concurrent_c_node.egg-info/top_level.txt +0 -0
- {concurrent_c_node-0.7.0 → concurrent_c_node-0.10.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: concurrent-c-node
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: JavaScript and npm packages from Python over the Concurrent-C bridge: one spawned Node child per domain, host-controlled lifetime.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/sreekotay/concurrent-c
|
|
@@ -40,7 +40,7 @@ dependencies, nothing to build. The domain **is** a spawned `node`
|
|
|
40
40
|
child (~28ms to first call), so you get real Node: full stdlib, native
|
|
41
41
|
addons, whatever npm installs. Promise-based APIs look synchronous
|
|
42
42
|
from Python, and bulk data crosses through **shared memory** — an 8MB
|
|
43
|
-
array in **
|
|
43
|
+
array in **9.5ms** where the same values as a JSON list take 499ms.
|
|
44
44
|
|
|
45
45
|
```
|
|
46
46
|
pip install concurrent-c-node # needs node on PATH (or point at one)
|
|
@@ -60,11 +60,12 @@ Import stays `import cc_node`. Examples ship in the wheel. The mirror of
|
|
|
60
60
|
- **The domain rules hold**: handles never cross bridges; `stats()` is
|
|
61
61
|
the handle ledger and `release()` drops one early; `destroy()` is
|
|
62
62
|
idempotent, every door answers `bridge is closed` after, and the
|
|
63
|
-
child dies with the bridge (and on host exit, via
|
|
63
|
+
child dies with the bridge (and on host exit, via wire-fd EOF).
|
|
64
64
|
Teardown is **cooperative** (farewell `close` + drain, then wait /
|
|
65
65
|
kill-fallback): in-flight calls may still return a correct value.
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
There is no clean cancel of CPU-bound JS work — wait, or kill the
|
|
67
|
+
child (`SIGKILL` / `process.abort`) and create a new domain. Hard death
|
|
68
|
+
must reject in-flight ops. See
|
|
68
69
|
[`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md).
|
|
69
70
|
|
|
70
71
|
## Async is free
|
|
@@ -146,21 +147,30 @@ From the Concurrent-C repo root (packs this wheel and the npm sibling):
|
|
|
146
147
|
From `python -m cc_node.examples.bench_wire` (sources under
|
|
147
148
|
[`cc_node/examples/`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/))
|
|
148
149
|
on a 4-vCPU x86-64 box, node 22 / python 3.11
|
|
149
|
-
([`perf/baselines/
|
|
150
|
+
([`perf/baselines/cc_node_bridge_py_20260810.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_bridge_py_20260810.txt);
|
|
150
151
|
catalog: [`perf/baselines/README.md`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/README.md)):
|
|
151
152
|
|
|
152
153
|
| what | result |
|
|
153
154
|
|---|---|
|
|
154
155
|
| spawn a domain (node child, first eval) | 28ms |
|
|
155
|
-
| wire round trip (smallest call) |
|
|
156
|
-
| Python-callback round trip (JS → Python → JS) |
|
|
157
|
-
| 8MB `array('d')` argument, shm spill | **9.
|
|
158
|
-
| the same 8MB as a JSON list |
|
|
159
|
-
|
|
160
|
-
The wire is strict request/response JSON
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
156
|
+
| wire round trip (smallest call) | 105µs |
|
|
157
|
+
| Python-callback round trip (JS → Python → JS) | 153µs |
|
|
158
|
+
| 8MB `array('d')` argument, shm spill | **9.5ms** |
|
|
159
|
+
| the same 8MB as a JSON list | 499ms — the spill is **52x** |
|
|
160
|
+
|
|
161
|
+
The wire is strict request/response JSON on dedicated fds — replies
|
|
162
|
+
pair by request id, and stdio stays yours, so `console.log` in
|
|
163
|
+
evaluated JS reaches the real stdout and can never collide with a
|
|
164
|
+
protocol reply — with the shared-memory spill for bulk data (private
|
|
165
|
+
0700 per-bridge directory, 0600 exclusive-create files, removed with
|
|
166
|
+
the bridge). The same discipline concurrent-c-python's isolated
|
|
167
|
+
domains speak, mirrored. True pinned zero-copy leases remain future
|
|
168
|
+
work.
|
|
169
|
+
|
|
170
|
+
One boundary, stated plainly: the domain is **crash isolation, not a
|
|
171
|
+
security sandbox** — the node child inherits your environment and runs
|
|
172
|
+
with your OS privileges, so do not run untrusted JavaScript through
|
|
173
|
+
it.
|
|
164
174
|
|
|
165
175
|
A worked tour (builtin Node modules, chains, callbacks, thenables,
|
|
166
176
|
buffers — no npm install needed):
|
|
@@ -29,7 +29,7 @@ dependencies, nothing to build. The domain **is** a spawned `node`
|
|
|
29
29
|
child (~28ms to first call), so you get real Node: full stdlib, native
|
|
30
30
|
addons, whatever npm installs. Promise-based APIs look synchronous
|
|
31
31
|
from Python, and bulk data crosses through **shared memory** — an 8MB
|
|
32
|
-
array in **
|
|
32
|
+
array in **9.5ms** where the same values as a JSON list take 499ms.
|
|
33
33
|
|
|
34
34
|
```
|
|
35
35
|
pip install concurrent-c-node # needs node on PATH (or point at one)
|
|
@@ -49,11 +49,12 @@ Import stays `import cc_node`. Examples ship in the wheel. The mirror of
|
|
|
49
49
|
- **The domain rules hold**: handles never cross bridges; `stats()` is
|
|
50
50
|
the handle ledger and `release()` drops one early; `destroy()` is
|
|
51
51
|
idempotent, every door answers `bridge is closed` after, and the
|
|
52
|
-
child dies with the bridge (and on host exit, via
|
|
52
|
+
child dies with the bridge (and on host exit, via wire-fd EOF).
|
|
53
53
|
Teardown is **cooperative** (farewell `close` + drain, then wait /
|
|
54
54
|
kill-fallback): in-flight calls may still return a correct value.
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
There is no clean cancel of CPU-bound JS work — wait, or kill the
|
|
56
|
+
child (`SIGKILL` / `process.abort`) and create a new domain. Hard death
|
|
57
|
+
must reject in-flight ops. See
|
|
57
58
|
[`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md).
|
|
58
59
|
|
|
59
60
|
## Async is free
|
|
@@ -135,21 +136,30 @@ From the Concurrent-C repo root (packs this wheel and the npm sibling):
|
|
|
135
136
|
From `python -m cc_node.examples.bench_wire` (sources under
|
|
136
137
|
[`cc_node/examples/`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/))
|
|
137
138
|
on a 4-vCPU x86-64 box, node 22 / python 3.11
|
|
138
|
-
([`perf/baselines/
|
|
139
|
+
([`perf/baselines/cc_node_bridge_py_20260810.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_bridge_py_20260810.txt);
|
|
139
140
|
catalog: [`perf/baselines/README.md`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/README.md)):
|
|
140
141
|
|
|
141
142
|
| what | result |
|
|
142
143
|
|---|---|
|
|
143
144
|
| spawn a domain (node child, first eval) | 28ms |
|
|
144
|
-
| wire round trip (smallest call) |
|
|
145
|
-
| Python-callback round trip (JS → Python → JS) |
|
|
146
|
-
| 8MB `array('d')` argument, shm spill | **9.
|
|
147
|
-
| the same 8MB as a JSON list |
|
|
148
|
-
|
|
149
|
-
The wire is strict request/response JSON
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
145
|
+
| wire round trip (smallest call) | 105µs |
|
|
146
|
+
| Python-callback round trip (JS → Python → JS) | 153µs |
|
|
147
|
+
| 8MB `array('d')` argument, shm spill | **9.5ms** |
|
|
148
|
+
| the same 8MB as a JSON list | 499ms — the spill is **52x** |
|
|
149
|
+
|
|
150
|
+
The wire is strict request/response JSON on dedicated fds — replies
|
|
151
|
+
pair by request id, and stdio stays yours, so `console.log` in
|
|
152
|
+
evaluated JS reaches the real stdout and can never collide with a
|
|
153
|
+
protocol reply — with the shared-memory spill for bulk data (private
|
|
154
|
+
0700 per-bridge directory, 0600 exclusive-create files, removed with
|
|
155
|
+
the bridge). The same discipline concurrent-c-python's isolated
|
|
156
|
+
domains speak, mirrored. True pinned zero-copy leases remain future
|
|
157
|
+
work.
|
|
158
|
+
|
|
159
|
+
One boundary, stated plainly: the domain is **crash isolation, not a
|
|
160
|
+
security sandbox** — the node child inherits your environment and runs
|
|
161
|
+
with your OS privileges, so do not run untrusted JavaScript through
|
|
162
|
+
it.
|
|
153
163
|
|
|
154
164
|
A worked tour (builtin Node modules, chains, callbacks, thenables,
|
|
155
165
|
buffers — no npm install needed):
|
|
@@ -24,7 +24,9 @@ import base64
|
|
|
24
24
|
import json
|
|
25
25
|
import math
|
|
26
26
|
import os
|
|
27
|
+
import shutil
|
|
27
28
|
import subprocess
|
|
29
|
+
import tempfile
|
|
28
30
|
|
|
29
31
|
__all__ = ["create", "JsError", "JsHandle", "__version__"]
|
|
30
32
|
__version__ = "0.4.0"
|
|
@@ -49,20 +51,11 @@ def _numpy():
|
|
|
49
51
|
return None
|
|
50
52
|
|
|
51
53
|
|
|
52
|
-
def
|
|
54
|
+
def _shm_base():
|
|
53
55
|
d = os.environ.get("CC_NODE_SHM_DIR")
|
|
54
56
|
if d:
|
|
55
57
|
return d
|
|
56
|
-
return "/dev/shm" if os.path.isdir("/dev/shm") else
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _shm_write(raw):
|
|
60
|
-
_shm_seq[0] += 1
|
|
61
|
-
path = os.path.join(_shm_dir(), "ccnode-%d-%d" % (os.getpid(),
|
|
62
|
-
_shm_seq[0]))
|
|
63
|
-
with open(path, "wb") as f:
|
|
64
|
-
f.write(raw)
|
|
65
|
-
return path
|
|
58
|
+
return "/dev/shm" if os.path.isdir("/dev/shm") else tempfile.gettempdir()
|
|
66
59
|
|
|
67
60
|
|
|
68
61
|
class JsError(RuntimeError):
|
|
@@ -123,17 +116,39 @@ class Bridge:
|
|
|
123
116
|
broker = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
124
117
|
"broker.cjs")
|
|
125
118
|
node = node or os.environ.get("CC_NODE_BIN", "node")
|
|
119
|
+
# Spills live in a private 0700 per-bridge directory (predictable
|
|
120
|
+
# names in a shared /dev/shm invite pre-creation races and
|
|
121
|
+
# umask-dependent exposure); the child writes its spills there
|
|
122
|
+
# too, and the directory goes with the bridge.
|
|
123
|
+
self._shm_dir = tempfile.mkdtemp(
|
|
124
|
+
prefix="ccnode-%d-" % os.getpid(), dir=_shm_base())
|
|
125
|
+
# The wire lives on dedicated fds; stdio is inherited, so
|
|
126
|
+
# console.log in evaluated JS reaches the real stdout and can
|
|
127
|
+
# never collide with a protocol reply. pass_fds keeps our fd
|
|
128
|
+
# numbers in the child, so the broker learns them from the env.
|
|
129
|
+
req_r, req_w = os.pipe()
|
|
130
|
+
resp_r, resp_w = os.pipe()
|
|
131
|
+
env = dict(os.environ,
|
|
132
|
+
CC_WIRE_IN=str(req_r), CC_WIRE_OUT=str(resp_w),
|
|
133
|
+
CC_NODE_SHM_DIR=self._shm_dir)
|
|
126
134
|
try:
|
|
127
135
|
self._p = subprocess.Popen(
|
|
128
136
|
[node, broker],
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
pass_fds=(req_r, resp_w),
|
|
138
|
+
env=env,
|
|
131
139
|
cwd=os.getcwd(),
|
|
132
140
|
)
|
|
133
141
|
except FileNotFoundError:
|
|
142
|
+
for fd in (req_r, req_w, resp_r, resp_w):
|
|
143
|
+
os.close(fd)
|
|
144
|
+
shutil.rmtree(self._shm_dir, ignore_errors=True)
|
|
134
145
|
raise JsError(
|
|
135
146
|
"cc-node: no node executable (install Node, or set "
|
|
136
147
|
"CC_NODE_BIN)") from None
|
|
148
|
+
os.close(req_r)
|
|
149
|
+
os.close(resp_w)
|
|
150
|
+
self._wire_w = os.fdopen(req_w, "wb")
|
|
151
|
+
self._wire_r = os.fdopen(resp_r, "rb")
|
|
137
152
|
self.closed = False
|
|
138
153
|
self._nid = 1
|
|
139
154
|
self._cbs = {}
|
|
@@ -150,8 +165,14 @@ class Bridge:
|
|
|
150
165
|
|
|
151
166
|
def _send(self, obj):
|
|
152
167
|
line = json.dumps(obj, allow_nan=False) + "\n"
|
|
153
|
-
|
|
154
|
-
|
|
168
|
+
try:
|
|
169
|
+
self._wire_w.write(line.encode("utf-8"))
|
|
170
|
+
self._wire_w.flush()
|
|
171
|
+
except (BrokenPipeError, ValueError, OSError):
|
|
172
|
+
# The child died under this write (e.g. killed from inside a
|
|
173
|
+
# callback). Every door answers the same way after death.
|
|
174
|
+
self.closed = True
|
|
175
|
+
raise JsError("cc-node: the node child exited") from None
|
|
155
176
|
|
|
156
177
|
def _queue_release(self, hid):
|
|
157
178
|
self._pending_release.append(hid)
|
|
@@ -166,6 +187,14 @@ class Bridge:
|
|
|
166
187
|
except Exception:
|
|
167
188
|
pass
|
|
168
189
|
|
|
190
|
+
def _shm_write(self, raw):
|
|
191
|
+
_shm_seq[0] += 1
|
|
192
|
+
path = os.path.join(self._shm_dir, "s%d" % _shm_seq[0])
|
|
193
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
194
|
+
with os.fdopen(fd, "wb") as f:
|
|
195
|
+
f.write(raw)
|
|
196
|
+
return path
|
|
197
|
+
|
|
169
198
|
def _flush_shm(self):
|
|
170
199
|
# The child unlinks spill files as it decodes; this sweep only
|
|
171
200
|
# matters when it died first (ENOENT is the normal case).
|
|
@@ -186,7 +215,7 @@ class Bridge:
|
|
|
186
215
|
if parked is not None:
|
|
187
216
|
return self._take_reply(parked)
|
|
188
217
|
while True:
|
|
189
|
-
line = self.
|
|
218
|
+
line = self._wire_r.readline()
|
|
190
219
|
if not line:
|
|
191
220
|
self.closed = True
|
|
192
221
|
raise JsError("cc-node: the node child exited")
|
|
@@ -235,7 +264,10 @@ class Bridge:
|
|
|
235
264
|
args = [self._decode_result(a) for a in msg["args"]]
|
|
236
265
|
self._send({"cbr": msg["cbid"], "v": self._encode(fn(*args))})
|
|
237
266
|
except Exception as e: # crosses back as a JS error, message intact
|
|
238
|
-
|
|
267
|
+
try:
|
|
268
|
+
self._send({"cbr": msg["cbid"], "e": str(e)})
|
|
269
|
+
except Exception:
|
|
270
|
+
pass # child gone; the in-flight op surfaces EOF next read
|
|
239
271
|
|
|
240
272
|
# ---- values ----
|
|
241
273
|
|
|
@@ -294,8 +326,8 @@ class Bridge:
|
|
|
294
326
|
raw = np.ascontiguousarray(a).tobytes()
|
|
295
327
|
if raw is None or kind is None:
|
|
296
328
|
return None
|
|
297
|
-
if len(raw) > _SHM_SPILL
|
|
298
|
-
path = _shm_write(raw)
|
|
329
|
+
if len(raw) > _SHM_SPILL:
|
|
330
|
+
path = self._shm_write(raw)
|
|
299
331
|
self._shm_out.append(path)
|
|
300
332
|
return {"$shm": path, "t": kind}
|
|
301
333
|
return {"$ta": kind, "b64": base64.b64encode(raw).decode("ascii")}
|
|
@@ -372,15 +404,22 @@ class Bridge:
|
|
|
372
404
|
self._close_pending = False
|
|
373
405
|
self.closed = True
|
|
374
406
|
try:
|
|
375
|
-
if self._p.poll() is None and self.
|
|
376
|
-
and not self._p.stdin.closed:
|
|
407
|
+
if self._p.poll() is None and not self._wire_w.closed:
|
|
377
408
|
rid = self._nid
|
|
378
409
|
self._nid += 1
|
|
379
410
|
self._send({"id": rid, "op": "close"})
|
|
380
411
|
except Exception:
|
|
381
412
|
pass
|
|
382
413
|
try:
|
|
383
|
-
self.
|
|
414
|
+
self._wire_w.close()
|
|
415
|
+
except Exception:
|
|
416
|
+
pass
|
|
417
|
+
# Drain to EOF so the broker's farewell reply has somewhere to
|
|
418
|
+
# land — closing the reply fd under its write is an EPIPE crash
|
|
419
|
+
# in the child.
|
|
420
|
+
try:
|
|
421
|
+
while self._wire_r.readline():
|
|
422
|
+
pass
|
|
384
423
|
except Exception:
|
|
385
424
|
pass
|
|
386
425
|
try:
|
|
@@ -390,6 +429,11 @@ class Bridge:
|
|
|
390
429
|
self._p.kill()
|
|
391
430
|
except Exception:
|
|
392
431
|
pass
|
|
432
|
+
try:
|
|
433
|
+
self._wire_r.close()
|
|
434
|
+
except Exception:
|
|
435
|
+
pass
|
|
436
|
+
shutil.rmtree(self._shm_dir, ignore_errors=True)
|
|
393
437
|
if self in _live:
|
|
394
438
|
_live.remove(self)
|
|
395
439
|
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/* cc-node broker: the Node end of Python's JS bridge.
|
|
3
3
|
*
|
|
4
|
-
* Line-delimited JSON
|
|
4
|
+
* Line-delimited JSON on DEDICATED wire fds, strict request/response —
|
|
5
|
+
* stdin/stdout/stderr stay the user's, so console.log in evaluated
|
|
6
|
+
* code reaches the real stdout and can never collide with a protocol
|
|
7
|
+
* reply. Requests arrive on fd 3, replies leave on fd 4; a parent
|
|
8
|
+
* that cannot pin fd numbers (python's pass_fds keeps the parent's
|
|
9
|
+
* numbering) says which via CC_WIRE_IN / CC_WIRE_OUT. Handles are
|
|
5
10
|
* integers into one table; results follow the bridge materialization
|
|
6
11
|
* rule — plain data (finite numbers, strings, booleans, null, arrays
|
|
7
12
|
* and plain objects of the same) crosses as a value, everything else
|
|
@@ -10,7 +15,7 @@
|
|
|
10
15
|
* Python callable crosses as {$f: id}; invoking it sends a nested `cb`
|
|
11
16
|
* request and BLOCKS on a synchronous read for the answer — legal
|
|
12
17
|
* because the protocol is strictly alternating, so nothing else can be
|
|
13
|
-
* in flight.
|
|
18
|
+
* in flight. EOF on the request fd is the host vanishing: exit.
|
|
14
19
|
*
|
|
15
20
|
* cc/include/ccc/script/js.cch embeds this file verbatim (the CC
|
|
16
21
|
* isolated tier speaks the same wire); js_iso_smoke pins the two
|
|
@@ -20,11 +25,14 @@
|
|
|
20
25
|
const fs = require('fs');
|
|
21
26
|
const { createRequire } = require('module');
|
|
22
27
|
|
|
28
|
+
const IN_FD = Number(process.env.CC_WIRE_IN || 3);
|
|
29
|
+
const OUT_FD = Number(process.env.CC_WIRE_OUT || 4);
|
|
30
|
+
|
|
23
31
|
/* Resolve packages from the HOST's cwd — `npm install lodash` next to
|
|
24
32
|
* your Python program is the point. */
|
|
25
33
|
const requireCwd = createRequire(process.cwd() + '/');
|
|
26
34
|
|
|
27
|
-
/* ---- one buffered reader over fd
|
|
35
|
+
/* ---- one buffered reader over the request fd, sync and async ---- */
|
|
28
36
|
const rbuf = { data: Buffer.alloc(0) };
|
|
29
37
|
|
|
30
38
|
function takeLine() {
|
|
@@ -42,7 +50,7 @@ function readLineSync() {
|
|
|
42
50
|
const chunk = Buffer.alloc(65536);
|
|
43
51
|
let n = 0;
|
|
44
52
|
try {
|
|
45
|
-
n = fs.readSync(
|
|
53
|
+
n = fs.readSync(IN_FD, chunk, 0, chunk.length, null);
|
|
46
54
|
} catch (e) {
|
|
47
55
|
if (e.code === 'EAGAIN') continue;
|
|
48
56
|
if (e.code === 'EOF') return null;
|
|
@@ -58,7 +66,7 @@ function readLineAsync() {
|
|
|
58
66
|
if (l !== null) return Promise.resolve(l);
|
|
59
67
|
return new Promise((resolve, reject) => {
|
|
60
68
|
const chunk = Buffer.alloc(65536);
|
|
61
|
-
fs.read(
|
|
69
|
+
fs.read(IN_FD, chunk, 0, chunk.length, null, (err, n) => {
|
|
62
70
|
if (err) return err.code === 'EOF' ? resolve(null) : reject(err);
|
|
63
71
|
if (n === 0) return resolve(null);
|
|
64
72
|
rbuf.data = Buffer.concat([rbuf.data, chunk.subarray(0, n)]);
|
|
@@ -68,7 +76,12 @@ function readLineAsync() {
|
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
function send(obj) {
|
|
71
|
-
|
|
79
|
+
try {
|
|
80
|
+
fs.writeSync(OUT_FD, JSON.stringify(obj) + '\n');
|
|
81
|
+
} catch (e) {
|
|
82
|
+
if (e.code === 'EPIPE') process.exit(0); /* host went away */
|
|
83
|
+
throw e;
|
|
84
|
+
}
|
|
72
85
|
}
|
|
73
86
|
|
|
74
87
|
/* ---- handles + materialization ---- */
|
|
@@ -102,7 +115,8 @@ function isPlain(v, depth) {
|
|
|
102
115
|
|
|
103
116
|
/* Typed buffers cross as tagged bytes: small inline as base64, big
|
|
104
117
|
* through the shared-memory spill (one memcpy per side; the receiver
|
|
105
|
-
* consumes-and-unlinks
|
|
118
|
+
* consumes-and-unlinks; files are 0600, exclusive-create, inside the
|
|
119
|
+
* host's private bridge dir). Same discipline as cc-python's wire. */
|
|
106
120
|
const TA_KIND = new Map([
|
|
107
121
|
[Float64Array, 'f64'], [Float32Array, 'f32'],
|
|
108
122
|
[Int32Array, 'i32'], [BigInt64Array, 'i64'], [Uint8Array, 'u8'],
|
|
@@ -120,8 +134,8 @@ let shmSeq = 0;
|
|
|
120
134
|
function encodeBuffer(kind, buf) {
|
|
121
135
|
if (buf.byteLength > SHM_SPILL) {
|
|
122
136
|
const p = require('path').join(
|
|
123
|
-
SHM_DIR, 'ccnode-' + process.pid + '-' + (++shmSeq));
|
|
124
|
-
fs.writeFileSync(p, buf);
|
|
137
|
+
SHM_DIR, 'ccnode-c' + process.pid + '-' + (++shmSeq));
|
|
138
|
+
fs.writeFileSync(p, buf, { flag: 'wx', mode: 0o600 });
|
|
125
139
|
return { shm: p, t: kind };
|
|
126
140
|
}
|
|
127
141
|
return { ta: kind, b64: buf.toString('base64') };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: concurrent-c-node
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: JavaScript and npm packages from Python over the Concurrent-C bridge: one spawned Node child per domain, host-controlled lifetime.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/sreekotay/concurrent-c
|
|
@@ -40,7 +40,7 @@ dependencies, nothing to build. The domain **is** a spawned `node`
|
|
|
40
40
|
child (~28ms to first call), so you get real Node: full stdlib, native
|
|
41
41
|
addons, whatever npm installs. Promise-based APIs look synchronous
|
|
42
42
|
from Python, and bulk data crosses through **shared memory** — an 8MB
|
|
43
|
-
array in **
|
|
43
|
+
array in **9.5ms** where the same values as a JSON list take 499ms.
|
|
44
44
|
|
|
45
45
|
```
|
|
46
46
|
pip install concurrent-c-node # needs node on PATH (or point at one)
|
|
@@ -60,11 +60,12 @@ Import stays `import cc_node`. Examples ship in the wheel. The mirror of
|
|
|
60
60
|
- **The domain rules hold**: handles never cross bridges; `stats()` is
|
|
61
61
|
the handle ledger and `release()` drops one early; `destroy()` is
|
|
62
62
|
idempotent, every door answers `bridge is closed` after, and the
|
|
63
|
-
child dies with the bridge (and on host exit, via
|
|
63
|
+
child dies with the bridge (and on host exit, via wire-fd EOF).
|
|
64
64
|
Teardown is **cooperative** (farewell `close` + drain, then wait /
|
|
65
65
|
kill-fallback): in-flight calls may still return a correct value.
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
There is no clean cancel of CPU-bound JS work — wait, or kill the
|
|
67
|
+
child (`SIGKILL` / `process.abort`) and create a new domain. Hard death
|
|
68
|
+
must reject in-flight ops. See
|
|
68
69
|
[`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md).
|
|
69
70
|
|
|
70
71
|
## Async is free
|
|
@@ -146,21 +147,30 @@ From the Concurrent-C repo root (packs this wheel and the npm sibling):
|
|
|
146
147
|
From `python -m cc_node.examples.bench_wire` (sources under
|
|
147
148
|
[`cc_node/examples/`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/))
|
|
148
149
|
on a 4-vCPU x86-64 box, node 22 / python 3.11
|
|
149
|
-
([`perf/baselines/
|
|
150
|
+
([`perf/baselines/cc_node_bridge_py_20260810.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_bridge_py_20260810.txt);
|
|
150
151
|
catalog: [`perf/baselines/README.md`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/README.md)):
|
|
151
152
|
|
|
152
153
|
| what | result |
|
|
153
154
|
|---|---|
|
|
154
155
|
| spawn a domain (node child, first eval) | 28ms |
|
|
155
|
-
| wire round trip (smallest call) |
|
|
156
|
-
| Python-callback round trip (JS → Python → JS) |
|
|
157
|
-
| 8MB `array('d')` argument, shm spill | **9.
|
|
158
|
-
| the same 8MB as a JSON list |
|
|
159
|
-
|
|
160
|
-
The wire is strict request/response JSON
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
156
|
+
| wire round trip (smallest call) | 105µs |
|
|
157
|
+
| Python-callback round trip (JS → Python → JS) | 153µs |
|
|
158
|
+
| 8MB `array('d')` argument, shm spill | **9.5ms** |
|
|
159
|
+
| the same 8MB as a JSON list | 499ms — the spill is **52x** |
|
|
160
|
+
|
|
161
|
+
The wire is strict request/response JSON on dedicated fds — replies
|
|
162
|
+
pair by request id, and stdio stays yours, so `console.log` in
|
|
163
|
+
evaluated JS reaches the real stdout and can never collide with a
|
|
164
|
+
protocol reply — with the shared-memory spill for bulk data (private
|
|
165
|
+
0700 per-bridge directory, 0600 exclusive-create files, removed with
|
|
166
|
+
the bridge). The same discipline concurrent-c-python's isolated
|
|
167
|
+
domains speak, mirrored. True pinned zero-copy leases remain future
|
|
168
|
+
work.
|
|
169
|
+
|
|
170
|
+
One boundary, stated plainly: the domain is **crash isolation, not a
|
|
171
|
+
security sandbox** — the node child inherits your environment and runs
|
|
172
|
+
with your OS privileges, so do not run untrusted JavaScript through
|
|
173
|
+
it.
|
|
164
174
|
|
|
165
175
|
A worked tour (builtin Node modules, chains, callbacks, thenables,
|
|
166
176
|
buffers — no npm install needed):
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "concurrent-c-node"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.10.0"
|
|
8
8
|
description = "JavaScript and npm packages from Python over the Concurrent-C bridge: one spawned Node child per domain, host-controlled lifetime."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{concurrent_c_node-0.7.0 → concurrent_c_node-0.10.0}/concurrent_c_node.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{concurrent_c_node-0.7.0 → concurrent_c_node-0.10.0}/concurrent_c_node.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|