Skip to content

Commit 24a38ee

Browse files
committed
sitecustomize.py based autoinstrumentation
1 parent 54f44d4 commit 24a38ee

9 files changed

Lines changed: 323 additions & 108 deletions

File tree

tests/test_cli.py

Lines changed: 86 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import json
44
import os
5-
from types import SimpleNamespace
65

76
import pytest
87

@@ -12,16 +11,20 @@
1211
from wildedge.runtime import runner as runtime_runner
1312

1413

15-
def test_cli_run_script_invokes_runner(monkeypatch):
16-
captured = {}
14+
def _fake_execle(captured: dict):
15+
def _execle(path, *args): # type: ignore[no-untyped-def]
16+
# Last positional arg is the env dict (os.execle convention).
17+
captured["path"] = path
18+
captured["argv"] = list(args[:-1])
19+
captured["env"] = args[-1]
1720

18-
def fake_run(cmd, env, check): # type: ignore[no-untyped-def]
19-
captured["cmd"] = cmd
20-
captured["env"] = env
21-
captured["check"] = check
22-
return SimpleNamespace(returncode=0)
21+
return _execle
2322

24-
monkeypatch.setattr(cli.subprocess, "run", fake_run)
23+
24+
def test_cli_run_execs_command_with_env(monkeypatch):
25+
captured: dict = {}
26+
monkeypatch.setattr(cli.os, "execle", _fake_execle(captured))
27+
monkeypatch.setattr(cli.shutil, "which", lambda cmd: f"/usr/bin/{cmd}")
2528

2629
rc = cli.main(
2730
[
@@ -32,24 +35,16 @@ def fake_run(cmd, env, check): # type: ignore[no-untyped-def]
3235
"1.2.3",
3336
"--debug",
3437
"--",
35-
"python",
36-
"app.py",
37-
"--foo",
38-
"bar",
38+
"gunicorn",
39+
"myapp.wsgi:app",
40+
"--workers",
41+
"4",
3942
]
4043
)
4144

4245
assert rc == 0
43-
assert captured["cmd"][1:8] == [
44-
"-m",
45-
"wildedge.runtime.runner",
46-
"--mode",
47-
"script",
48-
"--target",
49-
"app.py",
50-
"--",
51-
]
52-
assert captured["cmd"][8:] == ["--foo", "bar"]
46+
assert captured["path"] == "/usr/bin/gunicorn"
47+
assert captured["argv"] == ["/usr/bin/gunicorn", "myapp.wsgi:app", "--workers", "4"]
5348
assert (
5449
captured["env"][bootstrap.RUN_DSN_ENV]
5550
== "https://secret@ingest.wildedge.dev/key"
@@ -64,62 +59,86 @@ def fake_run(cmd, env, check): # type: ignore[no-untyped-def]
6459
)
6560

6661

67-
def test_cli_run_sets_no_propagate_and_strict(monkeypatch):
68-
captured = {}
62+
def test_cli_run_prepends_autoload_to_pythonpath(monkeypatch):
63+
captured: dict = {}
64+
monkeypatch.setattr(cli.os, "execle", _fake_execle(captured))
65+
monkeypatch.setattr(cli.shutil, "which", lambda cmd: f"/usr/bin/{cmd}")
66+
monkeypatch.delenv("PYTHONPATH", raising=False)
67+
68+
cli.main(["run", "--", "gunicorn", "myapp.wsgi:app"])
69+
70+
autoload_dir = str(cli.Path(__file__).parent.parent / "wildedge" / "autoload")
71+
assert captured["env"]["PYTHONPATH"] == autoload_dir
72+
73+
74+
def test_cli_run_preserves_existing_pythonpath(monkeypatch):
75+
captured: dict = {}
76+
monkeypatch.setattr(cli.os, "execle", _fake_execle(captured))
77+
monkeypatch.setattr(cli.shutil, "which", lambda cmd: f"/usr/bin/{cmd}")
78+
monkeypatch.setenv("PYTHONPATH", "/existing/path")
6979

70-
def fake_run(cmd, env, check): # type: ignore[no-untyped-def]
71-
captured["cmd"] = cmd
72-
captured["env"] = env
73-
captured["check"] = check
74-
return SimpleNamespace(returncode=0)
80+
cli.main(["run", "--", "gunicorn", "myapp.wsgi:app"])
7581

