Skip to content
Open
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
107 changes: 107 additions & 0 deletions designs/gantry-unbounded-backend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Gantry: Cache-Origin Chain

**Status:** Draft
**Date:** 2026-06-23

---

## Problem

Gantry fetches blobs missing from containerd directly from the upstream OCI registry.
On Unbounded-managed clusters, blobs may already be resident in unbounded-storage's
RDMA-accelerated P2P cache, but gantry ignores it and re-fetches over the internet.

Gantry also runs on plain Kubernetes clusters without unbounded-storage. Any integration
must be strictly opt-in with zero behavioral change when not configured.

---

## Approach

Introduce a **priority chain** of cache origins that gantry consults before falling

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would unbounded-storage's cache get filled in this case? It needs an upstream backend to fill from.

Maybe unbounded storage should implement an OCI backend that knows how to pull from registries. Or maybe Gantry exposes an HTTP endpoint on the loopback for storage to pull from on cache miss.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this initial proposal, I am treating unbounded-storage as a readonly cache ie. if the image is present in unbounded-storage then use it but if not then just pull it from internet.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But how will they get into unbounded-storage in that case?

Users don't write to unbounded storage - they read from a configured frontend, and it pulls from the corresponding backend impl if the value isn't in cache. So it's always a pull through cache. In order for Gantry to use unbounded-storage it needs both a way to pull values from unbounded AND the ability for unboudned to fetch those values on misses.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you show me an example of how it is done today?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This integration test is a pretty good example: https://github.com/Azure/unbounded/blob/main/internal/orca/inttest/storageboundary_test.go

It spins up a chain of unbounded-storage -> Orca -> Garage (for fake S3 origin), then downloads some blobs through the chain by hitting the unbounded-storage S3 frontend.

through to the OCI registry on the "cold fetch" path - the one pull per blob per node
that reaches the origin. Once a blob is committed to containerd it is served locally
forever; the cache chain is never consulted again for that blob.

The chain is a simple ordered list: try each entry in turn, fall through to the OCI
registry on any miss or failure. The OCI registry is always the mandatory final
fallback and cannot be misconfigured away.

**unbounded-storage** is the first supported cache origin. It runs as a daemon on each
node and is consulted over loopback (HTTP on port 8080). Cache hits are served at
local NVMe or RDMA speeds. On a cache miss, unbounded-storage closes the TCP connection
without sending an HTTP response - gantry treats this as a clean miss and falls through.

---

## Goals

- Gantry tries configured cache origins in order before the OCI registry.
- Adding a future cache origin type requires only a new implementation and one config
wiring change - no changes to the chain, config schema, or metrics.
- When no cache origins are configured, the code path is identical to today.
- Any cache origin failure or miss is transparent; gantry continues to function.

## Non-Goals

- Writing blobs into unbounded-storage. Gantry only reads from it; cache population

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this one defeats the purpose of the integration. The cache should be transparent to users

@vpatelsj Vaibhav Patel (vpatelsj) Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cache is indeed transparent to users in my mind too. For gantry, transparency is that users specify a container image in pod definition. Now whether the container image is pulled from origin from internet, or a gantry peer or unbounded storage is not visible to user nor their concern. Hydrating the unbounded-storage on cache miss is not a goal for this initial proposal. We can definitely that functionality add that later on and it will still remain transparent to user. Gantry will do the hydration behind the scenes.

is the operator's concern.
- OCI Distribution Spec support in unbounded-storage. It is treated as a plain HTTP
cache keyed by URL path.
- Circuit breaker or availability tracking in v1. Fall-through on every failure is

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How important is it to support multiple origins? I'm trying to understand the use case. It would be much simpler to just support one: no need to think about circuit breakers

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, I am positioning gantry as a component that can also run on non-unbounded clusters and in that spirit that shouldnt be necessary need unbounded-storage but could use it if its available.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand needing swappable origins, my question is about why you are proposing support for multiple chained origins. It would be simpler to fall through to a single configured origin, and I can't think of a use case where multiple are needed (maybe I'm missing something there)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's say a container image acr.foo.bar:latest manifest requires a blob with sha1234 needs to be pulled on a node A. This blob is not present on the node but it could be at multiple locations:

1). it can be on the another node's containerd cache.
2). it can be on unbounded storage - chunked across various nodes.
3). it can be on a harbor registry that is local to the datacenter or cluster
4). it can be on acr.foo.bar over the internet.
5). it can be just not present at all.

A single configured origin ie. any one option of above doesnt make sense to me. Chaining helps in hydrating the cache as well. Gantry can detect that its mirror failed across all the caches and went all the way to internet to pull the blob but now it push the downloaded to harbor or unbounded storage etc..

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(1) and (2) should be the only cases where it's possible to fall through e.g. cache misses or unavailability. But for 3-5 we should be able to try exactly one upstream server: local harbor would have a unique prefix in the image tag, same for public registries.

Conceptually, I think it makes sense to decouple the concept of pull through caching from the concept of origins/upstreams. Configure any trusted origins, then configure the cache layer as a separate concern. Every image resolves back to a single origin, and is cached by gantry's p2p and/or the configured external cache e.g. unbounded storage.

sufficient.
- Auth support for cache origin backends in v1.

---

## Design

### Data Flow

```
cold fetch (containerd miss)
--> PriorityChain
[1] unbounded-storage -- hit: done; miss or error: next
[2] ...future backends...
[N] OCI registry -- always the final fallback
```

### Components

**`internal/gantry/unstore/`** - protocol shim for unbounded-storage. Implements the
existing `OriginPuller` interface. Owns all wire quirks (notably: connection close
Comment thread
jveski marked this conversation as resolved.
signals a miss, not 404) so nothing outside this package knows about them.

**`internal/gantry/origin/chain.go`** - `PriorityChain`, a thin ordered list of
`OriginPuller` implementations. No logic beyond iterating entries and calling the
fallback on miss. Each entry owns its own metrics.

### Config

```yaml
cache_origins:
- type: unbounded-storage
endpoint: http://127.0.0.1:8080
timeout: 30s
```

An empty or absent `cache_origins` disables the feature entirely.

### Observability

Shared Prometheus counters per backend: pull attempts, hits, misses, transient errors.
All labeled with `backend` so a single dashboard query covers all cache origin types.

---

## Graceful Degradation

| Scenario | Behavior |
|---|---|
| `cache_origins` absent or empty | Feature off; OCI registry path unchanged |
| cache origin unreachable | WARN log; falls through to OCI registry |
| cache origin miss | Falls through to next entry or OCI registry |
| cache origin hit | Blob served; OCI registry not contacted |
| all cache origins miss and OCI also fails | Same error behavior as today |


Loading