Skip to content

Fix dangling retry processor and add resilience/validation to AI client - #182

Merged
DeFiVC merged 4 commits into
ChainLearnOfficial:mainfrom
balisdev:fix/retry-processor-and-ai-client-resilience
Jul 28, 2026
Merged

Fix dangling retry processor and add resilience/validation to AI client#182
DeFiVC merged 4 commits into
ChainLearnOfficial:mainfrom
balisdev:fix/retry-processor-and-ai-client-resilience

Conversation

@balisdev

Copy link
Copy Markdown
Contributor

Summary

  • startRetryProcessor #139startRetryProcessor fires 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 by startRetryProcessor()) 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.
  • AI #140 — AI client fetch has no timeout: this is a duplicate of AI client fetch has no timeout #74, which was already fixed and merged (1535a22, PR fix(quizzes): add configurable timeout to AI service fetch #85) — the fetch call already has an AbortController with a configurable AI_TIMEOUT_MS timeout. No functional change needed; adding a regression test to lock in the behavior and closing this as a duplicate.
  • AI #141 — AI client has no resilience patterns: unlike Stellar calls, the AI service client had no retry or circuit breaker, so a persistent outage hung every quiz request for the full timeout duration. Extracted the existing Stellar retry/circuit-breaker logic into a reusable factory (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.ts keeps its exact public API, now backed by the shared factory — no behavior change for Stellar.
  • AI #142 — AI client does not validate response shape: response.json() was cast straight to AiQuizResponse with no runtime check, so a malformed body could crash instead of falling back gracefully. Added a zod schema and replaced the unsafe cast with safeParse, 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 — clean
  • npm run lint — clean (only pre-existing warnings elsewhere)
  • New regression test for the retry-processor restart race (fails on the old code, passes with the fix)
  • New AI client test suite covering: valid response parsing, malformed response rejection (AI #142), retry on transient failure and circuit breaker opening (AI #141), and timeout behavior (AI #140 regression)
  • Existing resilience.test.ts and stellar/client.ts behavior unchanged after the factory extraction
  • Full suite run; pre-existing unrelated failures (no local Redis, Fastify version mismatch, Stellar account validation) confirmed present on main before these changes too

Closes #139, Closes #140, Closes #141, Closes #142

balisdev added 3 commits July 28, 2026 18:21
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
@drips-wave

drips-wave Bot commented Jul 28, 2026

Copy link
Copy Markdown

@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! 🚀

Learn more about application limits

@DeFiVC
DeFiVC merged commit bd57a2b into ChainLearnOfficial:main Jul 28, 2026
@grantfox-oss grantfox-oss Bot mentioned this pull request Jul 28, 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.

AI AI AI startRetryProcessor

2 participants