Skip to content

fix(intake): handle concurrent docker-run Conflict in run_clickhouse.sh#893

Open
ironcommit wants to merge 1 commit into
mainfrom
fix/ci-job-89395904003
Open

fix(intake): handle concurrent docker-run Conflict in run_clickhouse.sh#893
ironcommit wants to merge 1 commit into
mainfrom
fix/ci-job-89395904003

Conversation

@ironcommit

@ironcommit ironcommit commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fixes a flaky integration-test failure (CI job 89395904003) where test_publish_skips_nan_and_failed_scores failed at fixture setup with CalledProcessError (exit 125) from run_clickhouse.sh
  • Root cause: two pytest-xdist workers both observed "no container" in the docker ps checks and then raced to docker run, with the second caller receiving a Docker Conflict error because the first had already created nmp-intake-clickhouse
  • Fix: wrap docker run to capture stderr and treat a "Conflict / already in use" error as a non-fatal condition — the container was created by a concurrent caller and is already running; real docker errors still propagate normally

Root cause

run_clickhouse.sh uses a check-then-create pattern that is not atomic:

docker ps … → not running
docker ps -a … → does not exist
docker run -d --name nmp-intake-clickhouse …   ← race window

Under --dist loadscope with --max-worker-restart=2, a worker restart can cause tests from the same module to be split across two workers with independent session scopes, each independently calling the fixture that invokes this script.

Fix

if ! run_err=$(docker run -d --name "${container_name}"2>&1 >/dev/null); then
  if echo "${run_err}" | grep -qE "Conflict|already in use"; then
    echo "${container_name} created by a concurrent caller; reusing existing container"
  else
    echo "${run_err}" >&2
    exit 1
  fi
fi

Test plan

  • bash -n syntax check passes
  • shellcheck passes with no warnings
  • Manual simulation: Conflict error (exit 125) is handled gracefully, script exits 0
  • Manual simulation: Non-Conflict docker errors still propagate (script exits non-zero)
  • CI integration test suite passes (re-run of the failing job)

The failing commit (8adcee4, Anonymizer Studio scaffold) is entirely unrelated to this failure — it touches only TypeScript frontend files. This is a pre-existing flake in test infrastructure.

Summary by CodeRabbit

  • Bug Fixes

    • Improved ClickHouse startup synchronization so parallel test runs don’t race when creating/starting the container.
    • Made ClickHouse container and port configuration flexible via environment variables.
    • Enhanced readiness and error logging for clearer “running/reachable” status and actionable failure output.
  • Tests

    • Improved ClickHouse bring-up in integration tests with explicit endpoint readiness checks.
    • Improved per-worker log reporting when distributed test nodes shut down unexpectedly.

@ironcommit
ironcommit requested review from a team as code owners July 24, 2026 18:52
@github-actions github-actions Bot added the fix label Jul 24, 2026
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The ClickHouse launcher and evaluator integration tests now support configurable container endpoints, synchronized startup, TCP readiness checks, worker-level readiness logging, and propagation of the resolved ClickHouse URL to Intake services.

Changes

ClickHouse integration startup

Layer / File(s) Summary
Configurable ClickHouse launcher
services/intake/scripts/spans/run_clickhouse.sh
The launcher reads container, port, data, and lock settings from environment variables, coordinates startup with a lock, ensures directories, and logs readiness consistently.
Integration fixture endpoint wiring
plugins/nemo-evaluator/tests/integration/test_publish_to_intake.py
The fixture derives the endpoint from the configured URL, starts ClickHouse with explicit settings, waits for TCP readiness, logs the reachable endpoint, cleans up the container, and passes the URL to Intake services.
Worker readiness reporting
plugins/nemo-evaluator/tests/integration/conftest.py
A pytest node-down hook emits ClickHouse messages collected from worker output through the terminal reporter or stdout.

Sequence Diagram(s)

sequenceDiagram
  participant TestFixture
  participant ClickHouseScript
  participant Docker
  participant IntakeServices
  TestFixture->>ClickHouseScript: start with configured endpoint
  ClickHouseScript->>Docker: lock, inspect, and start container
  Docker-->>ClickHouseScript: configured ports become available
  TestFixture->>TestFixture: wait for TCP readiness
  TestFixture->>IntakeServices: pass NMP_INTAKE_CLICKHOUSE_URL
