@gscdump/lakehouse 2.0.3 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,7 +29,7 @@ Node.js 22 or newer is required.
29
29
  ```ts
30
30
  import { connectIcebergCatalog, listIcebergTables } from '@gscdump/lakehouse'
31
31
  import { encodeJsonBigintSafe } from '@gscdump/lakehouse/bigint'
32
- import { isCommitRateLimited } from '@gscdump/lakehouse/maintenance'
32
+ import { isCommitRateLimited, isCommitTransient } from '@gscdump/lakehouse/maintenance'
33
33
 
34
34
  const connection = await connectIcebergCatalog(config)
35
35
  const tables = await listIcebergTables(connection)
@@ -39,11 +39,18 @@ try {
39
39
  }
40
40
  catch (error) {
41
41
  if (isCommitRateLimited(error)) {
42
- // Retry using the caller's bounded backoff policy.
42
+ // 429 specifically: decorrelate concurrent writers (defer off-slot).
43
+ }
44
+ else if (isCommitTransient(error)) {
45
+ // 429 or an R2 5xx blip: worth another attempt at all.
43
46
  }
44
47
  }
45
48
  ```
46
49
 
50
+ `isCommitRateLimited` stays 429-only; `isCommitServerError` covers transient
51
+ R2 5xx responses; `isCommitTransient` is the union and is what the package's
52
+ own append-retry loop uses.
53
+
47
54
  The dataset registry is the normal authoring boundary. Raw Icebird table
48
55
  creation and append primitives are intentionally excluded from the package
49
56
  root so producers cannot silently bypass registered dataset definitions.
@@ -142,13 +142,43 @@ type IcebergAppendArgs = Parameters<typeof icebergAppend>[0];
142
142
  /**
143
143
  * True when `err` is an R2 Data Catalog commit rate-limit response
144
144
  * (`429 too many commits to this table`).
145
+ *
146
+ * Deliberately 429-ONLY. Callers use this to decide whether to decorrelate
147
+ * concurrent committers (defer the whole unit of work off-slot), which is the
148
+ * wrong response to a one-off server blip — see {@link isCommitServerError}.
145
149
  */
146
150
  declare function isCommitRateLimited(err: unknown): boolean;
147
151
  /**
148
- * `icebergAppend` wrapped with retry on R2 Data Catalog 429 commit
149
- * rate-limits, using full-jitter exponential back-off, plus a landed-check
150
- * idempotency guard so a 429 whose commit actually landed is never re-applied
151
- * (see `deriveAppendId`/`appendAlreadyLanded`).
152
+ * True when `err` is a transient server-side 5xx from R2 — a bare
153
+ * `500 Internal Server Error` on a data/metadata object PUT, or a 502/503/504
154
+ * from the catalog REST API.
155
+ *
156
+ * These carry no S3 error code and no body; they are single-request blips that
157
+ * clear on the next call. Distinct from {@link isCommitRateLimited} so a caller
158
+ * that must decorrelate writers on a 429 does not also do so on a server error.
159
+ */
160
+ declare function isCommitServerError(err: unknown): boolean;
161
+ /**
162
+ * The retry predicate: is `err` worth another append attempt at all?
163
+ *
164
+ * Union of {@link isCommitRateLimited} and {@link isCommitServerError}.
165
+ * Everything else — 4xx, catalog conflicts icebird already exhausted its own
166
+ * 412/409 retries on, schema/validation failures — is permanent and must
167
+ * propagate on the first attempt.
168
+ */
169
+ declare function isCommitTransient(err: unknown): boolean;
170
+ /**
171
+ * `icebergAppend` wrapped with retry on transient commit failures
172
+ * ({@link isCommitTransient}: 429 rate-limits and R2 5xx blips), using
173
+ * full-jitter exponential back-off, plus a landed-check idempotency guard so a
174
+ * failure whose commit actually landed is never re-applied (see
175
+ * `deriveAppendId`/`appendAlreadyLanded`).
176
+ *
177
+ * Retrying is safe because `icebergAppend` writes data + manifest files BEFORE
178
+ * the atomic catalog pointer swap, so a 5xx during the upload phase aborts
179
+ * before anything is referenced by a snapshot; the retry writes fresh files and
180
+ * commits once. A 5xx on the pointer swap itself is covered by the
181
+ * landed-check.
152
182
  */
