Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ members = [
"packages/prices-api",
"packages/backfill-freshness-probe",
"packages/mtls-notafter-probe",
"packages/write-amplification-probe",
"packages/pool-registry-seed",
]

Expand Down
4 changes: 1 addition & 3 deletions infra/envs/production.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,12 @@
"cleanup": "cron(0 3 * * ? *)",
"enrichment": "rate(1 hour)",
"backfillFreshnessProbe": "rate(15 minutes)",
"mtlsNotafterProbe": "rate(1 day)",
"writeAmplificationProbe": "rate(1 hour)"
"mtlsNotafterProbe": "rate(1 day)"
},
"opsAlarms": {
"sdexPushFreshnessSeconds": 604800,
"mtlsNotAfterDaysThreshold": 30,
"ledgerProcessorLagSeconds": 120,
"writeAmplificationRowsPerHour": 50000000,
"slack": {
"workspaceIdSsmParam": "/prices/production/slack-workspace-id",
"channelIdSsmParam": "/prices/production/slack-channel-id"
Expand Down
61 changes: 0 additions & 61 deletions infra/src/lib/stacks/eventbridge-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,6 @@ const MTLS_NOTAFTER_PROBE_ASSET_DIR =
process.env['MTLS_NOTAFTER_PROBE_ASSET_DIR'] ??
'../target/lambda/mtls-notafter-probe';

/** Cargo-lambda build output for the `write-amplification-probe` (task 0133). */
const WRITE_AMPLIFICATION_PROBE_ASSET_DIR =
process.env['WRITE_AMPLIFICATION_PROBE_ASSET_DIR'] ??
'../target/lambda/write-amplification-probe';

export interface EventBridgeStackProps extends cdk.StackProps {
readonly config: EnvironmentConfig;
}
Expand Down Expand Up @@ -89,15 +84,13 @@ export class EventBridgeStack extends cdk.Stack {
public readonly enrichmentRule: events.Rule;
public readonly backfillFreshnessProbeRule: events.Rule;
public readonly mtlsNotafterProbeRule: events.Rule;
public readonly writeAmplificationProbeRule: events.Rule;
public readonly assetDiscoveryFunction: lambda.Function;
public readonly cleanupFunction: lambda.Function;
public readonly supplyFunction: lambda.Function;
public readonly oracleFunction: lambda.Function;
public readonly enrichmentFunction: lambda.Function;
public readonly backfillFreshnessProbeFunction: lambda.Function;
public readonly mtlsNotafterProbeFunction: lambda.Function;
public readonly writeAmplificationProbeFunction: lambda.Function;

constructor(scope: Construct, id: string, props: EventBridgeStackProps) {
super(scope, id, props);
Expand Down Expand Up @@ -176,16 +169,6 @@ export class EventBridgeStack extends cdk.Stack {
},
);

this.writeAmplificationProbeRule = new events.Rule(
this,
'WriteAmplificationProbeRule',
{
ruleName: `prices-${env}-write-amplification-probe`,
description: `Publishes max rows-written/hour per prices table → Prices/Ingest MaxRowsWrittenPerHour (${env})`,
schedule: events.Schedule.expression(schedules.writeAmplificationProbe),
},
);

// -----------------------------------------------------------------
// Asset Discovery worker Lambda (task 0054) + its rule target.
// No VPC (ADR 0007 §6); mTLS to ClickHouse + S3 read on BE's ledger
Expand Down Expand Up @@ -577,56 +560,12 @@ export class EventBridgeStack extends cdk.Stack {
}),
);

// -----------------------------------------------------------------
// Write-amplification probe (task 0133) + its rate(1h) target. CH-only
// (no S3, no VPC): reads rows-written-per-hour per prices table from
// system.part_log and republishes the max as Prices/Ingest
// MaxRowsWrittenPerHour — the guardrail that would have caught the 0132
// egress bug (9,413× re-emit) in minutes instead of weeks. Reads as the
// `api` (prices_reader) identity, which is granted SELECT on system.part_log
// (task 0133 prerequisite) alongside its prices.* read — NOT the ingestion
// identity, keeping the probe read-only.
// -----------------------------------------------------------------
const writeAmplification = createWorkerLambda(this, {
config,
accountId,
mtlsSecretName: apiMtlsSecretName,
idPrefix: 'WriteAmplificationProbe',
name: 'write-amplification-probe',
assetDir: WRITE_AMPLIFICATION_PROBE_ASSET_DIR,
memorySize: 256,
// One aggregate SELECT over system.part_log + one PutMetricData; fast.
timeout: cdk.Duration.minutes(1),
secretsExtensionLayer,
chDomain,
rule: this.writeAmplificationProbeRule,
alarmDescription:
'Write-amplification probe invocation errors — the rows-written metric may be stale, blinding the write-amplification alarm.',
alarmPeriod: cdk.Duration.hours(1),
errorAlarmActions: [opsAlarmAction],
});
this.writeAmplificationProbeFunction = writeAmplification.function;

writeAmplification.role.addToPolicy(
new iam.PolicyStatement({
sid: 'PublishIngestMetrics',
actions: ['cloudwatch:PutMetricData'],
resources: ['*'],
conditions: {
StringEquals: { 'cloudwatch:namespace': 'Prices/Ingest' },
},
}),
);

new cdk.CfnOutput(this, 'BackfillFreshnessProbeFunctionName', {
value: this.backfillFreshnessProbeFunction.functionName,
});
new cdk.CfnOutput(this, 'MtlsNotafterProbeFunctionName', {
value: this.mtlsNotafterProbeFunction.functionName,
});
new cdk.CfnOutput(this, 'WriteAmplificationProbeFunctionName', {
value: this.writeAmplificationProbeFunction.functionName,
});

cdk.Tags.of(this).add('Project', 'stellar-prices-api');
cdk.Tags.of(this).add('ManagedBy', 'cdk');
Expand Down
58 changes: 0 additions & 58 deletions infra/src/lib/stacks/observability-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,6 @@ export class ObservabilityStack extends cdk.Stack {
public readonly sdexPushFreshnessAlarm: cloudwatch.Alarm;
/** mTLS client-cert expiry alarm (§7 / §11.4). */
public readonly mtlsNotAfterAlarm: cloudwatch.Alarm;

/** Write-amplification guardrail (task 0133). */
public readonly writeAmplificationAlarm: cloudwatch.Alarm;
/** Live ledger-processor ingestion-lag alarm (task 0056 finding B). */
public readonly ledgerProcessorLagAlarm: cloudwatch.Alarm;
/** Live ledger-processor invocation-error alarm (task 0056 finding B). */
Expand Down Expand Up @@ -485,61 +482,6 @@ export class ObservabilityStack extends cdk.Stack {
this.mtlsNotAfterAlarm.addAlarmAction(snsAction);
this.mtlsNotAfterAlarm.addOkAction(snsAction);

// Write amplification (task 0133 — the guardrail for the 0132 egress bug).
// The write-amplification-probe publishes the max rows-written-per-hour
// across all prices tables as Prices/Ingest MaxRowsWrittenPerHour; alarm
// when it stays above the operator-tuned threshold. A quiet hour publishes a
// real 0 (healthy), so missing data is non-breaching — probe-down is covered
// by the probe's own error alarm.
//
// ⚠️ system.part_log counts ALL writes to prices.* — including legitimate
// BULK loads (the 0088 backfill, coarse pre-rolls, enrichment bursts), which
// an absolute row count cannot distinguish from a re-emit amplification by
// *magnitude*: a 14-day part_log measurement (task 0133) found a legit
// one-hour `_bak` copy at 154M rows/hour — HIGHER than the 0132 bug's ~130M.
// What separates them is DURATION: legit bulk is bursty (the `_bak` was one
// hour; the sustained legit peak is price_ohlcv_1m at ~16M/hour on a backfill
// day), while a 0132-class runaway persists for days. Hence: (1) the alarm
// requires a *sustained* 3-hour breach (datapointsToAlarm=3), which clears the
// one-hour spikes; (2) the threshold (default 50M, measured to sit ~3× above
// the ~16M sustained legit peak and ~2.6× below the ~130M bug) is set so no
// legit event in the measured window breaches it for 3 sustained hours. An
// operator running a known heavy migration should still expect a possible fire
// and ack it or raise config.opsAlarms.writeAmplificationRowsPerHour for the
// window. A true "written vs deduplicated real rows" ratio (which a legit bulk
// load keeps ~1× while a re-emit pushes high) is the robust future enhancement.
//
// The 1h metric period is coupled to the probe's SQL window (INTERVAL 1 HOUR)
// and its rate(1 hour) schedule — see WINDOW_HOURS in the probe crate. All
// three must change together.
this.writeAmplificationAlarm = new cloudwatch.Alarm(
this,
'WriteAmplificationAlarm',
{
alarmName: `prices-${config.envName}-write-amplification`,
alarmDescription:
'A prices table has been written far above any legitimate steady-state rate for 3 consecutive hours (rows-written/hour above config.opsAlarms.writeAmplificationRowsPerHour). Likely a write-amplification regression like task 0132 (full-registry re-emit) — but a sustained heavy backfill/pre-roll can also trip it. Check system.part_log per table to find the offender; if it is a known bulk load, ack or raise the threshold for that window.',
metric: new cloudwatch.Metric({
namespace: 'Prices/Ingest',
metricName: 'MaxRowsWrittenPerHour',
dimensionsMap: { Environment: config.envName },
statistic: 'Maximum',
period: cdk.Duration.hours(1),
}),
threshold: config.opsAlarms.writeAmplificationRowsPerHour,
// Sustained 3-hour breach, not a single anomalous hour (task 0133
// review): the guardrail targets a persistent runaway, not a one-off
// legit burst.
evaluationPeriods: 3,
datapointsToAlarm: 3,
comparisonOperator:
cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
},
);
this.writeAmplificationAlarm.addAlarmAction(snsAction);
this.writeAmplificationAlarm.addOkAction(snsAction);

// -----------------------------------------------------------------
// Live ledger-processor health (task 0056 finding B). The core ingestion
// Lambda shipped unmonitored: `prices.ledger_processor.lag_seconds` existed
Expand Down
37 changes: 0 additions & 37 deletions infra/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,20 +113,6 @@ export interface EnvironmentConfig {
* Daily is ample for a 30-day threshold.
*/
readonly mtlsNotafterProbe: string;
/**
* Write-amplification probe (task 0133). Reads rows-written-per-hour per
* `prices.*` table from `system.part_log` (as `prices_reader`, which is
* granted `SELECT ON system.part_log`) and republishes the max as the
* `Prices/Ingest` `MaxRowsWrittenPerHour` metric the write-amplification
* alarm watches. Hourly is ample — the guardrail catches a sustained
* runaway (0132 bled for weeks), not a sub-hour spike.
*
* ⚠️ Must stay `rate(1 hour)`: it is coupled to the probe's trailing SQL
* window (`INTERVAL 1 HOUR`, `WINDOW_HOURS`) and the alarm's 1h metric
* period. Changing the cadence without changing the SQL window makes
* consecutive runs overlap (double-count) or gap the window.
*/
readonly writeAmplificationProbe: string;
};

// Ops alarms + notification (consumed by ObservabilityStack — task 0056)
Expand All @@ -151,20 +137,6 @@ export interface EnvironmentConfig {
readonly sdexPushFreshnessSeconds: number;
/** Days-to-NotAfter below which the mTLS cert-expiry alarm fires (30). */
readonly mtlsNotAfterDaysThreshold: number;
/**
* Rows-written-per-hour to any single `prices.*` table above which the
* write-amplification alarm fires — for the required 3-hour sustained window
* (task 0133; see the alarm in observability-stack.ts). Default 50,000,000,
* set from a 14-day `system.part_log` measurement, NOT a guess: the highest
* *sustained* legitimate load is `price_ohlcv_1m` at ~16M rows/hour during a
* backfill/reprice day (07-26, ~18h). One-hour bulk spikes go higher
* (rollup-rework `_bak` copies hit 154M/hour) but clear within the 3-hour
* window. Task 0132 ran ~130M/hour for days. 50M sits ~3× above the sustained
* legit peak (headroom for backfill growth) and ~2.6× below a 0132-class
* runaway, and no legit event in the measured window breaches it for 3
* sustained hours. Operator-tunable; raise it during a known heavy migration.
*/
readonly writeAmplificationRowsPerHour: number;
/**
* Ingestion-lag threshold (seconds) for the live ledger-processor alarm
* (task 0056 finding B). Watches the `prices-ingest-{env}` SQS queue's
Expand Down Expand Up @@ -358,7 +330,6 @@ export function validateConfig(config: EnvironmentConfig): void {
'enrichment',
'backfillFreshnessProbe',
'mtlsNotafterProbe',
'writeAmplificationProbe',
] as const;
for (const key of expectedKeys) {
const value = schedules[key];
Expand Down Expand Up @@ -400,14 +371,6 @@ export function validateConfig(config: EnvironmentConfig): void {
`opsAlarms.ledgerProcessorLagSeconds must be a positive integer (seconds), got: ${ops.ledgerProcessorLagSeconds}`,
);
}
if (
!Number.isInteger(ops.writeAmplificationRowsPerHour) ||
ops.writeAmplificationRowsPerHour < 1
) {
errors.push(
`opsAlarms.writeAmplificationRowsPerHour must be a positive integer (rows/hour), got: ${ops.writeAmplificationRowsPerHour}`,
);
}
if (ops.slack !== undefined) {
const isSsmName = (v: unknown): boolean =>
typeof v === 'string' && v.startsWith('/') && v.length > 1;
Expand Down
Loading
Loading