Skip to content

feat(finalizer): serve finalization jobs with Effect HTTP - #25

Merged
mrevanzak merged 13 commits into
finalizer/effect-workerfrom
finalizer/effect-server
Aug 17, 2026
Merged

feat(finalizer): serve finalization jobs with Effect HTTP#25
mrevanzak merged 13 commits into
finalizer/effect-workerfrom
finalizer/effect-server

Conversation

@mrevanzak

@mrevanzak mrevanzak commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add Effect-based JobStore, Ffmpeg finalization, and the existing HTTP job protocol.
  • Build the Node server bundle and Docker image from the Effect HTTP application.
  • Harden input bounds, per-job serialization, cleanup safety, and protocol coverage.
  • Keep R2/D1 transport failures retryable (RecordingsUnavailable, FinalizerDbUnavailable) instead of permanently failing recordings.
  • Fail or release claimed jobs after manifest/plan decode so terminal decode errors cannot leak a lease.
  • Seal finalize jobs atomically in JobStore (open → sealing) instead of a process-local Set.
  • Propagate unpublished-output delete transport errors so Queue can redeliver.

Residual

  • process.ts unpublished-output cleanup uses Effect.exit + flatMap, which is interruptible. A heartbeat raceFirst interrupt during publish can skip the delete and leave orphan R2. Original ignore-swallow on the LeaseLost/delete path is fixed.

Validation

  • pnpm --filter @interview-web/finalizer test (49 passed)
  • pnpm --filter @interview-web/finalizer check-types
  • pnpm --filter @interview-web/finalizer build:server
  • Docker build

@mrevanzak mrevanzak changed the title finalizer/effect server feat(finalizer): serve finalization jobs with Effect HTTP Aug 17, 2026
@access-time-code-pr-reviewer

access-time-code-pr-reviewer Bot commented Aug 17, 2026

Copy link
Copy Markdown

Review Summary

This PR introduces an Effect-native recording finalizer spanning the Cloudflare Worker and Node/ffmpeg container. It adds typed domain errors, scoped cleanup, validated manifests/plans, checksum and publication verification, retryable orchestration, and layered tests while preserving the existing HTTP and queue contracts.

Key Changes:

  • Adds shared Effect domain schemas, branded identifiers, media utilities, validation, and tagged failure types.
  • Reworks the Node finalizer into Effect services for HTTP handling, job storage, ffmpeg execution, segment assembly, and output publication.
  • Reworks Worker dispatch, queue, D1, R2, and container integrations behind Effect context layers with retries, leases, and cleanup.
  • Adds an esbuild-bundled Node server and Docker runtime configuration.
  • Adds domain, adapter, server, worker-layer, finalization, and orchestration test coverage.

Confidence Score: 4/5

Recommendation: Review complete.

Rationale: The supplied changed-file context covers the architecture and primary execution paths, and the implementation was inspected directly. Automated verification was unavailable because pnpm is not installed in the checkout environment.

Special Attention

- Lease heartbeat cancellation and behavior when heartbeat renewal fails. - Retry and redelivery semantics for container, D1, R2 publication, and cleanup failures. - Exact checksum, size, content-type, and idempotent publication verification. - Segment byte assembly and ffmpeg remux/concat behavior for multi-part recordings. - Docker/esbuild output freshness and separation of Worker-only versus Node-only dependencies.

Important Files Changed

File Overview
packages/finalizer/src/worker/process.ts Coordinates claim, validation, part transfer, container finalization, publication verification, lease heartbeats, retries, completion, release, and cleanup.
packages/finalizer/src/server/finalize.ts Assembles uploaded parts by segment, remuxes and concatenates media with ffmpeg, probes outputs, and records checksums and metadata.
packages/finalizer/src/worker/recordings.ts Provides the Effect-wrapped R2 adapter, including checksum and size validation for inputs and publication metadata handling.
packages/finalizer/src/domain/validate.ts Decodes and validates manifests and finalization plans, enforcing contiguous indexes, size limits, and plan consistency.
packages/finalizer/src/server/http.ts Implements the container HTTP job protocol, request limits, checksum validation, finalization dispatch, output serving, and error mapping.

Sequence Diagram

sequenceDiagram
  participant Q as Cloudflare Queue
  participant W as Finalizer Worker
  participant D as D1
  participant R as R2
  participant C as ffmpeg Container
  Q->>W: Deliver sessionId
  W->>D: Claim finalization lease
  D-->>W: Manifest and finalize plan
  loop Each manifest part
    W->>R: Read and verify part
    R-->>W: Bytes, size, checksum
    W->>C: Upload part
  end
  W->>C: Finalize segments
  C-->>W: Output stream, checksum, metadata
  W->>R: Publish output idempotently
  R-->>W: Publication metadata
  W->>D: Complete leased job
  W->>C: Best-effort delete job
  alt Retryable failure
    W->>D: Release lease for retry
  else Terminal failure
    W->>D: Mark finalization failed
  end
