Repository navigation
fix(slack): stop retrying replies rejected with cannot_reply_to_message - #1660
Conversation
b9e8787 to
1a66d92
Compare
1a66d92 to
b84f50d
Compare
|
@gjkim42 small bug fix around failure to post messages back into slack |
There was a problem hiding this comment.
1 issue found across 4 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="internal/reporting/watcher_test.go">
<violation number="1" location="internal/reporting/watcher_test.go:3067">
P3: The task in TestSlackTaskReporter_CannotReplyToMessageNotRetried never sets ObjectMeta.UID, so the second-cycle suppression keys on the empty UID (blockProgress("")/isProgressBlocked("")). That works in-process but diverges from production, where every task has a UID, and from the sibling TestSlackTaskReporter_PermanentProgressErrorNotReattempted, which sets UID "uid-124". Give the task a UID so the test exercises the same keying path as production and as the other suite.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| } | ||
| } | ||
|
|
||
| func TestSlackTaskReporter_CannotReplyToMessageNotRetried(t *testing.T) { |
There was a problem hiding this comment.
P3: The task in TestSlackTaskReporter_CannotReplyToMessageNotRetried never sets ObjectMeta.UID, so the second-cycle suppression keys on the empty UID (blockProgress("")/isProgressBlocked("")). That works in-process but diverges from production, where every task has a UID, and from the sibling TestSlackTaskReporter_PermanentProgressErrorNotReattempted, which sets UID "uid-124". Give the task a UID so the test exercises the same keying path as production and as the other suite.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/reporting/watcher_test.go, line 3067:
<comment>The task in TestSlackTaskReporter_CannotReplyToMessageNotRetried never sets ObjectMeta.UID, so the second-cycle suppression keys on the empty UID (blockProgress("")/isProgressBlocked("")). That works in-process but diverges from production, where every task has a UID, and from the sibling TestSlackTaskReporter_PermanentProgressErrorNotReattempted, which sets UID "uid-124". Give the task a UID so the test exercises the same keying path as production and as the other suite.</comment>
<file context>
@@ -3063,3 +3063,201 @@ func TestAppendActivityContext_DoesNotMutateBase(t *testing.T) {
}
}
+
+func TestSlackTaskReporter_CannotReplyToMessageNotRetried(t *testing.T) {
+ task := &kelos.Task{
+ ObjectMeta: metav1.ObjectMeta{
</file context>
|
/kelos claude-review |
|
🤖 Kelos Task Status Task |
|
🤖 Kelos Claude Reviewer Agent @gjkim42 Review SummaryVerdict: APPROVE Findings Overview
FindingsTests
Suggestions (optional)
Key takeaways
|
|
@gjkim42 bump here :) |
|
@gjkim42 Bump here if you have a chance |
|
Unsure if you can rerun E2E @gjkim42 ? Looks like it failed for unrelated reasons |
|
Unsure what the failure is here or why it is failing to load images into kind. I can rebase, but usnure how you would like to proceed @gjkim42 ? |
|
Can you rebase onto origin/main? |
The Slack reporting cycle posts thread replies for task phase transitions and progress snapshots. When Slack rejects a reply with cannot_reply_to_message (for example, the thread is closed), every subsequent attempt fails the same way, but the failed post prevented the task's slack-report-phase annotation from being persisted, so the reporting cycle retried the post every 30s forever. Treat cannot_reply_to_message as a permanent API error: log the failure and persist the phase so the cycle stops retrying. HTTP-level failures (timeouts, 5xx, rate limits) remain transient and keep the existing retry behavior. The set is limited to cannot_reply_to_message on purpose; isPermanentSlackError documents why other arguably-permanent codes are left retrying. For progress snapshots, track a per-task progressBlocked flag rather than caching the rejected snapshot text. A running task keeps producing new progress, so text deduplication alone would let each fresh snapshot post to the rejected thread again. Once blocked, updateProgress returns before reading pod logs. Both permanent-error paths set the flag, so a task whose transition reply was rejected does not read pod logs and post once more on the following cycle. The flag is cleared in clearProgressCache and swept in SweepProgressCache alongside the other per-task state. The flag is process-local, so a restart or leader failover costs one more rejected post per task before re-blocking. That bound is documented on the field rather than fixed here: persisting it needs a new annotation, and progressTS resets the same way and does post a duplicate progress message on restart, so durable per-task Slack state belongs in its own change. Log channel and threadTS on both post failures. Neither line recorded the thread timestamp, so diagnosing which Slack message was rejected meant joining the logs against the task's annotations by hand. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Head branch was pushed to by a user without write access
b84f50d to
0e5503c
Compare
|
@gjkim42 rebased |
What type of PR is this?
/kind bug
What this PR does / why we need it:
When Slack rejects a thread reply with
cannot_reply_to_message(for example, when the thread is closed or the bot cannot reply to it), thekelos-slack-serverreporting cycle currently retries the post forever. Because the failed post prevented the task'skelos.dev/slack-report-phaseannotation from being persisted, the reporter re-posted on every 30s tick — observed in production as two self-triggered tasks wedged in theacceptedphase with 198+ failed attempts in a single hour.This PR treats
cannot_reply_to_messageas a permanent API error:ReportTaskStatus): log the failure once and still persist the report phase, so the reporting cycle stops retrying that reply on every tick. HTTP-level failures (timeouts, 5xx, rate limits) keep the existing retry behavior.updateProgress): mark the snapshot as posted on a permanent error so it is not re-attempted every cycle.The permanent-error detection is isolated in a small
isPermanentSlackErrorhelper, living alongside the otherSlackReporterhelpers ininternal/reporting/slack.go, so the classification is easy to extend to other permanent Slack API errors later.Which issue(s) this PR is related to:
N/A
Special notes for your reviewer:
TestSlackTaskReporter_CannotReplyToMessageNotRetried— permanent error: one attempt, phase annotation persisted, no further attempts on the next cycle.TestSlackTaskReporter_TransientReplyErrorStillRetried— transient error: error surfaced, annotation not set, retry behavior unchanged.TestSlackTaskReporter_PermanentProgressErrorNotReattempted— permanent error on a progress snapshot: single attempt, not re-attempted next cycle.TestIsPermanentSlackError— classification of wrapped errors, other Slack API errors, HTTP status errors, and plain errors. Includesrate_limited, asserting rate limits stay transient and keep their existing retry behavior.isPermanentSlackErrormatchesslack.SlackErrorResponseby value only. That is the shape slack-go v0.20.0 returns fromSlackResponse.Err()(misc.go:180,misc.go:195),Error()has a value receiver, and no path in the library returns a pointer, so a*SlackErrorResponsebranch would be unreachable.breakrather than an early return, so it falls through to the existing tail ofReportTaskStatus:setActivityTargetis skipped (firstReplyTSis empty), and terminal phases still reachclearProgressCache/clearActivityStatebefore the phase is persisted.go build ./...,go vet,gofmt, andgo test ./internal/reporting/all pass. The new tests fail against the pre-fix code. The repo-wide failures (internal/controllerentrypoint tests,test/e2e,test/integration) are pre-existing environment issues (kubebuilder envtest binaries and a git sandbox limitation) that fail identically without this change.make test-integrationrequires envtest binaries not available in the sandbox environment.Does this PR introduce a user-facing change?
Summary by cubic
Stop retrying Slack thread replies rejected with cannot_reply_to_message by classifying them as permanent API errors. This prevents 30s retry loops, wedged tasks, and duplicate posts by persisting the report phase and suppressing progress posts.
isPermanentSlackErrorthat matchesslack.SlackErrorResponse.Err == "cannot_reply_to_message"; HTTP failures and rate limits remain retryable.kelos.dev/slack-report-phase, and set a per-task progress block so the next cycle does not attempt progress posts.clearProgressCacheand is swept inSweepProgressCache.Written for commit b84f50d. Summary will update on new commits.