# Hanzo Datastore Hanzo's analytics database — a fork of ClickHouse with Hanzo-side overlays. - **Repo**: https://github.com/hanzoai/datastore - **Upstream**: https://github.com/ClickHouse/ClickHouse (`upstream` remote) - **Image**: `ghcr.io/hanzoai/datastore` ## Architecture — S-Chain & the two-engine platform Datastore is the **OLAP** half of Hanzo's data platform, and it separates storage from compute. Its `MergeTree` parts are stored as objects on **Hanzo S3 / "S-Chain"** — the SeaweedFS-fork storage substrate ([hanzoai/s3](https://github.com/hanzoai/s3)) — so stateless compute replicas share one zero-copy copy of the data. This is a proven PoC: a 5M-row table whose storage policy targets S-Chain — `system.parts` reports disk `seaweed` — and two compute nodes scale out over a single physical copy via zero-copy replication. The gRPC and Arrow Flight RPC surfaces are removed; the live query path is the native TCP protocol (port 9000). **ZAP** (the Cap'n-Proto-derived RPC) is the intended replacement transport — the server stub has landed, but it is not yet the query path. The planned **OLTP** half is a decentralized SQL engine (SQLite/Base) — designed, not yet built. The direction for both halves: ride S-Chain storage and move coordination from Raft to **Quasar** (post-quantum, leaderless). Design paper: `hanzo-datastore/` in [hanzoai/papers](https://github.com/hanzoai/papers). ## Benchmark vs official ClickHouse (measured, honest) ClickBench (43 queries, 100M-row `hits` dataset), spark (aarch64), 4 runs/query, warm. **opt** = our PGO+ThinLTO+BOLT `datastore.bolt` 26.6.1.1; **stock** = the official `clickhouse` binary. | Metric | Official ClickHouse | Hanzo Datastore (opt) | Delta | |--------|--------------------|------------------------|-------| | Peak RSS (under load) | 14.52 GB | 13.96 GB | **−3.8%** | | Total query time (Σ per-query medians) | 16.83 s | 16.82 s | tied (+0.1%) | | Per-query | — | faster on 8/43; median 0.95× | ~neutral | | Binary, stripped | — | 398 MB | — | **Honest verdict:** under production-scale load the optimization buys ~4% less peak memory and is statistically tied on speed. **The scale matters and must be quoted:** an earlier "~half the memory / much faster" figure was real but measured at **10M rows**, where the removed XRay instrumentation + leaner binary are a large fraction of a small working set. At **100M rows** the data dominates RSS, so the same fixed savings shrink to ~4% and the speed win vanishes. Both are true at their scale — never cite "half the memory" without "at 10M rows." Caveats on the 100M run: only 4 runs/query (per-query noise ±25%, so "tied" is within noise), one workload, one machine, warm cache. The fork's real value is architectural (S-Chain storage separation, consensus2 coordination, gRPC removed), not raw OLAP speed over stock. ## Stack - **Server**: C++ (the ClickHouse codebase under `programs/`, `src/`, `base/`) - **Build**: CMake → `datastore` binary ## Ports | Port | Protocol | Purpose | |------|----------|---------| | 8123 | HTTP | ClickHouse HTTP interface | | 9000 | TCP | ClickHouse native binary protocol | | 9009 | TCP | ClickHouse interserver replication | | 9181 | TCP | embedded keeper client port (when `` is configured) | ## Layout ``` datastore/ ├── programs/, src/, base/, contrib/ ← upstream ClickHouse C++ ├── hanzo/ ← Hanzo overlay: compose, config.xml, schema.sql ├── packages/, pkg/ ← RPM/deb packaging (datastore-*.yaml) ├── docker/server/ ← Hanzo image build (Dockerfile.ubuntu, white-labeled) └── ci/ ← upstream + Hanzo CI (Praktika) ``` ## Build ```bash # Local dev (compose): cd hanzo && docker compose up # CI: .hanzo/workflows/cicd.yml calls hanzoai/ci, which reads the root hanzo.yml # and builds the repo-root Dockerfile from source, publishing # ghcr.io/hanzoai/datastore (linux/amd64). See hanzo.yml for why it is the root # Dockerfile and not docker/server/, and why there is no deploy block. ``` ## Upstream Sync Automated via `.hanzo/workflows/upstream-sync.yml` (weekly, Mon 06:00 UTC, plus `workflow_dispatch`). The workflow fetches `upstream/master`, merges it onto an `upstream-sync/` branch, and opens a `WIP:` pull request on git.hanzo.ai — it never auto-merges. On conflict the merge commit is pushed with the markers intact and the pull request lists the conflicting files. Naming the branch after the upstream commit makes a re-run on an unchanged upstream head a no-op rather than a duplicate proposal. There is exactly one upstream sync workflow. Do not add a second. Net-new product files live under `hanzo/` (disjoint from upstream, so most syncs land clean). White-labeling that edits upstream files in place — `docker/server/`, `packages/`, `programs/server/config.xml`, top-level `CMakeLists.txt` — are the watch areas for merge conflicts. ## Phase 1 — Single Binary (Embedded Coordination) `datastore` ships exactly one server binary. The standalone `clickhouse-keeper`/`datastore-keeper` entry-point is no longer a default build target. Coordination still works — it runs **in-process** inside `datastore-server` whenever `` is present in the runtime config. ### What changed - `programs/CMakeLists.txt` — the option defaults flipped to `OFF`: - `ENABLE_DATASTORE_KEEPER` (was `${ENABLE_DATASTORE_ALL}`) — drops the standalone keeper entry-point and the `datastore-keeper` symlink from the multipurpose binary. - `ENABLE_DATASTORE_KEEPER_CONVERTER` (was `${ENABLE_DATASTORE_ALL}`) — drops the ZooKeeper→Keeper snapshot converter tool. - `ENABLE_DATASTORE_KEEPER_CLIENT` (was `${ENABLE_DATASTORE_ALL}`) — drops the standalone keeper CLI client. - `docker/server/Dockerfile.ubuntu` — `PACKAGES` installs only `datastore-{client,server,common-static}`; the keeper package is not installed. The standalone "Docker keeper image" CI job (which built `docker/keeper/`) was retired — coordination is embedded. - `BUILD_STANDALONE_KEEPER` already defaulted `OFF` upstream; left untouched. ### What did NOT change (and why) - `src/Coordination/` is **kept** and is still compiled into the server library unconditionally (`src/CMakeLists.txt:61` `add_subdirectory(Coordination)`, `:360` `add_object_library(datastore_coordination Coordination)`). This is the embedded-Raft logic. - `Server.cpp:2468-2620` (the `if (config().has("keeper_server.server_id"))` block, gated `#if USE_NURAFT`) is the embedded-coordination boot path. It uses `getKeeperDispatcher` and `KeeperTCPHandlerFactory` from `src/Coordination/` and binds `keeper_server.tcp_port` (default `9181`), `keeper_server.tcp_port_secure`, and `keeper_server.http_control.port` *inside the server process*. No separate binary required. - `programs/keeper/keeper_embedded.xml` is **kept** — it's the canonical config shape for the embedded path. `keeper_config.xml` (the standalone-binary config) stays in the source tree but is no longer installed by the default build. - `programs/keeper/`, `contrib/NuRaft/`, `contrib/nuraft-cmake/` — **kept**. Removing them is Phase C and gated on the lux/consensus → Quasar replacement landing (see Phase B doc above). ### Packaging `packages/datastore-keeper.yaml` only ships content when `BUILD_STANDALONE_KEEPER=ON` (which we don't set). `packages/datastore-server.yaml` is unchanged and now provides the *only* binary. The datastore-keeper deb/rpm package can be deprecated after one release cycle — leave the spec in tree until then for upstream-sync stability. ### `programs/install/Install.cpp:440-441` `Install.cpp` (the `datastore install` system-installer subcommand, separate from the `install/` packaging recipes) still lists `datastore-keeper` and `datastore-keeper-converter` in its `tools` symlink array. Those `fs::create_symlink` calls are no-ops on a binary that no longer claims those modes — the symlinks would point at the multipurpose `datastore` binary which would dispatch by `argv[0]` and fail. We don't ship `datastore install` from `Dockerfile`, so this only matters for the deb/rpm path. Cleanup is queued for Phase 1.1; not load-bearing for this image. ### Validation cmake configure on this macOS workstation aborts at `CMakeLists.txt:59 — Submodules are not initialized` (same gate the Phase B agent hit). The Phase-1 changes are option-default flips and one Dockerfile edit; they do not introduce new files, new sources, or new libraries. The diff is safe to validate on the Linux build host. Per `~/.claude/CLAUDE.md` ("NEVER fucking build images on my computer"), CI runs the configure+build. ## Disabled External Engines (Phase A landed) Hanzo Datastore is OLAP-only. External system integrations (stream brokers, foreign-DB read engines, foreign-DB CDC) are out of scope and disabled at build time. Each engine retains its source tree (gated by a `USE_*` macro) so upstream merges stay clean; the corresponding `ENABLE_*` CMake option defaults `OFF`. No runtime config re-enables them. | Engine / Source | CMake option | Source tree | Contrib subtree | |-----------------|--------------|-------------|-----------------| | Kafka stream | `ENABLE_KAFKA` | `src/Storages/Kafka/` | `contrib/librdkafka{,-cmake}/`, `contrib/cppkafka{,-cmake}/`, `contrib/libgsasl{,-cmake}/` | | RabbitMQ | `ENABLE_AMQPCPP` | `src/Storages/RabbitMQ/` | `contrib/AMQP-CPP/`, `contrib/amqpcpp-cmake/` | | MySQL engine + dict + DB | `ENABLE_MYSQL` | `src/Storages/MySQL/`, `src/Databases/MySQL/`, `src/Core/MySQL/`, `src/Interpreters/MySQL/`, `src/Dictionaries/MySQLDictionarySource.{cpp,h}`, `src/Storages/StorageMySQL.{cpp,h}` | `contrib/mariadb-connector-c{,-cmake}/` | | PostgreSQL engine + MaterializedPostgreSQL CDC + dict | `ENABLE_LIBPQXX` | `src/Storages/PostgreSQL/`, `src/Databases/PostgreSQL/`, `src/Core/PostgreSQL/`, `src/Dictionaries/PostgreSQLDictionarySource.{cpp,h}`, `src/Storages/StoragePostgreSQL.{cpp,h}` | `contrib/libpqxx{,-cmake}/`, `contrib/postgres{,-cmake}/` | | MongoDB engine + dict | `USE_MONGODB` | `src/Storages/StorageMongoDB.{cpp,h}`, `src/Storages/StorageMongoDBPocoLegacy.cpp`, `src/Dictionaries/MongoDBDictionarySource.{cpp,h}` | `contrib/mongo-c-driver{,-cmake}/`, `contrib/mongo-cxx-driver{,-cmake}/` | | HDFS storage | `ENABLE_HDFS` | `src/Storages/ObjectStorage/HDFS/`, `src/Disks/DiskObjectStorage/ObjectStorages/HDFS/` | `contrib/libhdfs3{,-cmake}/` | | Hive metastore | `ENABLE_HIVE` | `src/Storages/Hive/`, `src/Processors/Formats/Impl/HiveTextRowInputFormat.{cpp,h}` | `contrib/hive-metastore{,-cmake}/` | | Cassandra dict | `ENABLE_CASSANDRA` | `src/Dictionaries/CassandraDictionarySource.{cpp,h}`, `src/Dictionaries/CassandraHelpers.{cpp,h}`, `src/Dictionaries/CassandraSource.{cpp,h}` | `contrib/cassandra{,-cmake}/` | | YTsaurus storage + dict + table fn | `ENABLE_YTSAURUS` | `src/Storages/YTsaurus/`, `src/Core/YTsaurus/`, `src/Dictionaries/YTsaurusDictionarySource.{cpp,h}`, `src/Processors/Sources/YTsaurusSource.{cpp,h}` | (no dedicated contrib) | Engines that are **kept** because they belong to an OLAP/HTTP/object-storage shape: `MergeTree` family, `Replicated*`, `Distributed`, `File`, `URL`, `S3`, `ObjectStorage`, `ObjectStorageQueue` (S3 SQS-style queue, in-tree), `Memory`, `Log`, `View`, `MaterializedView`, `RocksDB`, Hanzo DocDB-shaped engines, ZooKeeper/Keeper coordination, `Dictionary` engine (sans foreign-DB sources), `NATS` (Hanzo PubSub transport), `FileLog` (local-file, `USE_FILELOG=1` on Linux). Kerberos client auth (`Access/GSSAcceptor`, `Access/KerberosInit`) is server-side and unrelated to disabled engines — `contrib/krb5{,-cmake}/` and `contrib/cyrus-sasl{,-cmake}/` MUST be kept. ## Phase B — Source Deletion Plan (NOT executed) Phase A flipped defaults to OFF; Phase B physically deletes the disabled source/contrib trees. This conflicts with the Phase A note "Source trees stay … to keep upstream merges clean" — accept the conflict cost in exchange for a smaller compile surface and faster upstream sync (less code to merge). Cross-reference (`src/CMakeLists.txt`): some engine source trees are added unconditionally and require a CMake gate edit before deletion. Those rows are flagged `risky` below. ```sh # disabled: kafka (gated: if (TARGET ch_contrib::rdkafka) at src/CMakeLists.txt:129) git rm -r src/Storages/Kafka git rm -r contrib/librdkafka contrib/librdkafka-cmake git rm -r contrib/cppkafka contrib/cppkafka-cmake git rm -r contrib/libgsasl contrib/libgsasl-cmake # only consumers were rdkafka + libhdfs3 # disabled: rabbitmq (gated: if (TARGET ch_contrib::amqp_cpp) at src/CMakeLists.txt:154) git rm -r src/Storages/RabbitMQ git rm -r contrib/AMQP-CPP contrib/amqpcpp-cmake # disabled: hive (gated: if (TARGET ch_contrib::hivemetastore) at src/CMakeLists.txt:189) git rm -r src/Storages/Hive git rm -r src/Processors/Formats/Impl/HiveTextRowInputFormat.cpp src/Processors/Formats/Impl/HiveTextRowInputFormat.h git rm -r contrib/hive-metastore contrib/hive-metastore-cmake # disabled: postgresql (gated: if (USE_LIBPQXX) at src/CMakeLists.txt:158) git rm -r src/Storages/PostgreSQL src/Databases/PostgreSQL src/Core/PostgreSQL git rm src/Dictionaries/PostgreSQLDictionarySource.cpp src/Dictionaries/PostgreSQLDictionarySource.h git rm src/Storages/StoragePostgreSQL.cpp src/Storages/StoragePostgreSQL.h git rm src/Processors/Sources/PostgreSQLSource.cpp src/Processors/Sources/PostgreSQLSource.h git rm -r contrib/libpqxx contrib/libpqxx-cmake contrib/postgres contrib/postgres-cmake # disabled: mongodb (gated: if (USE_MONGODB) at contrib/CMakeLists.txt:172; src files use #if USE_MONGODB) git rm src/Storages/StorageMongoDB.cpp src/Storages/StorageMongoDB.h src/Storages/StorageMongoDBPocoLegacy.cpp git rm src/Dictionaries/MongoDBDictionarySource.cpp src/Dictionaries/MongoDBDictionarySource.h git rm src/Processors/Sources/MongoDBSource.cpp src/Processors/Sources/MongoDBSource.h git rm src/TableFunctions/TableFunctionMongoDB.cpp git rm -r contrib/mongo-c-driver contrib/mongo-c-driver-cmake contrib/mongo-cxx-driver contrib/mongo-cxx-driver-cmake # disabled: cassandra (dict source uses #if USE_CASSANDRA; contrib has its own gate) git rm src/Dictionaries/CassandraDictionarySource.cpp src/Dictionaries/CassandraDictionarySource.h git rm src/Dictionaries/CassandraHelpers.cpp src/Dictionaries/CassandraHelpers.h git rm src/Dictionaries/CassandraSource.cpp src/Dictionaries/CassandraSource.h git rm -r contrib/cassandra contrib/cassandra-cmake # disabled: hdfs (RISKY: Storages/ObjectStorage/HDFS added unconditionally at src/CMakeLists.txt:140) # Requires gating add_headers_and_sources(dbms Storages/ObjectStorage/HDFS) under TARGET ch_contrib::hdfs first. git rm -r src/Storages/ObjectStorage/HDFS git rm -r src/Disks/DiskObjectStorage/ObjectStorages/HDFS git rm -r contrib/libhdfs3 contrib/libhdfs3-cmake # disabled: ytsaurus (RISKY: Core/YTsaurus and Storages/YTsaurus added unconditionally at src/CMakeLists.txt:151-152) # Requires gating both lines under if (ENABLE_YTSAURUS) first. git rm -r src/Core/YTsaurus src/Storages/YTsaurus git rm src/Dictionaries/YTsaurusDictionarySource.cpp src/Dictionaries/YTsaurusDictionarySource.h git rm src/Processors/Sources/YTsaurusSource.cpp src/Processors/Sources/YTsaurusSource.h # disabled: mysql (RISKY: 4 add_object_library() calls unconditional at src/CMakeLists.txt:273,278,288,294; # also blocked by ch::mysqlxx hard-linking ch_contrib::mariadbclient — used by Dictionaries/Embedded # and other always-on call sites. Requires either dropping mysqlxx contract or gating it on ENABLE_MYSQL.) # DEFER until ch::mysqlxx is detached from mariadbclient. git rm -r src/Storages/MySQL src/Databases/MySQL src/Core/MySQL src/Interpreters/MySQL git rm src/Storages/StorageMySQL.cpp src/Storages/StorageMySQL.h git rm src/Dictionaries/MySQLDictionarySource.cpp src/Dictionaries/MySQLDictionarySource.h git rm src/Processors/Sources/MySQLSource.cpp git rm -r contrib/mariadb-connector-c contrib/mariadb-connector-c-cmake ``` ## Phase C — deferred deletions (do NOT do in Phase B) - `programs/keeper/`, `src/Coordination/`, `contrib/NuRaft/`, `contrib/nuraft-cmake/` — wait for the `keeper-replace` Blue (lux/consensus → Quasar) to land. Until then, ZooKeeper coordination is load-bearing for `Replicated*` engines. **Status: in progress — see "Coordination: Raft → Quasar" below.** - HDFS, YTsaurus, MySQL — see RISKY notes above; they require `src/CMakeLists.txt` edits to gate currently-unconditional `add_headers_and_sources` / `add_object_library` calls. That edit is itself a Phase C task because it touches code on the upstream merge path. ## Coordination: Raft → Quasar (the NuRaft rip) The fork is replacing the NuRaft consensus engine under Keeper with native Lux **Quasar** (`libluxconsensus`, from [luxfi/consensus](https://github.com/luxfi/consensus) `pkg/c`), linked into the C++ server. The **ZooKeeper API is kept** — `KeeperStateMachine` over `KeeperStorage` is the contract `Replicated*` speaks; only the *engine* (`raft_server`, leader election, asio peer transport) is replaced. **Why it's tractable:** NuRaft coupling is contained in `src/Coordination/` (25 files); the only references elsewhere are `src/CMakeLists.txt` + `configure_config.cmake` (build config, not code). Nothing in the query/storage/replication layers touches `nuraft::` types. The join point is `KeeperStateMachine::commit(log_idx, buf)`: NuRaft calls it today, the Quasar engine calls the identical method. **Staged so each step lands under green tests (never break it all at once):** 1. **Engine proven + reusable** ✅ — `src/Coordination/QuasarKeeperConsensus.{h,cpp}` orders + commits batches through the real `KeeperStateMachine` (single-node: always-leader, 1-of-1 finality). Tested by `src/Coordination/examples/` (`keeper_quasar_poc`, `keeper_quasar_engine_test`), opt-in behind `ENABLE_EXAMPLES AND LUXCONSENSUS_DIR`, build-verified on a 26.6.1.1 host. 2. **Engine into `dbms` + `KeeperServer` cutover** — wire `libluxconsensus` as a contrib (`ch_contrib::luxconsensus`), move the engine `.cpp` into `dbms`, and make `KeeperServer` construct `QuasarKeeperConsensus` instead of `nuraft::raft_server` for single-node. The dispatcher contract it must satisfy (`putRequestBatch` async result, `isLeader`/`isLeaderAlive`/`getLeaderID`, `createSnapshot`, `applyConfigUpdate`, recovery flags) is mapped in the engine README. Gate on the full `gtest_coordination` suite green. 3. **Multi-node Quasar over ZAP** — replace NuRaft's asio peer transport with ZAP (the canonical Hanzo transport); leadership/membership derive from the Quasar validator set. This is what makes a real keeper *ensemble* run on Quasar. 4. **Excise residual `nuraft::` data types** (`buffer`/`log_entry`/`snapshot`/`ptr` — trivial containers, ~300 sites) and **delete `contrib/NuRaft` + `contrib/nuraft-cmake`** + the `src/CMakeLists.txt`/`configure_config.cmake` refs. End state: no `libnuraft` in the fork; coordination is Quasar-native end to end. Stage 1 is merged. Stages 2–4 are the production cutover and are large (`KeeperServer` is ~1350 lines welded to raft internals, incl. a `KeeperRaftServer : nuraft::raft_server` subclass reaching protected members) — do them on a Linux build host with the Coordination test suite as the gate, not on a laptop. ## Phase B — execution status Branch `cleanup/phase-b` was created but **no deletions were committed**. Local cmake configure could not be validated: - macOS AppleClang is rejected by `cmake/tools.cmake`; switching to Homebrew clang reaches the next gate. - After installing GNU `findutils`, GNU `grep`, and pointing `OBJCOPY_PATH` at `llvm-objcopy`, configure reaches `CMakeLists.txt:59` and aborts: `Submodules are not initialized`. The repo's submodule set is multi-GB; initialization was not attempted under the rule "do not patch around configure failures". Per the rule "If cmake configure fails after deletion, revert that group and report it as risky — don't try to patch around it", baseline configure failure means *no* group can be safely validated locally on this host. Deletion was therefore not executed; the plan above is the deliverable. Run the deletion + configure check on a Linux build host (CI runner with submodules initialized) before committing any of these `git rm -r` blocks. ## MinIO: purged from the docs, deliberately kept in the test harness Hanzo S3 is SeaweedFS-derived and Apache-2.0 — never a MinIO fork. The user-facing S3 documentation said otherwise and handed out `minioadmin` / `minioadminpassword` as example credentials, so `docs/en/**` and the `src/**` doc-strings that mirror it now use placeholders and a Hanzo S3 endpoint. **The integration-test harness still runs MinIO, and that is the correct state until someone ports it properly.** Do not `sed` it. What holds it: - `minio==7.2.20` in `ci/docker/integration/runner/requirements.txt` is a real dependency: `helpers/cluster.py` does `from minio import Minio`, and 60 test files call 16 distinct methods on `cluster.minio_client` (`make_bucket`, `fput_object`, `set_bucket_policy`, `get_object_tags`, …). boto3 is already in the same requirements file, but its API is not method-for-method compatible, so this is a rewrite of the harness plus every call site. - 136 tests pass `with_minio=True`; ~120 storage-config XMLs hardcode the fixture credential `ClickHouse_Minio_P@ssw0rd`. - `compose/docker_compose_minio.yml` leans on MinIO-only surface: `--certs-dir` TLS (`test_s3_with_https`), `MINIO_PROMETHEUS_AUTH_TYPE`, a console address, and the `warehouse.minio` / `warehouse-rest.minio` aliases that exercise virtual-host-style bucket addressing. Hanzo S3 has equivalents (`-s3.domainName`, `/healthz`, `Hanzo_s3_*` metrics) but not the same flags. - `proxy1`/`proxy2` come from `clickhouse/s3-proxy`, built in-repo at `ci/docker/integration/s3_proxy` but **pulled from a registry** at test time — a migration needs that image rebuilt and republished, which is a CI change, not a source change. A port is a real project with a real acceptance test (the S3 integration suite green), and it cannot be validated on an arm64 workstation: `ghcr.io/hanzoai/s3` publishes linux/amd64 only, and the suite needs a built `datastore` binary. Run it on a Linux CI host or not at all. Also left: the MinIO-quirk comments in `src/IO/S3/**` and `src/IO/S3Common.h` (the transient `InvalidPart` / `NO_SUCH_KEY` retries). They explain why live retry logic exists, and they describe the server CI actually runs — deleting them would delete the rationale and make the comment less true, not more. `src/Parsers/obfuscateQueries.cpp` matches `minio` only as a prefix of the word `minion` in a word list. Grep case-sensitively **and** with `\bminio` before touching anything. ## Related - Used by: `hanzo/insights` (insights-datastore deployment in `~/work/hanzo/universe/infra/k8s/insights/`) ## Operating this thing (2026-08-09, learned the hard way) **There is no client binary.** Ours is named ours, so `clickhouse-client` does not exist in the pod. Query it over HTTP from an `insights-web` pod: http://datastore:8123/?query=&user=$DATASTORE_USER&password=$DATASTORE_PASSWORD **Logs are NOT on the PVC.** Only `/var/lib/hanzo-datastore` is persisted; the server logs to `/var/log/hanzo-datastore-server/` on the container filesystem, so a crashing container takes its error log with it and `kubectl logs` shows only the pre-logger startup lines. To see a startup failure, run a throwaway pod with the same image and configmap plus `1` — that is the only way to read the real exception. **Two config faults that make it refuse to start**, both found in production (`universe/charts/app/values/hanzo/datastore.yaml`, Helm-managed — a `kubectl` edit to the ConfigMap IS reverted by GitOps within about a minute): - **`--` is illegal inside an XML comment.** Prose explaining a previous outage used it six times across `log-retention.xml` and `memory.xml`; the server dies with `SAXParseException: Invalid token`. The running process holds its config in memory, so this stays invisible until a restart. Validate every embedded document parses before pushing. - **`background_pool_size` below the mutation threshold.** Setting it to 4 makes the server exit 36: `number_of_free_entries_in_pool_to_execute_mutation` defaults to 20 and must be LOWER than the pool. It also starves the merges you need most. **The memory death spiral.** `datastore-0` wedged above its own `max_server_memory_usage` (10.80 GiB of a 12Gi limit) and refused EVERY query including `SELECT 1`, so all analytics failed while the site still served 200. Mechanism: merges need memory → memory exhausted → merges stall → parts accumulate → per-part metadata consumes more memory. `event.metric_30m` reached ~126k of ~126.7k total active parts. The tiny parts were the SYMPTOM, not a bad writer — recent parts were ~4,600 rows each. A restart clears it and it drains on its own (~2.4 parts/sec, memory falling). Reducing the merge pool makes it worse, not better. The node has no headroom: memory limits at 161% overcommit, 12Gi limit against ~13.6Gi allocatable, so raising it in place is not available. **Migrations.** `migrate_datastore --check` failing is not always about migrations — it fails the same way when the server is unreachable or wedged, so read the error before concluding. As of 2026-08-09 there were 83 genuinely unapplied migrations (last applied `0223_event_name_backmap`), several of them materializations over a 15.5 GiB table; drain the parts backlog first and classify additive-vs-destructive before applying. ## The headers are translated by nginx, not emitted by the engine There is an **nginx header-translation proxy inside the container** (`Server: nginx`, master + 2 workers on 8123, engine demoted to 8124). It rewrites response headers from the engine's spelling to ours. So seeing `X-Datastore-*` on the wire is NOT evidence that a renamed binary shipped — it translated five and missed three, which is where `x-clickhouse-exception-tag` came from. The translation is complete now (zero `x-clickhouse-*`, error path included), but `Server: nginx` remains because stock nginx cannot rename that header. It goes away when the proxy does. **No published image contains the rename.** The newest revision-labelled build is 2026-03-11; the rename commits are 2026-06-21. Every March revision is unreachable — a later re-import of the overlay orphaned that history. `26.6.1.1`/`26.6` carry no revision label and are a laptop hand-build (arm64, `/etc/datastore-server` paths, `single_binary_location_url=localhost:8899`); do not pin them. The fast rebuild path is dead (`pkg.hanzo.ai` is now an npm proxy, tarball 404s), so a from-source build is the only route: ~245 min against a runner ceiling that was 3h and is now 6h. Two earlier runs died at 190.5 and 193.5 min and were misread as OOM — they were the ceiling. **26.2 → 26.7 is a one-way door** (data path AND part format); treat it as a reviewed migration. ## subPath mounts do not refresh — and a missing key blocks startup A ConfigMap key deleted while a `subPath` mount still names it makes the next pod stop at ContainerCreating. It is invisible until something restarts the pod, because subPath mounts never refresh. This nearly bit us: `f2ec4616c` removed the `paths.xml` key and left the mount. If you delete a config key, delete its mount in the same change. ## Two real bugs the rename left, worth knowing the shape of - `src/Client/BuzzHouse/Generator/ExternalIntegrations.cpp` read `is_clickhouse`, a member that exists nowhere (it is `is_datastore` three lines away). It compiles only because `ENABLE_BUZZHOUSE` is off — turning it on does not build. - `src/Common/FileChecker.cpp` wrote `{"datastore": …}` but read only `datastore`/`yandex`. A directory written by a build that said `clickhouse` reads back as an **empty file list** — no error, just a checker that believes the table has no data. ## The rename crossed the contract line in places, and those are breaking now Renaming a name is safe; renaming a contract is not. Already throwing or broken: `SET dialect='clickhouse'`; the dictionary source registers only as `datastore`, so `SOURCE(CLICKHOUSE(…))` and `` throw (48 examples in `FunctionDocumentation` are now invalid SQL and are served through `system.functions`); Prometheus metric prefixes are `Datastore*`, so any dashboard matching `ClickHouse*` is broken; the SSH-signature namespace is `datastore`, so SSH-key auth will not interop with upstream clients. Decide these deliberately — aliases are cheap where interop matters. ## ZAP is present but is a port-opener, not a transport `contrib/zap` (debranded Cap'n Proto) and `src/Server/ZapServer.{h,cpp}` (181 lines) exist, but the server serves a **null bootstrap capability** — no schema, no method, no SQL — and is compiled out (`USE_ZAP=0`, submodule empty). The `ProtocolServerAdapter` plumbing is done (~70 lines); that was never the expensive half. For scale: the gRPC server deleted to make room was 2,023 lines plus a 227-line schema. The decisive constraint: this stub speaks **capnp-family** RPC, while `hanzoai/s3` and the Hanzo Go services speak the **envelope** wire, and there is no C++ envelope runtime (zapgen emits Go only). Honest cost: demo-grade first cut 8-15 engineer-days; parity with the deleted gRPC transport +30-60; actually wire-compatible with the Go services +20-40 and needs a C++ runtime nobody has written yet. ## The from-source build fails on a macro the rename half-renamed `CMakeLists.txt` sets `DATASTORE_CLOUD` and `src/Common/config.h.in` emits it as `#cmakedefine01 DATASTORE_CLOUD`, so that is the only spelling that reaches the preprocessor. Eighteen `#if` sites in `src/` still read `CLICKHOUSE_CLOUD`, which is defined nowhere. This tree builds with `-Werror,-Wundef`, so an undefined macro in `#if` is a hard error, not a false branch. The trap is *when* it lands. Ninja reaches `Storages/` about two hours in, so the failure looks like the build died of exhaustion — and it was twice answered as memory (compile jobs 6 → 3) and once as a timeout. It is neither. Read the log for `is not defined, evaluates to 0` before you believe any resource theory. The definition side of the rename was already complete — CMake variable, config template and the contrib guards all say `DATASTORE_CLOUD`. Only the reads were missed. Check the whole class in one pass, because each miss costs a two-hour round trip: git grep -hoE '#\s*(if|ifdef|ifndef|elif)[^/]*\b(CLICKHOUSE|HANZO)_[A-Z_]+' -- src/ programs/ base/ utils/ comm -23 <(git grep -hoE '#\s*(if|elif)[^/]*\bDATASTORE_[A-Z_]+' -- src/ | grep -oE 'DATASTORE_[A-Z_]+' | sort -u) \ <(grep -oE 'DATASTORE_[A-Z_]+' src/Common/config.h.in | sort -u) Both must come back empty. ## The published 26.2.3.2 image was never built from this source `Dockerfile.hanzo` at the revision the live image carries (`8adc31826d42`) downloads upstream release tarballs and then runs `mv /usr/bin/clickhouse /usr/bin/hanzo-datastore`, copies `hanzo-paths.xml` (which is what resolves `path` to `/var/lib/hanzo-datastore/`), a port override that moves the engine to 8124, and `nginx-header-proxy.conf`. The binary is upstream ClickHouse. Two consequences worth holding onto. The engine emits `X-ClickHouse-*` because it *is* upstream — nginx was baked into the image from the beginning, not bolted on later. And there is no source-level rename on the 26.2 line to port forward: "put the rename on the same 26.2 base" is not a cherry-pick, it is a new from-source image. `origin/26.2` and `v26.2.3.2-stable` are pristine upstream, and `main` is not a descendant of either (43,008 commits of divergence). That build is feasible — 26.2 wants clang 21, the same floor `main` compiles under, and the from-source `Dockerfile` already takes the Rust nightly as an ARG (26.2 wants `nightly-2025-07-07`). What it needs decided first is the tag: `imgver` publishes `max(declared, published)`, which assumes one monotonic lineage and cannot express a 26.2-lineage image once 26.7 is published. ## Config that lives in git is not config the cluster has The runner job ceiling is `timeout:` in `infra/k8s/git-runner/config.yaml` in `hanzoai/universe`. Two independent gaps sit between committing it and a build benefiting from it. The universe CD Application is deliberately **non-enforcing** — no `syncPolicy.automated` — so a commit changes nothing on its own; it reports `OutOfSync` and waits. Reconcile the one directory with `kubectl diff -k infra/k8s/git-runner` then `apply -k`, which moves the cluster toward git rather than away from it. Then act_runner reads its config **once at boot**. The ConfigMap is a volume mount, so the file updates inside every running pod within about a minute — checking the file in a pod therefore proves nothing. Only a pod that booted after the apply is actually running the new ceiling. Cycle the StatefulSet and verify against `.status.updatedReplicas`, not pod age. ## Talking to the forge `git.hanzo.ai` serves only the git wire from outside; its API is not exposed there. Port-forward `svc/hanzo-git` and use **`/v1/`** — not `/api/v1/`, which 404s. Basic auth with the same credentials that push works. Useful paths: `/v1/repos/hanzoai/datastore/actions/tasks?limit=N`, `.../actions/runs//jobs`, and `/v1/repos/hanzoai/datastore/actions/jobs//logs` — note the log endpoint takes a *job* id and only one of a run's two job rows serves logs; the other 404s. The repo is **not a mirror any more** (`"mirror": false`), whatever `.hanzo/workflows/cicd.yml` says about mirror-sync. Push to it directly to trigger CI. Because the workflow sets `cancel-in-progress: true` on `main`, a second push cancels the build the first one started — batch your commits, and do not push docs while a build you want is running. ## An image-only tag swap starts green and reads nothing The live StatefulSet sets **no `command` and no `args`**, so the image's own ENTRYPOINT decides everything. That makes the image, not the manifest, the thing holding the contract — and the from-source image does not hold the same one the running image does. live (Dockerfile.hanzo) from-source (Dockerfile) /usr/bin/hanzo-datastore /usr/bin/datastore /etc/hanzo-datastore-server/ /etc/datastore-server/ /var/lib/hanzo-datastore/ /var/lib/datastore/ (config.xml:556) Every one of those is where the cluster has something mounted: the 200Gi PVC at `/var/lib/hanzo-datastore`, and the whole universe `config.d` set — including `paths.xml`, which is what actually resolves `path`, and which no image bakes — at `/etc/hanzo-datastore-server/config.d/`. So a tag swap does not fail. `entrypoint.sh` defaults `DATASTORE_CONFIG` to `/etc/datastore-server/config.xml`, a directory where none of those overlays are mounted, reads `path` from the compiled default, and initializes an **empty database on the container's ephemeral layer**. `/ping` answers 200, both probes pass, and the pod reports 1/1. The parts are still on the PVC, untouched — the engine simply never looks at them. Reverting the tag brings them back; writes taken during the window are gone. Worth being precise about the order of the two doors. Until the contract is aligned the new engine never touches the existing data directory, so it cannot rewrite a part — the part-format door is not even open yet. Align the paths and it opens on the first merge, and from there the way back is a volume snapshot rather than a tag revert. A migration therefore needs *both*, in one reviewed change: the contract (binary, config dir, path) and a snapshot taken before the engine is first pointed at real data.