Skip to content

ci: keep request replayer responsive under parallel tests - #4106

Merged
Leiyks merged 18 commits into
masterfrom
brian.marks/fix-request-replayer-concurrency
Aug 26, 2026
Merged

ci: keep request replayer responsive under parallel tests#4106
Leiyks merged 18 commits into
masterfrom
brian.marks/fix-request-replayer-concurrency

Conversation

@bm1549

@bm1549 bm1549 commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Description

High-level change and expected CI impact report

Stacked on #4097.

test_extension_ci runs as many as 12 PHPT workers against one single-threaded request-replayer. If its worker gets stuck processing a request, /replay also blocks. The test process then waits on agent requests until the job reaches its two-hour timeout.

Run request-replayer with 16 PHP CLI workers. Its file-backed queues use read-modify-write operations, so each session now has a file lock around state changes. MessagePack decoding stays outside the lock. Requests for different sessions can still run at the same time, including the un-tokened PHPT path.

The image is built and signed by CI (Services build in the ci-images pipeline) with src/ baked in, so no runtime source injection is needed. UDP metrics runs under a restart loop. It publishes an immutable versioned tag (php-request-replayer-3.0), replacing the manual Docker Hub push and the DataDog/images mirror digest bump.

This is separate from #4097 (now merged): that PR splits normal and valgrind passes and raises test parallelism; this PR keeps request-replayer responsive under that parallel workload.

Verification

  • The exact CI image retained 200 concurrent requests and passed replay, metrics, stats, remote-config, agent-info, clear, and next-response checks.
  • Killing the UDP metrics process produced a replacement process, which accepted and replayed a tagged metric.
  • The test harness passed 12 consecutive runs in the exact generate-templates image with one CPU and 256 MiB. Child logs use files rather than undrained pipes and are printed if the test fails.
  • PHP and shell syntax checks pass; git diff --check is clean.

Reviewer checklist

  • Test coverage seems ok.
  • Appropriate labels assigned.

@bm1549 bm1549 added the AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos label Aug 13, 2026
@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2 Bot commented Aug 13, 2026

Copy link
Copy Markdown

Pipelines  Tests

Unblock PR with BitsAI

⚠️ Warnings

Your PR has failed checks. Please review the issues below and take necessary action before merging.

🚦 8 Pipeline jobs failed

DataDog/apm-reliability/dd-trace-php | ASAN test_c with multiple observers: [8.3] — 🔧 Needs a code fix, caused by this PR

View more details · View in GitLab

DataDog/apm-reliability/dd-trace-php | test_extension_ci: [8.3] — 🔧 Needs a code fix, caused by this PR

View more details · View in GitLab

DataDog/apm-reliability/dd-trace-php | ASAN test_c: [8.5, arm64] — 🔄 Retry may pass, looks flaky

View more details · View in GitLab

View all 8 failed jobs.

ℹ️ Info

No other issues found (see more)

🧪 All tests passed
❄️ No new flaky tests detected

🔄 Datadog auto-retried 1 job - 0 passed on retry View in Datadog

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 60.63% (+0.00%)

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 3593b54 | Docs | View more details | Give us feedback!

Comment thread .gitlab/test-request-replayer-concurrency.php Outdated
Comment thread .gitlab/install-request-replayer-source.sh Outdated
Base automatically changed from bengl/ci-quickwins to master August 25, 2026 12:50
bengl and others added 9 commits August 25, 2026 14:57
The `test_extension_ci` job ran the extension .phpt suite twice in one
job: once normally, then again under valgrind for leak checking. The
valgrind pass is roughly an order of magnitude slower, and the pair
routinely exceeded the job's 120m timeout — discarding the normal pass's
results along with it. Over the last two weeks this job class failed 2440
times against 3403 successes, with `[8.0]` and `[7.4]` worst affected.

