WIP: replicated host catch-up gate rework + design research - #2060
Closed
sunsingerus wants to merge 8 commits into
Closed
WIP: replicated host catch-up gate rework + design research#2060sunsingerus wants to merge 8 commits into
sunsingerus wants to merge 8 commits into
Conversation
Signed-off-by: ku524 <yeonjuyeong@gmail.com>
Update the race-only test to use the current metav1.Object-based Registry API. Signed-off-by: Rohan Thakkar <rohant@twitter.com>
Reworks the sync gate from #2018 on top of the merge: - chopconf CRD: add the missing `sync` sub-schema so the block is no longer silently pruned when set through a ClickHouseOperatorConfiguration CR, and add `minimum: 1` on the integer fields - accept enum values case-insensitively (`abort`/`Abort`, `proceed`/`Proceed`), matching the other enum-valued options - rename the config block `sync:` -> `catchUp:` and the gate identifiers with it; the schemer helpers that issue SYSTEM SYNC REPLICA keep `sync`, since there the word names the SQL - move the catch-up wait ahead of the host ascend, and clear the caught-up marker unconditionally on storage loss - replace the 100-year poll timeout with a bounded per-pass wait plus a re-enqueue - parenthesise compound conditionals per project style KNOWN BROKEN - do not merge as-is. The bounded-wait path does not work: poller.Poll() returns an error on timeout rather than nil, so the retry sentinel is unreachable and the re-enqueue is dead code. Details and the rest of the review findings are in the PR description.
Adds 99-clickhouseoperatorconfiguration-max.yaml, the counterpart of 99-clickhouseinstallation-max.yaml for the operator config, covering 86 of the 100 leaves the chopconf CRD declares. 70-chop-config.yaml is left untouched: it stays the short deployable example and the source operatorhub.sh loads into the OLM CSV alm-examples annotation, where a large sample would be actively unhelpful (the annotation is JSON, so all of its comments are stripped). The 14 uncovered leaves are the reconcile.host.hooks fields, shown commented out - the chopconf CRD omits their `events` and `failurePolicy` sub-fields, so a hook set through the CR is accepted, stored and never fires. The file documents the merge rules up front: explicitly-set values pin over future defaults, list-valued settings append rather than replace, and metrics.excludeRegexp is the one exception.
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.
Follows on from #2018 (originating issue: #1682).
What is in the code
Two blocking bugs in #2018, fixed:
SETTINGSwas appended toSYSTEM SYNC REPLICA.SYSTEMstatements have noSETTINGSproduction — this is a parse-timeSYNTAX_ERROR(Code 62), so the gate could never have worked against a real server. The deadline now rides onopts.SetQueryTimeoutinstead.syncsub-schema, so the whole block was silently pruned when set through aClickHouseOperatorConfigurationCR — the gate read as disabled while the user's manifest said enabled. Both verified against a live apiserver.Also in this branch:
abort/Abort,proceed/Proceed), matching every other enum-valued option in this config.minimum: 1on the integer fields.sync:→catchUp:, and the gate identifiers with it. The schemer helpers that issueSYSTEM SYNC REPLICAkeepsync— there the word names the SQL. See "Naming" below; this rename is now considered a mistake.remote_serversfor the duration of the wait instead of receiving distributed queries while still behind.docs/chi-examples/99-clickhouseoperatorconfiguration-max.yaml: an exhaustive operator-config example, covering 86 of the 100 leaves the chopconf CRD declares.Blockers — why this is a draft
poller.Poll()returns an error on timeout (poller.go:109), not nil.waitHostHasNoReplicationDelayassumes nil and re-checks the predicate, soerrReplicationCatchUpNotFinishedis never reached andscheduleReplicationCatchUpRetrynever fires. On expiry the host is left out of the Service with nothing re-including it. Fix: apoller.ErrTimeoutsentinel pluserrors.Is.reconcileCRtakes the"No reconcile work - abort reconcile"exit. Stuck-host recovery needed an explicit bypass for exactly this shape; the catch-up retry has none.test_010079does not test the gate. Withsync.enabled: falsethe defaultwait.replicas.new: yesruns the legacy probe, which blocks the same way and writes the same marker — every assertion passes with the gate off. Needs a gate-exclusive discriminator (e.g. assertingSYSTEM SYNC REPLICAappears insystem.query_log).Design research
The config shape is wrong, and the rename made it worse
reconcile.host.wait.replicasalready is the catch-up configuration —all/newchoose which replicas wait,delaydefines what "caught up" means. #2018 bolted a sub-block on top whose switch is calledenabled, which reads as though it turns catch-up on and off. It does not: with it off, catch-up still runs via the legacy delay probe. And with it on butall: no, new: no, the gate is configured and silently never runs.Renaming the block
sync:→catchUp:fixed the vocabulary but worsened the contradiction, becausecatchUpnames what both paths do.syncwas the better block name — only that path syncs.The better answer: make catch-up uniform, and delete the toggle
Rather than a user-facing choice between two paths, let the operator decide at runtime:
SELECT MAX(absolute_delay) FROM system.replicasmeaningful on this host yet?SYSTEM SYNC REPLICA.This is mostly resequencing; every building block already exists (
HostAsyncLoadBarrier,PeerReplicatedObjectCount,HostSyncReplicatedObjects, the health window). The config then collapses, with nothing released moving:Why the second path is needed at all:
system.replicasonly has a row for a replicated table once that table is attached and loaded. On a replica recreated with an empty disk,MAX(absolute_delay)over an empty set returns 0 — indistinguishable from "caught up". The operator can therefore declare an empty replica healthy and take down a good one. The deep path asks a peer what the shard should hold, which is the only step that makes the measurement trustworthy.Known hard parts, before anyone implements this:
HostSyncReplicatedObjectsruns the async barrier twice, because syncing a Replicated database materialises new tables that then load asynchronously — so a barrier pass before the DB sync provably does not prove completeness. Preparation must therefore be cheap and safe enough to run when in doubt, not perfectly targeted.SYSTEM SYNC REPLICAinstead ofLIGHTWEIGHT; one busy table can consume the entire budget.SYSTEM SYNC REPLICAneeds a table-level privilege.ACCESS_DENIEDcurrently propagates as a hard error, andincludeHostIntoServiceruns only on success — so the host loses its ready mark and drops out of the Service pass after pass.SYSTEM WAIT LOADING PARTSis 23.1+ and is issued unguarded;system.asynchronous_loaderis 23.12+.status.hostsWithReplicaCaughtUpis byte-identical whichever path wrote it — a misfiring auto-detector would be invisible. Dissenting view worth recording: keep the toggle as a thirdauto|always|nevermode rather than removing the choice.Suggested order: fix the three blockers → harden preparation (version guards, per-statement bounds, soft-fail on permission/peer errors) → then collapse the paths.
Separate defects found along the way
Not caused by this branch; each needs its own fix.
doesHostHaveNoReplicationDelaydiscards its query error, so an unreachable host reads as delay 0 → "caught up" → the marker is persisted and the wait is skipped for that host forever. Bites under the shipped defaultall: no, new: yes.eventsandfailurePolicy, which the CHI and CHIT CRDs both declare. An operator-wide hook set through a CR is accepted, stored, and never fires;failurePolicy: Ignoresilently becomesFail. Same shape as thesyncprune bug. Schema-only fix.reconcile.host.wait.replicas.{all,new,delay}are declared in the CHI CRD at three positions with descriptions promising per-CHI behaviour, but every reader uses the globalchop.Config(). Three inert advertised fields.template.chk.{path,policy}ship inconfig.yamland 10 manifests, butOperatorConfigTemplatehas only aCHIfield — no Go field exists and the CRD prunes them. Dead knob, plus an unreadconfig/chk/templates.d/.logger.vmoduleandlogger.log_backtrace_atare parsed, normalised and in the CRD, butSetupLog()never applies them — the stubs are commented out.chSchemekey migrates intoClickHouse.Access.Passwordinstead ofScheme. Unconditional, ~4 years old; low impact only because the key is undocumented.reconcile.coordination.keeper.onKeeperResourceUpdateis commented out inconfig.yamldespite being live and read by the CHK watcher.Verification
go buildclean ·gofmtclean ·go vetclean · unit tests pass in the affected packages · generators idempotent with noVERBOSITYdrift · the regenerated chopconf CRD accepted by a live apiserver, withcatchUpround-tripping unpruned and the oldsynckey correctly rejected.Not run: the e2e suite, since the last green run predates the bounded-wait rework.