@cloudbitmaps/s3 0.11.1 → 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 +29 -9
- package/dist/backend.d.ts +29 -3
- package/dist/client-probe.d.ts +26 -0
- package/dist/index.js +413 -93
- package/dist/index.js.map +4 -4
- package/dist/read-timeout.d.ts +13 -0
- package/dist/registry.d.ts +79 -3
- package/dist/s3-errors.d.ts +8 -0
- package/dist/storage.d.ts +28 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ pnpm add @cloudbitmaps/roaring @cloudbitmaps/s3
|
|
|
16
16
|
|
|
17
17
|
On npm 12 and pnpm 10 and later, allow `roaring`'s one install script first, or the install exits 0 and the package throws at `import`. Put this in your `package.json`: `{ "allowScripts": { "roaring": true }, "pnpm": { "onlyBuiltDependencies": ["roaring"] } }`. npm 11 runs the script but warns until you allow it the same way; pnpm 9 needs nothing extra. [Details](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md#cannot-find-module-buildreleaseroaringnode-after-a-successful-install).
|
|
18
18
|
|
|
19
|
-
`@aws-sdk/client-s3` (`>=3.
|
|
19
|
+
`@aws-sdk/client-s3` (`>=3.700.0 <4`) is a real dependency of this package, so installing it is the whole step.
|
|
20
20
|
`@cloudbitmaps/core`, the engine underneath, arrives with it and you never name it.
|
|
21
21
|
|
|
22
22
|
## Use
|
|
@@ -42,26 +42,46 @@ It builds its own client from your usual AWS credentials. Any other key is refus
|
|
|
42
42
|
| `client` | your own `S3Client`; it carries its own region, endpoint and credentials |
|
|
43
43
|
| `region`, `endpoint`, `pathStyle`, `credentials` | build a client for you, such as one for MinIO; refused beside `client` |
|
|
44
44
|
| `partBytes`, `maxObjectBytes` | multipart sizing: part size (default 8 MiB; the upload buffers one part at a time) and the largest object (about 80 GiB by default, up to S3's 5 TiB) |
|
|
45
|
+
| `readTimeoutMs` | how long one read (a `GetObject` or `HeadObject`, its body included) may take before it throws `TransientError` and the store retries it; `0`, the default, sets no timeout. Writes are not timed |
|
|
46
|
+
| `conditionalDelete` | whether the registry removes a deleted row with a `DeleteObject` under `If-Match` rather than leaving a tombstone. On by default when the host the client resolves is an AWS S3 host, however its endpoint was set (`endpoint`, `AWS_ENDPOINT_URL_S3`, `AWS_ENDPOINT_URL`, the shared config file); off for any other host: set it for an S3-compatible store only once you know it applies the header (MinIO ignores it) |
|
|
45
47
|
|
|
46
48
|
## Before production
|
|
47
49
|
|
|
48
50
|
- **The bucket must honor conditional writes**: `If-None-Match: *` for a write-once generation and `If-Match` for the
|
|
49
51
|
pointer. AWS S3 does, and the test suite runs against MinIO. Another S3-compatible service that ignores the headers
|
|
50
52
|
turns write-once into overwrite, so check yours before you depend on it.
|
|
51
|
-
- **Use `@aws-sdk/client-s3` 3.
|
|
52
|
-
correctness floor, not a preference.
|
|
53
|
-
built on from 3.641.0: measured against
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
- **Use `@aws-sdk/client-s3` 3.700.0 or later** if you pin the SDK or pass your own `client`. This floor is a
|
|
54
|
+
correctness floor, not a preference. An SDK sends only the conditional headers it models, and drops one it does
|
|
55
|
+
not, without an error. `If-None-Match`, which write-once is built on, is modelled from 3.641.0: measured against
|
|
56
|
+
MinIO, 3.640.0 silently overwrites an existing object, which loses a published generation. `If-Match` on
|
|
57
|
+
`PutObject`, which the registry's compare-and-swap is built on, is modelled from 3.700.0: 3.699.0's serializer
|
|
58
|
+
omits it, so a fenced row write goes out unconditionally and can land over a concurrent writer's. This package's
|
|
59
|
+
own range never resolves below 3.700.0. The registry also checks, before its first request, that the client it is given sends `If-Match` on
|
|
60
|
+
`PutObject` and `DeleteObject`, and refuses a write it would send without its precondition.
|
|
56
61
|
- **Grant `s3:GetObject`, `s3:PutObject`, `s3:DeleteObject`, `s3:AbortMultipartUpload` and `s3:ListBucket`.** Without
|
|
57
62
|
`s3:ListBucket`, S3 answers a missing key with `403` instead of `404`.
|
|
58
63
|
- **Add a lifecycle rule that aborts incomplete multipart uploads**, and never one that expires current objects or the
|
|
59
64
|
`registry/` prefix.
|
|
60
65
|
- **A transient failure of a write throws `TransientError`, and the write may or may not have landed.** Re-run the
|
|
61
66
|
call, or check `store.generations(ref)`. The SDK's own retry is off for conditional writes, so a write that landed
|
|
62
|
-
is not reported as a conflict.
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
is not reported as a conflict. The driver sends each registry write once; a load that gets no answer reads the row and
|
|
68
|
+
sends a fresh compare-and-swap from it, at most three times. A generation's object is sent again, up to three more
|
|
69
|
+
times, only after a `503` (`SlowDown` is the usual one); it carries a random id in its user metadata
|
|
70
|
+
(`x-amz-meta-cbwid`), so a first send that landed is told from another writer's object. A load whose row writes all go
|
|
71
|
+
unanswered throws `TransientError` and keeps its object. The waits before an object's re-sends use real timers: the
|
|
72
|
+
store's injected clock does not reach them, and `S3Storage` takes no clock, so a test that throttles one object
|
|
73
|
+
write three times waits about 3.5 s at most (the publish's waits before a fresh row write do use the store's
|
|
74
|
+
clock). **A bare `429`** (AWS S3 does not send one; some
|
|
75
|
+
S3-compatible services do) **is not retried and is not classified transient**: it surfaces as the SDK's own error, so a
|
|
76
|
+
layer of yours that keys on `TransientError` will not retry it.
|
|
77
|
+
- **Reads can be timed, and writes are not.** Nothing is timed unless you set `readTimeoutMs`. Set, a read that has
|
|
78
|
+
not finished in that many ms throws `TransientError`, and the store runs it again; AWS's guidance is to retry a GET
|
|
79
|
+
of under 512 KB after about 2 seconds. The timer covers the body as well, so a connection that sends its headers and
|
|
80
|
+
then stalls is cut off too. It starts when the read is handed to the SDK, so a burst of reads larger than the
|
|
81
|
+
client's socket pool (50 by default) can time out while queued: set it above that queueing, or raise `maxSockets`.
|
|
82
|
+
For the writes and listings, give your client a timeout of its own.
|
|
83
|
+
|
|
84
|
+
The [production checklist](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/production.md) covers each item, with a sample IAM policy, and the ones every backend shares: a request timeout on your client for the requests the library does not time, backups of the data and the registry, and a schedule for your loads.
|
|
65
85
|
|
|
66
86
|
## Documentation
|
|
67
87
|
|
package/dist/backend.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* credential chain the SDK cannot infer (SSO, an assumed role, a custom retry strategy); pass `endpoint` +
|
|
12
12
|
* `pathStyle` + `credentials` for an S3-compatible store (MinIO, Ceph, R2). Both halves stay reachable as `.storage` and
|
|
13
13
|
* `.registry` for anyone wiring something the facade does not cover. `maxObjectBytes` and `partBytes` size the
|
|
14
|
-
* multipart upload.
|
|
14
|
+
* multipart upload, and `readTimeoutMs`, when set, bounds each read both halves make.
|
|
15
15
|
*/
|
|
16
16
|
import { STORAGE_BACKEND } from '@cloudbitmaps/core/driver-kit';
|
|
17
17
|
import type { IRegistryDriver, IStorageDriver, StorageBackend } from '@cloudbitmaps/core/driver-kit';
|
|
@@ -23,7 +23,8 @@ export interface S3StorageOptions {
|
|
|
23
23
|
readonly prefix?: string;
|
|
24
24
|
/**
|
|
25
25
|
* A constructed client. Supply one for a credential chain the SDK cannot infer; otherwise one is built. Its retry
|
|
26
|
-
* applies to every request except the conditional writes, which
|
|
26
|
+
* applies to every request except the conditional writes, which the driver sends with that retry off whatever it is
|
|
27
|
+
* configured to do: a registry row once, and a generation's object again only after a throttle, under its own backoff.
|
|
27
28
|
*/
|
|
28
29
|
readonly client?: S3Client;
|
|
29
30
|
/** Region for the client built when `client` is absent (refused beside `client`). Falls back to the SDK's own resolution. */
|
|
@@ -55,15 +56,40 @@ export interface S3StorageOptions {
|
|
|
55
56
|
/** Multipart part size in bytes (default 8 MiB; a smaller value is raised to the S3 5 MiB minimum). Must be a
|
|
56
57
|
* positive safe integer. Tunes peak write memory. */
|
|
57
58
|
readonly partBytes?: number;
|
|
59
|
+
/**
|
|
60
|
+
* How long one read may take before it is abandoned, in ms. `0`, the default, sets no timeout. When set, it bounds
|
|
61
|
+
* each `GetObject` and `HeadObject` either half sends, the response body included, so a connection that stops
|
|
62
|
+
* answering part-way through a body is cut off too. A read that runs out of time throws `TransientError`, which the
|
|
63
|
+
* store's read retry runs again. AWS's S3 guidance is to retry a GET of under 512 KB that has not answered in about
|
|
64
|
+
* 2 seconds. Must be a non-negative safe integer no larger than 2,147,483,647.
|
|
65
|
+
*
|
|
66
|
+
* The clock starts when the read is handed to the SDK, so it also counts the time the read waits for one of the
|
|
67
|
+
* client's sockets (50 by default) and the time spent fetching credentials, and under `retryMode: 'adaptive'` the
|
|
68
|
+
* SDK's rate-limiter wait. A burst of concurrent reads larger than the socket pool can therefore time out with
|
|
69
|
+
* nothing slow on the wire: size the timeout above the worst queueing your concurrency implies, or raise the client's
|
|
70
|
+
* `maxSockets`. On a client built with `cacheMiddleware: true`, a timed read resolves its middleware each time.
|
|
71
|
+
*
|
|
72
|
+
* Writes and listings are never timed: a write that hangs needs a timeout on the client (its `requestHandler`). The
|
|
73
|
+
* timeout is applied per request, so a `client` you pass gets it without being changed.
|
|
74
|
+
*/
|
|
75
|
+
readonly readTimeoutMs?: number;
|
|
58
76
|
/** Injected clock for the registry's `createdAt`/`updatedAt`; defaults to `Date.now`. */
|
|
59
77
|
readonly now?: () => number;
|
|
78
|
+
/**
|
|
79
|
+
* Whether the registry removes a deleted row for good, by a `DeleteObject` sent with `If-Match`, rather than leaving
|
|
80
|
+
* a tombstone a full listing reads forever. Defaults to `true` when the host the client resolves is an AWS S3 host,
|
|
81
|
+
* whichever way its endpoint was set (`endpoint`, `AWS_ENDPOINT_URL_S3`, `AWS_ENDPOINT_URL`, the shared config file),
|
|
82
|
+
* and to `false` for any other host: set it for an S3-compatible store only once you know the store applies `If-Match`
|
|
83
|
+
* on a delete. MinIO, for one, ignores it. It is never `true` for an SDK that does not send the header.
|
|
84
|
+
*/
|
|
85
|
+
readonly conditionalDelete?: boolean;
|
|
60
86
|
}
|
|
61
87
|
/**
|
|
62
88
|
* The keys `new S3Storage(options)` takes. Any other is refused by name rather than ignored: an ignored client or
|
|
63
89
|
* endpoint key builds a client from ambient credentials against the **public** endpoint, and for a store pointed at
|
|
64
90
|
* MinIO that is production traffic from a wiring typo.
|
|
65
91
|
*/
|
|
66
|
-
export declare const S3_STORAGE_OPTION_KEYS: readonly ["bucket", "prefix", "client", "region", "endpoint", "pathStyle", "credentials", "maxObjectBytes", "partBytes", "now"];
|
|
92
|
+
export declare const S3_STORAGE_OPTION_KEYS: readonly ["bucket", "prefix", "client", "region", "endpoint", "pathStyle", "credentials", "maxObjectBytes", "partBytes", "readTimeoutMs", "now", "conditionalDelete"];
|
|
67
93
|
export declare class S3Storage implements StorageBackend {
|
|
68
94
|
/** Cross-bundle brand, stamped non-enumerably in the constructor so a spread cannot carry it. */
|
|
69
95
|
readonly [STORAGE_BACKEND]: true;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { S3Client } from '@aws-sdk/client-s3';
|
|
2
|
+
/** What a client does with the requests the registry sends. */
|
|
3
|
+
export interface ClientFacts {
|
|
4
|
+
/** The host a request to the bucket is addressed to, as the client resolves it now. */
|
|
5
|
+
readonly host: string;
|
|
6
|
+
/** Whether a `DeleteObject` given `IfMatch` goes out with an `If-Match` header. */
|
|
7
|
+
readonly sendsDeleteIfMatch: boolean;
|
|
8
|
+
/** Whether a `PutObject` given `IfMatch` goes out with an `If-Match` header: the registry's compare-and-swap. */
|
|
9
|
+
readonly sendsPutIfMatch: boolean;
|
|
10
|
+
/** Whether a `PutObject` given `IfNoneMatch` goes out with an `If-None-Match` header: the registry's create. */
|
|
11
|
+
readonly sendsPutIfNoneMatch: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** A key no registry row or generation can hold; the probe's requests are never sent, so it is never used. */
|
|
14
|
+
export declare const PROBE_KEY = "cloudbitmaps-probe";
|
|
15
|
+
/**
|
|
16
|
+
* What `client` does with a conditional `DeleteObject` and `PutObject` to `bucket`, or `undefined` when it cannot be
|
|
17
|
+
* found out. Never throws, never sends a request, and never runs anything the caller added to `client`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function probeClient(client: S3Client, bucket: string): Promise<ClientFacts | undefined>;
|
|
20
|
+
/**
|
|
21
|
+
* Whether `hostname` is an AWS S3 host: under an AWS domain and naming S3 in one of its labels (`s3`, `s3-fips`,
|
|
22
|
+
* `s3-accesspoint`, `s3express-…`), so the FIPS, dual-stack, access-point and VPC interface forms all count, and another
|
|
23
|
+
* AWS service's host does not. A host of any other domain is not, whatever it speaks: a store behind one has to be
|
|
24
|
+
* vouched for by the caller.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isAwsS3Host(hostname: string): boolean;
|