76-
monkeypatch.setattr(cli.subprocess, "run", fake_run)
82+
pythonpath = captured["env"]["PYTHONPATH"]
83+
assert pythonpath.endswith(os.pathsep + "/existing/path")
84+
85+
86+
def test_cli_run_sets_no_propagate_and_strict(monkeypatch):
87+
captured: dict = {}
88+
monkeypatch.setattr(cli.os, "execle", _fake_execle(captured))
89+
monkeypatch.setattr(cli.shutil, "which", lambda cmd: f"/usr/bin/{cmd}")
7790

7891
rc = cli.main(
7992
[
8093
"run",
8194
"--strict-integrations",
8295
"--no-propagate",
8396
"--",
84-
"python",
85-
"-m",
86-
"pkg.main",
87-
"--foo",
97+
"gunicorn",
98+
"myapp.wsgi:app",
8899
]
89100
)
90101

91102
assert rc == 0
92-
assert captured["cmd"][1:8] == [
93-
"-m",
94-
"wildedge.runtime.runner",
95-
"--mode",
96-
"module",
97-
"--target",
98-
"pkg.main",
99-
"--",
100-
]
101103
assert captured["env"][bootstrap.RUN_PROPAGATE_ENV] == "0"
102104
assert captured["env"][bootstrap.RUN_STRICT_INTEGRATIONS_ENV] == "1"
103105

104106

105107
def test_cli_run_sets_print_startup_report(monkeypatch):
106-
captured = {}
108+
captured: dict = {}
109+
monkeypatch.setattr(cli.os, "execle", _fake_execle(captured))
110+
monkeypatch.setattr(cli.shutil, "which", lambda cmd: f"/usr/bin/{cmd}")
107111

108-
def fake_run(cmd, env, check): # type: ignore[no-untyped-def]
109-
captured["env"] = env
110-
return SimpleNamespace(returncode=0)
112+
rc = cli.main(["run", "--print-startup-report", "--", "gunicorn", "myapp.wsgi:app"])
111113

112-
monkeypatch.setattr(cli.subprocess, "run", fake_run)
113-
rc = cli.main(["run", "--print-startup-report", "--", "python", "app.py"])
114114
assert rc == 0
115115
assert captured["env"][bootstrap.RUN_PRINT_STARTUP_REPORT_ENV] == "1"
116116

117117

118-
def test_cli_rejects_non_python_command(capsys):
119-
rc = cli.main(["run", "--", "bash", "script.sh"])
120-
captured = capsys.readouterr()
121-
assert rc == 2
122-
assert "unsupported command format" in captured.err
118+
def test_cli_run_returns_127_for_missing_command(capsys, monkeypatch):
119+
monkeypatch.setattr(cli.shutil, "which", lambda cmd: None)
120+
monkeypatch.setattr(cli.Path, "is_file", lambda self: False)
121+
rc = cli.main(["run", "--", "nonexistent-command"])
122+
assert rc == 127
123+
assert "command not found" in capsys.readouterr().err
124+
125+
126+
def test_cli_run_wraps_python_script_with_interpreter(monkeypatch, tmp_path):
127+
script = tmp_path / "app.py"
128+
script.write_text("pass")
129+
captured: dict = {}
130+
monkeypatch.setattr(cli.os, "execle", _fake_execle(captured))
131+
monkeypatch.setattr(
132+
cli.shutil,
133+
"which",
134+
lambda cmd: None if cmd.endswith(".py") else f"/usr/bin/{cmd}",
135+
)
136+
137+
rc = cli.main(["run", "--", str(script)])
138+
139+
assert rc == 0
140+
assert captured["argv"][1] == str(script)
141+
assert "python" in captured["path"].lower()
123142

124143

125144
def test_install_runtime_requires_dsn(monkeypatch):
@@ -145,6 +164,9 @@ def flush(self, timeout): # type: ignore[no-untyped-def]
145164
def close(self): # type: ignore[no-untyped-def]
146165
pass
147166

