@bjornpagen/bumbledb-log 0.19.2 → 0.20.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/package.json +2 -2
- package/src/braids.ts +8 -6
- package/src/bytes.ts +5 -194
- package/src/chain.ts +67 -111
- package/src/codec.ts +150 -233
- package/src/descriptor.ts +67 -612
- package/src/errors.ts +91 -62
- package/src/index.ts +25 -21
- package/src/keys.ts +49 -62
- package/src/manifest.ts +81 -129
- package/src/replica.ts +82 -128
- package/src/store-s3.ts +1 -1
- package/src/store.ts +245 -229
- package/src/tenants.ts +19 -41
- package/src/vector.ts +5 -75
- package/src/writer.ts +275 -168
- package/src/value.ts +0 -336
package/src/tenants.ts
CHANGED
|
@@ -17,15 +17,13 @@ import * as path from "node:path"
|
|
|
17
17
|
import type { Schema, SchemaRelations } from "@bjornpagen/bumbledb"
|
|
18
18
|
import * as errors from "@superbuilders/errors"
|
|
19
19
|
import { regex } from "arkregex"
|
|
20
|
-
import { tenantPrefix } from "#keys.ts"
|
|
20
|
+
import { TEMP_NAMESPACE, tenantPrefix } from "#keys.ts"
|
|
21
21
|
import type { Replica } from "#replica.ts"
|
|
22
22
|
import { openReplica } from "#replica.ts"
|
|
23
23
|
import type { FsLease, ObjectStore } from "#store.ts"
|
|
24
|
-
import { acquireFsLease, releaseFsLease } from "#store.ts"
|
|
24
|
+
import { acquireFsLease, encodeLease, parseLease, releaseFsLease, sweepStaleTemps, syncedTemp } from "#store.ts"
|
|
25
25
|
|
|
26
26
|
const PINNED_TENANT = "_shared"
|
|
27
|
-
const SIGNED_DECIMAL = regex("^-?\\d+$")
|
|
28
|
-
const UNSIGNED_DECIMAL = regex("^\\d+$")
|
|
29
27
|
const TENANT_ID = regex("^[A-Za-z0-9._-]+$")
|
|
30
28
|
|
|
31
29
|
/** Directory-lease lifetime: one owner per replica directory. The pool
|
|
@@ -88,25 +86,6 @@ function codeOf(error: Error): string | undefined {
|
|
|
88
86
|
return (error as NodeJS.ErrnoException).code
|
|
89
87
|
}
|
|
90
88
|
|
|
91
|
-
function encodeLease(holder: bigint, token: bigint, expires: bigint): Uint8Array {
|
|
92
|
-
return new TextEncoder().encode(`${holder}\n${token}\n${expires}\n`)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function parseLease(raw: string): { holder: bigint; token: bigint; expires: bigint } | null {
|
|
96
|
-
const lines = raw.trim().split("\n")
|
|
97
|
-
if (lines.length !== 3) {
|
|
98
|
-
return null
|
|
99
|
-
}
|
|
100
|
-
const [holderLine, tokenLine, expiresLine] = lines
|
|
101
|
-
if (holderLine === undefined || tokenLine === undefined || expiresLine === undefined) {
|
|
102
|
-
return null
|
|
103
|
-
}
|
|
104
|
-
if (!SIGNED_DECIMAL.test(holderLine) || !UNSIGNED_DECIMAL.test(tokenLine) || !SIGNED_DECIMAL.test(expiresLine)) {
|
|
105
|
-
return null
|
|
106
|
-
}
|
|
107
|
-
return { holder: BigInt(holderLine), token: BigInt(tokenLine), expires: BigInt(expiresLine) }
|
|
108
|
-
}
|
|
109
|
-
|
|
110
89
|
async function fsyncDir(dir: string): Promise<void> {
|
|
111
90
|
const handle = await fs.open(dir, "r")
|
|
112
91
|
const synced = await errors.try(handle.sync())
|
|
@@ -131,23 +110,13 @@ async function renewDirLease(held: FsLease, ttlMs: number): Promise<void> {
|
|
|
131
110
|
if (lease === null || lease.holder !== held.holder || lease.token !== held.token) {
|
|
132
111
|
throw errors.new("directory lease is no longer ours")
|
|
133
112
|
}
|
|
134
|
-
const body = encodeLease(held.holder, held.token, BigInt(Date.now() + ttlMs))
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
await file.writeFile(body)
|
|
140
|
-
await file.sync()
|
|
141
|
-
})()
|
|
142
|
-
)
|
|
143
|
-
await file.close()
|
|
144
|
-
if (written.error) {
|
|
145
|
-
await fs.rm(tmp, { force: true })
|
|
146
|
-
throw written.error
|
|
147
|
-
}
|
|
148
|
-
const replaced = await errors.try(fs.rename(tmp, held.path))
|
|
113
|
+
const body = encodeLease({ holder: held.holder, token: held.token, expires: BigInt(Date.now() + ttlMs) })
|
|
114
|
+
// The temp lives under the reserved `{root}/~tmp` namespace, where a
|
|
115
|
+
// crash strands sweepable litter — never beside the lease path.
|
|
116
|
+
const temp = await syncedTemp(held.root, body)
|
|
117
|
+
const replaced = await errors.try(fs.rename(temp, held.path))
|
|
149
118
|
if (replaced.error) {
|
|
150
|
-
await fs.rm(
|
|
119
|
+
await fs.rm(temp, { force: true })
|
|
151
120
|
if (codeOf(replaced.error) === "ENOENT") {
|
|
152
121
|
return
|
|
153
122
|
}
|
|
@@ -199,6 +168,9 @@ function openTenants<Rels extends SchemaRelations>(options: OpenTenantsOptions<R
|
|
|
199
168
|
const renewEveryMs = Math.max(1, Math.floor(dirLeaseMs / 3))
|
|
200
169
|
const open = new Map<string, TenantSlot<Rels>>()
|
|
201
170
|
const opening = new Map<string, Promise<LiveHandle<Rels>>>()
|
|
171
|
+
/** The pool root's `~tmp` holds lease mint/renew/release temps; stale
|
|
172
|
+
* ones are crash litter swept once per pool open, best-effort. */
|
|
173
|
+
const swept = errors.try(sweepStaleTemps(path.join(options.dir, TEMP_NAMESPACE)))
|
|
202
174
|
let tick = 0
|
|
203
175
|
let closed = false
|
|
204
176
|
let renewTimer: ReturnType<typeof setInterval> | undefined
|
|
@@ -328,8 +300,14 @@ function openTenants<Rels extends SchemaRelations>(options: OpenTenantsOptions<R
|
|
|
328
300
|
}
|
|
329
301
|
|
|
330
302
|
async function openOne(tenant: string): Promise<LiveHandle<Rels>> {
|
|
303
|
+
await swept
|
|
331
304
|
const dir = path.join(options.dir, tenant)
|
|
332
|
-
|
|
305
|
+
/** The dir-lease mirrors Rust `acquire_named`: tokens live at
|
|
306
|
+
* `{options.dir}/~lease/{tenant}`, outside the disposable replica
|
|
307
|
+
* directory `{options.dir}/{tenant}` — the replica-open sweep
|
|
308
|
+
* rm-rfs the replica dir's own `~lease` and must not touch the
|
|
309
|
+
* held tenant lease. */
|
|
310
|
+
const lease = await acquireFsLease(options.dir, tenant, dirLeaseMs, "refuse")
|
|
333
311
|
let replica: LiveHandle<Rels>
|
|
334
312
|
try {
|
|
335
313
|
replica = brandLive(
|
|
@@ -432,5 +410,5 @@ function openTenants<Rels extends SchemaRelations>(options: OpenTenantsOptions<R
|
|
|
432
410
|
}
|
|
433
411
|
}
|
|
434
412
|
|
|
435
|
-
export type {
|
|
413
|
+
export type { OpenTenantsOptions, Tenants }
|
|
436
414
|
export { openTenants }
|
package/src/vector.ts
CHANGED
|
@@ -1,34 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The per-braid generation map. Sum, domination, checkpoint order,
|
|
3
|
-
* apply's increment
|
|
4
|
-
*
|
|
2
|
+
* The per-braid generation map. Sum, domination, checkpoint order, and
|
|
3
|
+
* apply's increment live here. Overflow is a refusal of `sum` and of
|
|
4
|
+
* nowhere else.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { U64_MAX } from "#bytes.ts"
|
|
8
8
|
import type { Braid } from "#descriptor.ts"
|
|
9
|
-
import { braidHex } from "#descriptor.ts"
|
|
10
9
|
import type { Generation } from "#keys.ts"
|
|
11
10
|
import { generation } from "#keys.ts"
|
|
12
11
|
|
|
13
|
-
/** One braid id and its applied count on the wire: u32le + u64le. */
|
|
14
|
-
const PAIR_BYTES = 12n
|
|
15
|
-
|
|
16
12
|
const Overflow = { tag: "overflow" } as const
|
|
17
13
|
type Overflow = typeof Overflow
|
|
18
14
|
|
|
19
15
|
type CheckpointOrder = "before" | "equal" | "after"
|
|
20
16
|
|
|
21
|
-
type VectorParse =
|
|
22
|
-
| { readonly tag: "ok"; readonly vector: Vector }
|
|
23
|
-
| { readonly tag: "truncated" }
|
|
24
|
-
| { readonly tag: "trailing"; readonly bytes: number }
|
|
25
|
-
| { readonly tag: "malformed" }
|
|
26
|
-
| { readonly tag: "overflow" }
|
|
27
|
-
|
|
28
|
-
function braidIdOf(id: Braid): number {
|
|
29
|
-
return Number.parseInt(id.slice(1), 16)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
17
|
/** Applied counts keyed by braid. Any vector is a legal restore point. */
|
|
33
18
|
class Vector {
|
|
34
19
|
readonly #counts: Map<Braid, bigint>
|
|
@@ -112,62 +97,7 @@ class Vector {
|
|
|
112
97
|
next.set(braid, current === U64_MAX ? U64_MAX : current + 1n)
|
|
113
98
|
return new Vector(next)
|
|
114
99
|
}
|
|
115
|
-
|
|
116
|
-
/** `u32le` count, then `(u32le braid, u64le g)` pairs in braid order. */
|
|
117
|
-
encode(): Uint8Array {
|
|
118
|
-
const braids = [...this.#counts.keys()].sort()
|
|
119
|
-
const out = new ByteWriter(4 + braids.length * 12)
|
|
120
|
-
out.u32le(braids.length)
|
|
121
|
-
for (const id of braids) {
|
|
122
|
-
out.u32le(braidIdOf(id))
|
|
123
|
-
out.u64le(this.#counts.get(id) ?? 0n)
|
|
124
|
-
}
|
|
125
|
-
return out.finish()
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Inverse of `encode`. A count the remaining bytes cannot open is
|
|
130
|
-
* truncated; entries must ascend; overflow is `sum`.
|
|
131
|
-
*/
|
|
132
|
-
static parse(bytes: Uint8Array): VectorParse {
|
|
133
|
-
const short = { tag: "short" } as const
|
|
134
|
-
const reader = new ByteReader(bytes, {
|
|
135
|
-
fail(): never {
|
|
136
|
-
throw short
|
|
137
|
-
}
|
|
138
|
-
})
|
|
139
|
-
try {
|
|
140
|
-
const count = BigInt(reader.u32le("count"))
|
|
141
|
-
if (count > 0n && BigInt(reader.remaining()) / PAIR_BYTES < count) {
|
|
142
|
-
return { tag: "truncated" }
|
|
143
|
-
}
|
|
144
|
-
const entries: Array<readonly [Braid, bigint]> = []
|
|
145
|
-
let last: Braid | undefined
|
|
146
|
-
for (let i = 0n; i < count; i++) {
|
|
147
|
-
const name = braidHex(reader.u32le("braid"))
|
|
148
|
-
const g = reader.u64le("g")
|
|
149
|
-
if (last !== undefined && last >= name) {
|
|
150
|
-
return { tag: "malformed" }
|
|
151
|
-
}
|
|
152
|
-
entries.push([name, g])
|
|
153
|
-
last = name
|
|
154
|
-
}
|
|
155
|
-
if (reader.remaining() !== 0) {
|
|
156
|
-
return { tag: "trailing", bytes: reader.remaining() }
|
|
157
|
-
}
|
|
158
|
-
const vector = new Vector(entries)
|
|
159
|
-
if (typeof vector.sum() !== "bigint") {
|
|
160
|
-
return { tag: "overflow" }
|
|
161
|
-
}
|
|
162
|
-
return { tag: "ok", vector }
|
|
163
|
-
} catch (error) {
|
|
164
|
-
if (error === short) {
|
|
165
|
-
return { tag: "truncated" }
|
|
166
|
-
}
|
|
167
|
-
throw error
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
100
|
}
|
|
171
101
|
|
|
172
|
-
export type { CheckpointOrder
|
|
102
|
+
export type { CheckpointOrder }
|
|
173
103
|
export { Overflow, Vector }
|