Loading

Suggested reviewers: sandychapman

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main fix: handling concurrent docker-run conflicts in run_clickhouse.sh.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ci-job-89395904003

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@services/intake/scripts/spans/run_clickhouse.sh`:
- Around line 41-44: Restrict the conflict handling in the container startup
flow to cases where the specific ${container_name} is confirmed running after
docker run fails. Before proceeding to docker exec, recheck the container with
the existing docker-ps mechanism and only swallow the error when it is running;
propagate port-binding and other failures instead.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 61308ed5-777b-429f-94af-3d7ebaf0dacd

📥 Commits

Reviewing files that changed from the base of the PR and between f518e45 and 72b386a.

📒 Files selected for processing (1)
  • services/intake/scripts/spans/run_clickhouse.sh

Comment thread services/intake/scripts/spans/run_clickhouse.sh Outdated
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 27158/34869 77.9% 62.2%
Integration Tests 15965/33581 47.5% 20.0%

The evaluator publish integration tests share one local ClickHouse and platform stack because ClickHouse is expensive to duplicate. Under pytest-xdist, worker restarts can instantiate session fixtures in multiple workers, so concurrent setup must be serialized instead of relying on Docker exit 125 or parsing Docker stderr text.

Add a cross-process startup lock to run_clickhouse.sh, keep the default nmp-intake-clickhouse singleton, and expose env overrides for the container name and host ports for callers that need isolation. The helper logs the resolved HTTP and native endpoints on every successful path.

Hold a fixture-level lock across the evaluator ClickHouse/platform lifecycle so another worker cannot start or tear down the singleton while it is in use. Wire the fixture through the ClickHouse URL, container, and port overrides.

Forward the ClickHouse endpoint diagnostic through pytest-xdist worker output so it remains visible in passing CI logs, not only in captured failure output.

Signed-off-by: Ryan S <267728323+ironcommit@users.noreply.github.com>
@ironcommit
ironcommit force-pushed the fix/ci-job-89395904003 branch from 72b386a to 80b5937 Compare July 24, 2026 19:14

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
plugins/nemo-evaluator/tests/integration/test_publish_to_intake.py (1)

66-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated CLICKHOUSE_CI_LOG_KEY string constant across two files. Same literal independently declared in both; if one drifts, pytest_testnodedown silently drops messages with no error.

  • plugins/nemo-evaluator/tests/integration/test_publish_to_intake.py#L66: keep as the single source of truth (or move to a shared module) and import it from conftest.py.
  • plugins/nemo-evaluator/tests/integration/conftest.py#L37: import CLICKHOUSE_CI_LOG_KEY from test_publish_to_intake.py (or shared module) instead of redeclaring the literal.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/nemo-evaluator/tests/integration/test_publish_to_intake.py` at line