167+
def _register_at_fork(self): # type: ignore[no-untyped-def]
168+
pass
169+
148170
monkeypatch.setattr(bootstrap, "WildEdge", FakeWildEdge)
149171
monkeypatch.setenv(bootstrap.RUN_DSN_ENV, "https://secret@ingest.wildedge.dev/key")
150172
monkeypatch.delenv(bootstrap.RUN_FLUSH_TIMEOUT_ENV, raising=False)
@@ -177,6 +199,9 @@ def flush(self, timeout): # type: ignore[no-untyped-def]
177199
def close(self): # type: ignore[no-untyped-def]
178200
events.append(("close", ""))
179201

202+
def _register_at_fork(self): # type: ignore[no-untyped-def]
203+
pass
204+
180205
monkeypatch.setattr(bootstrap, "WildEdge", FakeWildEdge)
181206
monkeypatch.setenv(bootstrap.RUN_DSN_ENV, "https://secret@ingest.wildedge.dev/key")
182207
monkeypatch.setenv(bootstrap.RUN_APP_VERSION_ENV, "2.0.0")
@@ -205,6 +230,9 @@ def __init__(self, *, dsn, app_version, debug): # type: ignore[no-untyped-def]
205230
def instrument(self, name): # type: ignore[no-untyped-def]
206231
raise RuntimeError("boom")
207232

233+
def _register_at_fork(self): # type: ignore[no-untyped-def]
234+
pass
235+
208236
monkeypatch.setattr(bootstrap, "WildEdge", FakeWildEdge)
209237
monkeypatch.setenv(bootstrap.RUN_DSN_ENV, "https://secret@ingest.wildedge.dev/key")
210238
monkeypatch.setenv(bootstrap.RUN_INTEGRATIONS_ENV, "onnx")
@@ -361,6 +389,9 @@ def flush(self, timeout): # type: ignore[no-untyped-def]
361389
def close(self): # type: ignore[no-untyped-def]
362390
pass
363391

