feat(api): add spec.cluster.privateBackend so flushed parts actually replicate - #17
Draft
tdakkota wants to merge 5 commits into
Draft
feat(api): add spec.cluster.privateBackend so flushed parts actually replicate#17tdakkota wants to merge 5 commits into
tdakkota wants to merge 5 commits into
Conversation
portSelfMetric was 8090, which is oteldb's admin API bind (admin.bind defaults to :8090 and the operator never overrides it), so the Service port named metrics routed to the admin API and UI. Fixes #12
fix(controller): move self-metrics port off oteldb's admin API bind
Without it cluster/partsync never runs, so only the unflushed head is replicated and every flushed part exists in exactly one copy regardless of spec.cluster.replicationFactor. A node that loses its PVC never recovers: bootstrapShard's syncParts no-ops for a supposedly shared backend, so no engine is ever created. spec.cluster.privateBackend is a *bool: unset derives from spec.storage.backend, since file is one PVC per pod (private) and s3 is a bucket every node shares. storage.cluster stays reserved from extraConfig, so the CRD field is the only route.
…aude/16-private-backend # Conflicts: # README.md # internal/controller/config.go # internal/controller/naming.go
This was referenced Aug 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #16.
Adds
spec.cluster.privateBackend, rendered asstorage.cluster.private_backendin the generatedoteldb config.
Why this is a durability fix, not a convenience field
storage.cluster.private_backendgatescluster/partsync. The ring replicates only the unflushedhead;
partsyncis what replicates flushed parts and what backfills a node that lost its disk.The operator never rendered the key, and it is unreachable via
spec.extraConfigbecausestorage.clusteris inreservedConfigPathsand a reserved path covers everything beneath it.Measured on kind (3 nodes,
backend: file,replicationFactor: 2,flushInterval: 10s) in #16:one node, 13 on another, none on the third. A single lost disk was permanent data loss.
bootstrapGainedTenants→ partsync →engine load, and parts land on 2 nodes as RF=2 implies.
bootstrapShardcallssyncParts, which no-ops for a supposedly shared backend, so no bucket index appears locally, noengine is created,
hasAnyEnginestays false, and the next maintenance tick rediscovers the sameshard. The loop has no exit.
So every clustered
OtelDBClusteron thefilebackend the operator has deployed has been runningwithout part-level redundancy.
The design decision: derived, not a plain bool
spec.cluster.privateBackendis a*bool. Unset derives fromspec.storage.backend—truefor
file,falsefors3. An explicit value overrides the derivation.The reasoning, since this matters more than the code:
booldefaultingfalsemakes the safe-looking deployment the non-durable one. Theoperator's default topology is
backend: fileon per-podReadWriteOncePVCs; there is noconfiguration in which the operator hands several pods one shared filesystem. So
fileisalways the private case here, and a false-by-default bool would leave every existing user
silently broken until they read the field docs.
s3backend is one bucket every pod addresses, i.e. always shared, soderiving
falsethere is equally exact.general it is not inferable from the backend type (a
filebackend on NFS is shared; per-node S3buckets exist) — but that generality is about arbitrary oteldb configs, not about the two
topologies this operator can produce. Deriving is right at this layer and explicit is right at
oteldb's layer.
privateBackend: falsefora
filebackend on aReadWriteManyvolume,privateBackend: truefor per-node S3 buckets. Bothare covered by tests.
storage.clusterstays reserved fromextraConfig— the CRD field is the supported route.The key is rendered unconditionally (including
private_backend: false) rather than omitted whenfalse, so the ConfigMap states the mode explicitly and the config hash changes on upgrade.
This changes the behaviour of already-deployed clusters, deliberately. Any existing
OtelDBClusteron the defaultfilebackend that does not setprivateBackendwill, after theoperator is upgraded, render
private_backend: true, get a new config hash, and roll. On the newpods
partsyncstarts running and flushed parts begin replicating to their RF peers.That is the correct behaviour and it is strictly additive — it copies data that should already have
been copied and removes nothing — but it is not a no-op:
replicas catch up, and a corresponding rise in disk usage on nodes that were previously missing
their share (up to roughly RF× the current per-node footprint in the steady state, which is what
the declared
replicationFactoralways implied).fileon a sharedReadWriteManyvolume must setprivateBackend: falsebefore upgrading, or they get pointless self-copying.Interaction with the oteldb/storage startup diagnostic
oteldb/storage#373 added a startup diagnostic that detects exactly this misconfiguration. It fires
when
o.Cluster != nil && !o.Cluster.PrivateBackend && backend.IsNodeLocal(o.Backend)and logs atWarn:
and surfaces the standing form as
ClusterStats.NodeLocalBackendUnsharedinInspect.The two agree. storage's
IsNodeLocal()istruefor thefilebackend andfalsefor S3,which is precisely the derivation implemented here — so the deployments this PR flips to
trueareexactly the ones the diagnostic flags today, and after this PR the warning goes quiet on the
operator's default topology instead of firing on every node. The one asymmetry is harmless: the
diagnostic does not check the inverse (a shared store declared private), so an explicit
privateBackend: trueons3is not second-guessed by storage either.Version caveat. Neither side is in a release the operator's pinned image can reach yet:
defaultImageisghcr.io/oteldb/oteldb:v0.46.0, which vendors storagev0.28.0; oteldb/oteldb#1264(the config field) is merged to oteldb
mainbut untagged, and storage#373 is on storagemainwithno tag containing it. oteldb's config loader is a non-strict
yaml.Unmarshal, so the rendered key issilently ignored by older images rather than fatal — the field is inert until
spec.image(or afuture
defaultImagebump) points at a build carrying #1264. The README row says so. That also meansthe behaviour change above lands when the image moves, not when the operator does, for anyone still
on the default pin.
Tests
TestRenderConfigPrivateBackend— table over file/s3/unset backends and both explicit overrides.TestRenderConfigPrivateBackendNotReachableViaExtraConfig— regression pinning thatstorage.clusterstays reserved.TestRenderConfigFileBackendDefaultsnow assertsprivate_backend: true.make lint,make test,go build ./...all clean.Based on
main; #14 and #15 also regenerate CRD manifests, so whichever merges second will need amechanical rebase of the generated YAML.
🤖 Generated with Claude Code