Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ A segment of cache data stored before its merged representation has been created
**Merge**:
The creation of a cache's consolidated stored representation from its Parts.

**Eager Merge**:
A Merge started at upload completion instead of on first download. Opt-in via `EAGER_MERGE`.
_Avoid_: pre-merge, upfront merge

**Server-side Merge**:
A Merge the storage backend performs by copying Parts into the merged object without their bytes passing through the server (S3 `UploadPartCopy`). Requires every Part to satisfy the backend's limits.
_Avoid_: server-side copy

**Merge Lease**:
A time-bound, fenced claim granting one worker authority to complete a Merge.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Eager Merge is opt-in and composes server-side on object storage

Direct downloads need a merged object, and the Merge runs lazily on first download, so the first Cache Hit of every entry is proxied through the server even with `ENABLE_DIRECT_DOWNLOADS`. `EAGER_MERGE=true` runs the Merge right after upload completion instead. It stays off by default because lazy merging never pays merge traffic for entries that are never downloaded.

Both paths share one merge runner. Merge Lease, renewal, lease-fenced completion, and rollback are identical, so eager and lazy merges race safely against each other and against `cleanup:merges`. Upload completion picks the strategy up front from the Part sizes it already lists to record `sizeBytes`. When the adapter offers `composeParts` and every Part satisfies its limits, the Merge is a Server-side Merge with no bytes passing through the server. On S3 that is `CreateMultipartUpload`, one `UploadPartCopy` per Part, `CompleteMultipartUpload`; the limits are every Part but the last at least 5 MiB, none above 5 GiB, at most 10,000 Parts. On GCS it is `compose`, folding 32 sources at a time into a top-level temp object and composing `merged` in one final call; there is no minimum Part size. Otherwise the existing streaming merge runs immediately. Buildx uploads 1 MiB blocks, so its entries always take the streaming path. A backend that rejects the copy call fails the Merge like any other merge failure; the entry then merges lazily on first download.

This does not reopen ADR-0004. The 5 GiB cap it cites applies to `CopyObject` used as a promote step. `UploadPartCopy` writes `merged` directly under the same lease fence, and Parts are immutable, so a merger that lost its lease can only write identical bytes.

Costs: every entry occupies twice its size until `cleanup:parts` removes the Parts, not only entries that were downloaded, and `sizeBytes` still counts Parts only. A worker killed mid-composition leaves an incomplete multipart upload on S3 (configure an `AbortIncompleteMultipartUpload` lifecycle rule) or a temp object on GCS (reclaimed as Orphaned Storage). `FinalizeCacheEntryUpload` waits only for the Merge Lease, not for the Merge. The filesystem driver has no server-side copy and always streams.
2 changes: 1 addition & 1 deletion install/kubernetes/github-actions-cache-server/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 1.3.0
version: 1.4.0

# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ Generate environment variables from config values.
value: {{ default (printf "http://%s.%s.svc.cluster.local:%v" (include "github-actions-cache-server.fullname" .) .Release.Namespace .Values.service.port) .Values.config.apiBaseUrl | quote }}
- name: ENABLE_DIRECT_DOWNLOADS
value: {{ .Values.config.enableDirectDownloads | quote }}
- name: EAGER_MERGE
value: {{ .Values.config.eagerMerge | quote }}
- name: CACHE_CLEANUP_OLDER_THAN_DAYS
value: {{ .Values.config.cacheCleanupOlderThanDays | quote }}
{{- if .Values.config.cacheMaxSizeBytes }}
Expand Down
6 changes: 6 additions & 0 deletions install/kubernetes/github-actions-cache-server/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ config:
# The runner must be able to reach the storage provider directly.
enableDirectDownloads: false

# -- Merge cache parts right after upload instead of on first download, so
# the first restore can be a direct download. On S3 (UploadPartCopy, parts
# but the last at least 5 MiB) and GCS (compose) the merge happens inside the
# bucket. Doubles storage per entry until the parts cleanup job runs.
eagerMerge: false

# -- Number of days to keep stale cache data before deleting it. Set to 0 to disable.
cacheCleanupOlderThanDays: 90

Expand Down
1 change: 1 addition & 0 deletions lib/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const envBaseSchema = type({
'DISABLE_CLEANUP_JOBS?': 'boolean',
'DEBUG?': 'unknown',
'ENABLE_DIRECT_DOWNLOADS': 'boolean = false',
'EAGER_MERGE': 'boolean = false',
'BENCHMARK': 'boolean = false',
'SKIP_TOKEN_VALIDATION': 'boolean = false',
'MANAGEMENT_API_KEY?': 'string',
Expand Down
Loading