66, Use a single source of truth for CLICKHOUSE_CI_LOG_KEY: retain the
definition in
plugins/nemo-evaluator/tests/integration/test_publish_to_intake.py:66, and
update plugins/nemo-evaluator/tests/integration/conftest.py:37 to import and
reuse it instead of redeclaring the literal.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugins/nemo-evaluator/tests/integration/test_publish_to_intake.py`:
- Around line 151-172: Update the _clickhouse fixture so _file_lock protects
only the startup subprocess.run call, allowing xdist workers to use the shared
container concurrently. Replace the unconditional docker rm -f teardown with
coordination that removes CLICKHOUSE_CONTAINER only after the final worker
releases the shared session, preserving the container for workers still using
it.

---

Nitpick comments:
In `@plugins/nemo-evaluator/tests/integration/test_publish_to_intake.py`:
- Line 66: Use a single source of truth for CLICKHOUSE_CI_LOG_KEY: retain the
definition in
plugins/nemo-evaluator/tests/integration/test_publish_to_intake.py:66, and
update plugins/nemo-evaluator/tests/integration/conftest.py:37 to import and
reuse it instead of redeclaring the literal.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: fd8b601a-23b0-404e-a829-c8ed95a10e79

📥 Commits

Reviewing files that changed from the base of the PR and between 72b386a and 80b5937.

📒 Files selected for processing (3)
  • plugins/nemo-evaluator/tests/integration/conftest.py
  • plugins/nemo-evaluator/tests/integration/test_publish_to_intake.py
  • services/intake/scripts/spans/run_clickhouse.sh

Comment on lines +151 to +172
def _clickhouse(request: pytest.FixtureRequest) -> Iterator[None]:
with _file_lock(CLICKHOUSE_TEST_LOCK_FILE):
if not _docker_available():
pytest.skip("Docker not available; required for ClickHouse-backed Intake")
with _pytest_capture_disabled(request):
subprocess.run(
["bash", str(REPO_ROOT / "services/intake/scripts/spans/run_clickhouse.sh")],
check=True,
cwd=REPO_ROOT,
env={
**os.environ,
"CLICKHOUSE_CONTAINER_NAME": CLICKHOUSE_CONTAINER,
"CLICKHOUSE_HTTP_PORT": str(CLICKHOUSE_HTTP_PORT),
"CLICKHOUSE_NATIVE_PORT": str(CLICKHOUSE_NATIVE_PORT),
},
)
try:
_wait_for_tcp(CLICKHOUSE_HOST, CLICKHOUSE_HTTP_PORT, timeout=60)
_log_clickhouse_endpoint(request)
yield
finally:
subprocess.run(["docker", "rm", "-f", CLICKHOUSE_CONTAINER], check=False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

File lock spans the whole session, serializing all xdist workers.

with _file_lock(...) wraps yield and the teardown, not just the run_clickhouse.sh call. Since _clickhouse is session-scoped and xdist runs one full session per worker, worker B's lock acquisition blocks until worker A's entire test session and its docker rm -f finish. This defeats the goal of letting workers share one container while running in parallel — they now run fully serialized.

Separately, even with a narrower lock, the unconditional docker rm -f in finally removes the shared container as soon as any single worker's session ends, breaking other workers still using it.

🔒 Narrow the lock to just startup (teardown-safety still needs a reference-count/last-worker design)
 def _clickhouse(request: pytest.FixtureRequest) -> Iterator[None]:
-    with _file_lock(CLICKHOUSE_TEST_LOCK_FILE):
-        if not _docker_available():
-            pytest.skip("Docker not available; required for ClickHouse-backed Intake")
-        with _pytest_capture_disabled(request):
-            subprocess.run(
-                ["bash", str(REPO_ROOT / "services/intake/scripts/spans/run_clickhouse.sh")],
-                check=True,
-                cwd=REPO_ROOT,
-                env={
-                    **os.environ,
-                    "CLICKHOUSE_CONTAINER_NAME": CLICKHOUSE_CONTAINER,
-                    "CLICKHOUSE_HTTP_PORT": str(CLICKHOUSE_HTTP_PORT),
-                    "CLICKHOUSE_NATIVE_PORT": str(CLICKHOUSE_NATIVE_PORT),
-                },
-            )
-        try:
-            _wait_for_tcp(CLICKHOUSE_HOST, CLICKHOUSE_HTTP_PORT, timeout=60)
-            _log_clickhouse_endpoint(request)
-            yield
-        finally:
-            subprocess.run(["docker", "rm", "-f", CLICKHOUSE_CONTAINER], check=False)
+    if not _docker_available():
+        pytest.skip("Docker not available; required for ClickHouse-backed Intake")
+    with _file_lock(CLICKHOUSE_TEST_LOCK_FILE):
+        with _pytest_capture_disabled(request):
+            subprocess.run(
+                ["bash", str(REPO_ROOT / "services/intake/scripts/spans/run_clickhouse.sh")],
+                check=True,
+                cwd=REPO_ROOT,
+                env={
+                    **os.environ,
+                    "CLICKHOUSE_CONTAINER_NAME": CLICKHOUSE_CONTAINER,
+                    "CLICKHOUSE_HTTP_PORT": str(CLICKHOUSE_HTTP_PORT),
+                    "CLICKHOUSE_NATIVE_PORT": str(CLICKHOUSE_NATIVE_PORT),
+                },
+            )
+        _wait_for_tcp(CLICKHOUSE_HOST, CLICKHOUSE_HTTP_PORT, timeout=60)
+    _log_clickhouse_endpoint(request)
+    yield
+    # TODO: teardown still needs a reference-count/last-worker check before
+    # `docker rm -f` — otherwise the last worker to finish kills a container
+    # other workers may still depend on.
🧰 Tools
🪛 ast-grep (0.44.1)

[error] 155-165: Command coming from incoming request
Context: subprocess.run(
["bash", str(REPO_ROOT / "services/intake/scripts/spans/run_clickhouse.sh")],
check=True,
cwd=REPO_ROOT,
env={
**os.environ,
"CLICKHOUSE_CONTAINER_NAME": CLICKHOUSE_CONTAINER,
"CLICKHOUSE_HTTP_PORT": str(CLICKHOUSE_HTTP_PORT),
"CLICKHOUSE_NATIVE_PORT": str(CLICKHOUSE_NATIVE_PORT),
},
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)


[error] 171-171: Command coming from incoming request
Context: subprocess.run(["docker", "rm", "-f", CLICKHOUSE_CONTAINER], check=False)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/nemo-evaluator/tests/integration/test_publish_to_intake.py` around
lines 151 - 172, Update the _clickhouse fixture so _file_lock protects only the
startup subprocess.run call, allowing xdist workers to use the shared container
concurrently. Replace the unconditional docker rm -f teardown with coordination
that removes CLICKHOUSE_CONTAINER only after the final worker releases the
shared session, preserving the container for workers still using it.


