fix(production): resolve all runtime errors and add CI migration automation

- Add ohlcv_bars table migration (050) for trading_service prediction loop
- Fix kube-state-metrics RBAC: add admissionregistration.k8s.io and
  certificates.k8s.io API groups to suppress forbidden errors
- Fix web-gateway gRPC stream reconnect: detect Unimplemented via both
  status code and error message text (proxy may remap codes)
- Add automated migration step to CI deploy pipeline with tracking table
  (_applied_migrations) so migrations are applied before service rollouts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-02-28 23:26:43 +01:00
parent fd7d646aa6
commit 8ee1f810e6
4 changed files with 63 additions and 1 deletions

View File

@@ -1419,6 +1419,32 @@ deploy:
# S3 binary share: services use generic runtime image + initContainer to fetch binaries from S3.
# Deploy = apply K8s manifests (picks up YAML changes) + rollout restart (fetch new binaries).
- echo "Deploying ${CI_COMMIT_SHA}..."
# Apply pending database migrations before deploying services
- |
echo "Applying database migrations..."
DB_POD=$(kubectl -n foxhunt get pod -l app.kubernetes.io/name=postgres -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [ -n "$DB_POD" ]; then
# Create tracking table if it doesn't exist
kubectl -n foxhunt exec "$DB_POD" -- psql -U foxhunt -d foxhunt -c \
"CREATE TABLE IF NOT EXISTS _applied_migrations (name TEXT PRIMARY KEY, applied_at TIMESTAMPTZ DEFAULT now())" 2>/dev/null || true
# Apply each migration in order, skipping already-applied ones
for f in $(ls migrations/*.sql | grep -v '\.down\.' | sort); do
MIGRATION_NAME=$(basename "$f")
ALREADY=$(kubectl -n foxhunt exec "$DB_POD" -- psql -U foxhunt -d foxhunt -tAc \
"SELECT 1 FROM _applied_migrations WHERE name = '${MIGRATION_NAME}'" 2>/dev/null || true)
if [ "$ALREADY" = "1" ]; then
echo " skip: $MIGRATION_NAME (already applied)"
else
echo " apply: $MIGRATION_NAME"
kubectl -n foxhunt exec -i "$DB_POD" -- psql -U foxhunt -d foxhunt < "$f"
kubectl -n foxhunt exec "$DB_POD" -- psql -U foxhunt -d foxhunt -c \
"INSERT INTO _applied_migrations (name) VALUES ('${MIGRATION_NAME}')"
fi
done
echo "Migrations complete"
else
echo "WARNING: postgres pod not found, skipping migrations"
fi
# Deploy monitoring stack (Loki, Tempo, Promtail, node-exporter, kube-state-metrics)
- kubectl apply -f infra/k8s/monitoring/loki.yaml -f infra/k8s/monitoring/tempo.yaml -f infra/k8s/monitoring/promtail.yaml -f infra/k8s/monitoring/node-exporter.yaml -f infra/k8s/monitoring/kube-state-metrics.yaml
# Apply proxy (picks up nginx config changes for dashboard.fxhnt.ai routing)

View File

@@ -183,6 +183,17 @@ async fn heartbeat_loop(tx: broadcast::Sender<String>) {
}
}
/// Check whether a gRPC error indicates the endpoint is not implemented.
/// Matches both the standard Unimplemented status code and error messages
/// containing "not implemented" (which proxies may return with a different code).
fn is_unimplemented(e: &tonic::Status) -> bool {
if e.code() == tonic::Code::Unimplemented {
return true;
}
let msg = e.message().to_lowercase();
msg.contains("not implemented") || msg.contains("unimplemented")
}
/// Reconnect wrapper with exponential backoff for a gRPC stream bridge task.
/// Stops retrying if the server returns Unimplemented (endpoint doesn't exist).
async fn reconnect_loop<F, Fut>(stream_name: &str, connect_fn: F)
@@ -202,7 +213,7 @@ where
backoff = Duration::from_secs(1);
info!("gRPC stream {} ended, reconnecting", stream_name);
}
Err(e) if e.code() == tonic::Code::Unimplemented => {
Err(e) if is_unimplemented(&e) => {
info!(
"gRPC stream {} not implemented on server, disabling",
stream_name

View File

@@ -65,6 +65,15 @@ rules:
resources:
- leases
verbs: ["list", "watch"]
- apiGroups: ["admissionregistration.k8s.io"]
resources:
- validatingwebhookconfigurations
- mutatingwebhookconfigurations
verbs: ["list", "watch"]
- apiGroups: ["certificates.k8s.io"]
resources:
- certificatesigningrequests
verbs: ["list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding

View File

@@ -0,0 +1,16 @@
-- OHLCV bar data for prediction generation
-- Used by trading_service prediction_generation_loop to query recent market data
CREATE TABLE IF NOT EXISTS ohlcv_bars (
id BIGSERIAL PRIMARY KEY,
symbol VARCHAR(32) NOT NULL,
timestamp TIMESTAMPTZ NOT NULL,
open DOUBLE PRECISION NOT NULL,
high DOUBLE PRECISION NOT NULL,
low DOUBLE PRECISION NOT NULL,
close DOUBLE PRECISION NOT NULL,
volume DOUBLE PRECISION NOT NULL DEFAULT 0,
UNIQUE (symbol, timestamp)
);
CREATE INDEX IF NOT EXISTS idx_ohlcv_bars_symbol_ts
ON ohlcv_bars (symbol, timestamp DESC);