392+
def _register_at_fork(self): # type: ignore[no-untyped-def]
393+
pass
394+
364395
monkeypatch.setattr(bootstrap, "WildEdge", FakeWildEdge)
365396
monkeypatch.setattr(
366397
bootstrap,

tests/test_consumer.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,3 +335,81 @@ def fake_register(fn, *args, **kwargs):
335335
consumer = self._make_consumer(queue, mock_transmitter)
336336
assert registered["fn"] == consumer.flush
337337
assert registered["args"] == (constants.DEFAULT_SHUTDOWN_FLUSH_TIMEOUT_SEC,)
338+
339+
340+
class TestConsumerForkSafety:
341+
def setup_method(self):
342+
self._consumers: list[Consumer] = []
343+
344+
def teardown_method(self):
345+
for c in self._consumers:
346+
c.stop()
347+
348+
def _make_consumer(self, queue, transmitter, **kwargs) -> Consumer:
349+
c = Consumer(
350+
queue=queue,
351+
transmitter=transmitter,
352+
device=DeviceInfo(app_version="1.0", device_id="d", device_type="linux"),
353+
get_models=lambda: {},
354+
session_id="sess-fork",
355+
**kwargs,
356+
)
357+
self._consumers.append(c)
358+
return c
359+
360+
def test_before_fork_stops_thread(self, monkeypatch):
361+
monkeypatch.setattr(Consumer, "run", lambda self: None)
362+
queue = EventQueue(max_size=100)
363+
mock_tx = MagicMock(spec=Transmitter)
364+
consumer = self._make_consumer(queue, mock_tx)
365+
366+
original_thread = consumer.thread
367+
consumer._before_fork()
368+
369+
assert consumer.stop_event.is_set()
370+
assert not original_thread.is_alive()
371+
# stopped is reset to False so _restart() can start a new thread
372+
assert consumer.stopped is False
373+
374+
def test_restart_creates_fresh_thread(self, monkeypatch):
375+
monkeypatch.setattr(Consumer, "run", lambda self: None)
376+
queue = EventQueue(max_size=100)
377+
mock_tx = MagicMock(spec=Transmitter)
378+
consumer = self._make_consumer(queue, mock_tx)
379+
380+
original_thread = consumer.thread
381+
consumer._before_fork()
382+
consumer._restart()
383+
384+
assert consumer.thread is not original_thread
385+
# thread was started (ident is set) even though the no-op run() exits fast
386+
assert consumer.thread.ident is not None
387+
assert not consumer.stop_event.is_set()
388+
389+
def test_restart_resets_backoff_and_snapshot(self, monkeypatch):
390+
monkeypatch.setattr(Consumer, "run", lambda self: None)
391+
queue = EventQueue(max_size=100)
392+
mock_tx = MagicMock(spec=Transmitter)
393+
consumer = self._make_consumer(queue, mock_tx)
394+
395+
consumer.backoff = constants.BACKOFF_MAX
396+
consumer._held_snapshot = ([], {})
397+
398+
consumer._before_fork()
399+
consumer._restart()
400+
401+
assert consumer.backoff == constants.BACKOFF_MIN
402+
assert consumer._held_snapshot is None
403+
404+
def test_flush_is_noop_after_before_fork_before_restart(self, monkeypatch):
405+
"""flush() on a pre-fork-stopped consumer with empty queue returns immediately."""
406+
monkeypatch.setattr(Consumer, "run", lambda self: None)
407+
queue = EventQueue(max_size=100)
408+
mock_tx = MagicMock(spec=Transmitter)
409+
consumer = self._make_consumer(queue, mock_tx)
410+
411+
consumer._before_fork()
412+
# stopped is False (reset by _before_fork) and queue is empty, so flush
413+
# calls drain_once which returns False immediately — no transmit calls.
414+
consumer.flush(timeout=0.1)
415+
mock_tx.send.assert_not_called()

wildedge/autoload/__init__.py

Whitespace-only changes.

wildedge/autoload/sitecustomize.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""WildEdge autoload sitecustomize.
2+
3+
Loaded automatically by the Python interpreter when wildedge/autoload/ is
4+
prepended to PYTHONPATH (by `wildedge run`). Calls install_runtime() before
5+
any user code runs, enabling framework instrumentation and fork-safe operation
6+
for gunicorn, celery, and other pre-fork servers.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import os
12+
import sys
13+
14+
_GUARD = "WILDEDGE_AUTOLOAD_ACTIVE"
15+
_RUN_DSN = "WILDEDGE_RUN_DSN"
16+
_DSN = "WILDEDGE_DSN"
17+
18+
19+
def _bootstrap() -> None:
20+
# Idempotent: skip if already initialized (e.g. in a forked worker that
21+
# exec'd a subprocess, or if sitecustomize.py is imported twice).
22+
if os.environ.get(_GUARD):
23+
return
24+
25+
# Require a DSN. Silently skip if missing so that processes which
26+
# inherit PYTHONPATH without wildedge config don't crash.
27+
if not (os.environ.get(_RUN_DSN) or os.environ.get(_DSN)):
28+
return
29+
30+
# Set the guard before importing wildedge to prevent re-entry.
31+
os.environ[_GUARD] = "1"
32+
33+
try:
34+
from wildedge.runtime.bootstrap import install_runtime # noqa: PLC0415
35+
36+
# Don't install signal handlers: the host process (gunicorn, celery,
37+
# etc.) manages SIGTERM/SIGINT itself.
38+
install_runtime(install_signal_handlers=False)
39+
except Exception as exc: # pragma: no cover
40+
print(f"wildedge: bootstrap failed: {exc}", file=sys.stderr)
41+
42+
43+
_bootstrap()
44+
45+
46+
# Chain any pre-existing sitecustomize that would otherwise be shadowed.
47+
# Use importlib to find and exec it directly — avoids sys.modules manipulation
48+
# which can trigger CPython's module GC and clear globals mid-execution.
49+
def _chain_sitecustomize() -> None:
50+
import importlib.util as _iutil
51+
52+
_autoload_dir = os.path.dirname(os.path.abspath(__file__))
53+
_saved = sys.path[:]
54+
sys.path = [p for p in sys.path if p != _autoload_dir]
55+
try:
56+
_spec = _iutil.find_spec("sitecustomize")
57+
if _spec is not None and _spec.origin is not None:
58+
_mod = _iutil.module_from_spec(_spec)
59+
sys.modules["sitecustomize"] = _mod
60+
_spec.loader.exec_module(_mod) # type: ignore[union-attr]
61+
except Exception:
62+
pass
63+
finally:
64+
sys.path = _saved
65+
66+
67+
_chain_sitecustomize()

0 commit comments

Comments
 (0)