Render storage
How the controller-manager writes renders and their history to an object store, and how the observer reads them.
The render store keeps the content of each render that convoy applies. It is an object store
outside etcd. etcd keeps only pointers to renders, for example status.lastSuccessful.renderRef.
The object store keeps the render content and the ordered history. This page describes the
implementation. For the user-level behavior, see Pin and history.
For the backend URLs and credentials, see Render store backends.
Layers#
The store has two layers:
| Package | Type | Responsibility |
|---|---|---|
internal/blobstore | BlobStore | Key to bytes. Five operations: Put, Get, Exists, List, Delete. |
internal/renderstore | RenderStore | The key layout, the history, the garbage collection and the deletion of a Release. |
blobstore.Open opens a gocloud bucket from the --render-store-url value. The registered
gocloud drivers are s3, gs, azblob, file and mem. List returns all keys under a
prefix in lexical order, without a delimiter. Delete of an absent key is not an error. Get
of an absent key returns blobstore.ErrNotFound.
The RenderStore does not know about Kubernetes types. A history entry has only three fields:
renderRef, syncedAt and summary.
Key layout#
All keys of a Release are under the prefix <namespace>/<name>/. There is no global prefix. To
isolate two convoy installations, use two buckets.
| Key | Content | Written by |
|---|---|---|
<namespace>/<name>/blobs/<renderRef> | The render, as a JSON array of objects | PutRender |
<namespace>/<name>/history/<time>-<renderRef>.json | One history entry, as JSON | AppendHistory |
renderRefissha256:and the hex SHA-256 of the JSON of each rendered object, in render order.<time>is the UTC sync time in the layout20060102T150405.000000000Z. The layout has a fixed width, so the lexical order of the keys is the chronological order.- The time contains no
-, and therenderRefcontains no-. The store finds therenderRefof an entry from its key, before the first-. It does not read the entry for this.
Write-once blobs#
The key of a blob is the hash of its content. PutRender first calls Exists. If the blob
exists, PutRender does not write it again. Thus convoy never changes a stored blob. It only
reads or deletes it.
Persist before apply#
The controller-manager writes the blob before it applies an object. The order in a sync is:
- The pre-apply dry run. A render that fails the dry run is not stored.
persistRenderwrites the blob. If this write fails, convoy applies nothing. The reason isDeployFailed, and the controller retries.- The
preSynchooks, the sync waves, the prune and thepostSynchooks. recordHistoryadds a history entry.runGCruns the garbage collection.
Steps 4 and 5 run only after a full success of step 3. A sync that stops at a wave gate, a hook or
an error has a stored blob but no history entry. The garbage collection keeps that blob, because
status.lastAttempted refers to it. See the protected renders below.
History entries#
recordHistory first calls HasHistoryRef. This call lists the history keys and compares the
renderRef of each key. If an entry for the render exists, recordHistory does not add one. The
history is a set of different renders, in the order of their first apply.
recordHistory does not fail the sync. If the store returns an error, the controller logs it.
The apply stays successful, and the history has a gap.
A rollback by spec.failurePolicy does not call recordHistory and does not call runGC. See
Rollout state machine.
Garbage collection#
runGC reads the effective keepHistory. An unset value gives 10. A value of 0 or less stops the
garbage collection, so convoy keeps all entries and all blobs.
For a positive value N, RenderStore.GC does these steps:
- It lists the history keys and deletes the oldest keys until
Nkeys remain. - It lists the history keys again and collects their
renderRefvalues. - It adds the protected renders to this set.
- It deletes each blob whose
renderRefis not in the set.
convoy reads the protected renders from the Release at the start of the reconcile:
- the render that this sync applied
status.lastSuccessful.renderRefstatus.lastAttempted.renderRefpin.renderRefstatus.lastHealthy.renderRef
The protection of status.lastAttempted covers the time between step 2 and step 4 of the sync.
The protection of status.lastHealthy keeps the rollback target. If the garbage collection
returns an error, the controller logs it and does not fail the reconcile.
Deletion of a Release#
When convoy finalizes a Release, it calls DeleteRelease after it deletes the ReleaseInventory.
DeleteRelease lists all keys under <namespace>/<name>/ and deletes each key. It skips a key
that does not start with the prefix. If a delete fails, convoy keeps the finalizer and retries.
See Deletion.
Observer reads#
The observer opens its own store from its own --render-store-url. It only reads:
| Reader | Reads | Use |
|---|---|---|
handleHistory | History | /history. The handler reverses the order, so the newest entry is first. |
diffStored | GetRender | /diff?render=<renderRef> |
pinnedRender | GetRender of pin.renderRef | The desired state of a pinned Release |
rolledBackRender | GetRender of status.lastHealthy.renderRef | The desired state of a RolledBack Release |
If pinnedRender or rolledBackRender does not find the blob, the observer renders the source
instead. A mem:// store is private to one process, so the observer cannot read the blobs of
the controller-manager.
Without a render store#
An empty --render-store-url gives a nil RenderStore. Each caller checks for nil:
persistRender,recordHistoryandrunGCdo nothing.status.resolvedRetentionPolicy.historyCountis 0.- A pin to a render other than the current render fails with reason
Pinned. applyLastHealthyreturns without an apply./historyreturns503./diff?render=returns an error.
Single writer#
The sharding gives each Release to one controller-manager replica. Only that replica writes the keys of the Release, so the history appends do not race. The store has no lock of its own. See Sharding.
In the code
convoy v0.9.0
internal/blobstore.Openinternal/blobstore/gocloud.go:40internal/renderstore.(*RenderStore).PutRenderinternal/renderstore/renderstore.go:100internal/renderstore.(*RenderStore).AppendHistoryinternal/renderstore/renderstore.go:128internal/renderstore.(*RenderStore).GCinternal/renderstore/renderstore.go:213internal/renderstore.(*RenderStore).DeleteReleaseinternal/renderstore/renderstore.go:290internal/controller/release.(*Controller).persistRenderinternal/controller/release/controller.go:1775internal/controller/release.(*Controller).recordHistoryinternal/controller/release/controller.go:1800internal/controller/release.(*Controller).runGCinternal/controller/release/controller.go:1856internal/observer.(*Controller).rolledBackRenderinternal/observer/observer.go:274