@@ -73,14 +73,17 @@ new table before the EXCHANGE. The replay matches the **full key**, not `id` alo
73736 . ** Cutover buffer knob ready** — ` databaseAnalytics.asyncInsertBusyTimeoutMaxMs ` (env
7474 ` ANALYTICS_DB_ASYNC_INSERT_BUSY_TIMEOUT_MAX_MS ` ), unset by default so the buffer inherits the
7575 ` async_insert_busy_timeout_max_ms=250 ` carried by ` queryParameters ` . Raise it to ~ 10000 for the cutover, then unset it
76- again. The ceiling is a backend per-query setting applied on the backend's own ClickHouse client, so the migration
77- scripts' direct ` clickhouse-client ` session ** cannot read or verify it** . It is therefore ** operator-asserted** :
78- ` exchange_and_wrap.sh ` refuses the EXCHANGE without ` --confirm-buffer-raised ` (a fail-fast acknowledgment gate — it
79- forces the operator to confirm the step, though it cannot prove the value took effect). Confirm it actually took
80- effect on the prod-clone/staging load test (the Go/No-Go "Async-insert ceiling confirmed" item) before production.
81- ** Also confirm client/SDK insert timeouts
82- exceed the widened buffer** (~ 10s) — with ` wait_for_async_insert=1 ` a raised ceiling blocks each insert until it
83- flushes, so a shorter client timeout would surface as ingestion errors during the window.
76+ again. ** Where it is set, the exact value, the rollout and the revert step are in
77+ [ "Where the buffer bump lives"] ( #where-the-buffer-bump-lives-and-how-to-revert-it ) ** — it is a temporary env var on the
78+ environment overlay, not a chart value (OPIK-7686). Have the overlay change written and reviewed * before* the window,
79+ so applying it is a merge, not an edit. The ceiling is a backend per-query setting applied on the backend's own
80+ ClickHouse client, so the migration scripts' direct ` clickhouse-client ` session ** cannot read or verify it** . It is
81+ therefore ** operator-asserted** : ` exchange_and_wrap.sh ` refuses the EXCHANGE without ` --confirm-buffer-raised ` (a
82+ fail-fast acknowledgment gate — it forces the operator to confirm the step, though it cannot prove the value took
83+ effect). Confirm it actually took effect on the prod-clone/staging load test (the Go/No-Go "Async-insert ceiling
84+ confirmed" item) before production. ** Also confirm client/SDK insert timeouts exceed the widened buffer** (~ 10s) —
85+ with ` wait_for_async_insert=1 ` a raised ceiling blocks each insert until it flushes, so a shorter client timeout would
86+ surface as ingestion errors during the window.
84877 . ** Schema-state flag wired, with a rollout plan** — ` databaseAnalyticsDataModel.traceColumnsNonNullable ` (env
8588 ` ANALYTICS_DB_DATA_MODEL_TRACE_COLUMNS_NON_NULLABLE ` , default ` false ` ). The successor's ` end_time ` /` ttft ` are
8689 ** non-nullable sentinel** columns, so the app must represent an absent value as the epoch/NaN sentinel — not ` null ` —
@@ -158,7 +161,9 @@ new table before the EXCHANGE. The replay matches the **full key**, not `id` alo
158161 It executes the reference statement in
159162 [` 000001_backfill_traces_local_v2.sql` ](scripts/ db- app- analytics/ 000001_backfill_traces_local_v2 .sql ) — the script
160163 reads that file and substitutes the window bounds, so the two never drift.
161- 2 . ** Raise the buffer ceiling** (config, see below), then ** [` scripts/delta_replay.sh` ](scripts/ delta_replay .sh )**
164+ 2 . ** Raise the buffer ceiling** (config — see
165+ [" Where the buffer bump lives" ](# where-the-buffer-bump-lives-and-how-to-revert-it) for the overlay entry, the value and
166+ the rollout wait), then ** [` scripts/delta_replay.sh` ](scripts/ delta_replay .sh )**
162167 (reference SQL [` 000002_delta_and_deletion_replay.sql` ](scripts/ db- app- analytics/ 000002_delta_and_deletion_replay .sql ))
163168 — delta- insert (anchored at ` backfill_start` ), then ** deletion replay** . The replay runs with
164169 ` lightweight_deletes_sync = 2` , so it returns only once the delete mutation has applied on ** every** replica.
@@ -263,6 +268,88 @@ new table before the EXCHANGE. The replay matches the **full key**, not `id` alo
263268delta one). This is normal — ` ReplacingMergeTree` collapses them on merge / under ` FINAL` / ` LIMIT 1 BY id` , highest
264269` last_updated_at` winning. Do not " fix" it.
265270
271+ # ## Where the buffer bump lives (and how to revert it)
272+
273+ ** Decision (OPIK- 7686 ): a temporary env var on the deployed chart' s environment overlay — not a chart value.** The three
274+ `ANALYTICS_DB_ASYNC_INSERT_*` knobs are deliberately absent from the Opik chart' s ` values.yaml` (OPIK- 6880 , # 7675).
275+ Rationale:
276+
277+ - ** No chart change is needed.** ` component.backend.env` is a free- form map rendered straight into the backend
278+ ConfigMap, so an overlay entry is already sufficient.
279+ - ** Removal is a clean one- step rollback .** Unset means " leave `queryParameters` alone" (` DatabaseAnalyticsFactory` ), so
280+ * deleting* the key restores the baked- in ` async_insert_busy_timeout_max_ms=250` . There is no " set it back to 250" edit,
281+ and therefore no pinned value that can later drift from the ` config.yml` default. For a time - boxed window that
282+ reversibility is the property worth optimising for.
283+ - ** The value is environment- and window- specific** — prod, during the cutover. An overlay keeps it version- controlled
284+ and auditable without turning a temporary state into a permanent chart default that every self- hosted install
285+ inherits.
286+ - ** A chart value would save no work** : you edit the overlay either way.
287+
288+ > For the record, the reason first given on # 7675 for excluding these — that rendering them would send empty strings
289+ > where the backend expects an integer , so it " would not be inert" — was ** wrong** . ` config.yml` ships
290+ > ` ${ANALYTICS_DB_ASYNC_INSERT_*:-}` as the default for all three, so the empty case is the normal path in every
291+ > environment today: an empty substitution leaves a bare YAML scalar that parses to a null ` Integer` , and ` @Min(1)` does
292+ > not fire on null . Exposing them in the chart with empty defaults * would* be safe. The decision above rests on
293+ > reversibility and scope, not on safety.
294+
295+ > ** Never bump it by editing ` ANALYTICS_DB_QUERY_PARAMETERS` .** That means re- pasting the entire tuning string
296+ > (` compress` , ` failover` , ` async_insert` , ` wait_for_async_insert` , the skip- index and shard settings, …), which risks
297+ > silently dropping one of the others and drifting from the ` config.yml` default. The dedicated
298+ > ` ANALYTICS_DB_ASYNC_INSERT_BUSY_TIMEOUT_MAX_MS` override exists precisely so the cutover states only the one value it
299+ > is changing.
300+
301+ ** Comet SaaS (the production cutover).** The overlay is ` values-production-opik.yaml` in ` comet-ml/comet-helm` , under
302+ ` opik.component.backend.env` (the ` opik` chart is a subchart there, so the values are namespaced under ` opik:` ):
303+
304+ ` ` ` yaml
305+ opik:
306+ component:
307+ backend:
308+ env:
309+ # OPIK-6875 traces cutover ONLY — delete this line to revert (do not set 250).
310+ ANALYTICS_DB_ASYNC_INSERT_BUSY_TIMEOUT_MAX_MS: "10000"
311+ ` ` `
312+
313+ Only the ** ceiling** changes. Leave ` …_MIN_MS` and ` …_MAX_DATA_SIZE` unset: the floor stays at the
314+ ` async_insert_busy_timeout_min_ms=100` carried by ` queryParameters` , and widening the ceiling alone is what parks the
315+ inserts.
316+
317+ ** The rollout is a pod restart, and it happens by itself.** The backend reads the ConfigMap through
318+ ` envFrom.configMapRef: opik-backend` , which Kubernetes injects at container start only — editing the ConfigMap does not
319+ reach a running pod. The chart annotates the pods ` reloader.stakater.com/auto: "true"` and Stakater Reloader runs on the
320+ cluster, so merging the overlay change triggers the rolling restart on its own; there is no ` kubectl rollout restart` to
321+ issue. What the operator ** must** do is wait for that roll to complete before continuing, because the ceiling has to be
322+ live on * every* instance:
323+
324+ ` ` ` bash
325+ kubectl rollout status deployment/opik-backend -n cometml-production
326+ kubectl get cm opik-backend -n cometml-production \
327+ -o jsonpath='{.data.ANALYTICS_DB_ASYNC_INSERT_BUSY_TIMEOUT_MAX_MS}{"\n "}'
328+ # and confirm no surviving pod predates the roll:
329+ kubectl get pods -n cometml-production -l component=opik-backend \
330+ -o custom-columns=NAME:.metadata.name,START:.status.startTime
331+ ` ` `
332+
333+ Four consequences to plan for:
334+
335+ - ** One roll, not two, before the tail.** ` traceColumnsNonNullable = true` (prereq 7 ) lives in the same overlay, the
336+ same ConfigMap and the same Reloader roll — and step 1 of " The final cutover window" asks for both. Land them in
337+ ** one commit ** so the fleet restarts once.
338+ - ** Keep the chosen ceiling below the pod' s termination grace period.** The revert is delivered by a second rolling
339+ restart, and at that moment pods are holding inserts parked for up to the widened ceiling. Neither the chart nor the
340+ overlay sets `terminationGracePeriodSeconds`, so it is the Kubernetes default **30s** — comfortably above the ~10000ms
341+ ceiling, but a much larger ceiling would let `SIGKILL` land on parked inserts. Check the two numbers against each other
342+ before picking a value.
343+ - **The roll itself costs ingestion capacity** (rolling-update `maxUnavailable` plus the backend PDB), so do it while
344+ there is slack — not between the final delta and the EXCHANGE.
345+ - **Reloader restarts the fleet on *any* change to that ConfigMap.** Hold unrelated overlay edits for the whole window,
346+ the same way prereq 12 freezes DDL.
347+
348+ **Self-hosted / on-premise.** Same env var and the same "delete to revert" semantics; only the delivery differs — a
349+ `component.backend.env` entry in whatever values file the install uses, or `ANALYTICS_DB_ASYNC_INSERT_BUSY_TIMEOUT_MAX_MS`
350+ in the compose `.env` (`docker-compose.yaml` already forwards all three). General tuning guidance for these knobs lives
351+ in the self-host [ClickHouse async-insert notes](../../../opik-documentation/documentation/fern/docs/self-host/troubleshooting.mdx).
352+
266353### The final cutover window (the zero-loss invariant)
267354
268355The buffer widening (prereq 6) is what makes the flip lossless, but the guarantee rests on a timing invariant worth
@@ -273,7 +360,9 @@ still land in the *old* `traces` in the gap between the last delta read and the
273360run. The binding constraint is therefore **not** "replay < buffer window"; it is that the **gap between the final delta
274361and the `EXCHANGE` completing must stay within the buffer hold**. So run the tail as tightly as possible:
275362
276- 1 . Widen the buffer, and ** roll out ` traceColumnsNonNullable = true` to every backend instance** (see below).
363+ 1. Widen the buffer, and **roll out `traceColumnsNonNullable = true` to every backend instance** (see below). Both are
364+ entries in the same overlay and the same backend ConfigMap, so land them in **one commit** and let the single restart
365+ carry both — see ["Where the buffer bump lives"](#where-the-buffer-bump-lives-and-how-to-revert-it).
2773662. Do the QA verify on an **earlier** pass (it can take minutes on a large table — do not let it be the last thing
278367 before the swap).
2793683. Run a **final** `delta_replay.sh` as the last write-facing step.
@@ -728,7 +817,8 @@ server's major version, either way:
728817 ` CLICKHOUSE_HOST ` ; for a ClickHouse on the host's own loopback, add ` --network=host ` via ` CLICKHOUSE_CLIENT_DOCKER_OPTS ` .
729818
730819** The only manual actions are not SQL:** (1) raising/restoring the async-insert buffer ceiling
731- (`databaseAnalytics.asyncInsertBusyTimeoutMaxMs`) around steps 2–3; (2) flipping
820+ (` databaseAnalytics.asyncInsertBusyTimeoutMaxMs ` ) around steps 2–3 — see
821+ [ "Where the buffer bump lives"] ( #where-the-buffer-bump-lives-and-how-to-revert-it ) ; (2) flipping
732822` databaseAnalyticsDataModel.traceColumnsNonNullable ` to ` true ` in lockstep with the EXCHANGE (and back on rollback) —
733823see "The final cutover window"; and (3) the go/no-go judgement between steps. All three are * backend config* / judgement changes (env + rolling
734824restart, or a config push) that these DB-facing scripts cannot and should not make. They are deliberately operator-owned;
@@ -1079,6 +1169,12 @@ cheap (stage A); the bridge stays enabled so nothing is lost on a retry.
10791169- [ ] ** Async-insert ceiling confirmed** — raising ` asyncInsertBusyTimeoutMaxMs ` demonstrably widens the adaptive buffer
10801170 under load, not just the cap. ` exchange_and_wrap.sh ` enforces the acknowledgment via ` --confirm-buffer-raised ` , but
10811171 that is an assertion only — this checklist item is the actual "it took effect under load" verification.
1172+ - [ ] ** The buffer-bump and revert changes are pre-written and reviewed** — the overlay entry raising
1173+ ` ANALYTICS_DB_ASYNC_INSERT_BUSY_TIMEOUT_MAX_MS ` (with ` traceColumnsNonNullable = true ` in the same commit) and the
1174+ revert that * deletes* the key, both prepared before the window so each is a merge rather than an edit under
1175+ pressure. Confirm the chosen ceiling is below ` terminationGracePeriodSeconds ` and that the Reloader-driven roll is
1176+ expected to complete inside the schedule — see
1177+ [ "Where the buffer bump lives"] ( #where-the-buffer-bump-lives-and-how-to-revert-it ) .
10821178- [ ] ** Data Retention confirmed disabled** for the cutover window (` RETENTION_ENABLED=false ` ). Retention deletes bypass
10831179 the deletion bridge, so a sweep in the window would leak/resurrect across the swap; ` exchange_and_wrap.sh ` and
10841180 ` rollback.sh ` (stages B/C) enforce ` --confirm-retention-paused ` , but that is an assertion — this item is the real
0 commit comments