Fix dangling retry processor and add resilience/validation to AI client - #182
Merged
DeFiVC merged 4 commits intoJul 28, 2026
Conversation
startRetryProcessor fired its first tick without awaiting it and used a plain boolean flag to gate the recursive setTimeout loop. If the processor was restarted while an earlier tick was still in flight (e.g. stop immediately followed by start), the stale tick would see the flag flipped back to true and reschedule itself, running two overlapping processing loops. Await the first tick and gate scheduling with a generation counter instead of a boolean, so a stale in-flight tick can never reschedule itself after a restart.
Pull the retry policy and circuit breaker logic out of src/stellar/resilience.ts into a generic src/utils/resilience.ts factory, so other external service clients can get the same retry+breaker+timeout protection without sharing Stellar's circuit breaker state. src/stellar/resilience.ts keeps its exact public API, now backed by the shared factory. Also treat AbortError/TimeoutError and "timed out" messages as transient, so a hanging dependency actually gets retried and trips the breaker instead of silently bypassing both.
The chainlearn-ai client had a request timeout but no retry or circuit breaker (unlike Stellar calls), so a persistent AI service outage hung every quiz request for the full timeout duration. It also cast the parsed response straight to AiQuizResponse with no runtime check, so a malformed body would crash instead of falling back gracefully. - Wrap the AI service call with the same retry + circuit breaker pattern used for Stellar calls, bounded to 3 attempts per request so a call still fails predictably rather than retrying forever. - Validate the response body with a zod schema and reject malformed shapes with a descriptive error instead of an unchecked cast. Both failure modes are still caught by quizService.generateQuiz's existing fallback to placeholder questions. Closes ChainLearnOfficial#139, Closes ChainLearnOfficial#140, Closes ChainLearnOfficial#141, Closes ChainLearnOfficial#142
|
@balisdev Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
startRetryProcessorfires tick without await: the retry processor started its first tick with a fire-and-forget call and gated the recursive loop with a plain boolean. Restarting the processor while an earlier tick was still in flight (e.g.stopRetryProcessor()immediately followed bystartRetryProcessor()) let the stale tick see the flag flipped back on and reschedule itself, leaving two overlapping processing loops running. Fixed by awaiting the first tick and gating scheduling with a generation counter instead of a boolean, so a stale in-flight tick can never reschedule itself after a restart.1535a22, PR fix(quizzes): add configurable timeout to AI service fetch #85) — thefetchcall already has anAbortControllerwith a configurableAI_TIMEOUT_MStimeout. No functional change needed; adding a regression test to lock in the behavior and closing this as a duplicate.src/utils/resilience.ts) and wired up an independent retry + circuit breaker instance for the AI client (bounded to 3 attempts per call so it still fails predictably rather than retrying forever).src/stellar/resilience.tskeeps its exact public API, now backed by the shared factory — no behavior change for Stellar.response.json()was cast straight toAiQuizResponsewith no runtime check, so a malformed body could crash instead of falling back gracefully. Added a zod schema and replaced the unsafe cast withsafeParse, throwing a descriptive error on shape mismatch.Both AI failure modes are still caught by
quizService.generateQuiz's existing fallback to placeholder questions, so no caller changes were needed.Test plan
npm run typecheck— cleannpm run lint— clean (only pre-existing warnings elsewhere)resilience.test.tsandstellar/client.tsbehavior unchanged after the factory extractionmainbefore these changes tooCloses #139, Closes #140, Closes #141, Closes #142