Loading
Audit history - 9c0afef0f3b484ac7ea7b5b677908ec9b28a90e7054ba63d7ba2acade66c6d8e [comment 3794752548] — mrevanzak: Cloudflare treats `pingEndpoint` as a host/path, not a path. `container/health` becomes `GET http://container:8080/health`. The server returns 204 on `/health` - b8b9a4d0a360f7e3a42facabbe92d6a8c53ab6935d22627b55ac63cf16d4a416 [comment 3794752808] — mrevanzak: `heartbeat` is `db.renew.pipe(Effect.repeat(Schedule.fixed("60 seconds")), Effect.asVoid)`. The infinite Schedule never succeeds; `raceFirst` only ends if the

@access-time-code-pr-reviewer access-time-code-pr-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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


🤖 review-bot · openai/gpt-5.6-luna · full · 182.8s · 1,333,784 tokens · diff: 18 files · preloaded context: 19 changed + 10 related · 1 noise filtered · policy: github-pr-review, thermo-nuclear-code-quality-review · optional skills invoked: none

)
);
}).pipe(
const manifest = yield* decodeManifest(job.manifest);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Critical

Manifest and finalize-plan validation runs outside the terminal-error handling around runAttempt. Invalid persisted data therefore leaves the row in finalizing until lease expiry and retries forever instead of marking it failed. Move both decode calls inside the same catchIf(isTerminalFinalization, ...) boundary.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/finalizer/src/worker/process.ts
Line: 186
Severity: critical
Comment: Manifest and finalize-plan validation runs outside the terminal-error handling around `runAttempt`. Invalid persisted data therefore leaves the row in `finalizing` until lease expiry and retries forever instead of marking it failed. Move both decode calls inside the same `catchIf(isTerminalFinalization, ...)` boundary.

How can I resolve this? If you propose a fix, please make it concise.

Think this is incorrect? Reply false positive: <reason>.

@mrevanzak
mrevanzak force-pushed the finalizer/effect-server branch from 4eba94b to ac3e1a4 Compare August 17, 2026 07:16

@access-time-code-pr-reviewer access-time-code-pr-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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


🤖 review-bot · openai/gpt-5.6-luna · full · 157.1s · 994,453 tokens · diff: 18 files · preloaded context: 19 changed + 10 related · 1 noise filtered · policy: github-pr-review, thermo-nuclear-code-quality-review · optional skills invoked: none

return json(404, { error: "not found" });
}
const job = decodeURIComponent(match[1] as string);
if (request.method === "DELETE" && !match[2] && !match[4]) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Critical

DELETE does not check sealing, so a client can delete the job while finalization is using it. The in-flight finalizer may then recreate the directory in setOutput, leaving a deleted job resurrected with a missing output path or inconsistent state. Reject deletion while sealing, or coordinate deletion through the same job lifecycle/lock.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/finalizer/src/server/http.ts
Line: 83
Severity: critical
Comment: DELETE does not check `sealing`, so a client can delete the job while finalization is using it. The in-flight finalizer may then recreate the directory in `setOutput`, leaving a deleted job resurrected with a missing output path or inconsistent state. Reject deletion while sealing, or coordinate deletion through the same job lifecycle/lock.

How can I resolve this? If you propose a fix, please make it concise.

Think this is incorrect? Reply false positive: <reason>.

if (size > limit) {
return Effect.succeed({ ...state, size, tooLarge: true });
}
if (state.tooLarge) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Warning

After a chunked request exceeds the limit, the handler continues consuming the entire remaining body because tooLarge only stops buffering. An attacker can stream an unbounded body and tie up the container despite the 512 MiB/2 MiB limits. Abort or destroy the request stream as soon as the limit is exceeded and return 413.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/finalizer/src/server/http.ts
Line: 54
Severity: warning
Comment: After a chunked request exceeds the limit, the handler continues consuming the entire remaining body because `tooLarge` only stops buffering. An attacker can stream an unbounded body and tie up the container despite the 512 MiB/2 MiB limits. Abort or destroy the request stream as soon as the limit is exceeded and return 413.

How can I resolve this? If you propose a fix, please make it concise.

Think this is incorrect? Reply false positive: <reason>.

@access-time-code-pr-reviewer access-time-code-pr-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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


🤖 review-bot · openai/gpt-5.6-luna · full · 166.1s · 1,066,650 tokens · diff: 18 files · preloaded context: 19 changed + 10 related · 1 noise filtered · policy: github-pr-review, thermo-nuclear-code-quality-review · optional skills invoked: react-useeffect, vercel-react-best-practices

