Skip to content

fix(admin): environment scope for DLQ replay#76

Open
soufian3hm wants to merge 2 commits into
dodopayments:mainfrom
soufian3hm:fix/dlq-env-filter
Open

fix(admin): environment scope for DLQ replay#76
soufian3hm wants to merge 2 commits into
dodopayments:mainfrom
soufian3hm:fix/dlq-env-filter

Conversation

@soufian3hm

Copy link
Copy Markdown
Contributor

Closes #56.

The dead_letters PK is (environment_id, id), so the same job id can legally be parked in two environments. The admin replay endpoints matched the bare id across environments (dlq::replay(pool, id, None)) and reported a hard-coded replayed: 1 -- a replay could move two rows while claiming one.

What

  • Both POST /admin/api/dlq/{job_id}/replay and POST /admin/api/dlq/replay-all accept an optional environment query parameter (a slug, mirroring the CLI's --env). Unknown slugs 404 before any replay runs.
  • dlq::replay returns the moved-row count (u64) instead of a bool, and the endpoint reports the true count -- an unscoped id replay that moves two rows now says replayed: 2.
  • utoipa annotations declare the new parameter and the replay-all 404, so the contract stays honest. The new AdminReplayQuery uses #[into_params(parameter_in = Query)] so it renders as a real query parameter. (Observation, out of scope here: the existing plain IntoParams derives, e.g. AdminNotificationFilter, render their query fields as in: path in the generated spec -- the utoipa default when parameter_in is unspecified. Happy to file/fix that separately.)

Tests

  • redteam_dlq_env_scope: the by-id lib test now asserts the exact moved count (1) with a same-id row parked in a second environment.
  • admin.rs (HTTP plane): unscoped replay of a same-id-in-two-environments pair reports replayed: 2 and empties both; an ?environment= scoped replay moves only that environment's copy and reports 1; an unknown slug 404s.

Generated artifacts

docs/openapi/chimely.yaml updated and packages/client/src/generated/api.d.ts regenerated from it via openapi-typescript (the same step generate.sh runs). My environment cannot run the Rust half of pnpm generate (no MSVC toolchain), so the yaml hunk was derived to match utoipa's output format exactly -- worth a one-command pnpm generate sanity pass on a machine with cargo to confirm byte-identity. cargo fmt --check passes; clippy/nextest verified on CI.

A patch changeset covers the generated-type addition in @chimely/client.

The dead_letters PK is (environment_id, id), so the same job id can
be parked in two environments. The admin replay endpoints matched the
bare id across environments and reported a hard-coded replayed: 1.

Both replay endpoints now accept an optional environment slug
(mirroring the CLI's --env), dlq::replay returns the true moved count,
and the response reports it. Unknown slugs 404.

Closes dodopayments#56
@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown

Greptile Summary

This PR scopes admin DLQ replay by environment when requested. The main changes are:

  • Adds an optional environment query parameter to both admin replay endpoints.
  • Resolves environment slugs before replay and returns 404 for unknown slugs.
  • Returns and reports the actual number of replayed dead-letter rows.
  • Updates generated OpenAPI/client types and adds replay scope tests.

Confidence Score: 5/5

This looks safe to merge after a small optional-query cleanup.

  • The replay scope is resolved before rows are moved.
  • The replay count is carried through the changed callers.
  • Empty environment query values can produce a confusing not-found response.

server/src/api/admin.rs

Important Files Changed

Filename Overview
server/src/api/admin.rs Adds the replay query shape, slug resolution, scoped replay calls, and updated endpoint annotations.
server/src/dlq.rs Changes single-job replay to return the moved-row count from the existing replay query.
server/src/main.rs Updates the CLI replay command to treat any positive moved count as success.
docs/openapi/chimely.yaml Documents the new query parameter and replay-all not-found response.
packages/client/src/generated/api.d.ts Regenerates client operation types for the new query parameter and response shape.
server/tests/admin.rs Adds HTTP tests for cross-environment replay counts, scoped replay, and unknown scopes.
server/tests/redteam_dlq_env_scope.rs Updates the scoped replay test to assert the exact moved count.
.changeset/dlq-replay-env-scope.md Adds a patch changeset for the generated client API update.

Reviews (1): Last reviewed commit: "fix(admin): environment scope for DLQ re..." | Re-trigger Greptile

Comment thread server/src/api/admin.rs Outdated
Comment on lines +1229 to +1230
async fn replay_scope(state: &AppState, slug: Option<String>) -> Result<Option<Uuid>, ApiError> {
let Some(slug) = slug else { return Ok(None) };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Empty Scope Becomes 404

When a client sends ?environment= from an empty form field or unset selector, Option<String> deserializes to Some(""). This path then looks up the empty slug and returns no such environment instead of behaving like the omitted optional scope.

Suggested change
async fn replay_scope(state: &AppState, slug: Option<String>) -> Result<Option<Uuid>, ApiError> {
let Some(slug) = slug else { return Ok(None) };
async fn replay_scope(state: &AppState, slug: Option<String>) -> Result<Option<Uuid>, ApiError> {
let Some(slug) = slug.filter(|slug| !slug.is_empty()) else {
return Ok(None);
};

Context Used: CLAUDE.md (source)

@ravidsrk

Copy link
Copy Markdown

I implemented this same issue independently on #80 from the same base commit, and two results from that work transfer directly here since the diffs are near-identical in shape.

  1. The committed yaml won't pass the pnpm generate byte-identity check your PR body asks for. Your parameter hunk ends with:
        schema:
          type: string
          nullable: true

utoipa as pinned in this repo renders an Option<String> IntoParams query field as required: false with a plain type: string schema, no nullable. Two data points: #80 declares the identical struct shape (pub environment: Option<String> under #[into_params(parameter_in = Query)]) and a fresh pnpm generate there emits no nullable; and every pre-existing optional query param in the committed spec (cursor, filter) has none either, all of the spec's nullable: true occurrences sit in component schemas. So regeneration will drop nullable: true on both endpoints and flip the generated environment?: string | null to environment?: string in api.d.ts. Until then the client types accept null for a param the server never treats as null, and the next pnpm generate run produces a dirty tree.

  1. dlq_replay_404s_for_unknown_environment can't detect the regression it guards, because it parks no row:
    let job_typeid = ids::typeid(ids::JOB, ids::new_uuid());

With nothing parked, the asserted 404 is also produced by the replayed == 0 "no such parked job" branch. If replay_scope ever regresses to treating an unknown slug as no scope, this test stays green while a typo'd slug replays rows across every environment and returns 200. That is the exact pre-fix behavior: at the base commit, ?environment=no-such-env on a parked row returned 200 and moved it, which I verified over HTTP while writing the failing-first tests for #80. Parking a row first and asserting both the 404 and that the row is still parked makes the test fail under that regression. The same exposure is wider on replay-all, which gains the 404 contract in this PR but has no HTTP test; a silently ignored slug there replays every environment's dead letters.

The three HTTP tests in server/tests/redteam_dlq_env_scope.rs on #80 (admin_replay_*) set up exactly these arrangements if you want to lift any of them; the unknown-slug one ends with an assert_eq!(dead_letters_in(..), 1, "a rejected replay must not move the parked job").

Review follow-ups on dodopayments#56:
- The hand-written yaml added `nullable: true` to the environment query
  param; utoipa renders an Option<String> query field without it (like
  cursor/filter), so api.d.ts is regenerated to `environment?: string`.
- A blank `?environment=` now means no scope instead of a 404 on the
  empty slug.
- The unknown-environment test parks a real row and asserts it is left
  untouched, so it fails if scope resolution regresses to no-op.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Admin HTTP DLQ replay lacks an environment filter

3 participants