Repository navigation
Fetch each cache miss once across instances - #570
Conversation
Instances started together on an empty database created the schema at the same time and all but one failed on duplicate types. Schema creation and migrations now run under a Postgres advisory lock. Eviction, reclaim, retention and the Gradle build cache sweep take a per-job advisory lock for each pass, and an instance that finds the job running elsewhere skips the pass. Before, every instance evicted the whole overshoot over max_size at once and emptied the cache. Mirror schedules record their last start in a new job_runs table, so each schedule runs once per interval instead of once per instance. Refs git-pkgs#543
The tests gated on PROXY_DATABASE_URL were always skipped in CI. A new job runs them against a Postgres 17 service container.
Windows cannot remove the temporary directory while the file is open.
Instances sharing a Postgres database each fetched a missed artifact from upstream; only callers within one instance shared a fetch. The instance that fetches now holds a lease on the miss in a new fetch_leases table and renews it while it downloads. Another instance missing the same key polls the cache and serves the stored copy, or takes the lease over when the holder releases it without storing anything or stops renewing it. Metadata misses use the same lease when metadata caching is on with a TTL above zero; without a stored copy there is nothing to wait for. proxy_fetch_lease_waits_total counts the misses that waited. Reclaim deletes leases left by stopped instances. Refs git-pkgs#543
andrew
left a comment
There was a problem hiding this comment.
In runSharedFetch the wait on another instance's lease runs under the request ctx, while the commit uses the detached commitCtx. DetachUpstreamFetch is there so a client giving up on a slow miss (pacman after 10s) doesn't end the shared fetch. Here, if the leading caller on one instance disconnects while waiting, f.err = ctx.Err() and every other client joined to that fetch on the same instance gets context canceled, even though their connections are fine. Please wait under context.WithoutCancel(ctx) when upstreamFetchDetached(ctx), with a test for it. The wait makes no upstream request, so it needs no detach slot, and the holder's fetch timeout and the lease expiry still bound it.
One to decide: when the holder's fetch fails, each waiting instance takes the lease once that fetch ends and runs the same fetch again. Against an upstream that hangs until timeout, a client on the k-th instance in line waits about k timeouts, where on main the instances fail in parallel. For metadata that delays the stale copy each waiter would serve. Either note it in the "Several instances" docs, or have the holder mark the lease as failed before releasing it so waiters go straight to their own stale copy or error.
A detached fetch waiting on another instance's lease ran the wait under the request context, so a client leaving cancelled every caller joined to it. The wait now runs past the client, bounded by the holder's fetch and the lease expiry. When the holder's fetch failed, each waiting instance took the lease in turn and repeated it, so against a hanging upstream the k-th instance waited about k timeouts. The holder now marks the lease failed. The instances waiting on it then serve their stale metadata copy if they have one, and otherwise fetch at the same time.
|
Both in 64162c7.
|
Point 3 of #543. This builds on #569: its migration comes after #569's, and the tests use #569's Postgres CI job. Until #569 is merged, the diff here includes its commits; the change is the last two commits, 84111c3 and 64162c7.
Instances sharing a Postgres database only coalesced cache misses within themselves, so each instance missing an artifact fetched it from upstream. Now the instance that fetches holds a lease on the miss, and the others wait for its result:
runSharedFetchtakes a lease in a newfetch_leases(key, owner, expires_at, failed)table, after its recheck and before the upstream fetch. The key is a hash of the existing coalescing key. Taking the lease is a singleINSERT … ON CONFLICT (key) DO UPDATE … WHERE expires_at < now(). Expiry uses the database clock.cache_metadataon andmetadata_ttlabove zero. Otherwise there is no stored copy to wait for.proxy_fetch_lease_waits_total{kind}counts the misses that waited for another instance.One behaviour to note: when a scanner blocks an artifact, the waiting instance fetches and scans it again. Within an instance, waiters get the holder's error instead.
Tested with three instances against Postgres 17 and one shared
file://directory. Three clients each fetched the same 30 npm tarballs, each client through a different instance, six requests at a time:All 90 requests succeeded in every run. No lease rows were left afterwards.
Tests (Postgres, run by the
test (postgres)job):TestFetchLeaseCoalescesArtifactAcrossInstances: two proxies share one database and storage, and the second one's miss waits for the first one's fetch.TestFetchLeaseFailedFetchLetsOtherInstanceFetchTestFetchLeaseFailedFetchWaitersFetchInParallel: three instances; after the holder fails, both waiting instances fetch at once.TestFetchLeaseFailedMetadataServesStaleTestFetchLeaseWaitOutlivesDetachedCallerTestFetchLeaseExpiredIsTakenOverTestFetchLeaseCoalescesMetadataAcrossInstancesTestFetchLeaseNotUsedWithoutMetadataCacheTestFetchLeasesandTestFetchLeaseFailure: acquire, renew, release, failure, takeover and cleanup on the table.TestFetchLeaseNotUsedOnSQLiteWith leases switched off, the four cross-instance tests fail.