Skip to content

Add ab-av1 worker websocket and shared CRF claiming#19

Open
mjc wants to merge 49 commits into
mainfrom
mjc/reenc-133-worker-websocket
Open

Add ab-av1 worker websocket and shared CRF claiming#19
mjc wants to merge 49 commits into
mainfrom
mjc/reenc-133-worker-websocket

Conversation

@mjc

@mjc mjc commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Adds the ab-av1 worker websocket path on the Phoenix endpoint with token auth, protocol validation, session tracking, and job assignment replies.

Also switches local CRF Broadway dispatch to the same atomic claim path so remote websocket workers and local Broadway cannot claim the same analyzed video twice.

Verification:

  • mix test test/reencodarr/broadway_producers_test.exs test/reencodarr_web/channels/worker_channel_test.exs test/reencodarr/ab_av1/worker_protocol_test.exs test/reencodarr/ab_av1/worker_sessions_test.exs
  • mix credo --strict
  • mix compile --warnings-as-errors

Summary by CodeRabbit

  • New Features
    • Added token-gated WebSocket support for AB-AV1 worker clients, including end-to-end worker protocol handling (announcements, work pulling, progress, results, completion, and transfer streaming).
    • Added a Workers page that lists connected worker sessions and shows active work status.
    • Extended the dashboard/diagnostics to display the worker WebSocket URL and a token fingerprint, plus live session details.
  • Bug Fixes
    • Improved CRF-search claiming to be atomic and avoid duplicate work; added reliable requeue/cleanup on worker disconnect.
  • Documentation/Chores
    • Added distributed worker protocol docs, contributor memory updates, worker-token setup for dev/Nix, runtime worker tuning options, and cache path ignore rules.
  • Tests
    • Added/expanded tests for worker protocol parsing, session lifecycle, WebSocket/channel behavior, worker config, and dashboard/workers UI.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds a websocket-based worker flow for CRF-search, including token authentication, session tracking, atomic work claiming, transfer streaming, dashboard and workers UI updates, diagnostics output, and supporting config/documentation.

Changes

Worker Websocket Feature

