microservices

Seeded cross-cutting service defects — missing timeouts, non-idempotent consumers, PII in traces.

What this is

9 deliberate defects, each carrying a marker naming the rule in standards/ it violates. It measures recall, and its pass condition is that every one of the 5 BLOCKER markers is flagged. The rule id in each marker means the scorer grades attribution too: a reviewer that finds the bug but cites the wrong rule counts as caught, and separately as misattributed.

How to onboard it

Nothing is installed and nothing here is ever merged. The corpus is used by opening a throwaway pull request on a pilot repository already onboarded for this stack, adding this directory and seeded/clean together. Label it redline-exempt so the readiness gate does not block a pull request nobody will merge, wait for the automated review to finish, then score it.

seed-canary.yml does exactly this on a schedule and closes the pull request afterwards, including when the run fails.

How to use it

terminal
$ GH_TOKEN=... npx redlinegate metrics score-seeds --repo <org>/<repo> --pr <n>
SeedSeverityRule it violatesDefect
1BLOCKERmicroservices/non-idempotent-consumerat-least-once consumer with no idempotency — a redelivery
3BLOCKERmicroservices/missing-timeoutoutbound call with no timeout — the default is infinite
4BLOCKERmicroservices/swallowed-errorsnon-2xx treated as success
5BLOCKERmicroservices/unversioned-breaking-changebreaking change to a published contract with no version bump:
7BLOCKERmicroservices/pii-in-telemetryPII in a metric label — unbounded cardinality and a data-exposure leak
2HIGHmicroservices/dual-write-no-outboxDB write and publish in one logical step with no outbox —
6HIGHmicroservices/retry-without-backoffretries with no backoff or jitter — turns a blip into a thundering herd
8HIGHmicroservices/readiness-liveness-conflatedreadiness that always reports healthy — traffic keeps arriving while
9HIGHmicroservices/unbounded-workunbounded batch accepted from external input

Expected output

A score from scripts/score-seeds.mjs: BLOCKER recall, false positives, and rule attribution. The canary appends it to data/seed-scores.jsonl and fails the run if BLOCKER recall drops below 1.0 or the clean corpus attracts a false positive. Of the 9 defects here, 5 BLOCKER and 4 HIGH — only the BLOCKER count is a pass condition.

How to edit it

Add a defect by adding the code and one marker line above it. There is no separate expectations file to keep in sync — scripts/score-seeds.mjs parses the markers straight out of the source:

marker format
SEED <n> [BLOCKER|HIGH|SUGGESTION] (<stack>/<rule-slug>) <short description>

scripts/validate.mjs fails CI if a seed cites a rule that does not exist, or claims a severity higher than that rule carries in the standard — so a marker cannot quietly drift away from the rule it is testing.

The full file

seeded/microservices/seeded_violations_service.ts · 66 lines · 2.7 KB
// DO NOT MERGE — Redline validation seed for the cross-cutting microservice rules.
// Every `SEED n [SEVERITY] (rule-id)` marker must be flagged at that severity or higher,
// citing that rule id. Score with scripts/score-seeds.mjs.

type ChargeMessage = { messageId: string; accountId: string; amountMinor: number };

const db = {} as { charge(a: string, m: number): Promise<void>; wasApplied(id: string): Promise<boolean> };
const bus = {} as { publish(topic: string, payload: unknown): Promise<void> };

export class ChargeConsumer {
  // SEED 1 [BLOCKER] (microservices/non-idempotent-consumer) at-least-once consumer with no idempotency — a redelivery
  // double-charges the customer
  async onMessage(msg: ChargeMessage) {
    await db.charge(msg.accountId, msg.amountMinor);

    // SEED 2 [HIGH] (microservices/dual-write-no-outbox) DB write and publish in one logical step with no outbox —
    // a crash here leaves the ledger and the topic permanently inconsistent
    await bus.publish('charges.applied', { accountId: msg.accountId });
  }
}

export async function callPricing(accountId: string) {
  // SEED 3 [BLOCKER] (microservices/missing-timeout) outbound call with no timeout — the default is infinite
  const res = await fetch(`https://pricing.internal/quote/${accountId}`);

  // SEED 4 [BLOCKER] (microservices/swallowed-errors) non-2xx treated as success
  if (!res.ok) {
    console.error('pricing failed');
  }
  return res.json();
}

// SEED 5 [BLOCKER] (microservices/unversioned-breaking-change) breaking change to a published contract with no version bump:
// `msisdn` was returned by v1 consumers and is now removed and retyped
export type QuoteResponseV1 = {
  accountId: string;
  amount: number; // was `amountMinor: string`
};

export async function retry(fn: () => Promise<void>) {
  // SEED 6 [HIGH] (microservices/retry-without-backoff) retries with no backoff or jitter — turns a blip into a thundering herd
  for (let i = 0; i < 5; i++) {
    try {
      return await fn();
    } catch {
      continue;
    }
  }
}

// SEED 7 [BLOCKER] (microservices/pii-in-telemetry) PII in a metric label — unbounded cardinality and a data-exposure leak
export function recordCharge(metrics: { inc(name: string, labels: object): void }, msisdn: string) {
  metrics.inc('charges_total', { msisdn });
}

// SEED 8 [HIGH] (microservices/readiness-liveness-conflated) readiness that always reports healthy — traffic keeps arriving while
// the dependency is down
export function ready() {
  return { status: 'ok' };
}

// SEED 9 [HIGH] (microservices/unbounded-work) unbounded batch accepted from external input
export async function bulk(ids: string[]) {
  return Promise.all(ids.map((id) => callPricing(id)));
}