From 1b217082618fdf6343931cd27c18b8bd0fd4e00b Mon Sep 17 00:00:00 2001 From: hugocasa Date: Thu, 3 Sep 2026 12:29:15 +0200 Subject: [PATCH] fix: bound the credential maintenance pass and gate the gitlab picker on a license Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01C1xHmkxuxYb1GYvth1BS75 --- ...c244505996be71d9caec9c6d69e32a9c3d2c.json} | 4 +-- backend/ee-repo-ref.txt | 2 +- backend/src/monitor.rs | 32 ++++++++++++++++++- .../lib/components/GitLabIntegration.svelte | 5 ++- .../git_sync/GitSyncRepositoryCard.svelte | 5 ++- 5 files changed, 42 insertions(+), 6 deletions(-) rename backend/.sqlx/{query-ddd41de79b23b0436bbc7997751c90d7d82bd20ea30f1dc08f2e2066fb8d6f4b.json => query-2184f787e6dffc76bfc28f2f2cefc244505996be71d9caec9c6d69e32a9c3d2c.json} (66%) diff --git a/backend/.sqlx/query-ddd41de79b23b0436bbc7997751c90d7d82bd20ea30f1dc08f2e2066fb8d6f4b.json b/backend/.sqlx/query-2184f787e6dffc76bfc28f2f2cefc244505996be71d9caec9c6d69e32a9c3d2c.json similarity index 66% rename from backend/.sqlx/query-ddd41de79b23b0436bbc7997751c90d7d82bd20ea30f1dc08f2e2066fb8d6f4b.json rename to backend/.sqlx/query-2184f787e6dffc76bfc28f2f2cefc244505996be71d9caec9c6d69e32a9c3d2c.json index 02c50b1e45..26098b3c09 100644 --- a/backend/.sqlx/query-ddd41de79b23b0436bbc7997751c90d7d82bd20ea30f1dc08f2e2066fb8d6f4b.json +++ b/backend/.sqlx/query-2184f787e6dffc76bfc28f2f2cefc244505996be71d9caec9c6d69e32a9c3d2c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT ws.workspace_id, ws.git_sync\n FROM workspace_settings ws\n JOIN workspace w ON w.id = ws.workspace_id\n WHERE NOT w.deleted\n AND ws.git_sync IS NOT NULL\n AND jsonb_typeof(ws.git_sync->'repositories') = 'array'", + "query": "SELECT ws.workspace_id, ws.git_sync\n FROM workspace_settings ws\n JOIN workspace w ON w.id = ws.workspace_id\n WHERE NOT w.deleted\n AND ws.git_sync IS NOT NULL\n AND jsonb_typeof(ws.git_sync->'repositories') = 'array'\n ORDER BY (\n SELECT min((elem->'credential'->>'checked_at')::bigint)\n FROM jsonb_array_elements(ws.git_sync->'repositories') AS elem\n ) ASC NULLS FIRST", "describe": { "columns": [ { @@ -22,5 +22,5 @@ true ] }, - "hash": "ddd41de79b23b0436bbc7997751c90d7d82bd20ea30f1dc08f2e2066fb8d6f4b" + "hash": "2184f787e6dffc76bfc28f2f2cefc244505996be71d9caec9c6d69e32a9c3d2c" } diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 5787d4f08d..584cff2458 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -03ef62314a7aa2a1ff4da23459a8fbf3753f495b +e38325c9bfd083b3b3308ee5ca96edbcbe308ae3 diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index 1c68e275ca..b3bc5c6357 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -4618,6 +4618,17 @@ const AUTO_PULL_POLL_SLACK_S: i64 = 30; #[cfg(all(feature = "enterprise", feature = "private"))] const GIT_CREDENTIAL_LOCK_ID: i64 = 737_483_923; +/// Wall-clock budget for one credential maintenance pass, spent between +/// repositories rather than inside one. +/// +/// `monitor_db` cancels every future in its `join!` at 600s, and an unreachable +/// GitLab costs a repository up to the client's 20s timeout, so enough of them +/// would take the whole pass — and the other maintenance futures with it. A +/// rotation must never be cancelled between GitLab issuing a token and Windmill +/// storing it, so the pass stops at a repository boundary instead and the +/// least-recently-checked ordering brings the rest along on the next tick. +const GIT_CREDENTIAL_PASS_BUDGET: Duration = Duration::from_secs(180); + /// Refresh every git-sync repository's credential status and rotate the ones near /// expiry, so a token dies visibly (and usually not at all) rather than taking /// sync down on its expiry date. @@ -4670,17 +4681,27 @@ async fn maintain_git_credentials_inner(db: &Pool) -> error::Result<() // Same deleted/archived exclusion as the auto-pull poller: a dead workspace's // settings row survives, and rotating a token for one would be pure damage. + // Least-recently-checked first, so a pass that runs out of budget resumes + // where it stopped instead of re-checking the same head of the list forever. + // A repository with no recorded credential sorts first and costs only a + // database round trip: it has no token to introspect. let rows = sqlx::query!( r#"SELECT ws.workspace_id, ws.git_sync FROM workspace_settings ws JOIN workspace w ON w.id = ws.workspace_id WHERE NOT w.deleted AND ws.git_sync IS NOT NULL - AND jsonb_typeof(ws.git_sync->'repositories') = 'array'"# + AND jsonb_typeof(ws.git_sync->'repositories') = 'array' + ORDER BY ( + SELECT min((elem->'credential'->>'checked_at')::bigint) + FROM jsonb_array_elements(ws.git_sync->'repositories') AS elem + ) ASC NULLS FIRST"# ) .fetch_all(db) .await?; + let started = Instant::now(); + let mut skipped = 0usize; for row in rows { let Some(git_sync) = row.git_sync else { continue; @@ -4697,6 +4718,10 @@ async fn maintain_git_credentials_inner(db: &Pool) -> error::Result<() }; for repo in &settings.repositories { + if started.elapsed() >= GIT_CREDENTIAL_PASS_BUDGET { + skipped += 1; + continue; + } let path = &repo.git_repo_resource_path; // This refreshes and records the status on every repository it looks at, // rotating only the ones near expiry, so it is the whole maintenance pass @@ -4715,6 +4740,11 @@ async fn maintain_git_credentials_inner(db: &Pool) -> error::Result<() } } } + if skipped > 0 { + tracing::info!( + "git credentials: pass budget reached, {skipped} repositories deferred to the next pass" + ); + } Ok(()) } diff --git a/frontend/src/lib/components/GitLabIntegration.svelte b/frontend/src/lib/components/GitLabIntegration.svelte index efcecb151f..1f7da2fb8a 100644 --- a/frontend/src/lib/components/GitLabIntegration.svelte +++ b/frontend/src/lib/components/GitLabIntegration.svelte @@ -1,5 +1,5 @@