Split the two passes into `test_extension_ci_normal` and
`test_extension_ci_valgrind` make targets, and run them as two jobs. Both
job names keep the `test_extension_ci:` prefix so they still match the
`test_extension_ci:*` glob in flaky-jobs.txt; merge-gate behaviour is
unchanged. `make test_extension_ci` still runs both, via recursive
`$(MAKE)` rather than prerequisites so that `make -jN` cannot run the two
passes concurrently over the same .phpt sandbox.

On PHP >= 7.4 the normal job now sets MAX_TEST_PARALLELISM to 12 to match
its CPU request, and its timeout drops to 45m. Both passes together have
a pc95 of ~30m and a worst case of ~39m on those versions, so the normal
pass alone has headroom. Below 7.4 the timeout stays at 120m and no
parallelism override is set: the Makefile only passes -j to run-tests.php
when RUN_TESTS_IS_PARALLEL is set, which is gated on PHP >= 7.4, so those
versions run serially. The inherited value of 8 from .base_test is unused
there. Both passes together currently take a pc95 of 82-89m and a worst
case of 91-99m on those versions; cutting that budget without per-pass
timing would risk timeouts that `default.retry` then pays for three
times over.

Note the split means the valgrind pass now runs even when the normal pass
fails, where previously `set -xe` skipped it. That is the cost of getting
the normal pass's results back promptly on a broken branch.

Separately, drop two `retry:` overrides that re-added `script_failure` on
top of the repo-wide infrastructure-only `default.retry`:

- `ASAN test_c` used the bare `retry: 2` shorthand, which retries every
  failure reason. Inheriting the default also restores the
  `exit_codes: [75, 128]` retry that the shorthand had replaced.
- `PHP Language Tests` spelled out a `when:` list including
  `script_failure`.

Both re-ran genuine test failures up to three times, tripling the compute
and wall clock of an already-failing pipeline. Neither job can block a
merge — both are listed in flaky-jobs.txt — so the retries bought no
signal. Expect ASAN failures to become more visible as a result.
Most of the comments added in the previous commit explained the change
rather than the code, which is PR context and goes stale. Drop the two
`retry:` notes entirely — the absence of an override is not itself
noteworthy — and cut the rest to a line or two.

Two are kept because they document non-obvious constraints a future
change could silently break: that run-tests.php only gets -j on PHP >=
7.4, which is why the pre-7.4 timeout is higher, and that the composite
`test_extension_ci` target uses recursive $(MAKE) so `make -jN` cannot
run both passes over the same .phpt sandbox.

Comments only; the generated YAML is unchanged apart from the comment
lines it emits.
Applies review suggestion. Comment only.
@Leiyks
Leiyks force-pushed the brian.marks/fix-request-replayer-concurrency branch from f9bd8de to 15fb2c2 Compare August 25, 2026 12:57
Leiyks added 3 commits August 25, 2026 15:00
Signed-off-by: Alexandre Rulleau <alexandre.rulleau@datadoghq.com>
The request-replayer service image was built by a manual `docker buildx
--push` from a developer laptop to Docker Hub, then consumed from the
digest-pinned `images/mirror/` namespace -- so shipping a source change
also required a lockfile PR in DataDog/images, documented nowhere.

Onboard `dockerfiles/services/` to generate-ci-images.php so the image is
built, signed and pushed by CI into this repo's own registry namespace.
Windows builds via the existing windows compose with an out-of-tree
context; its Dockerfile now takes CI_REGISTRY_IMAGE as an ARG so it
builds FROM the internal base, matching dockerfiles/ci/windows.

No consumer is switched over yet: this only adds the ability to build.
The `Services build` job is manual, like the other image builds.
Point the request-replayer service and the Windows docker run at
registry.ddbuild.io/ci/dd-trace-php/dd-trace-ci:php-request-replayer-3.0,
built and signed by the Services build job added in the previous commit.
The image bakes in src/ (COPY src /var/www + composer install), so the
runtime source-injection shim is no longer needed:

- install-request-replayer-source.sh is removed. It waited on a
  $CI_PROJECT_DIR path with no timeout, so a missing checkout hung every
  request until the job timeout, and it touched its ready file after an
  unverified cp, so a partial checkout served a parse error for the rest
  of the job.
