Skip to content

fix(sync): stop restart re-upload storm (ignore node_modules/.git; rate-limit Drive by query)#11

Merged
arumes31 merged 1 commit into
mainfrom
v0.1.3
Jun 19, 2026
Merged

fix(sync): stop restart re-upload storm (ignore node_modules/.git; rate-limit Drive by query)#11
arumes31 merged 1 commit into
mainfrom
v0.1.3

Conversation

@arumes31

@arumes31 arumes31 commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Problem

On restart the client appeared to re-upload everything. Investigation of the live DB/logs showed already-synced files are correctly skipped (zero overlap between the 1,612 synced rows and the upload log). The real issue: a large tree (node_modules, 18,620 files vs 1,612 synced) never finished syncing, so each restart re-attempted the unsynced backlog — which never completes because of Google Drive quota errors (logs full of Quota exceeded / rate limit exceeded).

Root causes & fixes

  1. node_modules synced; nested .git synced. Built-in ignores only matched .git/.gcrypt at the sync root, and node_modules wasn't ignored at all. Since EffectiveIgnorePatterns() returns a pair's custom patterns instead of the defaults, the fix had to go in the non-overridable built-in matcher:
    • Built-ins now match on any path component.git, .gcrypt, .svn, .hg, node_modules are skipped at any depth, on every pair.
    • DefaultIgnorePatterns also gains common build/cache trees (vendor, dist, build, target, bin, obj, __pycache__, .venv, .idea, .vscode, …) for pairs without custom patterns.
  2. Rate limiter counted operations, not Drive API queries. Each upload issues several queries (dedup search + encrypted folder-chain creation + upload), so the real query rate was several× the cap and blew the per-minute per-user quota. Now enforced at the HTTP transport (rateLimitedRoundTripper) so every request — across all pairs, upload chunks and token refreshes — counts; process-global since the quota is per-user. Default 100 q/s (~6,000/min, well under ~12,000/min).

Note: already-synced node_modules/.git files become ignored and are removed from Drive (trashed, recoverable) on the next scan, as intended.

Verified locally

build, go test ./..., golangci-lint (0), gosec (0), govulncheck (clean). Added a regression test for nested node_modules/.git/.svn/.hg ignoring.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added rate limiting for Google Drive API requests to control request throughput.
  • Bug Fixes

    • Expanded default ignore patterns to include additional build, cache, and editor artifacts.
    • Improved detection of version control and metadata directories at any depth in file paths.

… any depth + rate-limit Drive API by query

Two root causes made a large tree (e.g. node_modules) never finish syncing, so
every restart re-attempted the whole unsynced backlog (looking like "everything
re-uploads"; already-synced files were in fact correctly skipped):

1. Ignore matching only skipped .git/.gcrypt at the sync ROOT, and node_modules
   wasn't ignored at all — so 17k+ dependency/VCS files flooded the queue.
   - Built-in ignores are now matched on ANY path component: .git, .gcrypt,
     .svn, .hg, node_modules are skipped at any depth and on every pair
     (EffectiveIgnorePatterns returns a pair's custom patterns *instead of* the
     defaults, so this had to live in the non-overridable built-in matcher).
   - DefaultIgnorePatterns also gains the common build/cache trees (vendor,
     dist, build, target, bin, obj, __pycache__, .venv, .idea, .vscode, ...)
     for pairs without custom patterns.

2. The rate limiter counted operations, not Drive API queries — but each upload
   issues several queries (dedup search + encrypted folder-chain creation +
   upload), so the real query rate was several times the cap and blew Google's
   per-minute per-user quota (logs full of "Quota exceeded"/"rate limit").
   - Enforce the cap at the HTTP transport (rateLimitedRoundTripper), so EVERY
     request across all pairs, upload chunks and token refreshes counts —
     process-global since the quota is per-user. Default 100 q/s (~6,000/min,
     well under the ~12,000/min limit, with headroom for retries).

Note: already-synced node_modules/.git files become "ignored" and are removed
from Drive (trashed, recoverable) on the next scan, as intended.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR makes two independent improvements: it adds a process-global token-bucket rate limiter that gates all Drive HTTP requests (including OAuth refresh) through a custom RoundTripper wired into NewClient, and it expands ignore logic by making built-in never-sync directories (.git, .svn, .hg, node_modules) match at any path depth, while also enlarging the default overridable ignore pattern list.

Changes

Drive API Rate Limiting

Layer / File(s) Summary
Rate limiter infrastructure
internal/drive/client.go
Adds driveAPIQueriesPerSec constant, a shared apiLimiter, an apiRateLimiter context-aware token-wait function, and a rateLimitedRoundTripper struct that blocks each HTTP round-trip until a token is granted.
NewClient transport wiring
internal/drive/client.go
Wraps the OAuth refresh base transport with rateLimitedRoundTripper in NewClient and updates the timeout comment to reflect parallel-upload semantics instead of serialized sync.

Ignore Pattern Expansion

Layer / File(s) Summary
Built-in ignore at any path depth
internal/sync/ignore.go, internal/sync/ignore_test.go
Replaces root-/prefix-limited logic in matchBuiltIn with a per-component scan over a neverSyncDirs set (.gcrypt, .git, .svn, .hg, node_modules), so any path segment triggers the ignore regardless of depth. Tests add nested node_modules and VCS paths.
Default overridable ignore pattern list
internal/config/config.go, internal/config/config_test.go
Extends DefaultIgnorePatterns with vendor, dist, build, target, __pycache__, .venv, and other build/cache artifacts. Test switches from ordered slice comparison to set-membership assertions covering the expanded list.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • arumes31/gcrypt#8: Modifies internal/drive/client.go's NewClient setup to change Drive HTTP client construction and concurrency/rate-limiting assumptions, directly overlapping with this PR's rateLimitedRoundTripper wiring in the same function.

Poem

🐇 Hop, hop, the rabbit declares,
No .git folder leaks through the layers!
Rate tokens flow, one by one,
node_modules blocked before it's begun.
From root to depth, the ignores now spread —
Every stray artifact quietly put to bed! 🌿

🚥 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 accurately captures the two primary fixes: stopping restart re-upload issues by ignoring node_modules/.git and implementing rate-limiting for Drive API queries.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 v0.1.3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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 (4)
internal/drive/client.go (1)

47-52: 💤 Low value

Consider adding a cleanup mechanism for the ticker.

The ticker created here runs indefinitely without a stop mechanism. While this is acceptable for the singleton apiLimiter, it prevents clean shutdown and could complicate testing scenarios where the rate limiter might need to be recreated or stopped.

♻️ Optional: Add Stop method for graceful cleanup
 type apiRateLimiter struct{ ticker *time.Ticker }

 func newAPIRateLimiter(qps int) *apiRateLimiter {
 	if qps < 1 {
 		qps = 1
 	}
 	return &apiRateLimiter{ticker: time.NewTicker(time.Second / time.Duration(qps))}
 }

+// Stop stops the rate limiter's ticker. Call this only during shutdown or tests.
+func (l *apiRateLimiter) Stop() {
+	l.ticker.Stop()
+}
+
 // wait blocks until the next token is available or ctx is cancelled.
 func (l *apiRateLimiter) wait(ctx context.Context) error {
🤖 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 `@internal/drive/client.go` around lines 47 - 52, The ticker in the
apiRateLimiter struct created by newAPIRateLimiter function runs indefinitely
without a stop mechanism, which prevents graceful shutdown and complicates
testing. Add a Stop method to the apiRateLimiter type that calls the Stop()
method on the ticker field to allow callers to cleanly shut down the rate
limiter and reclaim resources when needed.
internal/sync/ignore.go (2)

96-101: 💤 Low value

Consider clarifying that files matching these names are also ignored.

The comment states "Directory names that are never worth syncing," but the implementation ignores paths where ANY component matches these names, including files (e.g., a file literally named node_modules at the root). This is likely acceptable since such filenames are unusual, but the comment could be more precise about this behavior to prevent future confusion.

🤖 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 `@internal/sync/ignore.go` around lines 96 - 101, Update the comment block
starting with "Directory names that are never worth syncing" to clarify that the
implementation ignores not just directory names but any path component
(including files) that match these names, since the behavior applies to ANY
component of the file path at any depth. Modify the comment to be more precise
about this behavior to prevent future confusion about whether files with these
names are also ignored.

264-279: Consolidate overlapping ignore patterns between sync and config packages.

The DefaultIgnorePatterns() function in internal/sync/ignore.go and its counterpart in internal/config/config.go share 9 overlapping patterns: Thumbs.db, .DS_Store, desktop.ini, *.tmp, ~$*, *.swp, *~, .~lock.*#, *.lock. Although the functions serve different purposes—sync patterns are always applied (including .gcrypt/, .git/) while config patterns are overridable defaults—consolidating the shared artifact patterns would reduce maintenance burden. Consider extracting common patterns into a shared constant or having one function compose the other.

🤖 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 `@internal/sync/ignore.go` around lines 264 - 279, The DefaultIgnorePatterns()
function in sync/ignore.go shares 9 overlapping patterns with a counterpart
function in the config package (Thumbs.db, .DS_Store, desktop.ini, *.tmp, ~$*,
*.swp, *~, .~lock.*#, *.lock). To fix this, extract these 9 common artifact
patterns into a shared constant in a common location accessible to both
packages. Then modify DefaultIgnorePatterns() to build its return value by
combining this shared constant with sync-specific patterns (.gcrypt/,
.gcrypt-ignore, .git/), and update the config package counterpart to use the
same shared constant for its defaults. This ensures a single source of truth for
artifact patterns while preserving the distinct purposes of each function.
internal/config/config.go (1)

144-159: 🏗️ Heavy lift

Function name collision with sync.DefaultIgnorePatterns creates maintainability risk.

Both internal/config/config.go and internal/sync/ignore.go export a function named DefaultIgnorePatterns() with overlapping pattern content. This creates confusion about:

  1. Which version should be called in different contexts
  2. Which version to update when adding new patterns
  3. Why patterns are duplicated across both

The comment on line 145 acknowledges the separate built-in ignores but doesn't explain why two functions with the same name and overlapping patterns should coexist. Consider renaming one (e.g., DefaultConfigIgnorePatterns) or consolidating them to prevent future maintainability issues.

🤖 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 `@internal/config/config.go` around lines 144 - 159, The DefaultIgnorePatterns
function in internal/config/config.go has a name collision with another
DefaultIgnorePatterns function in internal/sync/ignore.go, causing confusion
about which function to use and maintain. Rename the DefaultIgnorePatterns
function in internal/config/config.go to a more specific name like
DefaultConfigIgnorePatterns to differentiate it from the sync package version
and clarify its purpose in the config context.
🤖 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 `@internal/config/config.go`:
- Around line 144-159: The DefaultIgnorePatterns function in
internal/config/config.go has a name collision with another
DefaultIgnorePatterns function in internal/sync/ignore.go, causing confusion
about which function to use and maintain. Rename the DefaultIgnorePatterns
function in internal/config/config.go to a more specific name like
DefaultConfigIgnorePatterns to differentiate it from the sync package version
and clarify its purpose in the config context.

In `@internal/drive/client.go`:
- Around line 47-52: The ticker in the apiRateLimiter struct created by
newAPIRateLimiter function runs indefinitely without a stop mechanism, which
prevents graceful shutdown and complicates testing. Add a Stop method to the
apiRateLimiter type that calls the Stop() method on the ticker field to allow
callers to cleanly shut down the rate limiter and reclaim resources when needed.

In `@internal/sync/ignore.go`:
- Around line 96-101: Update the comment block starting with "Directory names
that are never worth syncing" to clarify that the implementation ignores not
just directory names but any path component (including files) that match these
names, since the behavior applies to ANY component of the file path at any
depth. Modify the comment to be more precise about this behavior to prevent
future confusion about whether files with these names are also ignored.
- Around line 264-279: The DefaultIgnorePatterns() function in sync/ignore.go
shares 9 overlapping patterns with a counterpart function in the config package
(Thumbs.db, .DS_Store, desktop.ini, *.tmp, ~$*, *.swp, *~, .~lock.*#, *.lock).
To fix this, extract these 9 common artifact patterns into a shared constant in
a common location accessible to both packages. Then modify
DefaultIgnorePatterns() to build its return value by combining this shared
constant with sync-specific patterns (.gcrypt/, .gcrypt-ignore, .git/), and
update the config package counterpart to use the same shared constant for its
defaults. This ensures a single source of truth for artifact patterns while
preserving the distinct purposes of each function.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f61f92c6-85b5-4a3b-b89c-c3629deeb9a3

📥 Commits

Reviewing files that changed from the base of the PR and between 5d488ad and fbe8838.

📒 Files selected for processing (5)
  • internal/config/config.go
  • internal/config/config_test.go
  • internal/drive/client.go
  • internal/sync/ignore.go
  • internal/sync/ignore_test.go

@arumes31
arumes31 merged commit fbe8838 into main Jun 19, 2026
6 checks passed
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