Repository navigation
Conversation
When BLMOVE failed, run() logged the traceback and retried at once. The comment assumed BLMOVE blocks and the client retries, but create_redis_client gives the client no retry and a refused connect returns immediately, so the loop spun at ~1k iterations/s while Redis was down, each one logging a ~30-line traceback (~0.8 GiB of Cloud Logging per minute, per pod). Redis connection errors and timeouts now wait 1s, doubling to a 30s cap, reset by the next reply. The wait polls the shutdown flag every 0.5s so SIGTERM still exits promptly. Other errors keep the immediate retry. UN-4457 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ritwik-g
left a comment
There was a problem hiding this comment.
Standardized review — round 1 (self-review), INITIAL
Reviewed tip: d376b0f77 · merge base 6da459096 · base fetched (base_fetch: ok); branch level with origin/main · 2 files / 300 lines (matches GitHub).
Review: unstract:review-deep (plugin unstract v2.1.72). review-shallow refused this PR: concurrency primitive (signal-interruptible wait in run()); its auth_authz_secrets trigger also fired, on the AuthenticationError class name.
Verdict — REQUEST CHANGES
Summary — Critical: 0 · High: 1 · Medium: 0 · Low: 6 · Lenses run: 19/19 (5 N/A: 5, 7, 12, 14, 18).
Signals: lenses 3, 6, 8, 10, 13, 16, 17, 19 raised (except/backoff/signal-handler path in workers/log_consumer/redis_stream_consumer.py, the new test file, new comments, a changed log line).
Dispatched (no name:): code-reviewer (lenses 1, 2, 3, 9, 10, 15), silent-failure-hunter (3), pr-test-analyzer (13), comment-analyzer (16), whole-diff cold read, and code-reviewer on infra lenses 6, 8, 11, 19 (the code is this session's own). Every agent returned a report.
boundary-sweep: 2 sinks traced, clean. The new branch logs the backoff seconds plus the same redis exception traceback the old line logged. redis-py's connection, auth and Sentinel error messages carry host, port and master name, never credentials.
Security deep-dive: done — no findings at confidence >= 8. The find sub-task returned no candidates, so no filter sub-tasks ran. This is a precision-biased scan, not a clean bill of health.
Lens 15 pinned gate (orchestrator): ruff@0.3.4 check exit 0; ruff@0.3.4 format --check exit 0, on both changed files.
Lens 17: lens17-merge-sweep.sh exit 1. Every hit is in this diff's own two files and agrees with the changed rule. No doc, compose or README restates the retry behaviour.
Findings are posted inline (7).
Unanchored findings
- The PR body says errors from a server that answered, such as
ReadOnlyError, "still retry immediately". That is false in Sentinel mode, where READONLY arrives asConnectionErrorand backs off. - The PR body's recovery bound ("at most about 30 s plus 1 s") covers BLMOVE failures only. If Redis is lost between BLMOVE and
lrem(L204) or during startup_recover_in_flight(L116), the error still exitsrun()and the container restarts under CrashLoopBackOff (up to 5 minutes). This is pre-existing and deserves its own follow-up, but the body should be scoped accordingly. - "Logs queue in Redis meanwhile and are not lost" is conditional: the producer drops once
log_stream_queuereaches 10,000 (unstract/core/src/unstract/core/pubsub_helper.py:245).
Lens checklist
- Spec & intent — See findings #1
- Architectural fit & precedent — See findings #1
- Correctness & edge cases — See findings #1
- Security — Clean
- Data integrity & migrations — N/A — no persisted data, schema or migration; the BLMOVE/LREM envelope path is untouched
- Concurrency — Clean
- API & contract compatibility — N/A — no wire format, status code or payload change; the changed log text has no consumers (grepped across the org)
- Reliability & resilience — See findings #1
- Performance & cost — See findings #1
- Observability — See findings #2
- Operational safety — Clean
- LLM/agent-specific — N/A — no model, prompt or agent path
- Testing — See findings #1, #4, #7
- Dependencies & build — N/A — no dependency or lockfile change
- Code quality — Clean
- Doc & comment accuracy — See findings #2, #3, #4, #5, #6
- Cross-document rule consistency — Clean
- On-prem deployment experience — N/A — no chart, sample values, schema, on-prem settings or build path touched
- Scaling & workload lifecycle — Clean
Assessed by the orchestrator rather than received from an agent: 4, 5, 7, 12, 14, 15 (pinned gate), 17, 18.
Open questions for the author
- After Redis recovers, the consumer can stay asleep for up to 30 s while the producer drops lines at 10,000 queued. Is peak log volume safely below ~333 lines/s?
- Does any deployment (cloud or on-prem) run standalone or URL mode against a managed Redis with
maxmemoryset? That raises the likelihood of #1.
Assumptions made
- Production runs redis-py 5.2.1 (
workers/uv.lock,<6.0.0). The "no client retry" premise depends on staying below 6.0, where the default retry changes.
cost: agents=6 wall=17:07 lenses-examined=14 files=2 lines=300
…rrors Review round 1 measured the same spin on error replies that recur until Redis changes state: OOM at maxmemory, READONLY on a standalone client left on a demoted primary, MISCONF after a failed RDB save, plus local errors such as a TLS misconfiguration. Each returned at once and was retried immediately, at 7k-20k failures/s locally, faster than the refused-connect case this branch fixes. Every BLMOVE failure now takes the same capped backoff, reset by the next successful BLMOVE. Comments and test docs now state that rule, and the inconsistent spin figures are replaced by the per-minute log volume. UN-4457 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ritwik-g
left a comment
There was a problem hiding this comment.
Standardized review — round 2 (self-review), FOLLOWUP
Reviewed tip: 5f968ccdc · round delta d376b0f77..5f968ccdc · merge base 6da459096 · base fetched (base_fetch: ok); branch level with origin/main · 2 files / 300 lines (matches GitHub).
Review: unstract:review-deep (plugin unstract v2.1.72); review-shallow is ineligible across the whole PR (concurrency primitive).
Verdict — REQUEST CHANGES
Summary — Critical: 0 · High: 0 · Medium: 1 · Low: 5 · Lenses run: 19/19 (5 N/A: 5, 7, 12, 14, 18).
Prior context fetched — followup-fetch.sh exited 0 (6 of 6 files). Previous-review SHA: d376b0f77 (review 5543035654).
Scope change — NO
Signals: lenses 3, 6, 8, 13, 16 raised (the backoff branch widened to except Exception, edited tests, rewritten comments).
Dispatched (no name:): code-reviewer (1, 2, 3, 9, 10, 15), silent-failure-hunter (3), pr-test-analyzer (13), comment-analyzer (16), whole-diff cold read, and code-reviewer on infra lenses 6, 8, 11, 19 (the code is this session's own). All returned reports.
boundary-sweep: 1 sink traced, clean. The single remaining log call carries the backoff seconds and the same exception traceback as before; no credentials reach it.
Security deep-dive: done — no findings at confidence >= 8 (the find sub-task returned no candidates). This is a precision-biased scan, not a clean bill of health.
Lens 15 pinned gate (orchestrator): ruff@0.3.4 check exit 0; format --check exit 0.
Lens 17: lens17-merge-sweep.sh exit 1. Every hit is in this diff's two files, except workers/queue_backend/pg_queue/consumer.py:1077, whose log line describes the same backoff-on-any-failure rule and agrees with it.
Prior findings status
#1[High] Non-connection BLMOVE failures spin, and the test pins the spin →RESOLVEDat5f968ccdc. The singleexcept Exceptionbacks off on every failure (workers/log_consumer/redis_stream_consumer.py:L164-L178). The parametrized test now covers OOM, READONLY, MISCONF and a localValueError(workers/tests/test_log_stream_consumer_backoff.py:L95-L106), andTestMixedFailuresasserts[1, 2, 4]. A mutation restoring the immediate retry fails 5 tests. Measured against a real Redis 7: OOM gave 3 failures in 8 s and READONLY 4 in 8 s, against 7k–20k/s before.#2[Low] "Server that answered" is not the rule, and "unavailable" is logged for auth errors →RESOLVEDat5f968ccdc. The split is gone, and the log text is now "backing off Ns before retrying".#3[Low] "Reset by the next reply" overstates the reset →RESOLVEDat5f968ccdc. It now reads "next successful BLMOVE (an envelope or an idle None)" (redis_stream_consumer.py:L66-L67,:L180).#4[Low] SIGTERM test comment overclaims →PARTIALLY RESOLVED. The new comment says the slice length is "pinned" byTestTheWaitItself, which only bounds it at ≤1 s. Superseded by new finding #9.#5[Low] Spin figures disagree →RESOLVEDat5f968ccdc. One figure remains (redis_stream_consumer.py:L69). Its sourcing is new finding #11.#6[Low] LOADING attributed to a replica →RESOLVEDat5f968ccdc("a restarted Redis still loading its dataset", test L93).#7[Low] Real-SIGTERM test not run under xdist →RESOLVED. The round-2 test reviewer ran the two files underpytest-xdist -n 4: 31 passed. No code change was needed.
New findings are posted inline (5).
Unanchored findings
- The PR body's "Notes on Testing" says 17 tests in the new file; pytest collects 21.
Lens checklist
- Spec & intent — Clean
- Architectural fit & precedent — Clean
- Correctness & edge cases — See findings #8
- Security — Clean
- Data integrity & migrations — N/A — no persisted data, schema or migration; the BLMOVE/LREM envelope path is untouched
- Concurrency — Clean
- API & contract compatibility — N/A — no wire format, status code or payload change
- Reliability & resilience — See findings #8
- Performance & cost — Clean
- Observability — Clean
- Operational safety — Clean
- LLM/agent-specific — N/A — no model, prompt or agent path
- Testing — See findings #9
- Dependencies & build — N/A — no dependency or lockfile change
- Code quality — Clean
- Doc & comment accuracy — See findings #8, #9, #10, #11, #12
- Cross-document rule consistency — Clean
- On-prem deployment experience — N/A — no chart, sample values, schema, on-prem settings or build path touched
- Scaling & workload lifecycle — Clean
Assessed by the orchestrator rather than received from an agent: 4, 5, 7, 12, 14, 15 (pinned gate), 17, 18.
Open questions / assumptions
- Does any cloud or on-prem deployment run standalone mode against an endpoint that can demote its primary? That decides how often N1 happens.
- Assumes redis-py 5.2.1 (
<6.0.0), as in round 1.
cost: agents=7 wall=07:47 lenses-examined=14 files=2 lines=300
Review round 2: redis-py drops the socket only on a connection error or timeout. After an error reply it keeps the connection, so a standalone client left on a demoted primary retried the same replica every 30s forever. Drop the pool before each backoff wait so the next attempt opens a fresh connection and re-resolves the host. Verified against a real two-node Redis: client id 3 was reused without the drop; ids 6 and 7 were new with it. Also tighten comments the review found overstated: most failures recur at once (not all), the TLS ValueError comes after the TCP connect, the wait test bounds the slice rather than pinning it, and the log-volume figure now names where it was measured. UN-4457 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ritwik-g
left a comment
There was a problem hiding this comment.
Standardized review — round 3 (self-review), FOLLOWUP
Reviewed tip: 51e97bb71 · round delta 5f968ccdc..51e97bb71 · merge base 6da459096 · base fetched (base_fetch: ok); branch level with origin/main · 2 files / 334 lines (matches GitHub).
Review: unstract:review-deep (plugin unstract v2.1.72); review-shallow is ineligible across the whole PR (concurrency primitive).
Verdict — REQUEST CHANGES
Summary — Critical: 0 · High: 0 · Medium: 2 · Low: 4 · Lenses run: 19/19 (5 N/A: 5, 7, 12, 14, 18).
Prior context fetched — followup-fetch.sh exited 0. Previous-review SHA: 5f968ccdc (review 5543105608).
Scope change — NO
Signals: lenses 3, 6, 8, 13, 16 raised (the pool disconnect in the error path, new tests, rewritten comments).
Dispatched (no name:): code-reviewer (1, 2, 3, 9, 10, 15), silent-failure-hunter (3), pr-test-analyzer (13), comment-analyzer (16), whole-diff cold read, and code-reviewer on infra lenses 6, 8, 11, 19 (the code is this session's own). All returned reports.
boundary-sweep: 1 sink traced, clean. It is unchanged since round 2.
Security deep-dive: done — no findings at confidence >= 8 (the find sub-task returned no candidates). This is a precision-biased scan, not a clean bill of health.
Lens 15 pinned gate (orchestrator): ruff@0.3.4 check exit 0; format --check exit 0.
Lens 17: lens17-merge-sweep.sh exit 1. All hits are in this diff's two files, plus workers/queue_backend/pg_queue/consumer.py:1077, which states the same rule and agrees with it.
Prior findings status
#4[Low] SIGTERM test comment overclaims →RESOLVEDat51e97bb71("TestTheWaitItself bounds each slice at 1s").#8[Medium] Standalone READONLY never reconnects →RESOLVEDat51e97bb71.redis_client.connection_pool.disconnect()runs before every backoff wait (workers/log_consumer/redis_stream_consumer.py:L176-L180);TestReconnectAfterFailurepins drop-then-wait; removing it fails 1 test.- Real Redis 7, node demoted with
REPLICAOF: without the drop the client reused id 3 on every retry; with it, ids 6 and 7. - Round-3 reviewers confirmed the call cannot raise out of the loop (redis-py swallows socket errors on close) and that Sentinel pools re-discover the master on reconnect.
- Real Redis 7, node demoted with
#9[Low] "Pinned" overstates the slice guard →RESOLVEDat51e97bb71.#10[Low] "Raised locally before any round trip" →PARTIALLY RESOLVED. The comment now says "while wrapping the socket"; the error is actually raised while redis-py configures the SSL context (redis/connection.py:1036-1038), before any wrap.#11[Low] "Recur at once" too broad; 1.7 GiB unsourced →RESOLVEDat51e97bb71("most such failures"; "(staging, Oct 2026)").#12[Low] Reconnect names fit only connection errors →RESOLVEDat51e97bb71. Every failure now reconnects, so_wait_before_reconnect/_RECONNECT_BACKOFF_*describe what happens.
New findings are posted inline (5).
Unanchored findings
- No backoff jitter: several replicas would reconnect in lockstep. Moot at the chart default
replicas: 1.
Lens checklist
- Spec & intent — Clean
- Architectural fit & precedent — Clean
- Correctness & edge cases — See findings #13
- Security — Clean
- Data integrity & migrations — N/A — no persisted data, schema or migration; the BLMOVE/LREM envelope path is untouched (the disconnect runs only on the failure path)
- Concurrency — Clean
- API & contract compatibility — N/A — no wire format, status code or payload change
- Reliability & resilience — See findings #13
- Performance & cost — Clean
- Observability — Clean
- Operational safety — Clean
- LLM/agent-specific — N/A — no model, prompt or agent path
- Testing — See findings #14, #15
- Dependencies & build — N/A — no dependency or lockfile change
- Code quality — Clean
- Doc & comment accuracy — See findings #16, #17
- Cross-document rule consistency — Clean
- On-prem deployment experience — N/A — no chart, sample values, schema, on-prem settings or build path touched
- Scaling & workload lifecycle — Clean
Assessed by the orchestrator rather than received from an agent: 4, 5, 7, 12, 14, 15 (pinned gate), 17, 18.
Open questions / assumptions
- Assumes redis-py 5.2.1 (
<6.0.0).
cost: agents=7 wall=14:17 lenses-examined=14 files=2 lines=334
Review round 3: with backoff on every failure, an error that never clears in-process (a rotated password read once at start, an ACL or TLS misconfiguration) left the pod Running, logging one line every 30s and draining nothing. After 10 consecutive waits at the 30s cap, run() now exits 1 so the container restarts, re-reads its Secrets, and shows up as CrashLoopBackOff. Any successful read resets the count. Tests now also cover a delivered envelope not dropping the pool, and assert the pool drop for every failure class. Comments: redis-py keeps the socket after an error reply (not "drops it only on connection errors"), the TLS ValueError is raised while configuring the SSL context, and BLMOVE's timeout is no longer the only shutdown checkpoint. UN-4457 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ritwik-g
left a comment
There was a problem hiding this comment.
Standardized review — round 4 (self-review), FOLLOWUP
Reviewed tip: 0849d201b · round delta 51e97bb71..0849d201b · merge base 6da459096 · base fetched (base_fetch: ok); branch level with origin/main · 2 files / 396 lines (matches GitHub).
Review: unstract:review-deep (plugin unstract v2.1.72); review-shallow is ineligible across the whole PR (concurrency primitive).
Verdict — REQUEST CHANGES
Summary — Critical: 0 · High: 1 · Medium: 1 · Low: 4 · Lenses run: 19/19 (5 N/A: 5, 7, 12, 14, 18).
Prior context fetched — followup-fetch.sh exited 0. Previous-review SHA: 51e97bb71 (review 5543199920).
Scope change — NO
Signals: lenses 3, 8, 11, 13, 16, 19 raised (the new process-exit path, new tests, rewritten comments).
Dispatched (no name:): code-reviewer (1, 2, 3, 9, 10, 15), silent-failure-hunter (3), pr-test-analyzer (13), comment-analyzer (16), whole-diff cold read, and code-reviewer on infra lenses 6, 8, 11, 19 (the code is this session's own). All returned reports.
boundary-sweep: 2 sinks traced, clean. The new give-up log line carries the wait count, the cap and the same exception traceback.
Security deep-dive: done — no findings at confidence >= 8 (the find sub-task returned no candidates). This is a precision-biased scan, not a clean bill of health.
Lens 15 pinned gate (orchestrator): ruff@0.3.4 check exit 0; format --check exit 0.
Lens 17: lens17-merge-sweep.sh hits are confined to this diff's two files.
Prior findings status
#10[Low] TLSValueErrorcomment →RESOLVEDat0849d201b("while redis-py configures the SSL context").#13[Medium] Permanent errors retried forever →RESOLVEDat0849d201b: after 10 waits at the cap,run()returns 1. Its side effect on long outages is new finding #18.#14[Medium] No-drop test covered only idle reads →RESOLVEDat0849d201b. It now scripts a delivered envelope; a mutation that drops the pool afterlremfails it.#15[Low] Pool drop asserted for 2 of 9 classes →RESOLVEDat0849d201b(assert_called_once()in the parametrized test; a partial-drop mutation fails 7 tests).#16[Low] "Drops the socket only…" overstated →RESOLVEDat0849d201b("keeps the socket after an error reply").#17[Low] "Only chance to notice a shutdown" →RESOLVEDat0849d201b(clause removed).
New findings are posted inline (6).
Lens checklist
- Spec & intent — See findings #18
- Architectural fit & precedent — Clean
- Correctness & edge cases — See findings #18
- Security — Clean
- Data integrity & migrations — N/A — no persisted data, schema or migration; an envelope on the processing list is recovered on restart (same HOSTNAME)
- Concurrency — Clean
- API & contract compatibility — N/A — no wire format, status code or payload change
- Reliability & resilience — See findings #18
- Performance & cost — Clean
- Observability — Clean
- Operational safety — See findings #18, #21
- LLM/agent-specific — N/A — no model, prompt or agent path
- Testing — See findings #22, #23
- Dependencies & build — N/A — no dependency or lockfile change
- Code quality — Clean
- Doc & comment accuracy — See findings #19, #20
- Cross-document rule consistency — Clean
- On-prem deployment experience — N/A — no chart, sample values, schema, on-prem settings or build path touched
- Scaling & workload lifecycle — See findings #18
Assessed by the orchestrator rather than received from an agent: 4, 5, 7, 12, 14, 15 (pinned gate), 17, 18.
cost: agents=7 wall=11:49 lenses-examined=14 files=2 lines=396
Review round 4: exiting after any 10 waits at the cap turned a long Redis outage into a crash loop. The restarted container's startup recovery hit Redis outside the backoff and died at once, so kubelet's restart back-off grew to 5 minutes and delivery resumed that long after Redis did, while the producer dropped lines at its 10k cap. Now only failures a restart can fix count toward the exit: an AuthenticationError (a rotated password read once at start), NOPERM, and local client config errors (ValueError, e.g. TLS). Outages keep backing off in-process, and any other failure resets the count. Startup recovery runs inside the same backoff, so a restart during an outage waits instead of crashing. Verified against a real Redis started 3s after the consumer: recovery backed off 1s, 2s, 4s and continued without exiting. UN-4457 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ritwik-g
left a comment
There was a problem hiding this comment.
Standardized review — round 5 (self-review), FOLLOWUP
Reviewed tip: 9d821f4db · round delta 0849d201b..9d821f4db · merge base 6da459096 · base fetched (base_fetch: ok); branch level with origin/main · 2 files / 459 lines (matches GitHub).
Review: unstract:review-deep (plugin unstract v2.1.72); review-shallow is ineligible across the whole PR (concurrency primitive).
Verdict — REQUEST CHANGES
Summary — Critical: 0 · High: 0 · Medium: 5 · Low: 5 · Lenses run: 19/19 (5 N/A: 5, 7, 12, 14, 18).
Prior context fetched — followup-fetch.sh exited 0. Previous-review SHA: 0849d201b (review 5543290502).
Scope change — NO
Signals: lenses 3, 8, 13, 16 raised (the exception-class split for the exit, startup recovery moved into the loop, new tests).
Dispatched (no name:): code-reviewer (1, 2, 3, 9, 10, 15), silent-failure-hunter (3), pr-test-analyzer (13), comment-analyzer (16), whole-diff cold read, and code-reviewer on infra lenses 6, 8, 11, 19 (the code is this session's own). All returned reports. The error-handling reviewer exercised a live Redis, Redis Sentinel and a TLS Redis.
boundary-sweep: 2 sinks traced, clean.
Security deep-dive: done — no findings at confidence >= 8 (the find sub-task returned no candidates). This is a precision-biased scan, not a clean bill of health.
Lens 15 pinned gate (orchestrator): ruff@0.3.4 check exit 0; format --check exit 0.
Lens 17: hits are confined to this diff's two files.
Author decision (10-16), recorded here: after this round the give-up exit is removed entirely. Findings #24, #25, #28, #29 and #30 concern only that mechanism, and the next commit deletes it rather than patching it further. A persistent auth error is then documented as needing a restart, with a follow-up ticket for a liveness signal.
Prior findings status
#18[High] Give-up exit crash-loops long outages →RESOLVEDat9d821f4db. Outages no longer count toward the exit (workers/log_consumer/redis_stream_consumer.py:L188-L191), and startup recovery runs inside the backoff (:L175-L179). Verified on a real Redis started 3 s after the consumer: recovery backed off 1, 2, 4 s and continued without exiting. Sentinel's eager client construction is new finding #26.#19[Medium] "Must not kill the pod" contradicted the exit →RESOLVEDat9d821f4db(clause removed).#20[Low] ACL example and "~5 min" →PARTIALLY RESOLVED. The wording changed, but NOPERM and the "at least ~5.5 min" figure are still wrong (#29, #30). BecomesOBSOLETEwhen the exit is removed.#21[Low] docker-compose restart keeps the old env →RESOLVEDat9d821f4db(stated in the comment).#22[Low] Give-up traceback not asserted →RESOLVEDat9d821f4db(assert last.kwargs.get("exc_info") is True; a mutation dropping it fails 3 tests).#23[Low] Two wall-clock shutdown tests →NOT RESOLVED. They are unchanged and kept deliberately: the real-signal test is the only end-to-end SIGTERM proof, and both bounds are generous (2 s against 0.3–0.5 s).
New findings are posted inline (9).
Unanchored findings
lremin thefinallyis still outside any handler: a Redis failure between BLMOVE and LREM exitsrun()and replays the envelope once on restart. This is pre-existing, and the comments must not claim that every Redis failure backs off.
Lens checklist
- Spec & intent — See findings #24
- Architectural fit & precedent — Clean
- Correctness & edge cases — See findings #24, #25
- Security — Clean
- Data integrity & migrations — N/A — no persisted data, schema or migration
- Concurrency — Clean
- API & contract compatibility — N/A — no wire format, status code or payload change
- Reliability & resilience — See findings #24, #26, #29
- Performance & cost — Clean
- Observability — See findings #32
- Operational safety — Clean
- LLM/agent-specific — N/A — no model, prompt or agent path
- Testing — See findings #27, #28
- Dependencies & build — N/A — no dependency or lockfile change
- Code quality — See findings #31
- Doc & comment accuracy — See findings #25, #26, #30
- Cross-document rule consistency — Clean
- On-prem deployment experience — N/A — no chart, sample values, schema, on-prem settings or build path touched
- Scaling & workload lifecycle — Clean
Assessed by the orchestrator rather than received from an agent: 4, 5, 7, 12, 14, 15 (pinned gate), 17, 18.
cost: agents=7 wall=10:00 lenses-examined=14 files=2 lines=459
Review round 5: the exit-after-repeated-failures path reliably helped only a standalone client with a rotated password. With Sentinel the same rotation surfaces as MasterNotFoundError (an outage), most TLS mistakes arrive as ConnectionError, and each patch to the exit turned up new edge cases. Remove it: every read or startup-recovery failure backs off 1s to 30s with a fresh connection, indefinitely. A failure only a restart can fix keeps logging an ERROR every 30s, as documented at the backoff constants. The failure log now names the step (recovery or read). A parametrized test pins that no failure class ever makes run() give up. UN-4457 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
ritwik-g
left a comment
There was a problem hiding this comment.
Self-review round 6 — FOLLOWUP (review-deep)
Round tip: 6a4557d1933ea6c8748b776fd0e25e18a06945e5. Round delta: 9d821f4db..6a4557d19 (the give-up exit is dropped and the loop always backs off). Review: unstract:review-deep; lite is ineligible because the PR adds a concurrency primitive and retry logic.
Verdict — APPROVE WITH NITS
Summary — Critical: 0 · High: 0 · Medium: 0 · Low: 3 · Lenses run: 19/19
Prior context fetched
followup-fetch.shexited 0 (6 of 6 fetches).- The "since" boundary is the previous review's SHA,
9d821f4db5e3671ecb4bb39c1382013678decd59(review 5543390630).
Scope change — NO. The delta only removes code (_RESTART_FIXABLE_ERRORS, _MAX_WAITS_AT_BACKOFF_CAP, waits_at_cap, the return 1 path) and relabels the failure log.
Security deep-dive: done, 0 findings. The find sub-task ran and returned no candidates, so no filter sub-tasks were needed.
boundary-sweep: 2 sinks traced, clean. The ERROR log with exc_info and the recovery WARNING count carry host:port or Sentinel service names only, never credentials.
Prior findings status
#20[Low] The give-up comment overstates what a restart fixes and how long it takes →OBSOLETE. The give-up exit and its comment are removed at 6a4557d.workers/log_consumer/redis_stream_consumer.py:66-72now says the loop never gives up.#23[Low] Two shutdown tests use real wall-clock time with a 2 s bound →WAIVED. A real SIGTERM has to be delivered against a realtime.sleepto test PEP 475 resumption. The observed exit is ~0.7 s against the 2 s bound, andTestTheWaitItselfcovers the slicing on a fake clock.#24[Medium] With Sentinel, a rotated password surfaces asMasterNotFoundError→OBSOLETE. No error classification remains. Every failure backs off (redis_stream_consumer.py:171-190).#25[Medium] "Local client config such as TLS" is false →OBSOLETE. The claim is removed with the restart-fixable set.#26[Medium] In Sentinel mode a restart during an outage still crashes increate_redis_client→PARTIALLY RESOLVED.- The code comment is now scoped to the startup LMOVE (
redis_stream_consumer.py:164), and the PR body states that Sentinel still connects eagerly before the loop. - Residual: the test comment at
workers/tests/test_log_stream_consumer_backoff.py:199, "A restart during an outage must wait in-process, not crash-loop", is true only in standalone mode. It is accepted as a Low test-comment wording item; the Sentinel constructor's own retry is unchanged by this PR.
- The code comment is now scoped to the startup LMOVE (
#27[Medium] "Outages never give up" is tested for refused connects only →RESOLVEDin 6a4557d.TestNeverGivesUp(test_log_stream_consumer_backoff.py:176-195) is parametrized over 9 failure classes. Reintroducing areturn 1fails all 9 (mutation-verified).#28[Medium] The give-up tests inject hand-built exceptions →OBSOLETE. The give-up tests are removed.#29[Low] A restart does not fix NOPERM →OBSOLETE. No restart path remains.#30[Low] "At least ~5.5 min" is wrong →OBSOLETE. The figure is removed.#31[Low] One comment line is about 130 characters →OBSOLETE. The line is removed.#32[Low] A startup-recovery failure is logged as "Log stream read failed" →RESOLVEDin 6a4557d. The label is computed inexcept(redis_stream_consumer.py:177-182) and pinned both ways by tests (test_log_stream_consumer_backoff.py:118-127and:209-213).
New findings
#33, #34 and #35 are posted inline below (all Low, below the floor and accepted as notes; #35 is pre-existing and a candidate follow-up).
Lens checklist
- Spec & intent — Clean (code-reviewer). Title follows
TICKET [TYPE] text. - Architectural fit — Clean (code-reviewer). Mirrors the PG-queue consumer's backoff.
- Correctness & edge cases — See findings #33 (code-reviewer, silent-failure-hunter, cold read).
- Security — Clean (security deep-dive done, 0 findings; boundary sweep clean; assessed by orchestrator).
- Data integrity & migrations — N/A — no schema, migration or persisted-format change (assessed by orchestrator).
- Concurrency — Clean (infra agent).
- API & contract compatibility — N/A — no wire, payload or status change. The log message text has no consumers (checked in an earlier round; assessed by orchestrator).
- Reliability & resilience — See findings #35 (infra agent, cold read).
- Performance & cost — Clean (code-reviewer).
- Observability — Clean (code-reviewer).
- Operational safety — Clean (infra agent). The missing liveness signal is tracked as a follow-up ticket.
- LLM/agent — N/A — no model, prompt or agent code (assessed by orchestrator).
- Testing — Clean (pr-test-analyzer; mutation-verified).
- Dependencies & build — N/A — no dependency or build change (assessed by orchestrator).
- Code quality — Clean (code-reviewer). Pinned gate
uvx ruff@0.3.4 checkandformat --checkare clean on both files (run by orchestrator). - Doc & comment accuracy — See findings #33, #34 (comment-analyzer).
- Cross-document rule consistency — Clean (
lens17-merge-sweep.shexit 1, every hit consistent with HEAD; run by orchestrator). - On-prem deployment experience — N/A — no chart, values, schema or on-prem path touched (assessed by orchestrator).
- Scaling & workload lifecycle — Clean (infra agent). The 0.5 s shutdown poll sits well inside the 60 s grace period and the compose default of 10 s.
Open questions / assumptions
- I assume a manual restart, plus the follow-up liveness/alert ticket, is the remedy for restart-only failures such as a rotated password. The code comment states this.
cost: agents=8 wall=06:36 lenses-examined=14 files=2 lines=389
[High impact] The PR appears safe to merge; no actionable issues were found.
|
Unstract test resultsPer-group results
Critical paths
|
muhammad-ali-e
left a comment
There was a problem hiding this comment.
LGTM. The change is limited to the failure path of the log stream consumer, so healthy-Redis behaviour is unchanged, and all review threads are resolved.
Optional, non-blocking: since this is the public repo, consider dropping the staging $ cost figures from the PR description.
Generated by Claude Code



What
workers/log_consumer/redis_stream_consumer.py) now waits before retrying whenever reading from Redis fails. That covers bothBLMOVEand the startup recovery of the processing list.Why
create_redis_clientgives the client no retry, and a refused connect returns at once.maxmemory, READONLY on a standalone client left on a demoted primary, and MISCONF after a failed RDB save. Review measured 7k–20k failures/s locally, so the backoff covers every read failure, not only connection errors.How
except ExceptionaroundBLMOVEnow logs, waits and doubles the wait. The log line names the step that failed (readorrecovery). The PG-queue consumer's poll loop already backs off on any exception in the same way (workers/queue_backend/pg_queue/consumer.py)._recover_in_flightruns inside the sametry, so a container started while Redis is down waits instead of crashing at startup.redis_client.connection_pool.disconnect()runs. redis-py keeps the socket after an error reply, so without it a standalone client left on a demoted primary would get READONLY forever. Now the next attempt opens a new connection and re-resolves the host._wait_before_reconnect()sleeps in 0.5 s slices and checks the shutdown flag between them. A plaintime.sleep(30)would not do, because since PEP 475 a sleep resumes after the signal handler returns. It polls the flag rather than usingthreading.Event, becauseEvent.set()from the signal handler can deadlock againstEvent.wait()on the same thread.Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)
log_stream_queueholds 10,000 (unstract/core/src/unstract/core/pubsub_helper.py). That was already true during an outage.terminationGracePeriodSeconds. Nothing depends on the loop running often: the deployment has no probes and no heartbeat file.BLMOVEandLREMstill exitsrun(), and the container restarts, as before. In Sentinel mode,create_redis_clientstill connects eagerly with its own retry before the loop starts.Database Migrations
Env Config
Relevant Docs
Related Issues or PRs
Dependencies Versions
Notes on Testing
Unit tests: new file
workers/tests/test_log_stream_consumer_backoff.py, 33 tests. They use the real redis-py exception classes; only the client and the clock are faked. They cover:ValueError;run()give up, even 40 failures in;Nonereply resets the backoff;recovery;run()return in under 2 s;Mutation testing: each of these mutations fails at least one test:
Targeted run: 43 passed, also under
pytest-xdist -n 4; ruff 0.3.4 clean.Against a real Redis (redis 7, the real
run()andcreate_redis_client):mainmaxmemory 1, 8 s)REPLICAOF, 8 s)For QA:
docker stop, or scale the Redis StatefulSet to 0) and watch theworker-log-stream-consumerlogs. Expect "backing off 1s … 2s … 4s … 30s", then one line every 30 s, with the pod staying up.Screenshots
Checklist
I have read and understood the Contribution Guidelines.
🤖 Generated with Claude Code