- The service command keeps only the metrics-server supervisor, since
  index.php's own spawn races under PHP_CLI_SERVER_WORKERS. It now caps
  restarts and logs them instead of respawning silently forever.
- test-request-replayer-concurrency.php is removed. It asserted the
  generated YAML still contained the router and installer strings, and
  ran in generate-templates ahead of the generators, so it would fail
  every pipeline. Requested by @bwoebi.
- The laptop push targets for this image are dropped from
  dockerfiles/services/Makefile.

Local-dev compose files still reference the Docker Hub 2.0 tag; they move
once Services publish works.
Comment thread .gitlab/generate-common.php Outdated
Comment thread dockerfiles/services/docker-compose.yml Outdated
The 3.0 image is now on Docker Hub, pushed by CI rather than from a
laptop, so the local-dev compose files can move off 2.0.
@Leiyks
Leiyks marked this pull request as ready for review August 25, 2026 14:51
@Leiyks
Leiyks requested review from a team as code owners August 25, 2026 14:51
Leiyks added 2 commits August 25, 2026 17:06
Drop the obsolete top-level `version:` key and use the filename Compose
looks for first, per review feedback. Teach generate-ci-images.php
Compose's documented filename precedence rather than hardcoding
docker-compose.yml, so the rename doesn't silently drop the Services
jobs -- and make an unparseable compose file fail the generator instead
of quietly emitting a pipeline with an OS missing.

Existing compose files are left as they are.
Per review feedback, publish as request-replayer:3.0 rather than
dd-trace-ci:php-request-replayer-3.0. The generator models the registry as
one pipeline-wide variable, so override it for the Services jobs only
instead of reshaping every matrix entry.

No consumer is switched over yet: jobs keep pulling the existing
dd-trace-ci tag until the new image has been built.
Comment thread .gitlab/generate-common.php Outdated
Leiyks added 3 commits August 25, 2026 17:55
Point the Linux service and the local dev compose files at
request-replayer:3.0, now that the image is built, signed and published
under its own repository.

Windows still consumes dd-trace-ci:php-request-replayer-3.0_windows: its
image is built from the shared windows compose, whose Dockerfiles resolve
their FROM base through the same CI_REGISTRY_IMAGE variable, so moving it
needs that base decoupled first.
Guard the UDP metrics server with flock() instead of a pid file, so the
replayer looks after itself under PHP_CLI_SERVER_WORKERS rather than
needing a shell supervisor in the CI service definition.

metricsserver.php now holds an exclusive non-blocking lock for its whole
lifetime, which the kernel releases when it dies. index.php trylocks the
same file as a liveness probe and only spawns when it succeeds. Two
workers racing is harmless: the loser's own LOCK_NB makes it exit before
binding, so udp/80 can never be double-bound -- which the pid file could
not prevent, since SO_REUSEADDR lets a second bind succeed and silently
steal datagrams, and the pid file was written asynchronously by the
wrapper shell after the spawn decision had already been made.

Also stop re-raising warnings through trigger_error(), which is a
ValueError on PHP 8 for any level outside E_USER_* and turned a failed
lock open into an HTML fatal served as HTTP 200.

The service command is not changed here: that follows once the image has
been rebuilt.
The rebuilt image guards the UDP metrics server with flock(), so the
shell supervisor in the service definition is redundant: restore the
plain command and drop the flag that suppressed the in-image spawn.

The replayer is now self-contained -- nothing about running it correctly
under PHP_CLI_SERVER_WORKERS lives in this file any more.

@bwoebi bwoebi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good to me, and tests seem to work

@Leiyks
Leiyks merged commit e24538c into master Aug 26, 2026
2163 of 2172 checks passed
@Leiyks
Leiyks deleted the brian.marks/fix-request-replayer-concurrency branch August 26, 2026 11:09
@github-actions github-actions Bot added this to the 1.25.0 milestone Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants