Skip to content

Commit d3cacde

Browse files
authored
feat(devservices): use distroless image in devservices (#7909)
This PR will switch the snuba container in sentry devservices to the "distroless" version. ``` $ docker ps -f "name=snuba-snuba-1" CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e487314c8b09 ghcr.io/getsentry/snuba:nightly-distroless "python3 /usr/src/sn…" 17 minutes ago Up 17 minutes (healthy) 127.0.0.1:1218-1219->1218-1219/tcp snuba-snuba-1 ``` This was previously smoke tested in #7829 on my machine. New events are being ingested and retrieved on sentry devserver. Also removing `honcho` package, replacing it with native `subprocess` library, to avoid spawning the missing shell (more detailed explanation in getsentry/sentry-analytics#26)
1 parent 04f6685 commit d3cacde

4 files changed

Lines changed: 105 additions & 32 deletions

File tree

devservices/config.yml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,15 @@ services:
7575
restart: unless-stopped
7676

7777
snuba:
78-
image: ghcr.io/getsentry/snuba:nightly
78+
image: ghcr.io/getsentry/snuba:nightly-distroless
7979
ports:
8080
- 127.0.0.1:1218:1218
8181
- 127.0.0.1:1219:1219
8282
command:
8383
- devserver
8484
- --${SNUBA_NO_WORKERS:+no-workers}
8585
healthcheck:
86-
test: curl -f http://localhost:1218/health_envoy
86+
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:1218/health_envoy')"]
8787
interval: 5s
8888
timeout: 5s
8989
retries: 3
@@ -114,7 +114,7 @@ services:
114114
- orchestrator=devservices
115115
restart: unless-stopped
116116
profiles-consumer:
117-
image: ghcr.io/getsentry/snuba:nightly
117+
image: ghcr.io/getsentry/snuba:nightly-distroless
118118
command: [
119119
rust-consumer,
120120
--storage=profiles,
@@ -143,7 +143,7 @@ services:
143143
- orchestrator=devservices
144144
restart: unless-stopped
145145
profile-chunks-consumer:
146-
image: ghcr.io/getsentry/snuba:nightly
146+
image: ghcr.io/getsentry/snuba:nightly-distroless
147147
command: [
148148
rust-consumer,
149149
--storage=profile_chunks,
@@ -172,7 +172,7 @@ services:
172172
- orchestrator=devservices
173173
restart: unless-stopped
174174
functions-consumer:
175-
image: ghcr.io/getsentry/snuba:nightly
175+
image: ghcr.io/getsentry/snuba:nightly-distroless
176176
command: [
177177
rust-consumer,
178178
--storage=functions_raw,
@@ -201,7 +201,7 @@ services:
201201
- orchestrator=devservices
202202
restart: unless-stopped
203203
metrics-consumer:
204-
image: ghcr.io/getsentry/snuba:nightly
204+
image: ghcr.io/getsentry/snuba:nightly-distroless
205205
command: [
206206
rust-consumer,
207207
--storage=metrics_raw,
@@ -230,7 +230,7 @@ services:
230230
- orchestrator=devservices
231231
restart: unless-stopped
232232
generic-metrics-distributions-consumer:
233-
image: ghcr.io/getsentry/snuba:nightly
233+
image: ghcr.io/getsentry/snuba:nightly-distroless
234234
command: [
235235
rust-consumer,
236236
--storage=generic_metrics_distributions_raw,
@@ -259,7 +259,7 @@ services:
259259
- orchestrator=devservices
260260
restart: unless-stopped
261261
generic-metrics-sets-consumer:
262-
image: ghcr.io/getsentry/snuba:nightly
262+
image: ghcr.io/getsentry/snuba:nightly-distroless
263263
command: [
264264
rust-consumer,
265265
--storage=generic_metrics_sets_raw,
@@ -288,7 +288,7 @@ services:
288288
- orchestrator=devservices
289289
restart: unless-stopped
290290
generic-metrics-counters-consumer:
291-
image: ghcr.io/getsentry/snuba:nightly
291+
image: ghcr.io/getsentry/snuba:nightly-distroless
292292
command: [
293293
rust-consumer,
294294
--storage=generic_metrics_counters_raw,
@@ -317,7 +317,7 @@ services:
317317
- orchestrator=devservices
318318
restart: unless-stopped
319319
generic-metrics-gauges-consumer:
320-
image: ghcr.io/getsentry/snuba:nightly
320+
image: ghcr.io/getsentry/snuba:nightly-distroless
321321
command: [
322322
rust-consumer,
323323
--storage=generic_metrics_gauges_raw,

pyproject.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ snuba = "snuba.cli:main"
7878
dev = [
7979
"devservices>=1.2.1",
8080
"freezegun>=1.5.5",
81-
"honcho>=1.1.0",
8281
"mypy>=1.1.1",
8382
"pre-commit>=4.2.0",
8483
"pytest>=8.3.3",
@@ -136,7 +135,6 @@ module = [
136135
"fastjsonschema",
137136
"fastjsonschema.exceptions",
138137
"granian",
139-
"honcho.manager",
140138
"jsonschema",
141139
"jsonschema.exceptions",
142140
"jsonschema2md",

snuba/cli/devserver.py

Lines changed: 95 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,29 @@
11
import os
2+
import signal
3+
import subprocess
24
import sys
3-
from subprocess import call, list2cmdline
5+
import threading
6+
from subprocess import call
47

58
import click
69

710
from snuba import settings
811

12+
# Match honcho: SIGTERM, then SIGKILL if children do not exit (avoids indefinite
13+
# hang when a child ignores SIGTERM; see also PEP 475 / wait() retry after signals).
14+
_SUBPROCESS_TERM_GRACE_SEC = 5.0
15+
16+
17+
def _reap_after_terminate(proc: subprocess.Popen[bytes], grace_sec: float) -> None:
18+
"""Wait for proc to exit after terminate(); kill -9 if still alive after grace_sec."""
19+
try:
20+
proc.wait(timeout=grace_sec)
21+
except subprocess.TimeoutExpired:
22+
if proc.poll() is None:
23+
proc.kill()
24+
proc.wait()
25+
26+
927
COMMON_RUST_CONSUMER_DEV_OPTIONS = [
1028
"--use-rust-processor",
1129
"--auto-offset-reset=latest",
@@ -21,8 +39,6 @@
2139
def devserver(*, bootstrap: bool, workers: bool, log_level: str) -> None:
2240
"Starts all Snuba processes for local development."
2341

24-
from honcho.manager import Manager
25-
2642
os.environ["PYTHONUNBUFFERED"] = "1"
2743

2844
if bootstrap:
@@ -518,13 +534,82 @@ def devserver(*, bootstrap: bool, workers: bool, log_level: str) -> None:
518534
),
519535
]
520536

521-
manager = Manager()
537+
sys.exit(_run_daemons(daemons))
538+
539+
540+
def _run_daemons(daemons: list[tuple[str, list[str]]]) -> int:
541+
procs: dict[str, subprocess.Popen[bytes]] = {}
542+
threads: list[threading.Thread] = []
543+
first_failure: list[int] = []
544+
done = threading.Event()
545+
cleanup_started = threading.Event()
546+
failure_lock = threading.Lock()
547+
supervisor_signal: list[int] = []
548+
549+
def shutdown(signum: int, frame: object) -> None:
550+
# Mark cleanup before terminate so stream threads do not treat SIGTERM as a
551+
# natural crash (honcho parity when one daemon exits or user interrupts).
552+
cleanup_started.set()
553+
if not supervisor_signal:
554+
supervisor_signal.append(signum)
555+
for proc in procs.values():
556+
if proc.poll() is None:
557+
proc.terminate()
558+
done.set()
559+
560+
signal.signal(signal.SIGINT, shutdown)
561+
signal.signal(signal.SIGTERM, shutdown)
562+
563+
def stream(name: str, proc: subprocess.Popen[bytes]) -> None:
564+
try:
565+
assert proc.stdout is not None
566+
for line in proc.stdout:
567+
sys.stdout.write(f"{name} | {line.decode(errors='replace')}")
568+
sys.stdout.flush()
569+
rc = proc.wait()
570+
with failure_lock:
571+
if rc != 0 and not cleanup_started.is_set():
572+
if not first_failure:
573+
first_failure.append(rc)
574+
except BaseException:
575+
with failure_lock:
576+
if not cleanup_started.is_set() and not first_failure:
577+
first_failure.append(1)
578+
raise
579+
finally:
580+
# Always unblock the supervisor (e.g. BrokenPipe/EPIPE on stdout write).
581+
done.set()
582+
522583
for name, cmd in daemons:
523-
manager.add_process(
524-
name,
525-
list2cmdline(cmd),
526-
quiet=False,
584+
proc = subprocess.Popen(
585+
cmd,
586+
stdin=subprocess.DEVNULL,
587+
stdout=subprocess.PIPE,
588+
stderr=subprocess.STDOUT,
527589
)
590+
procs[name] = proc
591+
t = threading.Thread(target=stream, args=(name, proc), daemon=True)
592+
t.start()
593+
threads.append(t)
594+
595+
done.wait()
596+
cleanup_started.set()
597+
# Any daemon exit ends the supervisor; terminate the rest (honcho parity).
598+
for proc in procs.values():
599+
if proc.poll() is None:
600+
proc.terminate()
601+
602+
for proc in procs.values():
603+
if proc.poll() is None:
604+
_reap_after_terminate(proc, _SUBPROCESS_TERM_GRACE_SEC)
605+
else:
606+
proc.wait()
607+
608+
for t in threads:
609+
t.join()
528610

529-
manager.loop()
530-
sys.exit(manager.returncode)
611+
if first_failure:
612+
return first_failure[0]
613+
if supervisor_signal:
614+
return 128 + supervisor_signal[0]
615+
return 0

uv.lock

Lines changed: 0 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)