Been running this in my homelab for a couple of days (single node k8s, Garage for S3, postgres for metadata) and my NuGet cache restores never work. Smaller caches are fine. Took me a while to pin down so figured I'd write it up.
Symptom, every single run, same key:
Cache hit for: nuget-Linux-c0767851...
Warning: Failed to restore: Premature close
Cache not found for input keys: nuget-Linux-c0767851..., nuget-Linux-
That cache has never restored once, not one time in two days. It saves fine, it just can't be read back.
Setup: 9.6.0, STORAGE_DRIVER=s3 pointed at Garage (path style), DB_DRIVER=postgres, ENABLE_DIRECT_DOWNLOADS=true, CACHE_MAX_SIZE_BYTES 50GiB but only ~11GB used so eviction isn't running, STORAGE_S3_SOCKET_TIMEOUT_MS unset so 10000. Runners are ARC using your actions-runner fork with CUSTOM_ACTIONS_RESULTS_URL.
Cut actions/cache out of it and just curl the download endpoint:
curl -sS --max-time 90 -o /dev/null -w 'bytes=%{size_download} t=%{time_total} code=%{http_code}\n'
http://gha-cache-server:3000/download/
curl: (28) Operation timed out after 90000 milliseconds with 67108864 bytes received
bytes=67108864 t=90.002414 code=200
67108864 is exactly 64MiB, which is exactly part 0 of that entry (884332399 bytes over 14 parts, so 13x64MiB plus a small one). It sends part 0 and then just stops. 200, headers sent, body stalls. curl exits 28 (timeout) and not 18, so the connection is still open and idle, nothing got reset. It'll sit like that forever.
The thing that made it click: it depends on how big the parts are, not how many. actions/cache uploads in 64MiB chunks and it's broken. BuildKit type=gha uploads ~1MiB chunks and it's completely fine. I tested an unmerged entry with 37 parts of ~1MiB (37880506 bytes) and it downloads in full in 2.9s and merges fine. So 37 part boundaries are no problem, but the first 64MiB one kills it.
Garage side for one failing restore:
01:17:55.758 GET /gha-cache/?list-type=2&prefix=gh-actions-cache/2007184102/parts/
01:17:55.773 GET /gha-cache/gh-actions-cache/2007184102/parts/0
01:17:55.995 POST .../merged CreateMultipartUpload
01:17:56.028 PUT .../merged?partNumber=1... 200
... 12 x UploadPart, all 200 ...
01:17:57.649 PUT .../merged?partNumber=12... 200
01:17:57.750 DELETE .../merged?uploadId=... AbortMultipartUpload
So the merge upload gets aborted 100ms after a PUT that succeeded, right where it would move from part 0 to part 1. GET parts/1 never happens at all. Garage 200s everything and logs no errors, it's serving that 64MiB part at ~32MB/s, so I don't think storage is the problem. Server logs are completely silent during this, nothing.
18 aborts over 48h, same folders over and over. Merges that don't involve big parts work fine, 126 CompleteMultipartUpload in that same window.
Two things I already ruled out so you don't have to:
- ENABLE_DIRECT_DOWNLOADS isn't involved. storage.ts:690 only signs merged entries and every affected one is unmerged. I also saw this before I turned that on.
- Not the socket timeout. The abort is 100ms after a successful PUT, well under the 10s default (and under the 3s 9.4.7 hardcoded).
Been running this in my homelab for a couple of days (single node k8s, Garage for S3, postgres for metadata) and my NuGet cache restores never work. Smaller caches are fine. Took me a while to pin down so figured I'd write it up.
Symptom, every single run, same key:
Cache hit for: nuget-Linux-c0767851...
Warning: Failed to restore: Premature close
Cache not found for input keys: nuget-Linux-c0767851..., nuget-Linux-
That cache has never restored once, not one time in two days. It saves fine, it just can't be read back.
Setup: 9.6.0, STORAGE_DRIVER=s3 pointed at Garage (path style), DB_DRIVER=postgres, ENABLE_DIRECT_DOWNLOADS=true, CACHE_MAX_SIZE_BYTES 50GiB but only ~11GB used so eviction isn't running, STORAGE_S3_SOCKET_TIMEOUT_MS unset so 10000. Runners are ARC using your actions-runner fork with CUSTOM_ACTIONS_RESULTS_URL.
Cut actions/cache out of it and just curl the download endpoint:
curl -sS --max-time 90 -o /dev/null -w 'bytes=%{size_download} t=%{time_total} code=%{http_code}\n'
http://gha-cache-server:3000/download/
curl: (28) Operation timed out after 90000 milliseconds with 67108864 bytes received
bytes=67108864 t=90.002414 code=200
67108864 is exactly 64MiB, which is exactly part 0 of that entry (884332399 bytes over 14 parts, so 13x64MiB plus a small one). It sends part 0 and then just stops. 200, headers sent, body stalls. curl exits 28 (timeout) and not 18, so the connection is still open and idle, nothing got reset. It'll sit like that forever.
The thing that made it click: it depends on how big the parts are, not how many. actions/cache uploads in 64MiB chunks and it's broken. BuildKit type=gha uploads ~1MiB chunks and it's completely fine. I tested an unmerged entry with 37 parts of ~1MiB (37880506 bytes) and it downloads in full in 2.9s and merges fine. So 37 part boundaries are no problem, but the first 64MiB one kills it.
Garage side for one failing restore:
01:17:55.758 GET /gha-cache/?list-type=2&prefix=gh-actions-cache/2007184102/parts/
01:17:55.773 GET /gha-cache/gh-actions-cache/2007184102/parts/0
01:17:55.995 POST .../merged CreateMultipartUpload
01:17:56.028 PUT .../merged?partNumber=1... 200
... 12 x UploadPart, all 200 ...
01:17:57.649 PUT .../merged?partNumber=12... 200
01:17:57.750 DELETE .../merged?uploadId=... AbortMultipartUpload
So the merge upload gets aborted 100ms after a PUT that succeeded, right where it would move from part 0 to part 1. GET parts/1 never happens at all. Garage 200s everything and logs no errors, it's serving that 64MiB part at ~32MB/s, so I don't think storage is the problem. Server logs are completely silent during this, nothing.
18 aborts over 48h, same folders over and over. Merges that don't involve big parts work fine, 126 CompleteMultipartUpload in that same window.
Two things I already ruled out so you don't have to: