tidaldb/tidal-stress/k8s/soak-monitor.yaml
jordan c97aaa8e5b fleet remediation: make the workspace gate runnable, then fix what it caught
`cargo test --workspace` could not run at all: dependency resolution failed with
"aws-types@1.3.16 requires rustc 1.91.1" on the 1.91.0 default toolchain, so the
gate the project documents was dead. Making it run exposed a compile break and
two wrong tests that had been invisible for months. Now green end to end:
143 suites, 3155 tests, exit 0.

Toolchain
- rust-toolchain.toml pins the DEV toolchain to 1.91.1. The published MSRV stays
  `rust-version = "1.91"` (the engine builds on 1.91.0); only tidalctl's AWS SDK
  chain needs the patch release, and it now declares that itself.

Consumer crates migrated to the current engine API (clean cutover)
- iknowyou-engine: `AgentPolicy` gained five m10 read/profile-override fields;
  the literal now spreads `..AgentPolicy::default()` as the engine's own doc
  example does, so future fields do not break it again.
- forage-engine: `RetrieveResult` gained p1 `reasons`. The app builds its own
  candidate pool, so it now tags what it knows: PreferenceMatch for the
  preference-vector blend, SemanticMatch (with the seed item) for
  similar-to-saved, ExplorationBudget for pinned discoveries.
- forage-engine: `url_to_item_id` folded into the u32 item universe. The engine
  narrows item IDs to a u32 slot in durable per-user state and rejects anything
  above u32::MAX rather than alias two items forever, so every add_item with a
  64-bit FNV hash failed. 9 of 28 smoke tests were failing on this alone.
- forage-engine: bridge items read the top-2 preference CLUSTERS via
  `query_vectors`, not the single centroid from `preference_vectors().get()`.
  Since m12 that accessor returns only the strongest cluster, so a tech+jazz user
  whose interests split into two clusters looked single-interest and never
  bridged. Falls back to top-2 dimensions when a user has one cluster.

Reconcile tests corrected to the shipped contract
- tidal/tests/m8p3_reconcile_production.rs asserted `3 + 5 == 8` for a windowed
  count after heal. `take_crdt_snapshot` deliberately keys signal contributions
  to ONE canonical contributor (ShardId::SINGLE) because signals are relayed from
  a single writer, so per-node attribution double-counted every replicated event
  on every reconcile. Merge is therefore LWW on (last_update_ns, score) plus
  PN-counter per-node max: nodes converge on the more complete accumulator. The
  old expectation was asserting the bug that fix removed.
- Rewrote to assert convergence, count survival (not 0), and no inflation, and
  added `repeated_reconcile_of_converged_nodes_does_not_creep` - the regression
  guard for the creep itself, which nothing covered.

Pre-commit hook unified
- hooks/pre-commit dropped `-D warnings`: each crate's `[lints]` table is the
  source of truth (`clippy::all`/`unwrap_used` deny, `pedantic` warn), and the
  flag promoted ~58 deliberate pedantic warnings in integration tests to errors,
  making every Rust commit impossible.
- It now lints all five tidal crates instead of path-matching `tidal/`, which
  silently skipped tidal-server, tidal-net, tidal-stress, tidalctl and
  applications/ - the rot above lived in exactly those crates. Ported the
  CODING_GUIDELINES file-length, println, and unsafe-SAFETY checks from the
  divergent untracked copy that this replaces.
- CONTRIBUTING.md now documents the real commands and the toolchain/MSRV split.

Fleet recovery and soak
- scripts/restore-fleet.sh: the fail-closed selective restore, promoted out of an
  ignored tmp/ directory into the repository. Preflights retained storage,
  digest-pinned images, parked state, and aggregate plus per-PV-node scheduler
  headroom before the first scale; writes a durable transcript under
  tmp/restore-logs/ with structured start/error/rollback/complete events.
- k8s manifests park the standalone store, the RF3 cluster, and the soak monitor
  at zero replicas with restore-fleet.sh as the only supported scale-up path.
- soak-eval/soak-watch and the nightly CronJob fail closed on stale or missing
  restart evidence instead of silently skipping the restart-aware half of the gate.
- docs/ops/capacity-planning.md corrects the RAM envelope to the real hot-tier
  formula and separates analytic totals from the measured process envelope.
2026-08-16 12:38:14 -06:00

206 lines
8.4 KiB
YAML

