osolmaz commited on
Commit
2b422fa
·
verified ·
1 Parent(s): 1b4196c

fix(consumer): skip config replay for enrichment batches

Browse files
.xtap-deployment.json CHANGED
@@ -1,18 +1,18 @@
1
  {
2
- "source_revision": "35f8318026a3ad56868fd0fc57a42142a50bef3b",
3
  "enrichment_revision_handoff": {
4
  "active_generation": 87,
5
  "activation_sha256": "0d09e98a8073db48faf0ea56d7600fc581c1106ae98312d2ef8fd2273a48f404",
6
  "run_id": "xtap-b0e01d0c7828c30def3948bcb4251e68",
7
  "plan_sha256": "b0e01d0c7828c30def3948bcb4251e68ddc4151366cb31d3d96ea01ba86b2d85",
8
  "plan_worker_revision": "6a3c4855dd21f469fab63705f77cd7b2f4095726",
9
- "target_worker_revision": "35f8318026a3ad56868fd0fc57a42142a50bef3b",
10
  "contract_sha256": "2a2814ad4162457c0b8fe6de9cc07e0813f9fffadd3bde21ca46c731cc58de69",
11
  "source_snapshot_revision": "5bcd681d22132a5009c5f3e72d33ffb6fb1ac1e1269a23dd3b84091f570a8dce",
12
- "checkpoint_pointer_sha256": "da49a267e516137c0eda63d197847ac307782f4cf31a8ea6624c04a1d4375c91",
13
- "checkpoint_sequence": 1,
14
- "checkpoint_key": "operations/enrichment/runs/xtap-b0e01d0c7828c30def3948bcb4251e68/checkpoints/sha256-2fce6497a6f99ca29439e57178122c9692a10c5cc01ad61f07a2ef7c265e4527/checkpoint.hfjob",
15
- "checkpoint_sha256": "2fce6497a6f99ca29439e57178122c9692a10c5cc01ad61f07a2ef7c265e4527",
16
- "checkpoint_bytes": 40241
17
  }
18
  }
 
1
  {
2
+ "source_revision": "cfd46b156aeec2c30225e63f9873c295fb247731",
3
  "enrichment_revision_handoff": {
4
  "active_generation": 87,
5
  "activation_sha256": "0d09e98a8073db48faf0ea56d7600fc581c1106ae98312d2ef8fd2273a48f404",
6
  "run_id": "xtap-b0e01d0c7828c30def3948bcb4251e68",
7
  "plan_sha256": "b0e01d0c7828c30def3948bcb4251e68ddc4151366cb31d3d96ea01ba86b2d85",
8
  "plan_worker_revision": "6a3c4855dd21f469fab63705f77cd7b2f4095726",
9
+ "target_worker_revision": "cfd46b156aeec2c30225e63f9873c295fb247731",
10
  "contract_sha256": "2a2814ad4162457c0b8fe6de9cc07e0813f9fffadd3bde21ca46c731cc58de69",
11
  "source_snapshot_revision": "5bcd681d22132a5009c5f3e72d33ffb6fb1ac1e1269a23dd3b84091f570a8dce",
12
+ "checkpoint_pointer_sha256": "55bfdde220bcf25eb008481e587d8292a13aa3ed956e9b92ebc51a6f32dab696",
13
+ "checkpoint_sequence": 237,
14
+ "checkpoint_key": "operations/enrichment/runs/xtap-b0e01d0c7828c30def3948bcb4251e68/checkpoints/sha256-30d20fbcf0ce6629c95fa0419e9e117cfcb7b4411a7414611b5df3dcf449d38c/checkpoint.hfjob",
15
+ "checkpoint_sha256": "30d20fbcf0ce6629c95fa0419e9e117cfcb7b4411a7414611b5df3dcf449d38c",
16
+ "checkpoint_bytes": 41668
17
  }
18
  }
space/src/configuration-tail.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { BucketLog, BucketSnapshot } from "./bucket-log.js";
2
+
3
+ /** Inspect only newly verified segments. Mixed append batches are not config writes. */
4
+ export async function hasConfigurationWrites(
5
+ log: Pick<BucketLog, "loadSegment">,
6
+ base: BucketSnapshot,
7
+ target: BucketSnapshot,
8
+ ): Promise<boolean> {
9
+ const known = new Set(base.files.map((file) => file.key));
10
+ for (const file of target.files) {
11
+ if (known.has(file.key)) continue;
12
+ if (!file.key.includes("/config/") && !file.key.includes("/mixed/")) continue;
13
+ const segment = await log.loadSegment(file);
14
+ if (segment.operations.some((operation) => operation.mode === "write")) return true;
15
+ }
16
+ return false;
17
+ }
space/src/server.ts CHANGED
@@ -12,6 +12,7 @@ import { consumerFetch, withConsumerDeadline } from "./consumer-deadline.js";
12
  import { createApp } from "./app.js";
13
  import type { AppReadiness } from "./app.js";
14
  import { loadConfig } from "./config.js";
 
15
  import { BucketLog, createRawBucketClient } from "./bucket-log.js";
16
  import type { BucketSnapshot } from "./bucket-log.js";
17
  import type { StorageState } from "./storage-state.js";
@@ -263,14 +264,6 @@ async function primeStorageTextCache(snapshot: BucketSnapshot, stage: string): P
263
  });
264
  }
265
 
