-
Notifications
You must be signed in to change notification settings - Fork 2.9k
fix(dingtalk): retry transient emotion failures #7329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -88,6 +88,8 @@ const ACK_REACTION_NAME = '👀'; | |||||||||||||||||||||||||||||||||||
| const ACK_EMOTION_ID = '2659900'; | ||||||||||||||||||||||||||||||||||||
| const ACK_EMOTION_BG_ID = 'im_bg_1'; | ||||||||||||||||||||||||||||||||||||
| const EMOTION_API = 'https://api.dingtalk.com/v1.0/robot/emotion'; | ||||||||||||||||||||||||||||||||||||
| const EMOTION_MAX_ATTEMPTS = 3; | ||||||||||||||||||||||||||||||||||||
| const EMOTION_RETRY_BASE_DELAY_MS = 250; | ||||||||||||||||||||||||||||||||||||
| const GROUP_MSG_API = 'https://api.dingtalk.com/v1.0/robot/groupMessages/send'; | ||||||||||||||||||||||||||||||||||||
| const DIRECT_MSG_API = | ||||||||||||||||||||||||||||||||||||
| 'https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend'; | ||||||||||||||||||||||||||||||||||||
|
|
@@ -677,31 +679,43 @@ export class DingtalkChannel extends ChannelBase { | |||||||||||||||||||||||||||||||||||
| ? await this.getProactiveToken() | ||||||||||||||||||||||||||||||||||||
| : this.getAccessToken(); | ||||||||||||||||||||||||||||||||||||
| if (!token) return; | ||||||||||||||||||||||||||||||||||||
| const resp = await fetch(`${EMOTION_API}/${endpoint}`, { | ||||||||||||||||||||||||||||||||||||
| method: 'POST', | ||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||
| 'x-acs-dingtalk-access-token': token, | ||||||||||||||||||||||||||||||||||||
| 'Content-Type': 'application/json', | ||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||
| body: JSON.stringify({ | ||||||||||||||||||||||||||||||||||||
| robotCode, | ||||||||||||||||||||||||||||||||||||
| openMsgId: msgId, | ||||||||||||||||||||||||||||||||||||
| openConversationId: conversationId, | ||||||||||||||||||||||||||||||||||||
| emotionType: 2, | ||||||||||||||||||||||||||||||||||||
| emotionName: ACK_REACTION_NAME, | ||||||||||||||||||||||||||||||||||||
| textEmotion: { | ||||||||||||||||||||||||||||||||||||
| emotionId: ACK_EMOTION_ID, | ||||||||||||||||||||||||||||||||||||
| emotionName: ACK_REACTION_NAME, | ||||||||||||||||||||||||||||||||||||
| text: ACK_REACTION_NAME, | ||||||||||||||||||||||||||||||||||||
| backgroundId: ACK_EMOTION_BG_ID, | ||||||||||||||||||||||||||||||||||||
| for (let attempt = 0; attempt < EMOTION_MAX_ATTEMPTS; attempt++) { | ||||||||||||||||||||||||||||||||||||
| const resp = await fetch(`${EMOTION_API}/${endpoint}`, { | ||||||||||||||||||||||||||||||||||||
| method: 'POST', | ||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||
| 'x-acs-dingtalk-access-token': token, | ||||||||||||||||||||||||||||||||||||
| 'Content-Type': 'application/json', | ||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| if (!resp.ok) { | ||||||||||||||||||||||||||||||||||||
| body: JSON.stringify({ | ||||||||||||||||||||||||||||||||||||
| robotCode, | ||||||||||||||||||||||||||||||||||||
| openMsgId: msgId, | ||||||||||||||||||||||||||||||||||||
| openConversationId: conversationId, | ||||||||||||||||||||||||||||||||||||
| emotionType: 2, | ||||||||||||||||||||||||||||||||||||
| emotionName: ACK_REACTION_NAME, | ||||||||||||||||||||||||||||||||||||
| textEmotion: { | ||||||||||||||||||||||||||||||||||||
| emotionId: ACK_EMOTION_ID, | ||||||||||||||||||||||||||||||||||||
| emotionName: ACK_REACTION_NAME, | ||||||||||||||||||||||||||||||||||||
| text: ACK_REACTION_NAME, | ||||||||||||||||||||||||||||||||||||
| backgroundId: ACK_EMOTION_BG_ID, | ||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| if (resp.ok) return; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const isTransient = resp.status === 429 || resp.status >= 500; | ||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: please add a focused 429 retry case. The new predicate has a distinct |
||||||||||||||||||||||||||||||||||||
| if (isTransient && attempt < EMOTION_MAX_ATTEMPTS - 1) { | ||||||||||||||||||||||||||||||||||||
| await resp.body?.cancel(); | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+705
to
+707
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] 429 responses are retried without honouring the server's
Suggested change
— qwen3.7-max via Qwen Code /review |
||||||||||||||||||||||||||||||||||||
| await new Promise((resolve) => | ||||||||||||||||||||||||||||||||||||
| setTimeout(resolve, EMOTION_RETRY_BASE_DELAY_MS * 2 ** attempt), | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+706
to
+710
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Intermediate retry attempts produce no log output — only the final failure is logged. — Concrete cost: at 3 AM, an oncall engineer sees
Suggested change
— qwen3.7-max via Qwen Code /review |
||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+705
to
+712
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] If a Consider wrapping the per-iteration fetch in its own try-catch that logs the error on the last attempt, so the outer catch only fires for truly unexpected errors. — qwen3.7-max via Qwen Code /review |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const detail = sanitizeLogText(await resp.text().catch(() => ''), 500); | ||||||||||||||||||||||||||||||||||||
| process.stderr.write( | ||||||||||||||||||||||||||||||||||||
| `[DingTalk:${this.name}] emotion/${endpoint} failed: ${resp.status} ${detail}\n`, | ||||||||||||||||||||||||||||||||||||
| `[DingTalk:${this.name}] emotion/${endpoint} failed after ${attempt + 1}/${EMOTION_MAX_ATTEMPTS} attempts: ${resp.status} ${detail}\n`, | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
714
to
717
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The terminal error log is identical whether the failure happened on the first attempt (non-transient 4xx) or after exhausting all 3 retries (persistent 500). An oncall engineer seeing
Suggested change
— qwen3.7-max via Qwen Code /review |
||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||
| // best-effort, don't break message flow | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] The
isTransientpredicate has a distinctresp.status === 429branch, but all three retry tests exercise only the>= 500path with status 500. A future refactor that simplifies the condition toresp.status >= 500would pass every existing test while silently dropping rate-limit retry support. — Failure scenario: 429 responses stop retrying, DingTalk rate-limit errors immediately log failure instead of backing off.Consider adding a test that returns 429 on the first attempt and 200 on the second, asserting
emotionAttempts === 2, or parameterize the existing retry test over[429, 500, 502].— qwen3.7-max via Qwen Code /review