@smthrs/artifacts 0.0.0-stage → 1.0.0-rc.4
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/CHANGELOG.md +77 -0
- package/LICENSE +21 -0
- package/README.md +173 -2
- package/dist/cjs/ArtifactBackupLease.d.ts +34 -0
- package/dist/cjs/ArtifactBackupLease.d.ts.map +1 -0
- package/dist/cjs/ArtifactBackupLease.js +175 -0
- package/dist/cjs/ArtifactBackupLease.js.map +7 -0
- package/dist/cjs/ArtifactStore.d.ts +129 -0
- package/dist/cjs/ArtifactStore.d.ts.map +1 -0
- package/dist/cjs/ArtifactStore.js +73 -0
- package/dist/cjs/ArtifactStore.js.map +7 -0
- package/dist/cjs/ArtifactStoreError.d.ts +82 -0
- package/dist/cjs/ArtifactStoreError.d.ts.map +1 -0
- package/dist/cjs/ArtifactStoreError.js +72 -0
- package/dist/cjs/ArtifactStoreError.js.map +7 -0
- package/dist/cjs/ArtifactStoreMetrics.d.ts +70 -0
- package/dist/cjs/ArtifactStoreMetrics.d.ts.map +1 -0
- package/dist/cjs/ArtifactStoreMetrics.js +51 -0
- package/dist/cjs/ArtifactStoreMetrics.js.map +7 -0
- package/dist/cjs/ArtifactSweep.d.ts +185 -0
- package/dist/cjs/ArtifactSweep.d.ts.map +1 -0
- package/dist/cjs/ArtifactSweep.js +163 -0
- package/dist/cjs/ArtifactSweep.js.map +7 -0
- package/dist/cjs/CombinedArtifacts.d.ts +103 -0
- package/dist/cjs/CombinedArtifacts.d.ts.map +1 -0
- package/dist/cjs/CombinedArtifacts.js +189 -0
- package/dist/cjs/CombinedArtifacts.js.map +7 -0
- package/dist/cjs/FileLease.d.ts +113 -0
- package/dist/cjs/FileLease.d.ts.map +1 -0
- package/dist/cjs/FileLease.js +201 -0
- package/dist/cjs/FileLease.js.map +7 -0
- package/dist/cjs/FileSystemArtifactStore.d.ts +93 -0
- package/dist/cjs/FileSystemArtifactStore.d.ts.map +1 -0
- package/dist/cjs/FileSystemArtifactStore.js +276 -0
- package/dist/cjs/FileSystemArtifactStore.js.map +7 -0
- package/dist/cjs/MemoryArtifactStore.d.ts +25 -0
- package/dist/cjs/MemoryArtifactStore.d.ts.map +1 -0
- package/dist/cjs/MemoryArtifactStore.js +84 -0
- package/dist/cjs/MemoryArtifactStore.js.map +7 -0
- package/dist/cjs/NoopArtifactStore.d.ts +17 -0
- package/dist/cjs/NoopArtifactStore.d.ts.map +1 -0
- package/dist/cjs/NoopArtifactStore.js +46 -0
- package/dist/cjs/NoopArtifactStore.js.map +7 -0
- package/dist/cjs/RemoteArtifacts.d.ts +237 -0
- package/dist/cjs/RemoteArtifacts.d.ts.map +1 -0
- package/dist/cjs/RemoteArtifacts.js +405 -0
- package/dist/cjs/RemoteArtifacts.js.map +7 -0
- package/dist/cjs/index.d.ts +65 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +47 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/ArtifactLocks.d.ts +30 -0
- package/dist/cjs/internal/ArtifactLocks.d.ts.map +1 -0
- package/dist/cjs/internal/ArtifactLocks.js +93 -0
- package/dist/cjs/internal/ArtifactLocks.js.map +7 -0
- package/dist/cjs/internal/ArtifactPath.d.ts +19 -0
- package/dist/cjs/internal/ArtifactPath.d.ts.map +1 -0
- package/dist/cjs/internal/ArtifactPath.js +74 -0
- package/dist/cjs/internal/ArtifactPath.js.map +7 -0
- package/dist/cjs/measureBytes.d.ts +18 -0
- package/dist/cjs/measureBytes.d.ts.map +1 -0
- package/dist/cjs/measureBytes.js +43 -0
- package/dist/cjs/measureBytes.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/snapshotBytes.d.ts +19 -0
- package/dist/cjs/snapshotBytes.d.ts.map +1 -0
- package/dist/cjs/snapshotBytes.js +41 -0
- package/dist/cjs/snapshotBytes.js.map +7 -0
- package/dist/cjs/validateDigest.d.ts +22 -0
- package/dist/cjs/validateDigest.d.ts.map +1 -0
- package/dist/cjs/validateDigest.js +42 -0
- package/dist/cjs/validateDigest.js.map +7 -0
- package/dist/esm/ArtifactBackupLease.d.ts +34 -0
- package/dist/esm/ArtifactBackupLease.d.ts.map +1 -0
- package/dist/esm/ArtifactBackupLease.js +117 -0
- package/dist/esm/ArtifactBackupLease.js.map +1 -0
- package/dist/esm/ArtifactStore.d.ts +129 -0
- package/dist/esm/ArtifactStore.d.ts.map +1 -0
- package/dist/esm/ArtifactStore.js +83 -0
- package/dist/esm/ArtifactStore.js.map +1 -0
- package/dist/esm/ArtifactStoreError.d.ts +82 -0
- package/dist/esm/ArtifactStoreError.d.ts.map +1 -0
- package/dist/esm/ArtifactStoreError.js +77 -0
- package/dist/esm/ArtifactStoreError.js.map +1 -0
- package/dist/esm/ArtifactStoreMetrics.d.ts +70 -0
- package/dist/esm/ArtifactStoreMetrics.d.ts.map +1 -0
- package/dist/esm/ArtifactStoreMetrics.js +77 -0
- package/dist/esm/ArtifactStoreMetrics.js.map +1 -0
- package/dist/esm/ArtifactSweep.d.ts +185 -0
- package/dist/esm/ArtifactSweep.d.ts.map +1 -0
- package/dist/esm/ArtifactSweep.js +202 -0
- package/dist/esm/ArtifactSweep.js.map +1 -0
- package/dist/esm/CombinedArtifacts.d.ts +103 -0
- package/dist/esm/CombinedArtifacts.d.ts.map +1 -0
- package/dist/esm/CombinedArtifacts.js +214 -0
- package/dist/esm/CombinedArtifacts.js.map +1 -0
- package/dist/esm/FileLease.d.ts +113 -0
- package/dist/esm/FileLease.d.ts.map +1 -0
- package/dist/esm/FileLease.js +239 -0
- package/dist/esm/FileLease.js.map +1 -0
- package/dist/esm/FileSystemArtifactStore.d.ts +93 -0
- package/dist/esm/FileSystemArtifactStore.d.ts.map +1 -0
- package/dist/esm/FileSystemArtifactStore.js +336 -0
- package/dist/esm/FileSystemArtifactStore.js.map +1 -0
- package/dist/esm/MemoryArtifactStore.d.ts +25 -0
- package/dist/esm/MemoryArtifactStore.d.ts.map +1 -0
- package/dist/esm/MemoryArtifactStore.js +67 -0
- package/dist/esm/MemoryArtifactStore.js.map +1 -0
- package/dist/esm/NoopArtifactStore.d.ts +17 -0
- package/dist/esm/NoopArtifactStore.d.ts.map +1 -0
- package/dist/esm/NoopArtifactStore.js +27 -0
- package/dist/esm/NoopArtifactStore.js.map +1 -0
- package/dist/esm/RemoteArtifacts.d.ts +237 -0
- package/dist/esm/RemoteArtifacts.d.ts.map +1 -0
- package/dist/esm/RemoteArtifacts.js +554 -0
- package/dist/esm/RemoteArtifacts.js.map +1 -0
- package/dist/esm/index.d.ts +65 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +65 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/ArtifactLocks.d.ts +30 -0
- package/dist/esm/internal/ArtifactLocks.d.ts.map +1 -0
- package/dist/esm/internal/ArtifactLocks.js +86 -0
- package/dist/esm/internal/ArtifactLocks.js.map +1 -0
- package/dist/esm/internal/ArtifactPath.d.ts +19 -0
- package/dist/esm/internal/ArtifactPath.d.ts.map +1 -0
- package/dist/esm/internal/ArtifactPath.js +59 -0
- package/dist/esm/internal/ArtifactPath.js.map +1 -0
- package/dist/esm/measureBytes.d.ts +18 -0
- package/dist/esm/measureBytes.d.ts.map +1 -0
- package/dist/esm/measureBytes.js +22 -0
- package/dist/esm/measureBytes.js.map +1 -0
- package/dist/esm/snapshotBytes.d.ts +19 -0
- package/dist/esm/snapshotBytes.d.ts.map +1 -0
- package/dist/esm/snapshotBytes.js +30 -0
- package/dist/esm/snapshotBytes.js.map +1 -0
- package/dist/esm/validateDigest.d.ts +22 -0
- package/dist/esm/validateDigest.d.ts.map +1 -0
- package/dist/esm/validateDigest.js +26 -0
- package/dist/esm/validateDigest.js.map +1 -0
- package/docs/README.md +200 -0
- package/docs/api.md +474 -0
- package/docs/concepts/content-addressing.md +174 -0
- package/docs/concepts/coordination.md +141 -0
- package/docs/concepts/tiers.md +147 -0
- package/docs/guides/fence-a-backup.md +123 -0
- package/docs/guides/reclaim-disk-space.md +143 -0
- package/docs/guides/serve-the-artifact-protocol.md +140 -0
- package/docs/guides/share-artifacts-across-machines.md +180 -0
- package/docs/guides/test-against-an-artifact-store.md +158 -0
- package/docs/installation.md +98 -0
- package/docs/quickstart.md +182 -0
- package/docs/troubleshooting.md +232 -0
- package/package.json +179 -3
- package/src/ArtifactBackupLease.ts +213 -0
- package/src/ArtifactStore.ts +152 -0
- package/src/ArtifactStoreError.ts +96 -0
- package/src/ArtifactStoreMetrics.ts +83 -0
- package/src/ArtifactSweep.ts +336 -0
- package/src/CombinedArtifacts.ts +369 -0
- package/src/FileLease.ts +380 -0
- package/src/FileSystemArtifactStore.ts +459 -0
- package/src/MemoryArtifactStore.ts +83 -0
- package/src/NoopArtifactStore.ts +30 -0
- package/src/RemoteArtifacts.ts +809 -0
- package/src/index.ts +71 -0
- package/src/internal/ArtifactLocks.ts +110 -0
- package/src/internal/ArtifactPath.ts +72 -0
- package/src/measureBytes.ts +28 -0
- package/src/snapshotBytes.ts +34 -0
- package/src/validateDigest.ts +31 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sweep half of artifact garbage collection: enumerate this host's blobs
|
|
3
|
+
* and delete one, fenced by its age.
|
|
4
|
+
*
|
|
5
|
+
* This surface is deliberately NOT part of `ArtifactStore.Service`. The store
|
|
6
|
+
* contract is shared by memory, filesystem, and network tiers, and a remote
|
|
7
|
+
* tier can neither enumerate its address space nor accept a delete — Bazel
|
|
8
|
+
* draws the same line: its disk-cache collector
|
|
9
|
+
* (class `DiskCacheGarbageCollector` in
|
|
10
|
+
* `com.google.devtools.build.lib.remote.disk`) walks the local directory, while
|
|
11
|
+
* the remote tier owns its own retention. Only the
|
|
12
|
+
* host-local filesystem store implements this.
|
|
13
|
+
*
|
|
14
|
+
* The *policy* — which digests are live, how old a dead blob must be before it
|
|
15
|
+
* goes — belongs to the engine composition, which is the only place the
|
|
16
|
+
* durable roots (attempt rows, cache entries) are visible. This module ships
|
|
17
|
+
* mechanics alone; `@smthrs/engine-store` owns the mark policy and grace
|
|
18
|
+
* period.
|
|
19
|
+
*
|
|
20
|
+
* @since 1.0.0-rc.0
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as Context from "effect/Context"
|
|
24
|
+
import * as Effect from "effect/Effect"
|
|
25
|
+
import * as FileSystem from "effect/FileSystem"
|
|
26
|
+
import * as Layer from "effect/Layer"
|
|
27
|
+
import * as Option from "effect/Option"
|
|
28
|
+
import * as PlatformError from "effect/PlatformError"
|
|
29
|
+
import * as ArtifactBackupLease from "./ArtifactBackupLease.ts"
|
|
30
|
+
import * as ArtifactStore from "./ArtifactStore.ts"
|
|
31
|
+
import * as ArtifactLocks from "./internal/ArtifactLocks.ts"
|
|
32
|
+
import * as ArtifactPath from "./internal/ArtifactPath.ts"
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One enumerated blob: its content address, when it was last written or
|
|
36
|
+
* freshened, and its size.
|
|
37
|
+
*
|
|
38
|
+
* `modifiedAtMs` is the age evidence a sweep fences on — the same signal git
|
|
39
|
+
* (`git prune --expire`) and Bazel's disk-cache collector read, because it is
|
|
40
|
+
* the one timestamp a filesystem maintains without any bookkeeping of ours.
|
|
41
|
+
*
|
|
42
|
+
* @category models
|
|
43
|
+
* @since 1.0.0-rc.0
|
|
44
|
+
* @slop
|
|
45
|
+
*/
|
|
46
|
+
export interface BlobStat {
|
|
47
|
+
readonly digest: string
|
|
48
|
+
readonly modifiedAtMs: number
|
|
49
|
+
readonly sizeBytes: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Fencing predicate for a sweep deletion.
|
|
54
|
+
*
|
|
55
|
+
* @category models
|
|
56
|
+
* @since 1.0.0-rc.0
|
|
57
|
+
* @slop
|
|
58
|
+
*/
|
|
59
|
+
export interface RemoveOptions {
|
|
60
|
+
/**
|
|
61
|
+
* Deletes the blob only while its mtime is still at or before this bound.
|
|
62
|
+
* A blob freshened after the caller computed its live set — a concurrent
|
|
63
|
+
* `put` re-referencing the same bytes — fails the fence and survives, the
|
|
64
|
+
* same shape as `CacheStore.evict`'s `ifRecordedBy`: the guard rides in the
|
|
65
|
+
* delete, never in a prior read alone.
|
|
66
|
+
*/
|
|
67
|
+
readonly ifUnmodifiedSinceMs?: number | undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Host-local blob enumeration and fenced deletion.
|
|
72
|
+
*
|
|
73
|
+
* @category models
|
|
74
|
+
* @since 1.0.0-rc.0
|
|
75
|
+
* @slop
|
|
76
|
+
*/
|
|
77
|
+
export interface Service {
|
|
78
|
+
/**
|
|
79
|
+
* Every content-addressed blob this host's store currently holds. In-flight
|
|
80
|
+
* `.tmp-*` scratch files, foreign files, and blobs whose age cannot be
|
|
81
|
+
* measured are excluded — what the sweep cannot judge it must not touch.
|
|
82
|
+
*/
|
|
83
|
+
readonly inventory: Effect.Effect<Array<BlobStat>, ArtifactStore.ArtifactStoreError>
|
|
84
|
+
/**
|
|
85
|
+
* Deletes the blob at `digest`, returning whether bytes were removed. A
|
|
86
|
+
* missing blob reports `false` rather than failing, so a crashed and
|
|
87
|
+
* re-run sweep converges instead of erroring on its own progress.
|
|
88
|
+
*
|
|
89
|
+
* `false` covers three outcomes a caller cannot tell apart, all of which mean
|
|
90
|
+
* "nothing was reclaimed, and retrying later is safe": the blob was already
|
|
91
|
+
* gone, it failed the `ifUnmodifiedSinceMs` fence, or a live backup lease
|
|
92
|
+
* fenced the deletion (see `ArtifactBackupLease`). The last one is not
|
|
93
|
+
* progress — the blob is still there — so a collector that counts reclaimed
|
|
94
|
+
* bytes must not count it, and the next pass after the backup finishes
|
|
95
|
+
* removes it.
|
|
96
|
+
*/
|
|
97
|
+
readonly remove: (
|
|
98
|
+
digest: string,
|
|
99
|
+
options?: RemoveOptions
|
|
100
|
+
) => Effect.Effect<boolean, ArtifactStore.ArtifactStoreError>
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Service tag for host-local artifact sweeping.
|
|
105
|
+
*
|
|
106
|
+
* The identity string equals this module's package path, per the house rule
|
|
107
|
+
* that a new identity is the defining module path.
|
|
108
|
+
*
|
|
109
|
+
* @category services
|
|
110
|
+
* @since 1.0.0-rc.0
|
|
111
|
+
* @slop
|
|
112
|
+
*/
|
|
113
|
+
export class ArtifactSweep extends Context.Service<ArtifactSweep, Service>()("@smthrs/artifacts/ArtifactSweep") {}
|
|
114
|
+
|
|
115
|
+
const error = (message: string, cause?: unknown): ArtifactStore.ArtifactStoreError =>
|
|
116
|
+
new ArtifactStore.ArtifactStoreError({
|
|
117
|
+
code: "unavailable",
|
|
118
|
+
message,
|
|
119
|
+
...(cause === undefined ? {} : { cause })
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const hostFailure = (cause: unknown): ArtifactStore.ArtifactStoreError =>
|
|
123
|
+
error(`the host filesystem refused a sweep operation: ${String(cause)}`, cause)
|
|
124
|
+
|
|
125
|
+
const isNotFound = (cause: unknown): boolean =>
|
|
126
|
+
cause instanceof PlatformError.PlatformError && cause.reason._tag === "NotFound"
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* How the sweep reaches the objects directory.
|
|
130
|
+
*
|
|
131
|
+
* Deliberately not `ArtifactStore.FileSystemOptions`: the sweep never writes a
|
|
132
|
+
* blob, so `durability` would be an accepted argument it ignores. Both fields
|
|
133
|
+
* must match the store built over the same directory — see `coordination`.
|
|
134
|
+
*
|
|
135
|
+
* @category models
|
|
136
|
+
* @since 1.0.0-rc.0
|
|
137
|
+
*/
|
|
138
|
+
export interface SweepOptions {
|
|
139
|
+
/**
|
|
140
|
+
* The objects directory to enumerate, the one the paired
|
|
141
|
+
* `ArtifactStore.makeFileSystem` publishes into. Defaults to
|
|
142
|
+
* `ArtifactStore.defaultDirectory`.
|
|
143
|
+
*/
|
|
144
|
+
readonly directory?: string | undefined
|
|
145
|
+
/**
|
|
146
|
+
* `required` takes the same per-digest lock file a publishing writer takes,
|
|
147
|
+
* and consults the backup lease, before deleting. It fences nothing unless
|
|
148
|
+
* the paired store was built with `required` too: a store on `process` never
|
|
149
|
+
* takes the lock this sweep waits on, so the fence would read as armed while
|
|
150
|
+
* protecting nothing. `process` is the explicit weaker browser/test mode.
|
|
151
|
+
*/
|
|
152
|
+
readonly coordination?: "required" | "process" | undefined
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Builds the filesystem-backed sweep surface over the same objects directory
|
|
157
|
+
* an `ArtifactStore.makeFileSystem` publishes into.
|
|
158
|
+
*
|
|
159
|
+
* Enumeration is conservative by construction. Only paths in the store's
|
|
160
|
+
* canonical fanout shape — `xx/<digest>` with `xx` equal to the digest's
|
|
161
|
+
* two-hex prefix — are blobs; anything else in the directory (a temp file, a
|
|
162
|
+
* lock file, a foreign file, a fanout directory itself) is skipped, never
|
|
163
|
+
* deleted. A blob that vanishes between listing and stat was removed by someone
|
|
164
|
+
* else and is likewise skipped.
|
|
165
|
+
*
|
|
166
|
+
* Pair it with the store over the SAME `directory` and the SAME `coordination`.
|
|
167
|
+
* Nothing checks the pairing, and a mismatch is silent: a sweep on `required`
|
|
168
|
+
* beside a store on `process` deletes blobs no writer is fenced against.
|
|
169
|
+
*
|
|
170
|
+
* Under `required`, one deletion costs two lock acquisitions — the per-digest
|
|
171
|
+
* lock, then the workspace-global backup-lease gate — which is roughly ten
|
|
172
|
+
* filesystem operations and two forked heartbeat fibers per blob. The gate is
|
|
173
|
+
* one file for the whole workspace, so concurrent deletions serialize through
|
|
174
|
+
* it. That is the price of never deleting a blob a running backup already
|
|
175
|
+
* recorded; size a collection window with it in mind.
|
|
176
|
+
*
|
|
177
|
+
* @category constructors
|
|
178
|
+
* @since 1.0.0-rc.0
|
|
179
|
+
* @slop
|
|
180
|
+
*/
|
|
181
|
+
export const makeFileSystem = (
|
|
182
|
+
fs: FileSystem.FileSystem,
|
|
183
|
+
options: SweepOptions = {}
|
|
184
|
+
): Service => {
|
|
185
|
+
const directory = (options.directory ?? ArtifactStore.defaultDirectory).replace(/([^/])\/+$/, "$1")
|
|
186
|
+
const coordination = options.coordination ?? "required"
|
|
187
|
+
const blobPath = (digest: string): string => `${directory}/${digest.slice(0, 2)}/${digest}`
|
|
188
|
+
|
|
189
|
+
const inventory: Service["inventory"] = Effect.gen(function*() {
|
|
190
|
+
// A store that never published anything has no directory; that is an
|
|
191
|
+
// empty inventory, not a failure.
|
|
192
|
+
const checkRoot = yield* ArtifactPath.guard(fs, directory).pipe(Effect.mapError(hostFailure))
|
|
193
|
+
const parents = yield* fs.readDirectory(directory).pipe(
|
|
194
|
+
Effect.catch((cause) => isNotFound(cause) ? Effect.succeed([] as Array<string>) : Effect.fail(hostFailure(cause)))
|
|
195
|
+
)
|
|
196
|
+
const entries: Array<string> = []
|
|
197
|
+
for (const parent of parents) {
|
|
198
|
+
if (!/^[0-9a-f]{2}$/.test(parent)) continue
|
|
199
|
+
yield* checkRoot.pipe(Effect.mapError(hostFailure))
|
|
200
|
+
const safe = yield* ArtifactPath.guard(fs, `${directory}/${parent}`).pipe(Effect.option)
|
|
201
|
+
if (Option.isNone(safe)) continue
|
|
202
|
+
const children = yield* fs.readDirectory(`${directory}/${parent}`).pipe(
|
|
203
|
+
Effect.orElseSucceed(() => [] as Array<string>)
|
|
204
|
+
)
|
|
205
|
+
for (const child of children) entries.push(`${parent}/${child}`)
|
|
206
|
+
}
|
|
207
|
+
const candidates = entries.filter((entry) => {
|
|
208
|
+
if (entry.includes(".tmp-")) return false
|
|
209
|
+
const separator = entry.indexOf("/")
|
|
210
|
+
const fan = entry.slice(0, separator)
|
|
211
|
+
const digest = entry.slice(separator + 1)
|
|
212
|
+
return /^[0-9a-f]{64}$/.test(digest) && fan === digest.slice(0, 2)
|
|
213
|
+
})
|
|
214
|
+
const blobs = yield* Effect.forEach(candidates, (entry) =>
|
|
215
|
+
Effect.gen(function*() {
|
|
216
|
+
const separator = entry.indexOf("/")
|
|
217
|
+
const fan = entry.slice(0, separator)
|
|
218
|
+
const digest = entry.slice(separator + 1)
|
|
219
|
+
// The stat is per-candidate and tolerant: a blob another process
|
|
220
|
+
// removed between the listing and here simply is not in the
|
|
221
|
+
// inventory, and one whose mtime the host cannot report offers no
|
|
222
|
+
// age evidence, so the sweep must never judge it.
|
|
223
|
+
const info = yield* Effect.gen(function*() {
|
|
224
|
+
yield* checkRoot
|
|
225
|
+
yield* ArtifactPath.guard(fs, `${directory}/${fan}`)
|
|
226
|
+
yield* ArtifactPath.guard(fs, `${directory}/${entry}`, "File")
|
|
227
|
+
return yield* fs.stat(`${directory}/${entry}`)
|
|
228
|
+
}).pipe(Effect.option)
|
|
229
|
+
if (Option.isNone(info) || info.value.type !== "File") return undefined
|
|
230
|
+
const mtime = Option.getOrUndefined(info.value.mtime)
|
|
231
|
+
if (mtime === undefined) return undefined
|
|
232
|
+
return {
|
|
233
|
+
digest,
|
|
234
|
+
modifiedAtMs: mtime.getTime(),
|
|
235
|
+
sizeBytes: Number(info.value.size)
|
|
236
|
+
}
|
|
237
|
+
}), { concurrency: 16 })
|
|
238
|
+
return blobs.filter((blob) => blob !== undefined)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const remove: Service["remove"] = Effect.fn("ArtifactSweep.remove")((digest, removeOptions) =>
|
|
242
|
+
Effect.gen(function*() {
|
|
243
|
+
const validated = yield* ArtifactStore.validateDigest(digest)
|
|
244
|
+
yield* Effect.annotateCurrentSpan({ digest: validated })
|
|
245
|
+
const checkRoot = yield* ArtifactPath.guard(fs, directory).pipe(Effect.mapError(hostFailure))
|
|
246
|
+
const checkParent = yield* ArtifactPath.guard(fs, `${directory}/${validated.slice(0, 2)}`).pipe(
|
|
247
|
+
Effect.mapError(hostFailure)
|
|
248
|
+
)
|
|
249
|
+
return yield* ArtifactLocks.withDigest(
|
|
250
|
+
fs,
|
|
251
|
+
directory,
|
|
252
|
+
validated,
|
|
253
|
+
coordination === "process"
|
|
254
|
+
? removeBlob()
|
|
255
|
+
: ArtifactBackupLease.unlessActive(fs, directory, removeBlob(), hostFailure).pipe(
|
|
256
|
+
Effect.map(Option.getOrElse(() => false))
|
|
257
|
+
),
|
|
258
|
+
hostFailure,
|
|
259
|
+
coordination
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
function removeBlob(): Effect.Effect<boolean, ArtifactStore.ArtifactStoreError> {
|
|
263
|
+
return Effect.gen(function*() {
|
|
264
|
+
const path = blobPath(validated)
|
|
265
|
+
yield* checkRoot.pipe(Effect.mapError(hostFailure))
|
|
266
|
+
yield* checkParent.pipe(Effect.mapError(hostFailure))
|
|
267
|
+
const checkBlob = yield* ArtifactPath.guard(fs, path, "File").pipe(Effect.mapError(hostFailure))
|
|
268
|
+
const bound = removeOptions?.ifUnmodifiedSinceMs
|
|
269
|
+
if (bound !== undefined) {
|
|
270
|
+
const info = yield* fs.stat(path).pipe(Effect.option)
|
|
271
|
+
// Already gone is a completed deletion, and an unmeasurable mtime is
|
|
272
|
+
// no proof of age: both refuse rather than delete.
|
|
273
|
+
if (Option.isNone(info)) return false
|
|
274
|
+
const mtime = Option.getOrUndefined(info.value.mtime)
|
|
275
|
+
if (mtime === undefined || mtime.getTime() > bound) return false
|
|
276
|
+
}
|
|
277
|
+
yield* checkRoot.pipe(Effect.mapError(hostFailure))
|
|
278
|
+
yield* checkParent.pipe(Effect.mapError(hostFailure))
|
|
279
|
+
yield* checkBlob.pipe(Effect.mapError(hostFailure))
|
|
280
|
+
return yield* fs.remove(path).pipe(
|
|
281
|
+
Effect.as(true),
|
|
282
|
+
// The blob may have been removed between the fence and here; only a
|
|
283
|
+
// refusal over bytes that still exist is a host failure.
|
|
284
|
+
Effect.catch((cause) =>
|
|
285
|
+
fs.exists(path).pipe(
|
|
286
|
+
Effect.mapError((probeCause) => hostFailure({ remove: cause, existenceProbe: probeCause })),
|
|
287
|
+
Effect.flatMap((present) => present ? Effect.fail(hostFailure(cause)) : Effect.succeed(false))
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
return { inventory, remove }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Provides the filesystem-backed sweep surface.
|
|
301
|
+
*
|
|
302
|
+
* Give it the same `directory` and `coordination` as the `ArtifactStore` layer
|
|
303
|
+
* it sweeps behind; see {@link makeFileSystem}.
|
|
304
|
+
*
|
|
305
|
+
* @category layers
|
|
306
|
+
* @since 1.0.0-rc.0
|
|
307
|
+
* @slop
|
|
308
|
+
*/
|
|
309
|
+
export const layerFileSystem = (
|
|
310
|
+
options: SweepOptions = {}
|
|
311
|
+
): Layer.Layer<ArtifactSweep, never, FileSystem.FileSystem> =>
|
|
312
|
+
Layer.effect(ArtifactSweep)(Effect.map(FileSystem.FileSystem, (fs) => makeFileSystem(fs, options)))
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Builds a sweep surface whose every operation fails as unavailable, with
|
|
316
|
+
* per-method overrides.
|
|
317
|
+
*
|
|
318
|
+
* @category constructors
|
|
319
|
+
* @since 1.0.0-rc.0
|
|
320
|
+
* @slop
|
|
321
|
+
*/
|
|
322
|
+
export const makeNoop = (overrides: Partial<Service> = {}): Service => ({
|
|
323
|
+
inventory: Effect.fail(error("inventory is unavailable")),
|
|
324
|
+
remove: Effect.fn("ArtifactSweep.remove")(() => Effect.fail(error("remove is unavailable"))),
|
|
325
|
+
...overrides
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Provides a no-op sweep surface.
|
|
330
|
+
*
|
|
331
|
+
* @category layers
|
|
332
|
+
* @since 1.0.0-rc.0
|
|
333
|
+
* @slop
|
|
334
|
+
*/
|
|
335
|
+
export const layerNoop = (overrides: Partial<Service> = {}): Layer.Layer<ArtifactSweep> =>
|
|
336
|
+
Layer.succeed(ArtifactSweep)(makeNoop(overrides))
|