Skip to content

fix: skip local quota spend on DNS/transport failures - #7

Merged
djbclark merged 1 commit into
mainfrom
fix/quota-skip-transport-ledger-charges
Aug 1, 2026
Merged

fix: skip local quota spend on DNS/transport failures#7
djbclark merged 1 commit into
mainfrom
fix/quota-skip-transport-ledger-charges

Conversation

@djbclark

@djbclark djbclark commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • Local YouTube quota ledger no longer charges for failures with no HTTP response (DNS outages, connection errors, pre-header timeouts).
  • Usage events are still recorded as transport_error with units=0 for forensics.
  • HTTP 4xx/5xx from Google remain charged (Google counts those).

Reproduces/fixes the 2026-08-01 incident where ~200 OAuth DNS failures burned the local day budget.

Test plan

  • python3 -m unittest tests.test_youtube_quota
  • Deploy to ~/.superbrain-server and restart playlist sync when idle

Made with Cursor

Summary by CodeRabbit

  • Bug Fixes
    • Network failures such as DNS, connection, TLS, and timeout errors are now correctly identified as transport errors.
    • Failed requests without an HTTP response no longer consume API quota.
    • HTTP error responses continue to be charged according to published quota costs.
    • Failure records now accurately distinguish network outages from HTTP service errors.

Connection and name-resolution errors never reach Google, so recording
them as playlistItems.insert spend emptied the local ledger during the
2026-08-01 outage. Keep the usage event for forensics at 0 units.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The quota module now detects wrapped DNS, connection, TLS, and pre-response timeout failures. It records these failures with zero quota units and excludes them from ledger updates. Tests cover classification, HTTP 503 distinction, and repeated DNS failures.

Changes

YouTube transport failure accounting

Layer / File(s) Summary
Transport failure classification
backend/core/youtube_quota.py, backend/tests/test_youtube_quota.py
The module traverses wrapped exceptions and classifies eligible pre-response failures as transport_error. Tests cover DNS failures and distinguish them from HTTP 503 responses.
Uncharged usage recording and ledger handling
backend/core/youtube_quota.py, backend/tests/test_youtube_quota.py
Transport failures record zero units and skip quota ledger updates. HTTP failures remain charged when quota costs are known. End-to-end tests verify repeated DNS failures consume no quota units.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant YouTubeRequest
  participant classify_result
  participant UsageRecording
  participant QuotaLedger
  YouTubeRequest->>classify_result: raised DNS or other transport exception
  classify_result->>classify_result: traverse exception chain
  classify_result-->>UsageRecording: transport_error without HTTP status
  UsageRecording->>UsageRecording: record zero quota units
  UsageRecording-->>QuotaLedger: skip ledger update
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: avoiding local quota charges for DNS and transport failures.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/quota-skip-transport-ledger-charges

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
backend/core/youtube_quota.py (1)

154-170: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the dead requests.exceptions.* extension; a bare OSError check already covers it.

requests.exceptions.RequestException subclasses IOError (an alias of OSError in Python 3), and ConnectionError, Timeout, ConnectTimeout, ReadTimeout, and SSLError all derive from RequestException. Built-in ConnectionError and TimeoutError are also OSError subclasses. So transport_types = (ConnectionError, TimeoutError, OSError) already reduces to OSError, and the local import requests plus the tuple extension at lines 162-168 add no new matches — isinstance(exc, OSError) alone already catches every listed requests.exceptions.* type.

This dead block is also the source of all three static-analysis findings (RUF005 concatenation, S110 try-except-pass, BLE001 blind except). Removing it fixes all three at once. Keep the name-based fallback at lines 174-183: urllib3-native classes like MaxRetryError and NameResolutionError derive from urllib3's HTTPError(Exception), not OSError, so that part is still load-bearing.

♻️ Proposed simplification
-    transport_types = (
-        ConnectionError,
-        TimeoutError,
-        OSError,
-    )
-    try:
-        import requests
-
-        transport_types = transport_types + (
-            requests.exceptions.ConnectionError,
-            requests.exceptions.Timeout,
-            requests.exceptions.ConnectTimeout,
-            requests.exceptions.ReadTimeout,
-            requests.exceptions.SSLError,
-        )
-    except Exception:
-        pass
+    # ConnectionError/TimeoutError are OSError subclasses, and every
+    # requests.exceptions.* transport error subclasses OSError via
+    # RequestException(IOError), so a single OSError check covers them all.
+    transport_types = (OSError,)