266
- function hasConfigurationTail(base: BucketSnapshot, final: BucketSnapshot): boolean {
267
- const baseKeys = new Set(base.files.map((file) => file.key));
268
- return final.files.some(
269
- (file) =>
270
- !baseKeys.has(file.key) && (file.key.includes("/config/") || file.key.includes("/mixed/")),
271
- );
272
- }
273
-
274
  function applyIndexStats(): void {
275
  const stats = index.stats();
276
  rebuilt = { files: stats.tweetFiles, tweets: stats.tweetRows };
@@ -304,7 +297,7 @@ async function refreshExternalEnrichment(): Promise<void> {
304
  if (!storageCredentialOk(storageCredential)) return;
305
  const baseSnapshot = index.sourceSnapshot();
306
  const discovered = await log.discoverSnapshot(baseSnapshot.files);
307
- const configChanged = hasConfigurationTail(baseSnapshot, discovered.snapshot);
308
  let nextTaxonomy = taxonomy;
309
  if (configChanged) {
310
  await primeStorageTextCache(discovered.snapshot, "configuration-refresh");
 
12
  import { createApp } from "./app.js";
13
  import type { AppReadiness } from "./app.js";
14
  import { loadConfig } from "./config.js";
15
+ import { hasConfigurationWrites } from "./configuration-tail.js";
16
  import { BucketLog, createRawBucketClient } from "./bucket-log.js";
17
  import type { BucketSnapshot } from "./bucket-log.js";
18
  import type { StorageState } from "./storage-state.js";
 
264
  });
265
  }
266
 
 
 
 
 
 
 
 
 
267
  function applyIndexStats(): void {
268
  const stats = index.stats();
269
  rebuilt = { files: stats.tweetFiles, tweets: stats.tweetRows };
 
297
  if (!storageCredentialOk(storageCredential)) return;
298
  const baseSnapshot = index.sourceSnapshot();
299
  const discovered = await log.discoverSnapshot(baseSnapshot.files);
300
+ const configChanged = await hasConfigurationWrites(log, baseSnapshot, discovered.snapshot);
301
  let nextTaxonomy = taxonomy;
302
  if (configChanged) {
303
  await primeStorageTextCache(discovered.snapshot, "configuration-refresh");
space/tests/configuration-tail.test.ts ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type {
3
+ BucketOperation,
4
+ BucketSegment,
5
+ BucketSnapshot,
6
+ BucketSnapshotFile,
7
+ } from "../src/bucket-log.js";
8
+ import { hasConfigurationWrites } from "../src/configuration-tail.js";
9
+
10
+ function file(kind: "config" | "mixed" | "tweet", n: number): BucketSnapshotFile {
11
+ const hash = String(n).padStart(64, "0");
12
+ return {
13
+ key: `v1/segments/${kind}/2026/09/08/1788868800000-00000000-0000-4000-8000-000000000000-${hash}.json.gz`,
14
+ oid: hash,
15
+ size: 100,
16
+ content_sha256: hash,
17
+ };
18
+ }
19
+ function snapshot(...files: BucketSnapshotFile[]): BucketSnapshot {
20
+ return {
21
+ schema_version: 1,
22
+ bucket: "owner/raw",
23
+ files: files.sort((a, b) => a.key.localeCompare(b.key)),
24
+ };
25
+ }
26
+ function reader(operations: BucketOperation[]) {
27
+ const segment: BucketSegment = {
28
+ schema_version: 1,
29
+ transaction_id: "00000000-0000-4000-8000-000000000000",
30
+ created_at: "2026-09-08T12:00:00.000Z",
31
+ operations,
32
+ };
33
+ return { loadSegment: vi.fn(() => Promise.resolve(segment)) };
34
+ }
35
+ const append: BucketOperation = {
36
+ mode: "append",
37
+ path: "enrichment/2026/09/enrichment-2026-09-08.jsonl",
38
+ lines: ["{}"],
39
+ };
40
+ const write: BucketOperation = { mode: "write", path: "config/labels.json", content: "{}" };
41
+
42
+ describe("configuration changes in verified source tails", () => {
43
+ it("does not reread old mixed/config segments or new tweet-only segments", async () => {
44
+ const base = snapshot(file("config", 1), file("mixed", 2));
45
+ const log = reader([write]);
46
+ expect(await hasConfigurationWrites(log, base, snapshot(...base.files, file("tweet", 3)))).toBe(
47
+ false,
48
+ );
49
+ expect(log.loadSegment).not.toHaveBeenCalled();
50
+ });
51
+
52
+ it("keeps mixed enrichment append batches out of the full configuration refresh", async () => {
53
+ const old = file("mixed", 1),
54
+ added = file("mixed", 2);
55
+ const log = reader([
56
+ append,
57
+ { ...append, path: "enrichment/attempts/2026/09/attempts-2026-09-08.jsonl" },
58
+ ]);
59
+ expect(await hasConfigurationWrites(log, snapshot(old), snapshot(old, added))).toBe(false);
60
+ expect(log.loadSegment).toHaveBeenCalledExactlyOnceWith(added);
61
+ });
62
+
63
+ it.each(["config", "mixed"] as const)(
64
+ "detects actual writes in new %s segments, including late arrivals",
65
+ async (kind) => {
66
+ const old = file(kind, 2),
67
+ added = file(kind, 1);
68
+ const log = reader(kind === "mixed" ? [append, write] : [write]);
69
+ expect(await hasConfigurationWrites(log, snapshot(old), snapshot(added, old))).toBe(true);
70
+ expect(log.loadSegment).toHaveBeenCalledExactlyOnceWith(added);
71
+ },
72
+ );
73
+
74
+ it("does not mistake an unreadable new segment for unchanged configuration", async () => {
75
+ const log = { loadSegment: vi.fn(() => Promise.reject(new Error("checksum mismatch"))) };
76
+ await expect(
77
+ hasConfigurationWrites(log, snapshot(), snapshot(file("mixed", 1))),
78
+ ).rejects.toThrow("checksum mismatch");
79
+ });
80
+ });