Skip to content

feat: [#949] make queue worker receive timeout configurable - #1553

Closed
wafik wants to merge 3 commits into
goravel:masterfrom
wafik:feat/949-queue-timeout
Closed

wafik wants to merge 3 commits into
goravel:masterfrom
wafik:feat/949-queue-timeout

Conversation

@wafik

@wafik wafik commented Sep 7, 2026

Copy link
Copy Markdown

Problem

runWithReceive hardcoded a 5-second timeout on each blocking Receive call, with a // TODO make the timeout configurable left in the code (goravel/goravel#949 quotes the original review note from #1435).

Approach

Per-connection config key queue.connections.<connection>.timeout (integer seconds, default 5), matching the existing per-connection style of concurrent / retry_after:

  • Worker gains a timeout field resolved once in NewWorker.
  • runWithReceive uses it instead of the hardcoded 5*time.Second.
  • No contract change, so no mocks regenerated.

Extending timeouts to Pop/Push was explicitly deferred to a future issue in the original comment, so the Driver interface is untouched here.

Testing

  • go build ./...
  • go test ./queue/... -count=1 — 189 passed
  • New subtest asserts a custom timeout config value reaches the worker; TestNewWorker happy path covers the default.

Resolves goravel/goravel#949: the 5s timeout on each blocking Receive
call in the worker (flagged in PR goravel#1435 review) is now read from
queue.connections.<connection>.timeout (seconds, default 5).

Co-Authored-By: Claude Code <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.71%. Comparing base (e8c0cdb) to head (ba5073c).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1553      +/-   ##
==========================================
+ Coverage   72.69%   72.71%   +0.01%     
==========================================
  Files         412      412              
  Lines       26772    26788      +16     
==========================================
+ Hits        19462    19478      +16     
  Misses       7308     7308              
  Partials        2        2              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@wafik

wafik commented Sep 7, 2026

Copy link
Copy Markdown
Author

The unit/integration failures are the same pre-existing example-app build break as #1552 (skeleton listener vs new event.Listener signature) — unrelated to this change. Framework tests, lint, mockery and codecov pass.

@goravel-coder

goravel-coder commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

🤖 Automated review

This is an AI-generated code review. Please double-check each finding before acting.

Summary

Adds a per-connection queue.connections.<connection>.timeout key so the worker's blocking Receive timeout is no longer hardcoded to 5s. The change is sound overall; it needs a guard against non-positive values, should be centralized as a dual-type Timeout(connection) accessor on queue.Config (accepting both integer seconds and duration strings), and the core behavior currently lacks a test.

Verdict

  • Must Fix: 0 · Should Fix: 3 · Nits: 6

Findings

Should Fix

  • queue/worker.go:75 Guard non-positive timeout values — config.GetInt only falls back to defaultReceiveTimeout when the key is absent, so timeout: 0 (or a negative value, or a duration string resolving to ≤ 0) yields context.WithTimeout(ctx, 0), which returns an already-expired context; Receive then returns DeadlineExceeded every iteration and the branch at line 326 skips logging and backoff, degrading the worker into a silent ~100ms no-op poll loop. Suggestion: clamp/validate > 0 (like concurrent's max(config.GetInt(...), 1) at queue/config.go:24) and fall back to defaultReceiveTimeout for non-positive or unparseable values.
  • queue/worker.go:75 Centralize as a dual-type Timeout(connection) accessor on queue.Config — the key format string is duplicated inline (also in config.go, application.go, driver_database.go), and concurrent/tries are resolved by the caller while timeout is resolved inside NewWorker; also, using GetDuration here would silently interpret timeout: 5 as 5 nanoseconds (Viper/cast treats numerics as ns). Suggestion: add Timeout(connection string) time.Duration to contracts/queue.Config (like Driver/Via), parse int/float64 as seconds and string via time.ParseDuration (fall back to the default on error/non-positive), then set timeout: config.Timeout(connection) — this also removes the fmt import from worker.go. Note: adding a contract method requires regenerating mocks/queue.Config via go tool mockery.
  • queue/worker_test.go:768 The core behavior is untested — every Receive expectation in Test_runWithReceive passes mock.Anything for the ctx and the RunAndReturn closures only do <-ctx.Done(), which proves a cancellation context exists but not that it carries r.timeout; a regression to the hardcoded 5*time.Second would pass the whole suite. Suggestion: add a subtest whose Receive captures ctx and asserts ctx.Deadline() ≈ now + r.timeout.

Nits

  • queue/worker.go:44 Field name — timeout vs. the const defaultReceiveTimeout; receiveTimeout would make the relationship to runWithReceive self-evident.
  • queue/worker.go:26 Default value placement — defaultReceiveTimeout is a config default (like the "1" in config.go:24) but lives in worker.go; it belongs beside the new Timeout() method in queue/config.go.
  • contracts/queue/config.go:7 Contract surface change — adding Timeout(connection) widens the public config contract, so mocks/queue.Config must be regenerated with go tool mockery (the PR's "no contract change" claim no longer holds).
  • queue/worker_test.go:69 Missing boundary tests — no test for 0, negative, or unparseable-string inputs covering the clamp/fallback paths in the new accessor.
  • queue/application.go:65 Not overridable per-worker — timeout is only settable via static config, while concurrent/tries can be tuned via Application.Worker(payloads...); fine if a pure config key is intended, but worth a deliberate choice (a duration string can't flow through queue.Args without widening that struct).
  • goravel/goravel config/queue.go Scaffold default config — the new key isn't reflected in the scaffold's default queue config; document that timeout: 5 (seconds) and timeout: "5s" are both valid.

Automated Checks

  • go build ./... — clean
  • go test ./queue/... -count=1 — passed (189 tests)
  • gofmt -l . — clean
  • go vet ./queue/ — clean

…ard, deadline tests

- Add Timeout(connection) time.Duration to contracts/queue Config and
  implement it in queue/config.go: int/int64/float64 seconds, duration
  strings via time.ParseDuration, non-positive/missing/garbage fall back
  to the 5s default (moved defaultReceiveTimeout here).
- NewWorker now uses config.Timeout(connection); rename worker field
  timeout -> receiveTimeout.
- Regenerate mocks/queue/Config.go with go tool mockery.
- Tests: Timeout() boundary table in config_test.go; runWithReceive
  deadline subtests asserting ctx.Deadline() ~ now + receiveTimeout for
  default and custom values.

Deliberately skipped review nits: per-worker timeout override and
queue.php scaffold config key (out of scope for this PR).
@wafik

wafik commented Sep 8, 2026

Copy link
Copy Markdown
Author

Addressed the automated review (all 3 Should-Fix + 2 nits) in ba5073c:

  • Centralized Timeout(connection string) time.Duration accessor on contracts/queue.Config (implemented in queue/config.go): accepts int/int64/float64 as seconds, string via time.ParseDuration (e.g. "5s"), and falls back to the 5s default for missing/non-positive/unparseable values. NewWorker now uses config.Timeout(connection) and no longer needs fmt.
  • Non-positive guard (Should-Fix 1): any value ≤ 0 resolves to the 5s default, so timeout: 0 can no longer produce an already-expired context / silent no-op poll loop.
  • Deadline test (Should-Fix 3): Test_runWithReceive now has table-driven subtests (default 5s and custom 2s) whose Receive mock captures ctx.Deadline() and asserts it is within ±100ms of now + receiveTimeout; plus a full TestTimeout boundary table (0, negative, "-5s", garbage, bool → default).
  • Nits taken: renamed field to receiveTimeout; moved defaultReceiveTimeout const into queue/config.go. The per-worker override and scaffold-config nits were left out deliberately (scope — happy to add if maintainers want them).
  • mocks/queue/Config.go regenerated via go tool mockery; go build ./..., go vet, and go test ./queue/... -count=1 all clean.

@hwbrzzl

hwbrzzl commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

@hwbrzzl

hwbrzzl commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Hey @wafik I solved the failed CI in another PR, and it had been merged. I'll close this PR, and thanks so much for your contribution.

@hwbrzzl hwbrzzl closed this Oct 4, 2026
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.

3 participants