Since this depends on the exception hierarchy of requests and CPython's OSError/IOError aliasing, please confirm this holds for the requests version pinned in this repository before merging the simplification.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/core/youtube_quota.py` around lines 154 - 170, Remove the local
requests import, broad exception handler, and transport_types tuple extension in
the transport exception setup. Keep transport_types as the built-in (effectively
OSError) types and preserve the existing name-based fallback for urllib3-native
exceptions such as MaxRetryError and NameResolutionError. Confirm the pinned
requests version retains the stated RequestException hierarchy before applying
the simplification.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@backend/core/youtube_quota.py`:
- Around line 154-170: Remove the local requests import, broad exception
handler, and transport_types tuple extension in the transport exception setup.
Keep transport_types as the built-in (effectively OSError) types and preserve
the existing name-based fallback for urllib3-native exceptions such as
MaxRetryError and NameResolutionError. Confirm the pinned requests version
retains the stated RequestException hierarchy before applying the
simplification.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 254a0b59-cee8-4b63-9d02-752d6120024e

📥 Commits

Reviewing files that changed from the base of the PR and between 30d5613 and f8e764b.

📒 Files selected for processing (2)
  • backend/core/youtube_quota.py
  • backend/tests/test_youtube_quota.py

@djbclark
djbclark merged commit d58b41e into main Aug 1, 2026
1 check passed
@djbclark
djbclark deleted the fix/quota-skip-transport-ledger-charges branch August 1, 2026 12:26
djbclark added a commit that referenced this pull request Aug 1, 2026
Co-authored-by: Cursor <cursoragent@cursor.com>
djbclark added a commit that referenced this pull request Aug 1, 2026
…ate HTTP calls, gate PostDetail taxonomy behind connectivity

Port reviewed fixes from upstream PR #7 (draft/android-category-sync @ 5975a02):

- PostDetailScreen: setEditedCategory(cat.id) not cat.name — upstream backend
  stores categories by id (lowercase slug), not display name
- PostDetailScreen: gate taxonomy fetch behind testConnection() to avoid
  wasted 404 round-trip on upstream servers without GET /taxonomy
- HomeScreen: loadCategories() accepts optional pre-fetched taxonomy param;
  post-sync reload reuses cached payload instead of redundant HTTP call
- api.ts: getTaxonomy() uses shared TaxonomyPayload type from taxonomySupport.ts

TypeScript passes. No scope change — same 8 superbrain-app/ files.
djbclark added a commit that referenced this pull request Aug 3, 2026
Port reviewed fixes from upstream PR #7 (draft/android-category-sync @
878a1dc, d629ccf, 3acb694) that landed on the draft branch after
c195a38 already ported its earlier fixes. draft stays on the old
upstream base to keep that PR a clean Android-only diff, so these
never reached main:

- database.py: get_posts_since() now orders by (updated_at, shortcode)
  and fetches limit+1/trims to compute real has_more — updated_at alone
  isn't a stable OFFSET cursor when timestamps tie, which could skip or
  duplicate rows across pages.
- api.py /sync: has_more/next_offset now come from the db layer instead
  of the len(results) == limit heuristic.
- syncService.ts deltaSync: hard page cap + non-advancing-cursor
  detection so an offset-blind server can't spin the loop forever, and
  the sync cursor only advances when pagination genuinely completed
  (not on page-cap/non-advancing/fetch-failure early exit) — otherwise
  the next sync would silently skip whatever changes existed past the
  stop point.
- api.ts syncPosts: distinguishes a transient fetch failure (`failed:
  true`) from a legitimate empty/final page; getTaxonomy()'s response
  type now matches TaxonomyPayload's optional precedence/guidance.
- HomeScreen.tsx: taxonomyRef (component-level, reset at the top of
  every loadPosts call) replaced with a per-call local plus a promise
  shared between loadCategories/loadPosts at bootstrap — fixes both the
  redundant double /taxonomy fetch and the race between overlapping
  loadPosts calls (focus listener vs. poll refresh) stomping each
  other's cache.

TypeScript passes; backend/tests/test_database_concurrency.py updated
for the new (results, has_more) return shape.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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