# Soak monitor — the always-on in-cluster observer for the 30-night soak.
#
# Two jobs, both writing to the SAME durable RWX result PVC the nightly CronJob
# writes its JSON summaries to, so EVERYTHING the operator needs to judge the
# 30-night streak lives in one place and survives the laptop session ending:
#
# 1. Restart watch: every minute, snapshot the tidaldb-{0,1,2} pod identity,
# counts + phase into /results/restarts.tsv. The GA bar is not just
# "30 green soak verdicts" — it is ZERO unrecovered failures over the
# window. An under-load pod restart during a soak night must be visible
# even if the soak Job itself still passed, so we record it independently.
#
# 2. HTTP read surface: serve /results over HTTP on :8080 so the operator can
# `kubectl port-forward deploy/tidal-soak-monitor 8080:8080 -n tidaldb-cluster`
# and read the ledger / nightly summaries / restart log from a browser at
# any time, from any machine, without exec'ing into a pod.
#
# The monitor does not generate load. The nightly Job is the authoritative gate;
# this deployment maintains the evidence stream and continuously recomputes the
# durable streak/status surface.
#
# Apply: kubectl apply -f tidal-stress/k8s/soak-monitor.yaml
# Read: kubectl port-forward deploy/tidal-soak-monitor 8080:8080 -n tidaldb-cluster
# then open http://localhost:8080/ledger.tsv (and /restarts.tsv, /)
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: tidal-soak-monitor
namespace: tidaldb-cluster
labels:
app.kubernetes.io/name: tidal-soak
app.kubernetes.io/part-of: tidaldb
automountServiceAccountToken: true
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: tidal-soak-monitor
namespace: tidaldb-cluster
labels:
app.kubernetes.io/name: tidal-soak
app.kubernetes.io/part-of: tidaldb
rules:
# Read-only: pod restart counts + phase, and the nightly soak Job verdicts.
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: ["batch"]
resources: ["jobs"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: tidal-soak-monitor
namespace: tidaldb-cluster
labels:
app.kubernetes.io/name: tidal-soak
app.kubernetes.io/part-of: tidaldb
subjects:
- kind: ServiceAccount
name: tidal-soak-monitor
namespace: tidaldb-cluster
roleRef:
kind: Role
name: tidal-soak-monitor
apiGroup: rbac.authorization.k8s.io
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: tidal-soak-monitor
namespace: tidaldb-cluster
labels:
app.kubernetes.io/name: tidal-soak
app.kubernetes.io/part-of: tidaldb
spec:
replicas: 0 # parked; scripts/restore-fleet.sh starts it only after capacity checks
selector:
matchLabels:
app.kubernetes.io/name: tidal-soak-monitor
template:
metadata:
labels:
app.kubernetes.io/name: tidal-soak-monitor
app.kubernetes.io/part-of: tidaldb
spec:
serviceAccountName: tidal-soak-monitor
securityContext:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
fsGroup: 1001
seccompProfile:
type: RuntimeDefault
containers:
# ── Restart watcher: direct Kubernetes API sampler ───────────────────
- name: restart-watch
image: registry.threesix.ai/tidal/stress:m12-fleet-remediation-20260815-r2@sha256:86bfa95f63ba01ed4afa120e41684cd8694743ec39aca72ef0bd68e5d9ef7f8b
imagePullPolicy: IfNotPresent
command: ["soak-watch"]
args:
- --namespace
- tidaldb-cluster
- --label-selector
- app.kubernetes.io/name=tidaldb
- --container
- tidaldb
- --results-file
- /results/restarts.tsv
- --interval-seconds
- "60"
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 100m, memory: 128Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
volumeMounts:
- name: results
mountPath: /results
# ── Streak evaluator: durable GA-bar status ────────────────────────────
# Joins the nightly verdict ledger with the restart evidence above.
# The nightly Job fails closed on this same evaluator; this sidecar keeps
# streak.tsv and one durable ALERT-<date>.txt current for operators.
- name: evaluator
image: registry.threesix.ai/tidal/stress:m12-fleet-remediation-20260815-r2@sha256:86bfa95f63ba01ed4afa120e41684cd8694743ec39aca72ef0bd68e5d9ef7f8b
imagePullPolicy: IfNotPresent
command: ["/bin/sh", "-c"]
args:
- |
echo "streak-evaluator up $(date -u +%FT%TZ); target=30 nights"
while true; do
# soak-eval reads /results/{ledger,restarts}.tsv, writes streak.tsv,
# and exits non-zero IFF the most recent night is non-green.
if soak-eval --results-dir /results --target 30; then
: # last night green (or no nights yet) — nothing to alert
else
# Non-green last night. Alert ONCE: key on the last streak.tsv
# date row so a persistent break does not re-fire every loop.
LAST="$(grep -v '^date' /results/streak.tsv 2>/dev/null | grep -v '^#' | tail -1 | cut -f1)"
SENT="$(cat /results/.last-alert 2>/dev/null || true)"
if [ -n "$LAST" ] && [ "$LAST" != "$SENT" ]; then
REASON="$(grep -v '^date' /results/streak.tsv | grep -v '^#' | tail -1 | cut -f4)"
MSG="tidalDB soak: night $LAST NON-GREEN — $REASON (streak reset)"
echo "ALERT: $MSG"
printf '%s\t%s\n' "$LAST" "$REASON" > "/results/ALERT-$LAST.txt"
echo "$LAST" > /results/.last-alert
fi
fi
# Bound the Retain volume: drop nightly summaries older than 31
# days (the full window + 1) so successive 30-night runs never grow
# the PVC without limit. ledger/restarts/streak are append/rewrite
# and stay small.
find /results -maxdepth 1 -name 'soak-*.json' -mtime +31 -delete 2>/dev/null || true
sleep 300
done
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 200m, memory: 128Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
volumeMounts:
- name: results
mountPath: /results
# ── HTTP read surface: serve the durable result dir ───────────────────
- name: http
image: busybox@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662
imagePullPolicy: IfNotPresent
# busybox httpd: one-shot static file server rooted at /results.
command: ["/bin/sh", "-c"]
args:
- |
echo "http surface up $(date -u +%FT%TZ) on :8080 serving /results"
exec httpd -f -p 8080 -h /results
ports:
- name: http
containerPort: 8080
# Readiness gates the Service endpoint on the HTTP listener. The
# result directory intentionally has no index file, so GET / returns
# 404 even while the static server is healthy.
readinessProbe:
tcpSocket: { port: http }
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
resources:
requests: { cpu: 10m, memory: 16Mi }
limits: { cpu: 100m, memory: 64Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
volumeMounts:
- name: results
mountPath: /results
readOnly: true
volumes:
- name: results
persistentVolumeClaim:
claimName: tidal-soak-results