153
183
  declare function icebergAppendRetrying(args: IcebergAppendArgs, options?: CommitRetryOptions): Promise<void>;
154
184
  /** A data file in the current snapshot's manifest, scoped to one partition. */
@@ -210,4 +240,4 @@ declare function invalidateSnapshotRef(cache: CatalogCache, namespace: string, t
210
240
  * the def's own spec + identity/dims values.
211
241
  */
212
242
  declare function resolveIcebergDataFiles(conn: IcebergConnection, opts: ResolveIcebergDataFilesOptions): Promise<IcebergListedDataFile[]>;
213
- export { CommitRetryOptions, ConnectIcebergOptions, IcebergAppendArgs, IcebergCatalogConfig, IcebergConnection, IcebergListedDataFile, IcebergPartitionSpec, IcebergPartitionSpecField, IcebergSchema, IcebergSchemaField, IcebergSortOrder, IcebergSortOrderField, IcebergTableOpResult, QueryProfiler, ResolveIcebergDataFilesOptions, catalogCacheScope, connectIcebergCatalog, dropIcebergTables, ensureIcebergNamespace, icebergAppendRetrying, invalidateSnapshotRef, isCommitRateLimited, listIcebergTables, resolveIcebergDataFiles };
243
+ export { CommitRetryOptions, ConnectIcebergOptions, IcebergAppendArgs, IcebergCatalogConfig, IcebergConnection, IcebergListedDataFile, IcebergPartitionSpec, IcebergPartitionSpecField, IcebergSchema, IcebergSchemaField, IcebergSortOrder, IcebergSortOrderField, IcebergTableOpResult, QueryProfiler, ResolveIcebergDataFilesOptions, catalogCacheScope, connectIcebergCatalog, dropIcebergTables, ensureIcebergNamespace, icebergAppendRetrying, invalidateSnapshotRef, isCommitRateLimited, isCommitServerError, isCommitTransient, listIcebergTables, resolveIcebergDataFiles };
package/dist/catalog.mjs CHANGED
@@ -129,6 +129,22 @@ function isCommitRateLimited(err) {
129
129
  const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
130
130
  return msg.includes("429") || msg.includes("too many commits") || msg.includes("rate limit");
131
131
  }
132
+ const TRANSIENT_SERVER_STATUSES = /* @__PURE__ */ new Set([
133
+ 500,
134
+ 502,
135
+ 503,
136
+ 504
137
+ ]);
138
+ const TRANSIENT_SERVER_MESSAGE_RE = /\b(?:500 internal server error|502 bad gateway|503 service unavailable|504 gateway time-?out)\b/i;
139
+ function isCommitServerError(err) {
140
+ const status = err && typeof err === "object" ? err.status : void 0;
141
+ if (typeof status === "number") return TRANSIENT_SERVER_STATUSES.has(status);
142
+ const msg = err instanceof Error ? err.message : String(err);
143
+ return TRANSIENT_SERVER_MESSAGE_RE.test(msg);
144
+ }
145
+ function isCommitTransient(err) {
146
+ return isCommitRateLimited(err) || isCommitServerError(err);
147
+ }
132
148
  function defaultCommitSleep(ms) {
133
149
  return new Promise((resolve) => setTimeout(resolve, ms));
134
150
  }
@@ -151,7 +167,7 @@ async function icebergAppendRetrying(args, options = {}) {
151
167
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
152
168
  const err = await icebergAppend(stampedArgs).then(() => void 0, (e) => e);
153
169
  if (err === void 0) return;
154
- if (!isCommitRateLimited(err)) throw err;
170
+ if (!isCommitTransient(err)) throw err;
155
171
  if (await appendAlreadyLanded(args, appendId)) return;
156
172
  if (attempt === maxAttempts - 1) throw err;
157
173
  const ceiling = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
@@ -181,7 +197,7 @@ async function icebergAppendBatchesRetrying(args, options) {
181
197
  batches: batchFactory()
182
198
  }).then(() => void 0, (e) => e);
183
199
  if (err === void 0) return true;
184
- if (!isCommitRateLimited(err)) throw err;
200
+ if (!isCommitTransient(err)) throw err;
185
201
  if (await appendAlreadyLanded(args, appendId)) return true;
186
202
  if (attempt === maxAttempts - 1) throw err;
187
203
  const ceiling = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
@@ -272,7 +288,11 @@ async function loadSnapshotId(conn, namespace, table, cache, now) {
272
288
  if (cache) {
273
289
  await cachePut(cache, snapshotRefKey(scope, namespace, table), snapshotId, SNAPSHOT_REF_TTL_MS, now);
274
290
  if (snapshotId != null) {
275
- if (stringifyBigintSafe(metadata).length <= MAX_CACHED_METADATA_BYTES) await cachePut(cache, metadataRefKey(scope, namespace, table, snapshotId), metadata, METADATA_TTL_MS, now);
291
+ const serialized = stringifyBigintSafe(metadata);
292
+ if (serialized.length <= MAX_CACHED_METADATA_BYTES) {
293
+ const cacheableMetadata = JSON.parse(serialized);
294
+ await cachePut(cache, metadataRefKey(scope, namespace, table, snapshotId), cacheableMetadata, METADATA_TTL_MS, now);
295
+ }
276
296
  }
277
297
  }
278
298
  return {
@@ -348,4 +368,4 @@ async function resolveIcebergDataFiles(conn, opts) {
348
368
  }
349
369
  return out;
350
370
  }
351
- export { catalogCacheScope, connectIcebergCatalog, dropIcebergTables, ensureIcebergNamespace, icebergAppendBatchesRetrying, icebergAppendRetrying, invalidateSnapshotRef, isCommitRateLimited, listIcebergTables, resolveIcebergDataFiles };
371
+ export { catalogCacheScope, connectIcebergCatalog, dropIcebergTables, ensureIcebergNamespace, icebergAppendBatchesRetrying, icebergAppendRetrying, invalidateSnapshotRef, isCommitRateLimited, isCommitServerError, isCommitTransient, listIcebergTables, resolveIcebergDataFiles };
@@ -1,3 +1,3 @@
1
- import { isCommitRateLimited } from "./catalog.mjs";
1
+ import { isCommitRateLimited, isCommitServerError, isCommitTransient } from "./catalog.mjs";
2
2
  import { SweepListPage, SweepListedObject, SweepStorageClient, SweepUncommittedOrphansOptions, SweepUncommittedOrphansResult, sweepUncommittedOrphans } from "./orphan-sweep.mjs";
3
- export { type SweepListPage, type SweepListedObject, type SweepStorageClient, type SweepUncommittedOrphansOptions, type SweepUncommittedOrphansResult, isCommitRateLimited, sweepUncommittedOrphans };
3
+ export { type SweepListPage, type SweepListedObject, type SweepStorageClient, type SweepUncommittedOrphansOptions, type SweepUncommittedOrphansResult, isCommitRateLimited, isCommitServerError, isCommitTransient, sweepUncommittedOrphans };
@@ -1,3 +1,3 @@
1
- import { isCommitRateLimited } from "./catalog.mjs";
1
+ import { isCommitRateLimited, isCommitServerError, isCommitTransient } from "./catalog.mjs";
2
2
  import { sweepUncommittedOrphans } from "./orphan-sweep.mjs";
3
- export { isCommitRateLimited, sweepUncommittedOrphans };
3
+ export { isCommitRateLimited, isCommitServerError, isCommitTransient, sweepUncommittedOrphans };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/lakehouse",
3
3
  "type": "module",
4
- "version": "2.0.3",
4
+ "version": "2.0.5",
5
5
  "description": "Dataset-agnostic Iceberg lakehouse layer + dataset registry for R2 Data Catalog producers (ADR-0021).",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",