Comment thread packages/finalizer/src/worker/process.ts Outdated

@access-time-code-pr-reviewer access-time-code-pr-reviewer Bot Aug 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

False positive

critical: The container health probe is configured for container/health, but the HTTP server only returns 204 for /health (http.ts:75). Containers will fail health checks and remain unavailable; align pingEndpoint with /health or add the configured route.

Reviewed by: mrevanzak
Reason: Cloudflare treats pingEndpoint as a host/path, not a path. container/health becomes GET http://container:8080/health. The server returns 204 on /health.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

false positive: Cloudflare treats pingEndpoint as a host/path, not a path. container/health becomes GET http://container:8080/health. The server returns 204 on /health.

@access-time-code-pr-reviewer access-time-code-pr-reviewer Bot Aug 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ False positive recorded

Reviewed by: mrevanzak
Reason: Cloudflare treats pingEndpoint as a host/path, not a path. container/health becomes GET http://container:8080/health. The server returns 204 on /health.

@mrevanzak
mrevanzak force-pushed the finalizer/effect-server branch from ac3e1a4 to 4ff6122 Compare August 17, 2026 07:21

@access-time-code-pr-reviewer access-time-code-pr-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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


🤖 review-bot · openai/gpt-5.6-luna · full · 157.1s · 833,020 tokens · diff: 18 files · preloaded context: 19 changed + 10 related · 1 noise filtered · policy: github-pr-review, thermo-nuclear-code-quality-review · optional skills invoked: none

return json(404, { error: "not found" });
}
const job = decodeURIComponent(match[1] as string);
if (request.method === "DELETE" && !match[2] && !match[4]) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Warning

DELETE is allowed while a job is sealing. A concurrent delete can remove the job directory during finalization, after which setOutput may recreate the directory and publish an output for a job the caller already deleted. Reject deletion while sealing.has(job) or coordinate deletion through the same job lock.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/finalizer/src/server/http.ts
Line: 83
Severity: warning
Comment: DELETE is allowed while a job is sealing. A concurrent delete can remove the job directory during finalization, after which `setOutput` may recreate the directory and publish an output for a job the caller already deleted. Reject deletion while `sealing.has(job)` or coordinate deletion through the same job lock.

How can I resolve this? If you propose a fix, please make it concise.

Think this is incorrect? Reply false positive: <reason>.

@mrevanzak
mrevanzak force-pushed the finalizer/effect-server branch from 4ff6122 to f407e36 Compare August 17, 2026 07:27
@mrevanzak
mrevanzak force-pushed the finalizer/effect-server branch from f407e36 to 2b2f317 Compare August 17, 2026 07:43

@access-time-code-pr-reviewer access-time-code-pr-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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


🤖 review-bot · openai/gpt-5.6-luna · full · 171.0s · 1,250,743 tokens · diff: 17 files · preloaded context: 18 changed + 10 related · 1 noise filtered · policy: github-pr-review, thermo-nuclear-code-quality-review · optional skills invoked: none

container.deleteJob(job).pipe(Effect.orDie)
);
return { outputKey, published };
return yield* Effect.raceFirst(work, heartbeat);

@access-time-code-pr-reviewer access-time-code-pr-reviewer Bot Aug 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

False positive

critical: heartbeat succeeds after renewing the lease, so Effect.raceFirst(work, heartbeat) can finish the entire finalization before work uploads, finalizes, and publishes the output. Long-running jobs may remain leased without producing output. Run the heartbeat in a scoped background fiber and await work, propagating lease-loss failures separately.

Reviewed by: mrevanzak
Reason: heartbeat is db.renew.pipe(Effect.repeat(Schedule.fixed("60 seconds")), Effect.asVoid). The infinite Schedule never succeeds; raceFirst only ends if the lease renew fails (LeaseLost). The bot treated repeat as a one-shot.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

false positive: heartbeat is db.renew.pipe(Effect.repeat(Schedule.fixed("60 seconds")), Effect.asVoid). The infinite Schedule never succeeds; raceFirst only ends if the lease renew fails (LeaseLost). The bot treated repeat as a one-shot.

@access-time-code-pr-reviewer access-time-code-pr-reviewer Bot Aug 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ False positive recorded

Reviewed by: mrevanzak
Reason: heartbeat is db.renew.pipe(Effect.repeat(Schedule.fixed("60 seconds")), Effect.asVoid). The infinite Schedule never succeeds; raceFirst only ends if the lease renew fails (LeaseLost). The bot treated repeat as a one-shot.

@mrevanzak
mrevanzak merged commit 564199e into main Aug 17, 2026
3 checks passed
@mrevanzak
mrevanzak deleted the finalizer/effect-server branch August 17, 2026 08:39
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.

1 participant