`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.
206 lines
8.4 KiB
YAML
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
|