Layer / File(s) Summary
Runtime token config and endpoint wiring
config/config.exs, config/runtime.exs, nix/dev-shell.nix, nix/module.nix, lib/reencodarr_web/endpoint.ex
Derives worker token and worker settings from environment values, mirrors token derivation in Nix scripts, and registers the worker websocket endpoint.
Worker protocol helpers and tests
lib/reencodarr/ab_av1/worker_config.ex, lib/reencodarr/ab_av1/worker_protocol.ex, test/reencodarr/ab_av1/worker_protocol_test.exs, test/reencodarr/ab_av1/worker_config_test.exs
Defines protocol constants, structs, parsers, response builders, config helpers, and matching tests for worker protocol payloads.
WorkerSessions GenServer and application wiring
lib/reencodarr/ab_av1/worker_sessions.ex, lib/reencodarr/application.ex, lib/reencodarr/diagnostics.ex, test/reencodarr/ab_av1/worker_sessions_test.exs
Implements the ETS-backed worker session GenServer, supervises it in the application tree, exposes it in diagnostics, and covers lifecycle and staleness behavior in tests.
Atomic video claim query
lib/reencodarr/media.ex, lib/reencodarr/media/video_queries.ex, lib/reencodarr/crf_searcher/broadway/producer.ex, test/reencodarr/broadway_producers_test.exs
Adds the transactional CRF-search video claim path, wires the Broadway producer to use it, and validates claim behavior in tests.
WorkerChannel and WorkerSocket implementation
lib/reencodarr_web/channels/worker_channel.ex, lib/reencodarr_web/channels/worker_socket.ex, test/support/channel_case.ex, test/reencodarr_web/channels/worker_channel_test.exs
Implements token-authenticated worker sockets, worker channel event handling, transfer streaming, termination requeueing, and channel test scaffolding and coverage.
Dashboard worker websocket UI
lib/reencodarr_web/live/dashboard_live.ex, test/reencodarr_web/live/dashboard_v2_live_test.exs
Adds worker websocket configuration to the dashboard UI and covers the rendered output in LiveView tests.
Workers LiveView and route
lib/reencodarr_web/live/workers_live.ex, lib/reencodarr_web/router.ex, test/reencodarr_web/live/workers_live_test.exs
Adds the workers listing LiveView, routes it at /workers, and covers its rendering behavior in tests.
Serena memory and repo docs
.gitignore, .serena/memories/*.md, docs/distributed_worker_protocol.md
Adds cache ignore rules and Serena memory documents covering conventions, core facts, maintenance, commands, task completion, tech stack, and the worker protocol documentation.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Worker
  participant WorkerSocket
  participant WorkerChannel
  participant WorkerSessions
  participant Media
  Worker->>WorkerSocket: connect(token)
  WorkerSocket-->>Worker: assign worker_id
  Worker->>WorkerChannel: join("workers:crf_search")
  Worker->>WorkerChannel: announce(payload)
  WorkerChannel->>WorkerSessions: register(attrs)
  WorkerChannel-->>Worker: accepted(protocol_version)
  Worker->>WorkerChannel: pull_work
  WorkerChannel->>Media: claim_next_video_for_crf_search
  Media-->>WorkerChannel: video or nil
  WorkerChannel->>WorkerSessions: assign_video
  WorkerChannel-->>Worker: work_assigned or no_work
  Worker->>WorkerChannel: crf_search_result / crf_search_completed
  WorkerChannel->>WorkerSessions: clear_video
  WorkerChannel-->>Worker: event_ack
Loading

Poem

A rabbit taps the wire with glee,
Workers hop in over websocket sea,
CRF jobs leap from queue to hand,
Sessions hum in ETS land,
Tokens sparkle, screens glow bright—
Hoppy review, and all feels right 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: the new ab-av1 worker websocket and shared CRF claiming path.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mjc/reenc-133-worker-websocket

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@mjc mjc marked this pull request as ready for review July 7, 2026 17:23
Copilot AI review requested due to automatic review settings July 7, 2026 17:23
Comment thread config/runtime.exs
Comment thread docs/ab_av1_worker_websocket.md Outdated
Comment thread lib/reencodarr/ab_av1/worker_protocol.ex
Comment thread lib/reencodarr/ab_av1/worker_protocol.ex Outdated
Comment thread lib/reencodarr/crf_searcher/broadway/producer.ex Outdated
Comment thread lib/reencodarr/diagnostics.ex

Copilot AI 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.

Pull request overview

Adds a Phoenix WebSocket/Channel surface for remote ab-av1 CRF-search workers, and unifies CRF job dispatching behind an atomic “claim” so local Broadway and remote workers don’t double-claim the same analyzed video.

Changes:

  • Introduces authenticated worker WebSocket endpoint (/workers/socket) with protocol validation, session tracking, heartbeat, and job assignment replies.
  • Adds an atomic DB claim path for CRF-search work and switches the local CRF Broadway producer to use it.
  • Extends diagnostics + dashboard UI/docs/tests to surface worker connection info and validate protocol/session behavior.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
test/support/channel_case.ex Adds a ChannelCase helper for channel tests with DB sandbox setup.
test/reencodarr/broadway_producers_test.exs Adds tests asserting atomic claim transitions and prevents redispatch.
test/reencodarr/ab_av1/worker_sessions_test.exs Adds tests for worker session lifecycle, expiry, and diagnostics output.
test/reencodarr/ab_av1/worker_protocol_test.exs Adds tests for building job_assigned payloads.
test/reencodarr_web/live/dashboard_v2_live_test.exs Tests dashboard display of worker WebSocket token fingerprint and URL.
test/reencodarr_web/channels/worker_channel_test.exs End-to-end channel tests for auth, announce, work claiming, heartbeat, and disconnect requeue.
lib/reencodarr/media/video_queries.ex Implements atomic “claim next CRF-search video” query + state transition.
lib/reencodarr/media.ex Exposes claim_next_video_for_crf_search/0 from the Media context.
lib/reencodarr/diagnostics.ex Adds worker session listing to diagnostics output.
lib/reencodarr/crf_searcher/broadway/producer.ex Switches Broadway dispatch to atomic claim to avoid double-claiming.
lib/reencodarr/application.ex Starts the WorkerSessions GenServer under the main supervision tree.
lib/reencodarr/ab_av1/worker_sessions.ex New GenServer/ETS-backed worker session registry with expiry/touch/assignment.
lib/reencodarr/ab_av1/worker_protocol.ex New helpers for protocol validation and server reply payloads.
lib/reencodarr_web/live/dashboard_live.ex Displays worker socket URL and token fingerprint (without exposing the token).
lib/reencodarr_web/endpoint.ex Mounts the worker socket at /workers/socket.
lib/reencodarr_web/channels/worker_socket.ex Implements token-authenticated socket connect and worker socket ID.
lib/reencodarr_web/channels/worker_channel.ex Implements worker channel join/announce/heartbeat/work request/disconnect behavior.
docs/ab_av1_worker_websocket.md Documents initial worker WebSocket contract and payload shapes.
config/runtime.exs Adds runtime worker-token config (env var or derived fallback).
.serena/memories/tech_stack.md Adds project “memory” notes (tech stack).
.serena/memories/task_completion.md Adds project “memory” notes (finish gates).
.serena/memories/suggested_commands.md Adds project “memory” notes (common commands).
.serena/memories/memory_maintenance.md Adds project “memory” notes (maintenance guidance).
.serena/memories/core.md Adds project “memory” notes (repo structure/entry point).
.serena/memories/conventions.md Adds project “memory” notes (key conventions).
.gitignore Ignores Serena cache directory.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/reencodarr_web/channels/worker_channel.ex Outdated
Comment thread lib/reencodarr/media/video_queries.ex
Comment thread docs/ab_av1_worker_websocket.md Outdated
Comment thread docs/ab_av1_worker_websocket.md Outdated
@mjc

mjc commented Jul 7, 2026

Copy link
Copy Markdown
Owner Author

needs more type specs

@coderabbitai coderabbitai 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.

Actionable comments posted: 12

🤖 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 `@config/runtime.exs`:
- Around line 23-32: The worker token derivation in runtime config uses a raw
SHA-256 hash of secret_key_base concatenated with a label, which should be
replaced with an HMAC-based derivation. Update the worker_token logic in the
runtime config to use :crypto.mac(:hmac, :sha256, secret_key_base,
"ab-av1-worker-token") instead of :crypto.hash/2 with string concatenation,
while keeping the existing Base.url_encode64 formatting and the surrounding
System.get_env("REENCODARR_WORKER_TOKEN") / SECRET_KEY_BASE fallback behavior.

In `@docs/ab_av1_worker_websocket.md`:
- Around line 28-32: The websocket docs example reply is missing the protocol
version field, so update the expected payload in the documentation to match what
WorkerProtocol.accepted/1 actually returns. Locate the “Expected reply payload”
example in the ab_av1 worker websocket doc and change it to include
protocol_version alongside accepted, keeping the documented contract aligned
with the server response.

In `@lib/reencodarr_web/channels/worker_channel.ex`:
- Around line 55-67: Add a catch-all handle_in/3 clause in WorkerChannel to
handle unexpected worker events instead of letting FunctionClauseError crash the
channel process. Keep the existing handle_in("heartbeat", ...),
handle_in("request_work", ...), and handle_in("pull_work", ...) clauses, then
add a fallback that matches any other event and replies with a structured
WorkerProtocol.error-style response so unknown messages are safely rejected
without dropping the socket.

In `@lib/reencodarr_web/channels/worker_socket.ex`:
- Around line 13-18: The token check in worker_socket’s connect/3 uses a normal
pattern match on the fetched :worker_token, which can leak timing information.
Update the authentication branch in connect/3 to compare the provided token
against the stored token with Plug.Crypto.secure_compare/2, and keep the
assign(socket, :worker_id, worker_id()) path only when that constant-time
comparison succeeds.

In `@lib/reencodarr/ab_av1/worker_sessions.ex`:
- Around line 98-137: The `handle_call/3` clauses for `{:touch, ...}`,
`{:assign_video, ...}`, and `{:clear_video, ...}` in `worker_sessions` duplicate
the same lookup/update/reply flow. Extract that shared logic into a private
helper such as `update_session_reply/3` (or similar) that takes
`server_worker_id`, `state`, and an update function, and have each clause
delegate to it while preserving the existing `lookup_session/1`,
`put_session/1`, and `{:error, :unknown_worker_session}` behavior.
- Around line 71-96: The `handle_call({:register, attrs}, _from, state)` path is
crashing on missing keys because it and `build_session/2` rely on
`Map.fetch!/2`, which can take down the GenServer and destroy the private
ETS-backed sessions. Add upfront validation for all required fields in
`register/1` or make `build_session/2` return a safe error instead of raising,
and ensure malformed attrs reply with an error rather than crashing. Use the
`handle_call({:register, ...})` and `build_session/2` symbols to locate the
unsafe fetches and guard them consistently.

In `@lib/reencodarr/media/video_queries.ex`:
- Around line 191-202: The initial candidate lookup in
claim_next_video_for_crf_search_in_tx/1 only fetches one Video id, which leaves
claim_next_video_for_crf_search_in_tx/2 with no rest list to retry when the
guarded update loses contention. Change the query to select a small ordered
batch of analyzed Video ids instead of a single row, so the recursive fallback
in claim_next_video_for_crf_search_in_tx/2 can continue to the next-best
candidate when the first claim fails.

In `@test/reencodarr_web/live/dashboard_v2_live_test.exs`:
- Around line 85-95: The dashboard_v2_live_test test mutates process-wide
Application config while the test module is async, which can leak the temporary
worker token into concurrent tests. Make this test module or the specific "shows
the worker websocket token when configured" test non-async, or refactor the
token lookup used by the LiveView so it can be injected/isolated instead of
relying on Application.get_env/put_env during the test. Keep the restore logic
in on_exit, but ensure no other async test can observe the transient
worker_token value.

In `@test/reencodarr/ab_av1/worker_protocol_test.exs`:
- Around line 1-27: Add unit test coverage in WorkerProtocolTest for the
untested WorkerProtocol.parse_announcement/1 and WorkerProtocol.error/1 paths.
Add cases that exercise invalid/untrusted announcement payloads and verify
parse_announcement/1 handles them as expected, including the missing catch-all
behavior noted in worker_protocol.ex, and add assertions that error/1 maps
worker failures to the correct protocol payload. Use the existing
work_assigned/2 test as a pattern and keep the new tests focused on the public
protocol API used by worker_channel.ex.

In `@test/reencodarr/ab_av1/worker_sessions_test.exs`:
- Around line 12-85: The same worker session attrs map is duplicated across all
four tests, so extract it into a shared helper/fixture and reuse it. Update the
tests in WorkerSessionsTest to call a local helper like worker_session_attrs/1,
or move the shared data into test/support/fixtures.ex if that is the standard
test fixture location, so future changes to the session fields happen in one
place.
- Around line 1-86: Add tests in WorkerSessionsTest to cover the untested error
branches: verify WorkerSessions.register/1 rejects a second registration with
the same server_worker_id or client_worker_id and returns {:error,
:duplicate_worker_id}, and add separate assertions that touch/1, assign_video/2,
and clear_video/1 each return {:error, :unknown_worker_session} when called with
a non-existent worker session identifier. Use the existing WorkerSessions
setup/reset pattern and the same register/list helpers to keep the new tests
consistent with the current module.

In `@test/reencodarr/broadway_producers_test.exs`:
- Around line 119-146: Add a test for the CRF claim path that asserts the same
`:video_mutated` PubSub broadcast behavior already covered for the analyzer
claim flow. Extend `test/reencodarr/broadway_producers_test.exs` near the
existing `CrfProducer.handle_demand/2` tests by invoking
`claim_next_video_for_crf_search` indirectly through `CrfProducer`, then verify
the broadcast payload alongside the state transition. Use
`VideoQueries.claim_next_video_for_crf_search/1` and the existing broadcast
assertion helpers/pattern from the analyzer coverage as the reference for
parity.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7cd47dea-01fd-4913-821a-94612a8358b9

📥 Commits

Reviewing files that changed from the base of the PR and between 6dd1955 and 31219d1.

📒 Files selected for processing (26)
  • .gitignore
  • .serena/memories/conventions.md
  • .serena/memories/core.md
  • .serena/memories/memory_maintenance.md
  • .serena/memories/suggested_commands.md
  • .serena/memories/task_completion.md
  • .serena/memories/tech_stack.md
  • config/runtime.exs
  • docs/ab_av1_worker_websocket.md
  • lib/reencodarr/ab_av1/worker_protocol.ex
  • lib/reencodarr/ab_av1/worker_sessions.ex
  • lib/reencodarr/application.ex
  • lib/reencodarr/crf_searcher/broadway/producer.ex
  • lib/reencodarr/diagnostics.ex
  • lib/reencodarr/media.ex
  • lib/reencodarr/media/video_queries.ex
  • lib/reencodarr_web/channels/worker_channel.ex
  • lib/reencodarr_web/channels/worker_socket.ex
  • lib/reencodarr_web/endpoint.ex
  • lib/reencodarr_web/live/dashboard_live.ex
  • test/reencodarr/ab_av1/worker_protocol_test.exs
  • test/reencodarr/ab_av1/worker_sessions_test.exs
  • test/reencodarr/broadway_producers_test.exs
  • test/reencodarr_web/channels/worker_channel_test.exs
  • test/reencodarr_web/live/dashboard_v2_live_test.exs
  • test/support/channel_case.ex

Comment thread config/runtime.exs Outdated
Comment thread docs/ab_av1_worker_websocket.md Outdated
Comment thread lib/reencodarr_web/channels/worker_channel.ex Outdated
Comment thread lib/reencodarr_web/channels/worker_socket.ex Outdated
Comment thread lib/reencodarr/ab_av1/worker_sessions.ex
Comment thread test/reencodarr_web/live/dashboard_v2_live_test.exs
Comment thread test/reencodarr/ab_av1/worker_protocol_test.exs
Comment thread test/reencodarr/ab_av1/worker_sessions_test.exs
Comment thread test/reencodarr/ab_av1/worker_sessions_test.exs
Comment thread test/reencodarr/broadway_producers_test.exs

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
lib/reencodarr/ab_av1/worker_protocol.ex (1)

662-663: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve explicit false values in payload lookup.

Enum.find_value/2 skips false, so fields like "chosen" => false and "retriable" => false are parsed as nil. Use key-presence lookup instead.

🐛 Proposed fix
 defp fetch_any(payload, keys) do
-  Enum.find_value(keys, fn key -> Map.get(payload, key) end)
+  Enum.find_value(keys, fn key ->
+    if Map.has_key?(payload, key), do: {:ok, Map.fetch!(payload, key)}
+  end)
+  |> case do
+    {:ok, value} -> value
+    nil -> nil
+  end
 end
🤖 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 `@lib/reencodarr/ab_av1/worker_protocol.ex` around lines 662 - 663, The payload
lookup in fetch_any/2 drops explicit false values because Enum.find_value/2
treats false as absent, so fields like “chosen” and “retriable” can be parsed as
nil. Update fetch_any/2 to search by key presence instead of truthiness, using
the existing payload Map access logic so it returns the stored value even when
it is false.
lib/reencodarr/ab_av1/worker_sessions.ex (1)

145-149: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Broadcast after scheduled stale-session expiration.

The manual expire_stale path broadcasts when sessions expire, but the scheduled sweep drops expired sessions silently. Worker/session UI subscribers can keep showing expired workers until another event occurs.

🐛 Proposed fix
 def handle_info(:expire_stale, state) do
-  _ = expire_stale_sessions(timeout_seconds())
+  {expired_sessions, _} = expire_stale_sessions(timeout_seconds())
+
+  if expired_sessions != [] do
+    broadcast_sessions()
+  end
+
   schedule_expire_stale()
   {:noreply, state}
 end
🤖 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 `@lib/reencodarr/ab_av1/worker_sessions.ex` around lines 145 - 149, The
scheduled stale-session sweep in handle_info(:expire_stale, state) expires
sessions silently, unlike the manual path. Update the expire_stale_sessions flow
used by WorkerSessions so it returns the expired workers/sessions (or otherwise
exposes what changed), then broadcast the same removal/refresh event after the
scheduled sweep before rescheduling with schedule_expire_stale(). Keep the fix
localized around expire_stale_sessions and handle_info(:expire_stale, state) so
UI subscribers are notified whenever stale sessions are removed.
lib/reencodarr_web/live/dashboard_live.ex (1)

54-55: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Avoid keeping the raw worker token in socket assigns.

worker_token is stored in assigns solely to render its fingerprint. Keeping the secret in long-lived process state widens exposure (crash dumps, assigns inspection). Prefer computing the fingerprint (and a worker_token_configured? boolean) at mount and assigning only those.

🤖 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 `@lib/reencodarr_web/live/dashboard_live.ex` around lines 54 - 55, The
dashboard LiveView currently keeps the raw worker token in socket assigns just
to derive display state. Update the mount/assignment logic in the DashboardLive
module so it computes the token fingerprint and a worker_token_configured? flag
up front, then stores only those derived values (plus worker_socket_url) in
assigns. Remove any direct worker_token assign and make sure the render path
uses the derived fingerprint/configured state instead of the secret itself.
🤖 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 `@lib/reencodarr/ab_av1/worker_protocol.ex`:
- Around line 111-114: Update the public completion result contract to include
:failed everywhere it is exposed: extend the result type in WorkerProtocol.t and
the completion_result() type alias so they match parse_completion_result/1
returning {:ok, :failed}. Make sure any related Completion.t() or
protocol/documentation references use the same union so callers and Dialyzer see
:failed as a valid completion outcome alongside the existing :ok, :cancelled,
:shutdown, and {:error, term()} cases.

---

Outside diff comments:
In `@lib/reencodarr_web/live/dashboard_live.ex`:
- Around line 54-55: The dashboard LiveView currently keeps the raw worker token
in socket assigns just to derive display state. Update the mount/assignment
logic in the DashboardLive module so it computes the token fingerprint and a
worker_token_configured? flag up front, then stores only those derived values
(plus worker_socket_url) in assigns. Remove any direct worker_token assign and
make sure the render path uses the derived fingerprint/configured state instead
of the secret itself.

In `@lib/reencodarr/ab_av1/worker_protocol.ex`:
- Around line 662-663: The payload lookup in fetch_any/2 drops explicit false
values because Enum.find_value/2 treats false as absent, so fields like “chosen”
and “retriable” can be parsed as nil. Update fetch_any/2 to search by key
presence instead of truthiness, using the existing payload Map access logic so
it returns the stored value even when it is false.

In `@lib/reencodarr/ab_av1/worker_sessions.ex`:
- Around line 145-149: The scheduled stale-session sweep in
handle_info(:expire_stale, state) expires sessions silently, unlike the manual
path. Update the expire_stale_sessions flow used by WorkerSessions so it returns
the expired workers/sessions (or otherwise exposes what changed), then broadcast
the same removal/refresh event after the scheduled sweep before rescheduling
with schedule_expire_stale(). Keep the fix localized around
expire_stale_sessions and handle_info(:expire_stale, state) so UI subscribers
are notified whenever stale sessions are removed.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: be54cfbb-23af-42b1-9f8b-b36e59caebca

📥 Commits

Reviewing files that changed from the base of the PR and between 27c2e8f and 6652845.

📒 Files selected for processing (14)
  • lib/reencodarr/ab_av1/worker_protocol.ex
  • lib/reencodarr/ab_av1/worker_sessions.ex
  • lib/reencodarr/crf_searcher/broadway/producer.ex
  • lib/reencodarr/media/video_queries.ex
  • lib/reencodarr_web/channels/worker_channel.ex
  • lib/reencodarr_web/components/layouts/app.html.heex
  • lib/reencodarr_web/live/dashboard_live.ex
  • lib/reencodarr_web/live/workers_live.ex
  • lib/reencodarr_web/router.ex
  • test/reencodarr/ab_av1/worker_protocol_test.exs
  • test/reencodarr/broadway_producers_test.exs
  • test/reencodarr_web/channels/worker_channel_test.exs
  • test/reencodarr_web/live/dashboard_v2_live_test.exs
  • test/reencodarr_web/live/workers_live_test.exs
💤 Files with no reviewable changes (1)
  • test/reencodarr/broadway_producers_test.exs

Comment thread lib/reencodarr/ab_av1/worker_protocol.ex

@coderabbitai coderabbitai 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.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
lib/reencodarr/ab_av1/worker_sessions.ex (1)

340-357: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Release the claimed video when expiring stale sessions
lib/reencodarr/ab_av1/worker_sessions.ex:340-357 drops expired sessions without requeue_active_video/1, so a crashed or partitioned worker can leave its video claimed forever. cancel/1 and drain/0 already requeue before dropping; stale expiry needs the same cleanup.

🤖 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 `@lib/reencodarr/ab_av1/worker_sessions.ex` around lines 340 - 357,
expire_stale_sessions/1 currently drops stale sessions without releasing the
claimed video, unlike cancel/1 and drain/0. Update the stale-session cleanup
path in WorkerSessions to call requeue_active_video/1 for each expired session
before drop_session/1, using the same session/server_worker_id data already
available in expire_stale_sessions/1. Ensure the expiration flow mirrors the
existing cancellation/drain cleanup so claimed videos are returned before the
session is removed.
lib/reencodarr_web/channels/worker_channel.ex (2)

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

Duplicate CRF-search finalization logic.

choose_result_from_report/2 (Lines 360-374) and finish_successful_crf_search/4 (Lines 376-391) both mark the chosen VMAF, call Media.mark_as_crf_searched/1, and resolve CRF-search failures. Any future change to this finalization sequence (e.g., adding a new side effect) risks being applied to only one of the two call sites.

♻️ Suggested consolidation
-  defp choose_result_from_report(video, results) do
-    chosen_crf =
-      Enum.find_value(results, fn result ->
-        if Map.get(result, :chosen, false), do: Map.get(result, :crf)
-      end) || Map.get(List.first(results) || %{}, :crf)
-
-    case chosen_crf do
-      nil -> :ok
-      crf -> _ = Media.mark_vmaf_as_chosen(video.id, crf)
-    end
-
-    _ = Media.mark_as_crf_searched(video)
-    _ = Media.resolve_crf_search_failures(video.id)
-    :ok
-  end
+  defp choose_result_from_report(video, results) do
+    chosen_crf =
+      Enum.find_value(results, fn result ->
+        if Map.get(result, :chosen, false), do: Map.get(result, :crf)
+      end) || Map.get(List.first(results) || %{}, :crf)
+
+    finalize_crf_search(video, chosen_crf)
+  end

And reuse the same finalize_crf_search/2 helper from finish_successful_crf_search/4.

🤖 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 `@lib/reencodarr_web/channels/worker_channel.ex` around lines 360 - 391, There
is duplicated CRF-search finalization logic in choose_result_from_report/2 and
finish_successful_crf_search/4. Extract the shared post-selection steps into a
single helper such as finalize_crf_search/2 that handles mark_vmaf_as_chosen,
Media.mark_as_crf_searched, and Media.resolve_crf_search_failures, then call
that helper from both functions; keep finish_successful_crf_search/4 responsible
only for the choose-best-vmaf fallback and clear_assigned_video/2.

215-247: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Worker gets stranded if the assigned file is missing.

If File.exists?(video.path) is false at Line 237, no :stream_transfer_chunk message is sent, so open_transfer_stream/1 and its handle_transfer_failure/3 path never run. The worker receives job_assigned but no transfer_started/transfer_failed ever arrives, leaving it stuck waiting indefinitely for a video the server knows doesn't exist on disk. The existing File.open error path in open_transfer_stream/1 (Lines 414-430) already handles this correctly (records failure, notifies client, clears assignment) — simply relying on that instead of pre-checking existence closes this gap.

🐛 Proposed fix
-        if File.exists?(video.path) do
-          send(self(), :stream_transfer_chunk)
-        end
+        send(self(), :stream_transfer_chunk)
🤖 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 `@lib/reencodarr_web/channels/worker_channel.ex` around lines 215 - 247, The
assign_claimed_work/3 flow in WorkerChannel leaves the worker waiting forever
when video.path is missing because the File.exists? pre-check prevents
open_transfer_stream/1 from running. Remove the existence gate and always
trigger the transfer startup path so open_transfer_stream/1 can hit its existing
File.open failure handling and handle_transfer_failure/3 logic, which will
report the failure and clear the assignment. Keep the fix localized to
assign_claimed_work/3 and the transfer-stream startup path so the worker always
gets either transfer_started or transfer_failed.
lib/reencodarr/diagnostics.ex (1)

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

Factor out the shared worker-status lookup. worker_session_state/1 and ReencodarrWeb.WorkersLive.worker_status/1 both derive the same nil/lookup/missing status, but format "idle"/"Idle" differently. Move the status derivation into one shared helper and let each caller handle display text. If this list can grow, batch the Media.get_video/1 lookups too.

🤖 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 `@lib/reencodarr/diagnostics.ex` around lines 668 - 699, The worker status
logic is duplicated between format_worker_sessions/1’s worker_session_state/1
and ReencodarrWeb.WorkersLive.worker_status/1, with only display casing
differing. Extract the nil/lookup/missing state derivation into a shared helper
that returns a normalized status, then have each caller map that result to its
own display text (e.g. idle vs Idle). If worker_sessions can grow, also consider
batching the Reencodarr.Media.get_video/1 lookups instead of resolving each
session one by one.

Source: Coding guidelines

🤖 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 `@config/runtime.exs`:
- Around line 23-28: Update parse_bool_env in runtime.exs so it no longer
defaults every unrecognized string to true; only explicitly accepted truthy
values should enable the worker websocket feature, and any typo, unexpected
casing, or unknown input should fall back to false. Mirror the stricter pattern
used by parse_check_origin by making the function enumerate the allowed values
and keep the behavior safe by default.

In `@lib/reencodarr_web/live/dashboard_live.ex`:
- Around line 1053-1075: The dashboard is rendering the full worker token in the
Token section, which exposes a live credential in the HTML. Update the view
logic in DashboardLive to stop displaying the raw token by default, keep showing
only the fingerprint from `@worker_token_state`, and add an explicit reveal/copy
interaction for the token instead of embedding it in the page.

In `@lib/reencodarr_web/live/workers_live.ex`:
- Around line 126-133: The worker status rendering is doing one
Media.get_video/1 lookup per worker, which creates repeated DB hits and
duplicates the same pattern used in
Reencodarr.Diagnostics.worker_session_state/1. Refactor worker_status/1 to use a
shared helper that batches active video state lookups for all workers in one
query, then have both WorkersLive and Diagnostics consume that preloaded map
instead of calling Media.get_video/1 per worker.

In `@lib/reencodarr/ab_av1/worker_sessions.ex`:
- Around line 138-148: The progress setters in WorkerSessions are causing
redundant full-session PubSub broadcasts by flowing through update_session_reply
and put_session/1, which always calls broadcast_sessions(). Update
set_transfer_progress and set_crf_search_progress to avoid triggering the full
session list broadcast for these progress-only changes, using a non-broadcasting
session update path while keeping the existing WorkerChannel progress events for
transfer/crf search updates.
- Around line 20-21: Add missing type specs to the new public API in
WorkerSessions: `set_transfer_progress/2`, `set_crf_search_progress/2`,
`cancel/1`, and `drain/0` should all have explicit `@spec` declarations matching
their actual inputs and return values. Also tighten the `session` type by
replacing the generic `map() | nil` fields for `transfer_progress` and
`crf_search_progress` with `WorkerProtocol.TransferProgress.t() | nil` and
`WorkerProtocol.CrfSearchProgress.t() | nil` so the contract matches the new
protocol types.
- Around line 307-318: The requeue_active_video/1 flow in WorkerSessions
currently ignores failures from Reencodarr.Media.mark_as_analyzed/1, which can
leave the video in the wrong state while still returning :ok. Update the
%Reencodarr.Media.Video{} branch to explicitly handle the return value from
mark_as_analyzed/1, branching on {:error, reason} to log or surface the failure
instead of discarding it, and keep the nil case unchanged.

In `@test/reencodarr/ab_av1/worker_sessions_test.exs`:
- Around line 79-104: The cancel/drain test is using videos already in :analyzed
state, so it never verifies the requeue path. Update the setup in
WorkerSessions.cancel/1 and WorkerSessions.drain/0 coverage to create or
transition the fixtures into an active claimed state such as :crf_searching
before assignment, then keep the assertions on Media.get_video/1 so the test
actually proves requeue_active_video and mark_as_analyzed are invoked.

---

Outside diff comments:
In `@lib/reencodarr_web/channels/worker_channel.ex`:
- Around line 360-391: There is duplicated CRF-search finalization logic in
choose_result_from_report/2 and finish_successful_crf_search/4. Extract the
shared post-selection steps into a single helper such as finalize_crf_search/2
that handles mark_vmaf_as_chosen, Media.mark_as_crf_searched, and
Media.resolve_crf_search_failures, then call that helper from both functions;
keep finish_successful_crf_search/4 responsible only for the choose-best-vmaf
fallback and clear_assigned_video/2.
- Around line 215-247: The assign_claimed_work/3 flow in WorkerChannel leaves
the worker waiting forever when video.path is missing because the File.exists?
pre-check prevents open_transfer_stream/1 from running. Remove the existence
gate and always trigger the transfer startup path so open_transfer_stream/1 can
hit its existing File.open failure handling and handle_transfer_failure/3 logic,
which will report the failure and clear the assignment. Keep the fix localized
to assign_claimed_work/3 and the transfer-stream startup path so the worker
always gets either transfer_started or transfer_failed.

In `@lib/reencodarr/ab_av1/worker_sessions.ex`:
- Around line 340-357: expire_stale_sessions/1 currently drops stale sessions
without releasing the claimed video, unlike cancel/1 and drain/0. Update the
stale-session cleanup path in WorkerSessions to call requeue_active_video/1 for
each expired session before drop_session/1, using the same
session/server_worker_id data already available in expire_stale_sessions/1.
Ensure the expiration flow mirrors the existing cancellation/drain cleanup so
claimed videos are returned before the session is removed.

In `@lib/reencodarr/diagnostics.ex`:
- Around line 668-699: The worker status logic is duplicated between
format_worker_sessions/1’s worker_session_state/1 and
ReencodarrWeb.WorkersLive.worker_status/1, with only display casing differing.
Extract the nil/lookup/missing state derivation into a shared helper that
returns a normalized status, then have each caller map that result to its own
display text (e.g. idle vs Idle). If worker_sessions can grow, also consider
batching the Reencodarr.Media.get_video/1 lookups instead of resolving each
session one by one.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b870b971-eef4-4a6b-b848-f9977fbdf133

📥 Commits

Reviewing files that changed from the base of the PR and between 6652845 and c8a6736.

📒 Files selected for processing (17)
  • config/config.exs
  • config/runtime.exs
  • docs/distributed_worker_protocol.md
  • lib/reencodarr/ab_av1/worker_config.ex
  • lib/reencodarr/ab_av1/worker_protocol.ex
  • lib/reencodarr/ab_av1/worker_sessions.ex
  • lib/reencodarr/diagnostics.ex
  • lib/reencodarr_web/channels/worker_channel.ex
  • lib/reencodarr_web/channels/worker_socket.ex
  • lib/reencodarr_web/live/dashboard_live.ex
  • lib/reencodarr_web/live/workers_live.ex
  • test/reencodarr/ab_av1/worker_config_test.exs
  • test/reencodarr/ab_av1/worker_protocol_test.exs
  • test/reencodarr/ab_av1/worker_sessions_test.exs
  • test/reencodarr_web/channels/worker_channel_test.exs
  • test/reencodarr_web/live/dashboard_v2_live_test.exs
  • test/reencodarr_web/live/workers_live_test.exs

Comment thread config/runtime.exs
Comment on lines +23 to +28
parse_bool_env = fn
"false" -> false
"0" -> false
"no" -> false
_ -> true
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

parse_bool_env fails open on unrecognized values.

Only "false", "0", "no" are treated as false; any other string (including typos like "flase" or unexpected casing) is treated as true. Since this gates whether the worker websocket feature is enabled at all, a misconfigured env var silently turns it on. Contrast with parse_check_origin below, which explicitly enumerates "true"/"false".

🛡️ Proposed fix — default to false for unrecognized values
 parse_bool_env = fn
-  "false" -> false
-  "0" -> false
-  "no" -> false
-  _ -> true
+  "true" -> true
+  "1" -> true
+  "yes" -> true
+  _ -> false
 end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
parse_bool_env = fn
"false" -> false
"0" -> false
"no" -> false
_ -> true
end
parse_bool_env = fn
"true" -> true
"1" -> true
"yes" -> true
_ -> false
end
🤖 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 `@config/runtime.exs` around lines 23 - 28, Update parse_bool_env in
runtime.exs so it no longer defaults every unrecognized string to true; only
explicitly accepted truthy values should enable the worker websocket feature,
and any typo, unexpected casing, or unknown input should fall back to false.
Mirror the stricter pattern used by parse_check_origin by making the function
enumerate the allowed values and keep the behavior safe by default.

Comment thread lib/reencodarr_web/live/dashboard_live.ex
Comment on lines +126 to +133
defp worker_status(%{active_video_id: nil}), do: "Idle"

defp worker_status(%{active_video_id: video_id}) do
case Media.get_video(video_id) do
%Media.Video{state: state} -> Atom.to_string(state)
nil -> "missing"
end
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how often worker session broadcasts fire (e.g., per progress update vs per state change)
rg -n 'broadcast_sessions' lib/reencodarr/ab_av1/worker_sessions.ex -B5 -A2
rg -n 'broadcast_sessions\(' lib/reencodarr -n

Repository: mjc/reencodarr

Length of output: 2615


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant files first, then inspect the small slices around the worker status logic.
ast-grep outline lib/reencodarr_web/live/workers_live.ex --view expanded || true
ast-grep outline lib/reencodarr/diagnostics.ex --view expanded || true

printf '\n--- workers_live.ex ---\n'
sed -n '1,220p' lib/reencodarr_web/live/workers_live.ex | cat -n

printf '\n--- diagnostics.ex ---\n'
sed -n '1,220p' lib/reencodarr/diagnostics.ex | cat -n

printf '\n--- search for worker_status helpers ---\n'
rg -n 'worker_status|worker_session_state|get_video\(' lib/reencodarr_web/live/workers_live.ex lib/reencodarr/diagnostics.ex lib/reencodarr -n

Repository: mjc/reencodarr

Length of output: 17025


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- diagnostics helper around worker_session_state ---\n'
sed -n '650,700p' lib/reencodarr/diagnostics.ex | cat -n

printf '\n--- worker_sessions update/broadcast paths ---\n'
sed -n '1,360p' lib/reencodarr/ab_av1/worker_sessions.ex | cat -n | sed -n '1,360p'

printf '\n--- focused search for state/progress update calls that broadcast ---\n'
rg -n 'broadcast_sessions\(\)|set_state|progress|update_session|touch_session|active_video_id' lib/reencodarr/ab_av1/worker_sessions.ex lib/reencodarr -n

Repository: mjc/reencodarr

Length of output: 36940


Batch worker status lookups. worker_status/1 does one Media.get_video/1 per worker on every 5s refresh and on each worker-session update, so the render path grows linearly with connected workers. The same lookup pattern also exists in Reencodarr.Diagnostics.worker_session_state/1; a shared helper that preloads all active video states in one query would remove the duplication and the per-worker DB hit.

🤖 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 `@lib/reencodarr_web/live/workers_live.ex` around lines 126 - 133, The worker
status rendering is doing one Media.get_video/1 lookup per worker, which creates
repeated DB hits and duplicates the same pattern used in
Reencodarr.Diagnostics.worker_session_state/1. Refactor worker_status/1 to use a
shared helper that batches active video state lookups for all workers in one
query, then have both WorkersLive and Diagnostics consume that preloaded map
instead of calling Media.get_video/1 per worker.

Comment on lines +20 to +21
transfer_progress: map() | nil,
crf_search_progress: map() | nil,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing @spec on new public API; consider typed progress fields.

set_transfer_progress/2, set_crf_search_progress/2, cancel/1, and drain/0 have no type specs, unlike the sibling WorkerConfig/WorkerProtocol modules added in this PR. The PR comments explicitly ask for more type specs. Also, transfer_progress/crf_search_progress in the session type (lines 20-21) are typed as generic map() | nil — using WorkerProtocol.TransferProgress.t() | nil and WorkerProtocol.CrfSearchProgress.t() | nil would make the contract precise.

Also applies to: 50-65

🤖 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 `@lib/reencodarr/ab_av1/worker_sessions.ex` around lines 20 - 21, Add missing
type specs to the new public API in WorkerSessions: `set_transfer_progress/2`,
`set_crf_search_progress/2`, `cancel/1`, and `drain/0` should all have explicit
`@spec` declarations matching their actual inputs and return values. Also
tighten the `session` type by replacing the generic `map() | nil` fields for
`transfer_progress` and `crf_search_progress` with
`WorkerProtocol.TransferProgress.t() | nil` and
`WorkerProtocol.CrfSearchProgress.t() | nil` so the contract matches the new
protocol types.

Comment on lines +138 to +148
def handle_call({:set_transfer_progress, server_worker_id, progress}, _from, state) do
update_session_reply(server_worker_id, state, fn session ->
%{session | transfer_progress: progress}
end)
end

def handle_call({:set_crf_search_progress, server_worker_id, progress}, _from, state) do
update_session_reply(server_worker_id, state, fn session ->
%{session | crf_search_progress: progress}
end)
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Every progress update broadcasts the full session list.

set_transfer_progress/set_crf_search_progress go through update_session_replyput_session/1, which unconditionally calls broadcast_sessions() (line 324). Transfer/CRF-search progress can update frequently (per chunk / per tick), so each tick re-broadcasts the entire sorted session list over PubSub to every dashboard/workers LiveView, triggering a full assign(socket, :workers, sessions) re-render each time (see workers_live.ex handle_info({:worker_sessions_updated, ...})). This looks redundant with the dedicated :transfer_progress/:crf_search_progress events WorkerChannel already broadcasts separately for the same data.

Consider skipping the broadcast_sessions() call for progress-only updates (e.g., a variant of put_session that doesn't broadcast), relying on the existing per-event progress broadcasts instead.

Also applies to: 320-326

🤖 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 `@lib/reencodarr/ab_av1/worker_sessions.ex` around lines 138 - 148, The
progress setters in WorkerSessions are causing redundant full-session PubSub
broadcasts by flowing through update_session_reply and put_session/1, which
always calls broadcast_sessions(). Update set_transfer_progress and
set_crf_search_progress to avoid triggering the full session list broadcast for
these progress-only changes, using a non-broadcasting session update path while
keeping the existing WorkerChannel progress events for transfer/crf search
updates.

Comment on lines +307 to +318
defp requeue_active_video(%{active_video_id: nil}), do: :ok

defp requeue_active_video(%{active_video_id: video_id}) do
case Reencodarr.Media.get_video(video_id) do
%Reencodarr.Media.Video{} = video ->
_ = Reencodarr.Media.mark_as_analyzed(video)
:ok

nil ->
:ok
end
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify mark_as_analyzed delegates to VideoStateMachine and check its return contract
ast-grep run --pattern 'def mark_as_analyzed($_) do $$$ end' --lang elixir lib/reencodarr/media.ex
rg -n 'def mark_as_analyzed' -A 15 lib/reencodarr/media.ex

Repository: mjc/reencodarr

Length of output: 793


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the state machine implementation and relevant callers.
fd -a 'video_state_machine.ex' lib
fd -a 'video_state_machine*.ex' lib
rg -n 'def mark_as_analyzed|mark_as_analyzed\(' lib/reencodarr -A 6 -B 4

Repository: mjc/reencodarr

Length of output: 6224


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the state transition implementation and any cleanup paths that handle errors.
sed -n '1,260p' lib/reencodarr/media/video_state_machine.ex
printf '\n---\n'
sed -n '260,460p' lib/reencodarr/media/video_state_machine.ex
printf '\n---\n'
sed -n '168,190p' lib/reencodarr/ab_av1/crf_search.ex
printf '\n---\n'
sed -n '890,905p' lib/reencodarr/ab_av1/crf_search.ex

Repository: mjc/reencodarr

Length of output: 17474


Handle Media.mark_as_analyzed/1 failures in lib/reencodarr/ab_av1/worker_sessions.ex:309-313. requeue_active_video/1 ignores the {:error, reason} return, so cancel/drain can drop the session while the video remains in its previous state. Log or branch on the error instead of always returning :ok.

🤖 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 `@lib/reencodarr/ab_av1/worker_sessions.ex` around lines 307 - 318, The
requeue_active_video/1 flow in WorkerSessions currently ignores failures from
Reencodarr.Media.mark_as_analyzed/1, which can leave the video in the wrong
state while still returning :ok. Update the %Reencodarr.Media.Video{} branch to
explicitly handle the return value from mark_as_analyzed/1, branching on
{:error, reason} to log or surface the failure instead of discarding it, and
keep the nil case unchanged.

Source: Path instructions

Comment thread test/reencodarr/ab_av1/worker_sessions_test.exs

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
docs/distributed_worker_protocol.md (1)

18-19: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the work-request event names consistent.

pull_work / request_work are introduced here, but neither appears in the later "Worker Events" list. That makes the protocol contract ambiguous for client authors.

🤖 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 `@docs/distributed_worker_protocol.md` around lines 18 - 19, The work-request
event names are inconsistent between the protocol steps and the “Worker Events”
list, which makes the contract ambiguous. Update the distributed worker protocol
documentation so the same request event name(s) used in the “Client requests
work” section are also defined in the “Worker Events” list, and ensure the later
event list explicitly includes the chosen symbol(s) such as pull_work and
request_work for easy reference.
test/reencodarr_web/channels/worker_channel_test.exs (2)

42-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the announce_payload/1 helper for consistency.

This test hardcodes the announce map inline while every other test uses announce_payload/1 (defined at Line 566). Using the helper here keeps the default payload in one place and avoids drift if the default shape changes.

♻️ Suggested diff
-      assert_reply push(socket, "announce", %{
-                     "worker_id" => "abav1-dev",
-                     "protocol_version" => 1,
-                     "version" => "0.10.0",
-                     "capabilities" => %{"crf_search" => true}
-                   }),
+      assert_reply push(socket, "announce", announce_payload()),
                    :ok,
                    %{accepted: true, protocol_version: 1}
🤖 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 `@test/reencodarr_web/channels/worker_channel_test.exs` around lines 42 - 49,
The announce request in the worker channel test is hardcoded inline instead of
using the shared announce_payload/1 helper, which can drift from the default
payload used elsewhere. Update the assertion in the worker_channel_test case to
build the payload through announce_payload/1, then override any test-specific
fields if needed, so the test stays aligned with the helper defined in the same
module.

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

Repeated connect/join/announce boilerplate across tests.

The connect(WorkerSocket, %{"token" => token})subscribe_and_join(socket, "workers:crf_search")announce push sequence is duplicated near-verbatim across ~10 tests. Extracting a private helper (e.g. connect_and_announce/1, returning the joined+announced socket) would reduce duplication and make each test body focus on its actual assertions.

♻️ Example helper extraction
defp connect_and_announce(overrides \\ []) do
  token = "test-worker-token"
  Application.put_env(:reencodarr, :worker_token, token)
  {:ok, socket} = connect(WorkerSocket, %{"token" => token})
  {:ok, _join_payload, socket} = subscribe_and_join(socket, "workers:crf_search")

  assert_reply push(socket, "announce", announce_payload(overrides)),
               :ok,
               %{accepted: true, protocol_version: 1}

  socket
end
🤖 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 `@test/reencodarr_web/channels/worker_channel_test.exs` around lines 30 - 524,
The worker channel tests repeat the same connect, join, and announce setup in
many cases, so extract that flow into a shared private helper in
WorkerChannelTest. Create a helper such as connect_and_announce/1 that handles
setting the worker token, calling connect/2 on WorkerSocket,
subscribe_and_join/2 for "workers:crf_search", and the announce push via
announce_payload/1, then return the ready socket for each test to use. Update
the existing tests to call the helper and keep only the scenario-specific
assertions so the suite is shorter and easier to maintain.
test/reencodarr/ab_av1/worker_protocol_test.exs (1)

214-227: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Global env mutation under async: true can make this test flaky.

Application.put_env/3 changes a node-wide setting, so this case can race with any other async test that reads :worker_chunk_size_bytes. Mark this module async: false or move the env-mutating case into a dedicated non-async test module.

🤖 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 `@test/reencodarr/ab_av1/worker_protocol_test.exs` around lines 214 - 227, The
test in WorkerProtocol mutates a global Application env value with
Application.put_env/3, which can race with other async tests. Move this
env-changing case out of the async test module by either marking the
worker_protocol_test module async: false or placing the chunk_size_bytes test
into a dedicated non-async module, and keep the existing cleanup around
:worker_chunk_size_bytes in the test body.
🤖 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.

Outside diff comments:
In `@docs/distributed_worker_protocol.md`:
- Around line 18-19: The work-request event names are inconsistent between the
protocol steps and the “Worker Events” list, which makes the contract ambiguous.
Update the distributed worker protocol documentation so the same request event
name(s) used in the “Client requests work” section are also defined in the
“Worker Events” list, and ensure the later event list explicitly includes the
chosen symbol(s) such as pull_work and request_work for easy reference.

In `@test/reencodarr_web/channels/worker_channel_test.exs`:
- Around line 42-49: The announce request in the worker channel test is
hardcoded inline instead of using the shared announce_payload/1 helper, which
can drift from the default payload used elsewhere. Update the assertion in the
worker_channel_test case to build the payload through announce_payload/1, then
override any test-specific fields if needed, so the test stays aligned with the
helper defined in the same module.
- Around line 30-524: The worker channel tests repeat the same connect, join,
and announce setup in many cases, so extract that flow into a shared private
helper in WorkerChannelTest. Create a helper such as connect_and_announce/1 that
handles setting the worker token, calling connect/2 on WorkerSocket,
subscribe_and_join/2 for "workers:crf_search", and the announce push via
announce_payload/1, then return the ready socket for each test to use. Update
the existing tests to call the helper and keep only the scenario-specific
assertions so the suite is shorter and easier to maintain.

In `@test/reencodarr/ab_av1/worker_protocol_test.exs`:
- Around line 214-227: The test in WorkerProtocol mutates a global Application
env value with Application.put_env/3, which can race with other async tests.
Move this env-changing case out of the async test module by either marking the
worker_protocol_test module async: false or placing the chunk_size_bytes test
into a dedicated non-async module, and keep the existing cleanup around
:worker_chunk_size_bytes in the test body.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5407a3ae-29c7-4e16-b535-957c0573a60a

📥 Commits

Reviewing files that changed from the base of the PR and between c8a6736 and 7f80c38.

📒 Files selected for processing (7)
  • config/config.exs
  • config/runtime.exs
  • docs/distributed_worker_protocol.md
  • lib/reencodarr/ab_av1/worker_config.ex
  • lib/reencodarr/ab_av1/worker_protocol.ex
  • test/reencodarr/ab_av1/worker_protocol_test.exs
  • test/reencodarr_web/channels/worker_channel_test.exs

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.

2 participants