Skip to content

fix(examples): keep the default Course loadable under browser override spam - #235

Merged
ttbombadil merged 1 commit into
mainfrom
fix/default-course-load-priority
Oct 9, 2026
Merged

ttbombadil merged 1 commit into
mainfrom
fix/default-course-load-priority

Conversation

@ttbombadil

Copy link
Copy Markdown
Collaborator

Root cause (verified)

Loads of the operator's default Course and browser overrides shared every bound of ExamplesLoadController and ExamplesCache. Any client can trigger overrides with arbitrary repositories and refs, also cross-site via GET.

  • Load-start budget (UNOSIM_EXAMPLES_GLOBAL_LOAD_STARTS_PER_MINUTE, 20/min):
    • a start is recorded before the GitHub budget check, so failing overrides count too;
    • a single client sending 20 overrides per minute made every fresh default load fail with RATE_LIMITED;
    • resolveTutorContent needs a fresh default for Tutor requests without a loaded example, so new Tutor sessions failed with 400 for all learners.
  • Load slots and queue (4 + 32): slow overrides fill them, and the default load gets LOAD_CAPACITY_EXCEEDED.
  • Source LRU (UNOSIM_EXAMPLES_MAX_SOURCES): successful overrides evict the default source.
  • Outbound fetch cap: default fetches queue FIFO behind override fetches.

The GitHub quota reserve from #227 only covered the GitHub REST quota, not these limiters.

Proof on the old code: all four cases in tests/server/examples/default-source-protection.test.ts were red:

  • RATE_LIMITED after sequential override spam;
  • LOAD_CAPACITY_EXCEEDED with occupied override slots;
  • default source evicted;
  • default fetch still blocked after 200 ms.

Fix

  • ExamplesLoadController:
    • separate load slots, queues and load-start budgets for default and override, each of the configured size, so the semantics per pool are unchanged;
    • a failing override spends only the override budget;
    • runLoad(operation, signal, priority) keeps override as its default; SourceProvider passes context.sourcePriority ?? "default", like the GitHub budget.
  • Outbound reserve: override loads may hold at most three quarters of UNOSIM_EXAMPLES_MAX_OUTBOUND_FETCHES (16 → 12); the rest stays free for the default. The priority reaches the fetches through AsyncLocalStorage, without API changes in loader, resolver or fetcher. Fetches outside a load are not limited by the share.
  • ExamplesCache: protectedSourceKeys. ExamplesRepository protects the configured default source; LRU eviction skips it.
  • Docs: ExternalExamples SSOT, SECURITY.md.

Tests

tests/server/examples/default-source-protection.test.ts, with real SourceProvider, ExamplesLoadController and ExamplesCache:

  1. 30 sequential failing overrides (incl. RATE_LIMITED), then a fresh default load succeeds.
  2. Overrides occupy all slots and queue positions (the next override gets LOAD_CAPACITY_EXCEEDED); the default still loads.
  3. Overrides fill the source cache; the default source stays, and the next default load is a cache hit without a new ref lookup.
  4. Override loads saturate their outbound share (maximum below the cap); a default fetch still runs.

Existing tests are unchanged, including the controller test that uses the full outbound cap outside a load. npm run check, npm run test:unit, npm run check:docs, Knip, git diff --check, the full ./run-tests.sh (also after rebasing onto #234) and the pre-push gate are green.

🤖 Generated with Claude Code

…e spam

Default and browser-override loads shared one load-start budget, one load
semaphore with its queue, the outbound fetch cap and the source LRU. One
client sending 20 failing overrides a minute made every fresh default load
fail with RATE_LIMITED (new Tutor sessions without an example failed for
all learners); slow overrides could fill the queue and successful ones
could evict the default source.

- Separate load slots, queues and load-start budgets per priority, each of
  the configured size; a failing override only spends override budget.
- Override loads may hold at most three quarters of the outbound fetch cap;
  the priority reaches the fetches through AsyncLocalStorage.
- The source cache never evicts the configured default source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ttbombadil
ttbombadil merged commit cabbc5c into main Oct 9, 2026
6 checks passed
@ttbombadil
ttbombadil deleted the fix/default-course-load-priority branch October 9, 2026 10:49
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.

1 participant