@cloudbitmaps/gcs 0.11.2 → 0.12.0

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
@@ -42,6 +42,8 @@ It builds its own client from Application Default Credentials. Any other key is
42
42
  | `client` | your own `Storage` client. Pass it as `client`; `storage` is refused, since in `CloudRoaring` that word means the backend. Build it with `retryOptions: { autoRetry: false }` (see below) |
43
43
  | `projectId`, `apiEndpoint` | build a client for you, such as one for fake-gcs-server; refused beside `client` |
44
44
  | `simpleUploadThresholdBytes`, `maxObjectBytes` | the size up to which an object is one simple request (default 8 MiB) and the largest object (default 5 TiB, GCS's maximum) |
45
+ | `readTimeoutMs` | cut off a read that has run this long, in milliseconds (see below); `0`, the default, sets no timeout |
46
+ | `conditionalDelete` | whether the registry removes a deleted row with an object delete under `ifGenerationMatch`, rather than leaving a tombstone every full listing reads. Off by default, on the public endpoint too: whether real GCS applies `ifGenerationMatch` to a delete has not been verified by a run against the service, and fake-gcs-server ignores it, so CI cannot show it. Set `true` to remove rows for good. Needs delete permission on the registry prefix |
45
47
 
46
48
  ## Before production
47
49
 
@@ -62,8 +64,38 @@ It builds its own client from Application Default Credentials. Any other key is
62
64
  resumable uploads on that client, which the library does not retry; the client `GcsStorage` builds keeps them.
63
65
  - **A client `timeout` does not bound a download on `@google-cloud/storage` 8.x**, so a read whose server stalls waits
64
66
  for it. Measured against a local server that never answers: still pending after 12 s with `timeout: 2000`.
67
+ **`readTimeoutMs` does, and it is off unless you set it.** It bounds each read as a whole: a generation's tail (with
68
+ the metadata read it falls back on for an empty object), a range of it, or a registry row. One deadline covers every
69
+ attempt the driver makes and the backoff between them, from the call into the driver (a credential fetch and any
70
+ wait for a socket count) to the end of the body. It counts time the process spends busy too: Node runs a due timer
71
+ before it reads a socket, so a synchronous stretch longer than the timeout fails the reads in flight even when their
72
+ responses have arrived. When it passes, the read throws `TransientError` naming the read and the timeout, and no
73
+ further attempt starts; the store's read retry runs it again, so with `readTimeoutMs: 2_000` a read that stalls on
74
+ every attempt fails after about 8.35 s (4 × 2 s plus up to 0.35 s of backoff; measured: 8.1 to 8.2 s). Uploads,
75
+ deletes and listings are not timed.
76
+ - **A read that times out before its server answers leaves its connection open.** The SDK cannot cancel a request
77
+ whose response has not begun, so that request holds its connection until the server answers or closes it, and until
78
+ then keeps a Node process from exiting: one per driver read, up to four per call through the store's retry, so an
79
+ outage at N reads a second adds about 4N held connections each second. On 8.x a read cut off after its response
80
+ began closes its own; on 7.x (checked on 7.22.0) it stays open too. A 404 whose error body arrives after the
81
+ deadline is reported as a `TransientError`, not `NotFoundError`.
82
+ - **Downloads go out on Node's global agent, so one the driver cuts off or refuses resets nothing else.** When a
83
+ download fails part-way, the SDK destroys the HTTP agent it went out on, and its own agent is one keep-alive pool the
84
+ whole process shares, so every other request in flight would be reset with it, uploads included. The SDK never
85
+ destroys the global agent. Measured on 8.1.0 with no timeout: a body cut off part-way reset a 64-byte upload and a
86
+ registry write on a second backend on the SDK's agent, and on the global agent all of them completed. The cost: the
87
+ global agent closes a connection idle for 5 seconds (the SDK's own pool does not), so a read after a longer pause
88
+ opens a new connection, with its TCP and TLS handshake, and downloads share that agent with any other `http` or
89
+ `https` request in the process. Raising `https.globalAgent.options.timeout` keeps idle connections longer.
65
90
  - **A transient failure of a write throws `TransientError`, and the write may or may not have landed.** Re-run the
66
- call, or check `store.generations(ref)`.
91
+ call, or check `store.generations(ref)`. The driver sends each registry write once; a load that gets no answer reads
92
+ the row and sends a fresh compare-and-swap from it, at most three times. A generation's object of up to
93
+ `simpleUploadThresholdBytes` is sent again, up to three more times, only after a `429` or `503`; every object carries
94
+ a random id in its metadata, so a first send that landed is told from another writer's object. A load whose row writes
95
+ all go unanswered throws `TransientError` and keeps its object. The waits before an object's re-sends use real
96
+ timers: the store's injected clock does not reach them, and `GcsStorage` takes no clock, so a test that throttles
97
+ one object write three times waits about 3.5 s at most (the publish's waits before a fresh row write do use the
98
+ store's clock).
67
99
 
68
100
  The [production checklist](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/production.md) covers each item, and the ones every backend shares: backups of the data and the registry, and a schedule for your loads.
69
101
 
package/dist/backend.d.ts CHANGED
@@ -18,8 +18,9 @@ export interface GcsStorageOptions {
18
18
  readonly prefix?: string;
19
19
  /**
20
20
  * A constructed `@google-cloud/storage` client. One is built from the ambient credentials when absent. Its retry
21
- * options apply to every request except the single-request conditional writes, which are sent once whatever they
22
- * say. In `@google-cloud/storage` 7.x and 8.x a download the SDK retries after a 408, 429, 500, 502, 503 or 504 can crash the process
21
+ * options apply to every request except the single-request conditional writes, which the driver sends without
22
+ * that retry whatever they say: a registry row once, and a generation's object again only after a throttle, under
23
+ * its own backoff. In `@google-cloud/storage` 7.x and 8.x a download the SDK retries after a 408, 429, 500, 502, 503 or 504 can crash the process
23
24
  * with `ERR_STREAM_UNABLE_TO_PIPE`, so build it with `retryOptions: { autoRetry: false }`; the driver retries
24
25
  * downloads itself. That also turns off the SDK's retries of listings, metadata reads and resumable uploads on
25
26
  * this client, which the store does not retry. The client built here needs none of this: only its downloads are sent once.
@@ -36,13 +37,38 @@ export interface GcsStorageOptions {
36
37
  readonly simpleUploadThresholdBytes?: number;
37
38
  /** Injected clock for the registry's `createdAt`/`updatedAt`; defaults to `Date.now`. */
38
39
  readonly now?: () => number;
40
+ /**
41
+ * Cut off a read that has run this long, in milliseconds. `0`, the default, sets no timeout. A client's own `timeout`
42
+ * does not bound a download on `@google-cloud/storage` 8.x (the SDK hands it to an HTTP client that has no such
43
+ * option), so this is what does.
44
+ *
45
+ * It bounds each read as a whole — a generation's tail (with the metadata read it falls back on for an empty
46
+ * object), a range of it, a registry row — with one deadline across every attempt the driver makes and the backoff
47
+ * between them. The clock starts at the call into the driver, so a credential fetch and any wait for a socket count
48
+ * (Node's agents set no socket limit unless your process sets one), and runs until the whole body has arrived. It
49
+ * counts time the process spends busy too: Node runs a due timer before it reads a socket, so a synchronous stretch
50
+ * longer than the timeout fails the reads in flight even when their responses have arrived. When it passes, the read
51
+ * throws `TransientError` naming the read and the timeout and no further attempt starts, for the store's read retry
52
+ * to run again: about 8.35 s in all at `2_000` with the default retry policy. Uploads, deletes, listings and the
53
+ * conditional writes are not timed. The SDK cannot cancel a request whose response has not begun, so a read timed
54
+ * out before its server answers leaves that connection open until the server answers or closes it: one per read, up
55
+ * to four per call through the store's retry. A non-negative safe integer no larger than 2,147,483,647.
56
+ */
57
+ readonly readTimeoutMs?: number;
58
+ /**
59
+ * Whether the registry removes a deleted row for good, by an object delete sent with `ifGenerationMatch`, rather
60
+ * than leaving a tombstone a full listing reads forever. Defaults to `false`, on the public endpoint too: whether real
61
+ * GCS applies `ifGenerationMatch` to a delete has not been verified by a run against the service, and fake-gcs-server
62
+ * accepts the precondition and ignores it, so CI cannot show it. Set `true` to remove rows for good.
63
+ */
64
+ readonly conditionalDelete?: boolean;
39
65
  }
40
66
  /**
41
67
  * The keys `new GcsStorage(options)` takes. Any other is refused by name rather than ignored: an ignored client key
42
68
  * — `storage`, as the lower-level storage driver calls it — falls back to ambient credentials and the **public** endpoint, and
43
69
  * for a client pointed at an emulator that is production traffic from a wiring typo.
44
70
  */
45
- export declare const GCS_STORAGE_OPTION_KEYS: readonly ["bucket", "prefix", "client", "projectId", "apiEndpoint", "maxObjectBytes", "simpleUploadThresholdBytes", "now"];
71
+ export declare const GCS_STORAGE_OPTION_KEYS: readonly ["bucket", "prefix", "client", "projectId", "apiEndpoint", "maxObjectBytes", "simpleUploadThresholdBytes", "now", "readTimeoutMs", "conditionalDelete"];
46
72
  export declare class GcsStorage implements StorageBackend {
47
73
  /** Cross-bundle brand, stamped non-enumerably in the constructor so a spread cannot carry it. */
48
74
  readonly [STORAGE_BACKEND]: true;
@@ -1,2 +1,8 @@
1
- /** Call `download`, and again after each retryable fault, up to {@link RETRIES} more times; rethrow the last error. */
2
- export declare function retryDownload<T>(download: () => Promise<T>): Promise<T>;
1
+ import { type Deadline } from './read-timeout.js';
2
+ /** Under a deadline, a retry is not sent with less than this left of it. */
3
+ export declare const MIN_RETRY_MS = 10;
4
+ /**
5
+ * Call `download`, and again after each retryable fault, up to {@link RETRIES} more times, within `deadline` when there
6
+ * is one; rethrow the last error, or the deadline's once it has passed.
7
+ */
8
+ export declare function retryDownload<T>(download: () => Promise<T>, deadline?: Deadline): Promise<T>;
@@ -12,6 +12,12 @@
12
12
  * returns **412 Precondition Failed**. Maps to `WriteConflictError` (caller OCC), never a blind retry.
13
13
  */
14
14
  export declare function isPreconditionFailed(err: unknown): boolean;
15
+ /**
16
+ * GCS asked the client to slow down: `429` (`rateLimitExceeded`) or `503` (`backendError`), which the SDK raises as an
17
+ * `ApiError` with the status as a numeric `code`. GCS does not document that a throttled request was not applied, so
18
+ * an upload sent again after one must still tell a landed first send apart.
19
+ */
20
+ export declare function isThrottle(err: unknown): boolean;
15
21
  /** The object / generation does not exist (GCS returns 404). */
16
22
  export declare function isNotFound(err: unknown): boolean;
17
23
  /** A range request started past EOF (HTTP 416 Requested Range Not Satisfiable). */