@pytest.fixture(scope="session")
def _clickhouse(request: pytest.FixtureRequest) -> Iterator[None]:
with _file_lock(CLICKHOUSE_TEST_LOCK_FILE):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider dropping this fixture-level _file_lock entirely and relying on the flock now added to run_clickhouse.sh. Each xdist worker spawns run_clickhouse.sh, and the script serializes the check-then-docker run under flock on ${TMPDIR}/<container>.lock (same container name -> same lock path), so the second caller falls into the "already running" branch and a Docker Conflict is no longer reachable. The startup race is thus fully handled at the shell level, making this Python-side lock redundant. It's also the specific cause of the full-session serialization flagged in the critical review comment, since it wraps yield and teardown rather than just startup. Removing it fixes that regression by construction.

_log_clickhouse_endpoint(request)
yield
finally:
subprocess.run(["docker", "rm", "-f", CLICKHOUSE_CONTAINER], check=False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even once the lock is narrowed to startup, this unconditional docker rm -f still races: the first worker to finish teardown removes the shared singleton while other workers are still mid-test. Note that "just leak the container" isn't a safe alternative here either, because session_id_for() is deterministic (f"{run_id}:{trial_id}", see plugins/nemo-evaluator/src/nemo_evaluator/intake/mapping.py:62) -- stale rows under the same session id would contaminate re-runs, and this teardown is what currently guarantees a clean slate. This needs a last-worker / reference-count guard (or an "only remove if this process created it" check, keyed off run_clickhouse.sh's created-vs-reused signal) before calling docker rm -f.

native_port="${CLICKHOUSE_NATIVE_PORT:-9000}"
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd -- "${script_dir}/../../../.." && pwd)"
data_dir="${CLICKHOUSE_DATA_DIR:-${repo_root}/tmp/intake-clickhouse}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data_dir isn't namespaced by container_name, so the new CLICKHOUSE_CONTAINER_NAME / port overrides don't actually give isolation. A caller who sets only the container name (or only the ports) without also setting CLICKHOUSE_DATA_DIR gets a second server bound to the same /var/lib/clickhouse volume -> lock contention / data corruption. Either derive data_dir (and the lock file) from container_name, or document that CLICKHOUSE_CONTAINER_NAME, the ports, and CLICKHOUSE_DATA_DIR must be overridden together to isolate an instance.

return sock.connect_ex((host, port)) == 0


def pytest_testnodedown(node, error) -> None: # noqa: ANN001, ARG001

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming this weird method name matches up with a hook from the xdist package?

clickhouse_user="${CLICKHOUSE_USER:-default}"
clickhouse_password="${CLICKHOUSE_PASSWORD:-}"
http_port="${CLICKHOUSE_HTTP_PORT:-8123}"
native_port="${CLICKHOUSE_NATIVE_PORT:-9000}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

native port?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants