Add POST /v1/repo/{owner}/{name}/refresh for user-triggered refresh#7
Add POST /v1/repo/{owner}/{name}/refresh for user-triggered refresh#7rainxchzed merged 3 commits intomainfrom
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdds POST /v1/repo/{owner}/{name}/refresh plus a RepoRefreshCoordinator enforcing per-repo 30s cooldowns and a rolling 1000/hour budget; coordinator calls GitHubSearchClient.refreshRepo, persists via provided persist function, and routes return refreshed repo data with Cache-Control: no-store or mapped error statuses. ChangesRepository Refresh Feature
Sequence DiagramsequenceDiagram
participant Client
participant Route as RepoRefreshRoute
participant Coord as RepoRefreshCoordinator
participant GitHub as GitHubSearchClient
participant DB as RepoRepository
Client->>Route: POST /v1/repo/{owner}/{name}/refresh\n(optional X-GitHub-Token)
Route->>Route: validate owner/name\nextract token
Route->>Coord: refresh(owner, name, userToken)
Coord->>Coord: check per-repo cooldown
Coord->>Coord: rotate/check hourly budget
alt Cooldown active
Coord-->>Route: Outcome.Cooldown(retryAfter)
else Budget exhausted
Coord-->>Route: Outcome.BudgetExhausted(retryAfter)
else Allowed
Coord->>GitHub: refreshRepo(fullName, userToken)
GitHub-->>Coord: RefreshResult
alt Success with release
Coord->>DB: persist(RepoWithRelease)
DB-->>Coord: persisted
Coord-->>Route: Outcome.Ok(repo, metadataPersisted=true)
else Success without release
Coord-->>Route: Outcome.Ok(repo, metadataPersisted=false)
else NotFound / Archived / Error
Coord-->>Route: Outcome.NotFound / Archived / UpstreamError
end
end
Route->>Client: 200 + repo data + Cache-Control: no-store\nor mapped error status (429/404/410/502/etc.)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main/kotlin/zed/rainxch/githubstore/ingest/RepoRefreshCoordinator.kt`:
- Around line 63-86: The cooldown check in refresh is subject to a TOCTOU race
because lastRefreshAt is read and then later updated; change the logic to use
lastRefreshAt.compute(key) (or computeIfAbsent/compute) to atomically inspect
the existing Instant and, if allowed, set the new now timestamp inside the same
compute call so concurrent coroutines cannot both pass the cooldown gate; ensure
the compute returns the stored Instant for future calls, and adjust flow so the
timestamp claim happens before the budget gate (not after) so that a
budget-exhausted request still occupies the cooldown slot.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2566ff2d-0b0c-4a8e-8bdf-5349c4b26626
📒 Files selected for processing (7)
CLAUDE.mdsrc/main/kotlin/zed/rainxch/githubstore/AppModule.ktsrc/main/kotlin/zed/rainxch/githubstore/ingest/RepoRefreshCoordinator.ktsrc/main/kotlin/zed/rainxch/githubstore/routes/RepoRefreshRoutes.ktsrc/main/kotlin/zed/rainxch/githubstore/routes/RepoRoutes.ktsrc/main/kotlin/zed/rainxch/githubstore/routes/Routing.ktsrc/test/kotlin/zed/rainxch/githubstore/ingest/RepoRefreshCoordinatorTest.kt
Summary
User-driven refresh button on the details screen. Refreshes one repo on demand: re-fetches from GitHub, upserts Postgres + Meili, returns the live data. Counterpart to the cache-first GET path.
Endpoint
POST /v1/repo/{owner}/{name}/refreshX-GitHub-Token(same as the GET).GET /v1/repo/{owner}/{name}.Cache-Control: no-store-- never CDN-cached.searchrate-limit bucket (240/min/key).POST chosen over GET-with-
?refresh=trueso Cloudflare doesn't cache the trigger and the state-changing nature is reflected in the verb.Safety nets
(owner, name)regardless of callerCooldown stamp is recorded before the upstream call returns, so concurrent refreshes for the same repo see the stamp and back off rather than racing.
Outcomes -> HTTP status
RepoResponsefrom DBRepoResponsefrom GitHub fetchRetry-After{"error":"cooldown","message":"Try again in Ns"}Retry-After{"error":"budget_exhausted",...}{"error":"not_found"}{"error":"archived"}{"error":"github_unreachable"}Implementation notes
RepoRefreshCoordinator(iningest/) is a thin gate-and-dispatch layer. It acceptsrefreshUpstream+persistFnas lambdas instead of a directGitHubSearchClientreference so unit tests don't need a real Ktor HttpClient. AppModule wires both tosearchClient::refreshRepoandsearchClient::persist.internalbecauseGitHubSearchClient.RefreshResultandRepoWithReleaseare internal data classes. All callers live in the same module.toMetadataOnlyResponse(nowinternalinstead ofprivate) for the no-usable-release case.What this does NOT do
Test plan
./gradlew test-- 9 new coordinator tests + existing suites all green.curl -X POST https://api.github-store.org/v1/repo/sindresorhus/refined-github/refresh-- expect 200 + fresh data.Summary by CodeRabbit
New Features
Documentation
Tests