feat(monitoring): replicate the metrics pipeline, and stop health repairs causing outages - #1430
Merged
Merged
Conversation
Eandalf-Bigstack
force-pushed
the
jim.lin/feat/replace-monasca-1
branch
from
September 7, 2026 08:02
3b4411e to
b9416d3
Compare
SekiXu
approved these changes
Sep 8, 2026
SekiXu
left a comment
Contributor
There was a problem hiding this comment.
LGTM.
Checked the things that would only surface at build or run time, since CI here is CodeQL/DCO with no build gate:
config_prometheus.cppuseshex_string_util::splitwithout including<hex/string_util.h>. It resolves transitively throughhex/config_tuning.h, the same wayconfig_apache2.cppandconfig_cinder.cppalready do, so this compiles.TargetRF():GetClusterSize()returns 1 when!ha(core/cube_sdk_library/src/cluster.cpp:95), so non-HA keeps RF 1 and matches the oldha ? 2 : 1; HA gets min(ctrlAddrs, 3).kafka_target_rf()scubectl node list -r control | wc -lmatches the four existing uses of that idiom insdk_health.sh/sdk_ceph.sh, so no off-by-one from a header row.- The Grafana datasource move to
http://localhost/prometheusworks: bothcube_cos_httpandcube_cos_httpsalready routepath_beg /prometheusatprometheus_backend. option httpchk GET /-/readyis the same formceph_prometheus_epalready uses in the srvlist table.
The _health_mysql_repairable gate is the part I most wanted to see here. Deciding reachable/joining/synced/down before anything gets restarted, and testing reachability first so a correctly evicted node cannot trigger a bootstrap, is the right way round.
Upstream's sample server.properties ships default.replication.factor, offsets.topic.replication.factor and transaction.state.log.replication.factor at 1 and config_kafka never overrode them, so every topic Kafka created for itself was born unreplicated -- __consumer_offsets above all, which is why losing one broker of three also lost consumer offsets and made kafka-consumer-groups --describe time out instead of reporting the outage. Topic creation asked for 2 where it asked at all. TargetRF is one replica per control node, capped at 3, and both the broker defaults and --create now use it. Three rather than two because kafka's data directory is not carried across the A/B partition switch -- there is no CONFIG_MIGRATE here -- so an upgraded node rejoins with an empty log dir and re-replicates every partition it holds. At RF 2 the partitions that node led sit on a single good copy for the length of that catch-up, once per node of a rolling upgrade; at RF 3 two copies remain throughout. Capped rather than fixed at 3 because cubesys.control.addrs decides the size and a 2-control-node HA is expressible -- the zookeeper block already guards for csize < 3 -- and asking for more replicas than brokers fails outright. Two settings are deliberately left alone. auto.create.topics.enable stays true: oslo.messaging names notification topics by priority, so notifications.warn and notifications.error appear the first time a service emits at that level and are not in the managed list. notifications.warn is present on accept-3cc today; turning auto-create off would silently drop it, and default.replication.factor is what makes it arrive replicated instead. min.insync.replicas stays 1: this is a 5-minute, 64MB transport buffer, so availability beats durability, and at min.insync 2 a two-broker loss would fail every produce. Verified on accept-3cc: an auto-created topic lands at RF 3 with ISR [2,1,0], and a rolling restart of all three brokers reports 0 unavailable partitions throughout. On jim-1cc TargetRF is 1 and nothing changes. Refs #672 Signed-off-by: Jim Lin <jim.lin@bigstack.co> Co-authored-by: Eandalf <clinah@connect.ust.hk> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The broker settings bind only when a topic is created, so on their own they fix a fresh install and nothing else. kafka-topics.sh --create --if-not-exists is a no-op on a topic that already exists, and the --alter beside it moves partitions, never replicas -- so every cluster that has already run keeps whatever RF its topics were born with. That covers more than 3.1.10-and-older upgrades. A rolling upgrade hits it too: the node being upgraded loses its zookeeper database with the partition switch, rejoins the ensemble, and resyncs the old RF-1 topic definitions back from the two nodes that have not rolled yet. So the topics are already there, at the wrong RF, on exactly the path that is supposed to be introducing the right one. Raising RF in place is a partition reassignment, so kafka_topic_rf_reconcile builds one. It keeps the existing replicas -- the leader stays first, so no leadership churn -- and fills the rest from the least-loaded broker not already holding the partition, which is what keeps the new followers spread. On accept-3cc it moved every topic from RF 1 to RF 2 in 37s and later to RF 3 in 16s, ending at 111/111/111 replicas per broker. Three guards make it safe to call from UpdateTopics, which every control node runs: it returns immediately when the target is 1, defers with a warning unless every broker is online -- a reassignment naming a dead broker never completes and blocks the next one -- and skips partitions already at the target, so the second and third node find nothing to do. Bounded at 600s from the config module and non-fatal either way. Verified on accept-3cc at RF 3: with two of three brokers stopped there are 0 unavailable partitions and every topic still produces and consumes, which RF 2 cannot do; kafka-consumer-groups --describe answers during the outage instead of timing out as it did while __consumer_offsets was RF 1. Second run is a 7s no-op. On jim-1cc the target is 1, the call returns in under a second and all 11 topics stay at RF 1. Note this does not buy two-node fault tolerance: zookeeper is a 3-node ensemble, so losing two nodes stops kafka whatever the RF. The two-broker test above kept zookeeper running on all three. Refs #672 Signed-off-by: Jim Lin <jim.lin@bigstack.co> Co-authored-by: Eandalf <clinah@connect.ust.hk> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Prometheus runs as three independent replicas that never exchange data -- prometheus.yml has no remote_write, remote_read, federation or rule_files at all. While a node is down that is honest enough, the target reads up=0. The problem is the node that comes back: its own TSDB keeps a permanent hole for the outage, and whoever queries it serves that hole with no indication anything is missing. Thanos closes it without a second storage system. The sidecar exports each node's Prometheus over the Store API; the querier fans out to all of them and deduplicates by replica, so a gap in one copy is answered from the others. Sidecar and querier only. Object storage -- store gateway, compactor -- buys retention beyond what Prometheus already keeps locally, which is a different problem from replicas being islands, so it is left out. Installed from the upstream release tarball. There is no rpm anywhere: thanos publishes tarballs only, and the packagecloud repo that carried a 0.37.2 build was deleted from the tree with the prometheus move for being stale. Checksum verified against the release's own sha256sums.txt -- weaker than kafka's cross-host KEYS, but it is what upstream offers, and the download still lands on .part until the digest matches. Both units run as prometheus rather than adding a system user, and --tsdb.path already points at that user's data directory, which is what an object-storage upload would need if one is ever added. Installed disabled; config_prometheus enables them per role. Refs #672 Signed-off-by: Jim Lin <jim.lin@bigstack.co> Co-authored-by: Eandalf <clinah@connect.ust.hk> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Generates everything the two units read and starts them per role, so the querier answers from all three Prometheus replicas instead of whichever one the caller happened to reach. prometheus.yml gains an external_labels block carrying the node's hostname as the replica label. That is not decoration: the sidecar refuses to start against a Prometheus with no external labels at all, and replica is the label the querier strips when it deduplicates -- which is why a query through thanos returns exactly the series a raw prometheus did, with no extra label leaking into the result. It is written unconditionally, so enabling HA later needs no prometheus config change, and on a single node it is one label nobody matches. The sidecar endpoint list comes from cubesys.control.addrs, which this module already observes, so a control-membership change re-commits and rewrites it -- no cron, unlike the lachesis compute list, whose membership re-commits nothing. The file is thanos's own EndpointConfig schema, not prometheus file_sd: a bare list of targets parses cleanly and then discovers nothing at all, which is a silent way to end up with a querier that answers from no stores. Two more shapes worth recording in the generated flags. The sidecar reaches prometheus at /prometheus because --web.external-url puts every endpoint behind that prefix. The querier serves under the same prefix so haproxy's route and grafana's datasource keep working when the backend moves to it -- but thanos keeps /-/ready at the root regardless, which is what a health check has to use. The querier is also moved off 10901/10902 because the sidecar already has them. Both stay off unless the node is a control node and HA is on: with one replica the querier would fan out to a single sidecar and dedupe nothing. Verified on accept-3cc: each querier discovers all three sidecars with distinct replica labels, and after stopping cc3's prometheus for four minutes its own TSDB holds 9 samples of a series over the window where its peers hold 15 -- while its querier answers 16. On jim-1cc nothing starts and prometheus is unchanged apart from the label. Refs #672 Signed-off-by: Jim Lin <jim.lin@bigstack.co> Co-authored-by: Eandalf <clinah@connect.ust.hk> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
prometheus_backend had exactly one server, 127.0.0.1:9091, and grafana's datasource named the same port directly. So every query was answered by one node's Prometheus -- whichever node the caller reached -- with no load balancing across the replicas and no deduplication. The queriers the previous commit starts would have sat there unasked. On HA the route now goes to all three queriers on 10904. Each one fans out to every control node's sidecar, so the answer no longer depends on which node holds the VIP, and listing all three means losing a querier fails the route over rather than blanking it. Health-checked on /-/ready, which thanos keeps at the root even though it serves the UI and API under /prometheus. Non-HA is unchanged and still points straight at the local Prometheus. Grafana's datasource moves to the node's own haproxy rather than a fixed port, so one provisioning file covers both shapes and follows whatever /prometheus is wired to. It also means a dead local querier fails over with the route instead of blanking the datasource. This is the part that closes the silent-degradation hole. Measured on accept-3cc after stopping cc3's prometheus for four minutes: counting stored samples of a series over the following fifteen, cc3's own Prometheus holds 9 where its peers hold 15 -- and through cc3's haproxy, the exact case of the VIP landing on the node that was down, /prometheus now answers 16. All three queriers report UP in the backend and grafana provisions with no errors. Worth knowing when checking this by hand: query_range hides it. Its five-minute lookback fills a four-minute gap and reports the same count everywhere; count_over_time on stored samples is what shows the hole. Refs #672 Signed-off-by: Jim Lin <jim.lin@bigstack.co> Co-authored-by: Eandalf <clinah@connect.ust.hk> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
_health_mysql_repair copies the whole datadir before bootstrapping a fresh galera cluster, health_mysql_repair calls it in a for i in 1 2 3 loop, the health machinery can call that loop again, and nothing ever pruned the copies. So the repair's cost is unbounded in the one resource whose exhaustion it cannot survive. It has now filled accept-3cc twice. Both times six copies per node with timestamps a minute apart -- about 100GB across the cluster -- and / at 100%. In August the ceph mons died with ENOSPC and stayed down eight hours. This week the mons held on but ceph went HEALTH_ERR, opensearch blew its 85% disk watermark and turned red with an unassigned primary, and logstash's output stalled behind it: log_transformer lost every consumer, sat at 2.9M lag, and no logs-<today>-* index was created at all. The visible symptom was two components away from the cause. It also cost the database: ENOSPC truncated ib_logfile0 on two of three nodes, so they needed a full SST to come back. Two bounds now. Prune first, so at most one copy exists; and skip the copy when free space is under twice the datadir, which is exactly the state the loop creates for its own later attempts. When it skips, the repair still runs -- the copy is a safety net, and refusing to fix a database because the disk is full would be its own kind of wrong. Done as a per-node loop rather than one cmd -c, so each node is judged against its own disk, and so the skip can be reported with log_error: cmd ships a bare string over ssh, where the sdk's log helpers are not defined. Checked both branches on the live cluster: a 4364MB datadir with 661MB free is correctly identified as no-room and skipped, where the unbounded version would have taken a seventh copy; with 24GB free it copies as before. Refs #672 Signed-off-by: Jim Lin <jim.lin@bigstack.co> Co-authored-by: Eandalf <clinah@connect.ust.hk> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… absent health_mysql_check is strict on purpose -- wsrep_cluster_size equal to the control-node count, every node Synced, every mariadb unit active -- and it must stay that way: that strictness is what makes it the right gate for a roll to advance on (handbook rolling-upgrade-no-galera-health-gate.md, #651). But 'the cluster is short a member' and 'the cluster is broken' are different statements, and only the second is repairable. The repair did not distinguish them, so on every boot -- PostBootRecovery -> cluster_check_repair_async -> the mysql entry of the full check_repair suite, whose only readiness gate is cube_cluster_ready, three marker files that say nothing about wsrep -- it killed mariadb cluster-wide, copied every datadir and bootstrapped a new cluster against a galera cluster that was simply still assembling itself. Decide in the repair, not the check. _health_mysql_node_state classifies each control node reachability-first: unreachable | joining | synced | down. Testing whether a node is 'in the cluster' means nothing on its own -- a node the network cannot reach was evicted correctly, and bootstrapping cannot bring it back, only destroy the cluster the survivors still have. 'joining' covers Joining/Joined/Donor/Desynced and a unit still in 'activating', since a joiner receiving an SST answers no query for the whole transfer. The repair runs only when some node is down: reachable, but neither in the cluster nor joining. Preserves #155 (a stale mariadbd on a reachable node still repairs) and #1328 (the live-primary check is untouched). Verified on accept-3cc: healthy -> no-op; mariadb stopped on a reachable cc3 -> down, repair fires; mid-rejoin -> joining, no-op; unknown host -> unreachable. Signed-off-by: Jim Lin <jim.lin@bigstack.co> Co-authored-by: Eandalf <clinah@connect.ust.hk> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…le tier health_mysql_repair opened with a blanket 'cmd -cor systemctl restart mariadb' across every control node. So a single failed member took the entire galera cluster down with it: the survivors were Synced and serving, and got killed to repair a peer. During a roll that is the difference between a cluster short one node and no cluster at all -- and it fires even when the repair is otherwise warranted, so the gate added in the previous commit does not cover it. Pick the nodes from _health_mysql_node_state: skip any node already Synced, and any the network cannot reach (it cannot be restarted anyway, and its absence is a power or network fault that galera has already handled correctly). Same command, same reverse order, one at a time. This matches what the repair's own live-primary path already does -- it rejoins only the non-Synced nodes. Verified on accept-3cc: all Synced selects no node; mariadb stopped on cc3 selects cc3 alone, leaving cc1 and cc2 untouched; after rejoin the selection is empty again and all three report wsrep_cluster_size 3. Signed-off-by: Jim Lin <jim.lin@bigstack.co> Co-authored-by: Eandalf <clinah@connect.ust.hk> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…per attempt health_mysql_repair calls _health_mysql_repair up to three times and every attempt bootstraps from the same datadir, so the second and third copies capture state the first already has. They cost another full datadir of disk and several GB of I/O against a database that is trying to start, and buy nothing. 29e3279 capped what was kept on disk; this stops making them in the first place. _health_mysql_repair takes the attempt number as its first argument and copies only on the first, so the decision travels with the call rather than through a flag two functions have to keep in step. It defaults to 1, so a direct 'hex_sdk _health_mysql_repair' still takes its copy. The live-primary rejoin path returns before the copy as it always has, so #1328's path is unchanged. Signed-off-by: Jim Lin <jim.lin@bigstack.co> Co-authored-by: Eandalf <clinah@connect.ust.hk> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
There is no hostname() in the sdk, so 'remote_run $N "$HEX_SDK hostname"' returned 1081 lines of hex_sdk usage -- 23652 bytes -- into the command substitution, and the comparison against $N could never match. The elif branch was dead, and with it the whole moderator/edge path that hands the cluster check to a real control node. Control nodes never noticed: they return at the is_control_node guard above it. Use plain hostname. Audited every $HEX_SDK invocation in the tree against the 1079 defined sdk functions; this was the only genuine miss. Verified on accept-3cc: old form matches on none of cc1/cc2/cc3, fixed form matches on all three; cube_remote_cluster_check still returns 1 with no output on a control node and 'cluster check' stays at 28 clean lines. Signed-off-by: Jim Lin <jim.lin@bigstack.co> Co-authored-by: Eandalf <clinah@connect.ust.hk> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
influxd-systemd-start.sh runs `influx_inspect buildtsi -compact-series-file` before it launches influxd, unconditionally, on every boot and every restart -- not as a one-off migration. The compaction is O(data on disk), and core/heavyfs/Makefile sets DefaultTimeoutStartSec=300s, which influxdb inherits, so the ceiling is only ~20-25G of retained metrics. Past that ceiling the unit does not just start slowly, it never starts again: Type=forking keeps systemd waiting on the wrapper, KillMode=control-group tears influx_inspect down mid-compaction at the deadline, and Restart=on-failure then restarts the rebuild from the beginning. Nothing in the loop makes progress and nothing bounds it. health_influxdb_repair compounds it, since its remote_systemd_restart kills a rebuild that was partway through. 1800s covers ~120G at the slower measured rate. Erring large is the safe direction: config_influxdb's SystemdCommitService runs `systemctl start` synchronously with retry=false, so a wedged influxd stalls that one commit until the timeout and is then reported -- bounded and visible, unlike a silent unbounded restart loop. Verified on cube4510: 21G of /var/lib/influxdb rebuilds in 3m26s (cube451), 25G in 6m12s (cube452). cube452 restart-looped indefinitely after its 3.1.0 -> 3.1.10 upgrade, taking Notifications and Metrics down; with the drop-in it completed in 6m12s and the bootstrap ran through to 83/83 modules committed. systemd parses the drop-in to TimeoutStartUSec=30min and `systemd-analyze verify influxdb.service` is clean. Signed-off-by: Jim Lin <jim.lin@bigstack.co> Co-authored-by: Eandalf <clinah@connect.ust.hk> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neither had any health coverage -- sdk_health.sh contained no mention of either -- so a dead prometheus or a thanos querier that had lost its peers was invisible to 'cluster check' and to auto-repair. prometheus is probed under its route prefix: config_prometheus sets --web.external-url=http://localhost/prometheus/, so :9091/-/healthy is a 404 on a healthy server and :9091/prometheus/-/healthy is the live endpoint. Beyond liveness it checks that at least one scrape target is up, since a server that is up and scraping nothing reports healthy while monitoring nothing. thanos is HA-only (config_prometheus gates both units on enabled && s_ha), so the check reports non-HA and passes on a single-control cluster. Unlike prometheus, thanos keeps /-/healthy and /-/ready at the root and moves only its API under the prefix. The last check asks the querier how many sidecars it can see: a querier that has lost its peers passes every other probe while serving one node's view of the cluster, which defeats the point of running thanos. Auto-repair declines the two faults a restart cannot fix: prometheus ERR 3 (up, scraping nothing -- config or service discovery) and thanos ERR 4 (a peer's sidecar unreachable -- usually a node that is down, and during a roll it would restart a querier on every pass). Verified on accept-3cc: healthy rc=0 for both; thanos-sidecar stopped on cc3 -> rc=1 'thanos-sidecar on cc3 is not running' -> repair -> rc=0; prometheus stopped on cc3 -> rc=1 -> repair -> rc=0; errcode lookups resolve. Signed-off-by: Jim Lin <jim.lin@bigstack.co> Co-authored-by: Eandalf <clinah@connect.ust.hk> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Surfaces the new health checks in 'cluster check' and 'check_repair' as their own service rather than folding them into Metrics, which is collection and visualisation. influxdb and kapacitor stay under Notifications for now: kapacitor is a write proxy and alerting engine here, not only storage, so moving them is a separate decision with its own API and UI blast radius. Added to both SRVS and EDGE_SRVS, with prometheus and thanos as repairable comps. Matches the metricsDb service now landed in cube-cos-openapi, cube-cos-api and cube-cos-ui. Verified by compiling cli_cluster.cpp under -Wall -Werror, and by expanding the X-macro standalone: TOP=29 with S[MetricsDb] resolving to MetricsDb. Signed-off-by: Jim Lin <jim.lin@bigstack.co> Co-authored-by: Eandalf <clinah@connect.ust.hk> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
CodeQL flagged all three fopen calls in WriteThanosConf as cpp/world-writable-file-creation (high) -- the mode is left to the process umask rather than stated. Same finding, same fix as 61d0ed8 did for config_apache2: WriteFile() from filesystem.hpp opens with an explicit S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, which is the only shape of this alert the repo has ever cleared. Output is byte-identical: the old fprintf path and the new vector<string> path were compiled side by side and their output diffed, matching exactly for the endpoints list and both ARGS lines. Existing files keep their current mode either way, since O_CREAT only applies the mode on creation -- so an upgrade sees no permission change. Verified in the build jail per the hex_config gate: clean CXX config_prometheus.o under -Wall -Werror and a clean LD hex_config, with the three generated literals still present in the object. Signed-off-by: Jim Lin <jim.lin@bigstack.co> Co-authored-by: Eandalf <clinah@connect.ust.hk> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eandalf-Bigstack
force-pushed
the
jim.lin/feat/replace-monasca-1
branch
from
September 8, 2026 09:54
b9416d3 to
7ecd4ac
Compare
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.
What type of PR is this?
What this PR does / why we need it
The next slice of the telemetry replacement: make the metrics pipeline survive a control node going away, and stop the health machinery from turning transient faults into outages.
TargetRF()ismin(control nodes, 3)— not a flat 3, which would fail topic creation on a 2-control HA cluster — and it now backsRecreateTopic,UpdateCfg, and the three broker defaults (default.replication.factor,offsets.topic.replication.factor,transaction.state.log.replication.factor).kafka_topic_rf_reconcileraises existing topics, which is what carries v3.1.10-and-older clusters across the upgrade: it prunes, places by least-loaded broker, is idempotent, and defers entirely while any broker is offlinesha256sums.txt), systemd units running asprometheus, installed disabled and turned on byhex_configlike everything else.config_prometheus.cppwritesexternal_labels: replica: <hostname>, the sidecar/querier defaults and/etc/thanos/endpoints.yml; the querier deduplicates onreplica. Thanos is HA-only —thanosEnabled = enabled && s_ha— and haproxy routes/prometheusat the three queriers on HAhealth_mysql_repaircausing the outage it was sent to fix. Four defects, one shape. It fired on a galera cluster that was merely still forming, restarted mariadb on every control node to repair one, copied the whole datadir once per retry, and never pruned the copies. On accept-3cc that filled/twice — once taking every ceph mon down with ENOSPC for 8h, once blowing OpenSearch's 85% watermark and stalling log ingestion behind itinfluxd-systemd-start.shrunsinflux_inspect buildtsi -compact-series-filebefore every start — not once, every boot and every restart — and inheritsDefaultTimeoutStartSec=300s. Past the ceiling the unit never starts again:Type=forkingwaits,KillMode=control-groupkills the compaction at the deadline,Restart=on-failurerestarts it from zero, andhealth_influxdb_repairkills it again. A drop-in raises the ceiling to 1800s (~120G)MetricsDbservice. Neither had any:sdk_health.shcontained no mention of either, so a dead Prometheus or a querier that had lost its peers was invisible tocluster checkand to auto-repaircube_remote_cluster_checkcompares a real hostname. There is nohostname()in the sdk, soremote_run $N "$HEX_SDK hostname"returned 1081 lines of usage into the substitution and the comparison could never match — the moderator/edge remote-check path was dead. Reported by a peer session on 3.1.10Which issue(s) this PR fixes
Special notes for your reviewer
Why the four galera fixes are one story.
health_mysql_checkis strict on purpose —wsrep_cluster_sizeequal to the control-node count, every nodeSynced, every unit active — and that strictness is what makes it the right gate for a roll to advance on (handbookrolling-upgrade-no-galera-health-gate.md, #651). So the check is deliberately not weakened here. The fix is in the repair: "the cluster is short a member" and "the cluster is broken" are different statements, and only the second is repairable._health_mysql_node_stateclassifies each control node reachability first — asking galera whether a node is in the cluster means nothing on its own, because a node the network cannot reach was evicted correctly, and bootstrapping cannot bring it back, only destroy the cluster the survivors still have.Why this class of bug keeps recurring. Four of the fixes here are the same failure mode: a repair fires at a system that is recovering normally, and the repair is what breaks it. Galera still forming; Synced members restarted to fix a peer; a datadir copied per attempt; a TSI rebuild killed mid-flight. The house precedent for the right answer already existed in
741f4093(pacemaker_settled(), "the fix is to wait for quorum, not repair"). The three questions worth asking of anyhealth_*_repair: can the check tell mid-recovery from failed, does the repair touch only the faulty member, and is its work bounded per invocation.Why the operator is never gated. An earlier revision of this branch deferred essential-tier repair during a roll. That was backwards —
cluster_check_repair_essentialhas no in-tree caller, so its only caller is a human typing the command, and the gate would only ever have blocked the person who asked for it. It was dropped. The roll is protected byhealth_mysql_repairitself declining to act on a joining node, which covers every caller.Thanos ports and prefixes are not uniform, and the checks had to measure rather than assume. Prometheus derives
--web.route-prefixfrom--web.external-url, so:9091/-/healthyis a 404 on a healthy server and:9091/prometheus/-/healthyis the live endpoint. Thanos keeps/-/healthyand/-/readyat the root even though the querier runs--web.route-prefix=/prometheus— only its API moves under the prefix, which is why haproxy'soption httpchk GET /-/readyworks and:10904/api/v1/storesis a 404 while:10904/prometheus/api/v1/storesis not.Two auto-repairs deliberately decline.
prometheusERR 3 (up, scraping nothing) is a config or service-discovery problem a restart cannot fix, so repairing it would be a restart loop against a server answering fine.thanosERR 4 (a peer's sidecar unreachable) is usually a node that is down; restarting the local querier does not bring a peer back, and during a roll it would restart a querier on every pass.Companion PRs, same
metricsDbservice: bigstack-oss/cube-cos-openapi#111 (base), bigstack-oss/cube-cos-api#650, bigstack-oss/cube-cos-ui#867.Additional documentation
Verified on
accept-3cc(cc1/cc2/cc3, control-converged, HA) unless stated otherwise.For the requirement "Kafka survives a control node going away",
At RF 1 the same probe showed 2 leaderless partitions per topic with a single broker down. An auto-created topic lands at RF 3 with ISR
[2,1,0], and a rolling restart of all three brokers keeps unavailable partitions at 0 throughout.For the requirement "queries reach more than one replica",
The querier fills its own node's hole from its peers, which is the whole point — before Thanos, cc3 answered 9 and there was no way to get 16 out of it.
For the requirement "a galera repair only fires when a node is truly down",
B is the control: a genuinely dead mariadbd on a reachable node still repairs, so #155's stale-mariadbd case is preserved. C is the fix — during the rejoin the unit sits in
activatingand answers no query at all for the whole state transfer, which is the longest phase of joining and the only window the old code could not see.For the requirement "a repair never takes the healthy members with it",
And the datadir safety copy is now once per repair rather than once per attempt — 1 copy whether the repair succeeds on attempt 1 or 3, re-armed on the next call, and still skipped entirely on the live-primary rejoin path (#1328).
For the requirement "prometheus and thanos are checked and repaired",
The endpoint behaviour the checks are built on, measured rather than assumed:
For the requirement "the MetricsDb service is reported",
For the requirement "influxdb starts past the 300s ceiling" — verified on
cube4510, not accept-3cc, because that is where the large datadirs are:cube452 had been restart-looping indefinitely after its 3.1.0 → 3.1.10 upgrade, taking Notifications and Metrics down with it; with the drop-in it completed in 6m12s and the bootstrap ran through to 83/83 modules committed.
For the requirement "the remote cluster check compares a real hostname",
Audited every
$HEX_SDK <word>invocation in the tree against the 1079 defined sdk function names; this was the only genuine miss. Note the usage listing goes to stdout, so inside$( )it is captured rather than printed — a control node returns at theis_control_nodeguard above it and shows nothing, which is whycluster checkthere stays at 28 clean lines.