@namzu/sdk 60.0.0 → 60.2.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/CHANGELOG.md +40 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +1 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +19 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/acp/update.d.ts.map +1 -1
- package/dist/bridge/acp/update.js +1 -0
- package/dist/bridge/acp/update.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +28 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/constants/continuation.d.ts +9 -0
- package/dist/constants/continuation.d.ts.map +1 -1
- package/dist/constants/continuation.js +9 -0
- package/dist/constants/continuation.js.map +1 -1
- package/dist/contracts/session/api.d.ts +8 -2
- package/dist/contracts/session/api.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts +7 -1
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +20 -0
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/peers/dir.d.ts.map +1 -1
- package/dist/peers/dir.js +3 -0
- package/dist/peers/dir.js.map +1 -1
- package/dist/provider/collect-chat-completion.d.ts.map +1 -1
- package/dist/provider/collect-chat-completion.js +4 -0
- package/dist/provider/collect-chat-completion.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +11 -0
- package/dist/provider/fallback.js.map +1 -1
- package/dist/provider/idle-timeout.d.ts.map +1 -1
- package/dist/provider/idle-timeout.js +5 -0
- package/dist/provider/idle-timeout.js.map +1 -1
- package/dist/provider/mock.d.ts +2 -0
- package/dist/provider/mock.d.ts.map +1 -1
- package/dist/provider/mock.js +16 -8
- package/dist/provider/mock.js.map +1 -1
- package/dist/provider/model-window-suffix.d.ts.map +1 -1
- package/dist/provider/model-window-suffix.js +1 -0
- package/dist/provider/model-window-suffix.js.map +1 -1
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +5 -0
- package/dist/provider/retry.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -0
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +1 -0
- package/dist/public-runtime.js.map +1 -1
- package/dist/runtime/query/events.d.ts.map +1 -1
- package/dist/runtime/query/events.js +17 -0
- package/dist/runtime/query/events.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +23 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +88 -9
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +14 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +14 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/effort-clamp.d.ts +13 -0
- package/dist/runtime/query/iteration/effort-clamp.d.ts.map +1 -0
- package/dist/runtime/query/iteration/effort-clamp.js +42 -0
- package/dist/runtime/query/iteration/effort-clamp.js.map +1 -0
- package/dist/runtime/query/iteration/index.d.ts +34 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +199 -6
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +6 -0
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +6 -0
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +12 -2
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +2 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +6 -5
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/session/paths.d.ts +15 -22
- package/dist/session/paths.d.ts.map +1 -1
- package/dist/session/paths.js +19 -46
- package/dist/session/paths.js.map +1 -1
- package/dist/store/memory/disk.d.ts.map +1 -1
- package/dist/store/memory/disk.js +1 -1
- package/dist/store/memory/disk.js.map +1 -1
- package/dist/store/memory/markdown.d.ts.map +1 -1
- package/dist/store/memory/markdown.js +1 -1
- package/dist/store/memory/markdown.js.map +1 -1
- package/dist/store/memory/operation-lock.d.ts +20 -4
- package/dist/store/memory/operation-lock.d.ts.map +1 -1
- package/dist/store/memory/operation-lock.js +235 -58
- package/dist/store/memory/operation-lock.js.map +1 -1
- package/dist/store/session-log/core.d.ts +43 -0
- package/dist/store/session-log/core.d.ts.map +1 -1
- package/dist/store/session-log/core.js +190 -3
- package/dist/store/session-log/core.js.map +1 -1
- package/dist/store/session-log/disk.d.ts +1 -1
- package/dist/store/session-log/disk.d.ts.map +1 -1
- package/dist/store/session-log/disk.js +1 -0
- package/dist/store/session-log/disk.js.map +1 -1
- package/dist/store/session-log/index.d.ts +1 -1
- package/dist/store/session-log/index.d.ts.map +1 -1
- package/dist/store/session-log/index.js +1 -1
- package/dist/store/session-log/index.js.map +1 -1
- package/dist/tools/builtins/bash-cwd.d.ts.map +1 -1
- package/dist/tools/builtins/bash-cwd.js +2 -3
- package/dist/tools/builtins/bash-cwd.js.map +1 -1
- package/dist/tools/builtins/read-media.js +3 -3
- package/dist/tools/builtins/read-media.js.map +1 -1
- package/dist/turn/reporter.d.ts.map +1 -1
- package/dist/turn/reporter.js +13 -0
- package/dist/turn/reporter.js.map +1 -1
- package/dist/types/agent/query.d.ts +2 -0
- package/dist/types/agent/query.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +8 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/message/index.d.ts +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +1 -0
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/provider/chat.d.ts +19 -0
- package/dist/types/provider/chat.d.ts.map +1 -1
- package/dist/types/provider/config.d.ts +7 -0
- package/dist/types/provider/config.d.ts.map +1 -1
- package/dist/types/provider/index.d.ts +1 -1
- package/dist/types/provider/index.d.ts.map +1 -1
- package/dist/types/provider/interface.d.ts +15 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/provider/stream.d.ts +6 -0
- package/dist/types/provider/stream.d.ts.map +1 -1
- package/dist/types/session/events.d.ts +72 -2
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js +4 -0
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/records.d.ts +34 -34
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +5 -0
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/session/stop-reason.d.ts +11 -0
- package/dist/types/session/stop-reason.d.ts.map +1 -1
- package/dist/utils/temp.d.ts +82 -0
- package/dist/utils/temp.d.ts.map +1 -0
- package/dist/utils/temp.js +196 -0
- package/dist/utils/temp.js.map +1 -0
- package/package.json +1 -1
- package/src/agents/QueryAgent.ts +1 -0
- package/src/bridge/a2a/mapper.ts +22 -0
- package/src/bridge/acp/update.ts +6 -1
- package/src/bridge/sse/mapper.ts +29 -0
- package/src/constants/continuation.ts +13 -0
- package/src/contracts/session/api.ts +8 -0
- package/src/manager/session/turn-recorder.ts +22 -0
- package/src/peers/dir.ts +3 -0
- package/src/provider/collect-chat-completion.ts +3 -0
- package/src/provider/fallback.ts +17 -0
- package/src/provider/idle-timeout.ts +9 -0
- package/src/provider/mock.ts +19 -9
- package/src/provider/model-window-suffix.ts +1 -0
- package/src/provider/retry.ts +9 -0
- package/src/public-runtime.ts +13 -0
- package/src/runtime/query/events.ts +19 -0
- package/src/runtime/query/executor.ts +92 -8
- package/src/runtime/query/index.ts +27 -0
- package/src/runtime/query/iteration/effort-clamp.ts +46 -0
- package/src/runtime/query/iteration/index.ts +260 -6
- package/src/runtime/query/iteration/phases/context.ts +2 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +6 -0
- package/src/runtime/query/iteration/stream-turn.ts +5 -0
- package/src/runtime/query/review-policy.ts +13 -2
- package/src/runtime/query/tooling.ts +3 -0
- package/src/sandbox/provider/local.ts +8 -5
- package/src/session/paths.ts +32 -57
- package/src/store/memory/disk.ts +1 -0
- package/src/store/memory/markdown.ts +1 -0
- package/src/store/memory/operation-lock.ts +288 -57
- package/src/store/session-log/core.ts +235 -3
- package/src/store/session-log/disk.ts +7 -1
- package/src/store/session-log/index.ts +4 -0
- package/src/tools/builtins/bash-cwd.ts +2 -3
- package/src/tools/builtins/read-media.ts +3 -3
- package/src/turn/reporter.ts +14 -0
- package/src/types/agent/query.ts +2 -0
- package/src/types/hitl/index.ts +8 -0
- package/src/types/message/index.ts +5 -3
- package/src/types/provider/chat.ts +20 -0
- package/src/types/provider/config.ts +7 -0
- package/src/types/provider/index.ts +1 -0
- package/src/types/provider/interface.ts +20 -0
- package/src/types/provider/stream.ts +6 -0
- package/src/types/session/events.ts +106 -6
- package/src/types/session/records.ts +5 -0
- package/src/types/session/stop-reason.ts +11 -0
- package/src/utils/temp.ts +250 -0
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import
|
|
2
|
+
import type { Stats } from 'node:fs'
|
|
3
|
+
import { lstat, open, readFile, unlink } from 'node:fs/promises'
|
|
4
|
+
import { hostname as osHostname } from 'node:os'
|
|
3
5
|
import { setTimeout as delay } from 'node:timers/promises'
|
|
4
6
|
|
|
5
7
|
import { NamzuError } from '../../types/errors/index.js'
|
|
8
|
+
import type { Logger } from '../../utils/logger.js'
|
|
6
9
|
|
|
7
|
-
/** Wait bound, not a lease: a slow
|
|
10
|
+
/** Wait bound, not a lease: a slow owner that is still running is never replaced. */
|
|
8
11
|
export const DEFAULT_MEMORY_LOCK_TIMEOUT_MS = 10_000
|
|
9
12
|
|
|
10
13
|
export function validateMemoryLockTimeout(timeoutMs: number): void {
|
|
@@ -17,83 +20,311 @@ export function validateMemoryLockTimeout(timeoutMs: number): void {
|
|
|
17
20
|
}
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
/**
|
|
24
|
+
* An empty or unparsable lock file is only believed abandoned once it is older
|
|
25
|
+
* than this. A live acquirer writes its record immediately after creating the
|
|
26
|
+
* file, so a gap of seconds means the writer died in between (disk full, kill).
|
|
27
|
+
*/
|
|
28
|
+
export const MEMORY_LOCK_RECORD_GRACE_MS = 5_000
|
|
29
|
+
|
|
30
|
+
export interface MemoryOperationLockOptions {
|
|
31
|
+
readonly logger?: Logger
|
|
32
|
+
/** Clock compared against file mtimes. */
|
|
33
|
+
readonly now?: () => number
|
|
34
|
+
/** True when a process with this pid exists on this machine. */
|
|
35
|
+
readonly isPidAlive?: (pid: number) => boolean
|
|
36
|
+
readonly hostname?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface OwnerRecord {
|
|
40
|
+
readonly pid: number
|
|
41
|
+
readonly token: string
|
|
42
|
+
readonly acquiredAt: number
|
|
43
|
+
readonly hostname?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface Inspection {
|
|
47
|
+
readonly stats: Stats
|
|
48
|
+
readonly content: string
|
|
49
|
+
readonly pid: number | undefined
|
|
50
|
+
/** Set only when the owner is provably gone. */
|
|
51
|
+
readonly staleReason: string | undefined
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface Resolved {
|
|
55
|
+
readonly logger: Logger | undefined
|
|
56
|
+
readonly now: () => number
|
|
57
|
+
readonly isPidAlive: (pid: number) => boolean
|
|
58
|
+
readonly hostname: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Residual risk, deliberately accepted: a pid that is alive is treated as the
|
|
63
|
+
* owner even when it is an unrelated process that reused the number after the
|
|
64
|
+
* real owner died. That cannot be disproved from here, so such a lock stays and
|
|
65
|
+
* the caller gets the plain "another process is using it" error. It is not
|
|
66
|
+
* bounded by `acquiredAt` age: a lock's age is not a lease.
|
|
67
|
+
*/
|
|
68
|
+
function defaultIsPidAlive(pid: number): boolean {
|
|
69
|
+
try {
|
|
70
|
+
process.kill(pid, 0)
|
|
71
|
+
return true
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return (error as NodeJS.ErrnoException).code !== 'ESRCH'
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function lockError(path: string, message: string, timeoutMs: number): NamzuError {
|
|
21
78
|
return new NamzuError({
|
|
22
79
|
code: 'storage_error',
|
|
23
|
-
message
|
|
80
|
+
message,
|
|
24
81
|
details: { lockPath: path, timeoutMs },
|
|
25
82
|
retryable: false,
|
|
26
83
|
})
|
|
27
84
|
}
|
|
28
85
|
|
|
86
|
+
function busyError(path: string, pid: number | undefined, timeoutMs: number): NamzuError {
|
|
87
|
+
const who =
|
|
88
|
+
pid === undefined ? 'Another Namzu process is' : `Another Namzu process (pid ${pid}) is`
|
|
89
|
+
return lockError(
|
|
90
|
+
path,
|
|
91
|
+
`${who} using the stored memory. Close it, or wait for it to finish.`,
|
|
92
|
+
timeoutMs,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseOwner(content: string): OwnerRecord | undefined {
|
|
97
|
+
try {
|
|
98
|
+
const value: unknown = JSON.parse(content)
|
|
99
|
+
if (!value || typeof value !== 'object') return undefined
|
|
100
|
+
const pid = (value as { pid?: unknown }).pid
|
|
101
|
+
if (typeof pid !== 'number' || !Number.isSafeInteger(pid) || pid <= 0) return undefined
|
|
102
|
+
const host = (value as { hostname?: unknown }).hostname
|
|
103
|
+
return {
|
|
104
|
+
pid,
|
|
105
|
+
token: String((value as { token?: unknown }).token ?? ''),
|
|
106
|
+
acquiredAt: Number((value as { acquiredAt?: unknown }).acquiredAt ?? 0),
|
|
107
|
+
...(typeof host === 'string' && host.length > 0 ? { hostname: host } : {}),
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
return undefined
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Reads the file at `path`; undefined when it is gone or was swapped while reading. */
|
|
115
|
+
async function inspect(path: string, env: Resolved): Promise<Inspection | undefined> {
|
|
116
|
+
const before = await lstat(path).catch((error: NodeJS.ErrnoException) => {
|
|
117
|
+
if (error.code === 'ENOENT') return undefined
|
|
118
|
+
throw error
|
|
119
|
+
})
|
|
120
|
+
if (!before) return undefined
|
|
121
|
+
if (!before.isFile() || before.isSymbolicLink())
|
|
122
|
+
return { stats: before, content: '', pid: undefined, staleReason: undefined }
|
|
123
|
+
const content = await readFile(path, 'utf8').catch((error: NodeJS.ErrnoException) => {
|
|
124
|
+
if (error.code === 'ENOENT') return undefined
|
|
125
|
+
throw error
|
|
126
|
+
})
|
|
127
|
+
if (content === undefined) return undefined
|
|
128
|
+
const after = await lstat(path).catch(() => undefined)
|
|
129
|
+
if (!after || after.dev !== before.dev || after.ino !== before.ino) return undefined
|
|
130
|
+
|
|
131
|
+
const owner = parseOwner(content)
|
|
132
|
+
if (owner) {
|
|
133
|
+
// A record from another host on a shared filesystem cannot be checked from
|
|
134
|
+
// here; it is live. A record without a host predates the field: local.
|
|
135
|
+
if (owner.hostname !== undefined && owner.hostname !== env.hostname) {
|
|
136
|
+
return { stats: before, content, pid: owner.pid, staleReason: undefined }
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
stats: before,
|
|
140
|
+
content,
|
|
141
|
+
pid: owner.pid,
|
|
142
|
+
staleReason: env.isPidAlive(owner.pid) ? undefined : `owner process ${owner.pid} is gone`,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const age = env.now() - before.mtimeMs
|
|
146
|
+
return {
|
|
147
|
+
stats: before,
|
|
148
|
+
content,
|
|
149
|
+
pid: undefined,
|
|
150
|
+
staleReason:
|
|
151
|
+
age > MEMORY_LOCK_RECORD_GRACE_MS
|
|
152
|
+
? `lock holds no readable owner record and is ${Math.round(age / 1000)}s old`
|
|
153
|
+
: undefined,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Creates `path` exclusively and writes this process's owner record. Returns
|
|
159
|
+
* undefined when the path is occupied. A failed write (a full disk) leaves no
|
|
160
|
+
* file behind.
|
|
161
|
+
*/
|
|
162
|
+
async function createOwned(
|
|
163
|
+
path: string,
|
|
164
|
+
env: Resolved,
|
|
165
|
+
): Promise<{ identity: Stats; owner: string } | undefined> {
|
|
166
|
+
let handle: Awaited<ReturnType<typeof open>>
|
|
167
|
+
try {
|
|
168
|
+
handle = await open(path, 'wx', 0o600)
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if ((error as NodeJS.ErrnoException).code === 'EEXIST') return undefined
|
|
171
|
+
throw error
|
|
172
|
+
}
|
|
173
|
+
const owner = `${JSON.stringify({
|
|
174
|
+
pid: process.pid,
|
|
175
|
+
token: randomUUID(),
|
|
176
|
+
acquiredAt: Date.now(),
|
|
177
|
+
hostname: env.hostname,
|
|
178
|
+
})}\n`
|
|
179
|
+
let identity: Stats | undefined
|
|
180
|
+
try {
|
|
181
|
+
identity = await handle.stat()
|
|
182
|
+
await handle.writeFile(owner, 'utf8')
|
|
183
|
+
await handle.sync()
|
|
184
|
+
} catch (error) {
|
|
185
|
+
await handle.close().catch(() => undefined)
|
|
186
|
+
// We created this path exclusively a moment ago; drop it so no empty or
|
|
187
|
+
// partial record is left to block the next start.
|
|
188
|
+
const current = await lstat(path).catch(() => undefined)
|
|
189
|
+
if (!identity || (current?.dev === identity.dev && current.ino === identity.ino)) {
|
|
190
|
+
await unlink(path).catch(() => undefined)
|
|
191
|
+
}
|
|
192
|
+
throw error
|
|
193
|
+
}
|
|
194
|
+
await handle.close()
|
|
195
|
+
return { identity, owner }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function releaseOwned(
|
|
199
|
+
path: string,
|
|
200
|
+
identity: Stats,
|
|
201
|
+
owner: string,
|
|
202
|
+
timeoutMs: number,
|
|
203
|
+
): Promise<void> {
|
|
204
|
+
const current = await lstat(path)
|
|
205
|
+
if (
|
|
206
|
+
!current.isFile() ||
|
|
207
|
+
current.isSymbolicLink() ||
|
|
208
|
+
current.dev !== identity.dev ||
|
|
209
|
+
current.ino !== identity.ino ||
|
|
210
|
+
(await readFile(path, 'utf8')) !== owner
|
|
211
|
+
) {
|
|
212
|
+
throw lockError(
|
|
213
|
+
path,
|
|
214
|
+
`Memory operation lock ${path}: the lock owner changed before release.`,
|
|
215
|
+
timeoutMs,
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
await unlink(path)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function sameFile(a: Stats, b: Stats): boolean {
|
|
222
|
+
return a.dev === b.dev && a.ino === b.ino
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Unlinks `path` only if it is still exactly the stale file that was inspected. */
|
|
226
|
+
async function unlinkIfStillStale(path: string, seen: Inspection, env: Resolved): Promise<boolean> {
|
|
227
|
+
const again = await inspect(path, env)
|
|
228
|
+
if (!again || !sameFile(again.stats, seen.stats) || again.content !== seen.content) return false
|
|
229
|
+
if (again.staleReason === undefined) return false
|
|
230
|
+
// Narrow the window further: the inode must still be the inspected one now.
|
|
231
|
+
const last = await lstat(path).catch(() => undefined)
|
|
232
|
+
if (!last || !sameFile(last, seen.stats)) return false
|
|
233
|
+
await unlink(path).catch((error: NodeJS.ErrnoException) => {
|
|
234
|
+
if (error.code !== 'ENOENT') throw error
|
|
235
|
+
})
|
|
236
|
+
return true
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Removes a lock whose owner is provably gone. Two processes that see the same
|
|
241
|
+
* stale lock are serialised through `<lock>.break`; the lock is re-inspected
|
|
242
|
+
* under it and only the inspected inode is unlinked. Returns whether the lock
|
|
243
|
+
* was removed.
|
|
244
|
+
*/
|
|
245
|
+
async function breakStaleLock(
|
|
246
|
+
path: string,
|
|
247
|
+
seen: Inspection,
|
|
248
|
+
timeoutMs: number,
|
|
249
|
+
env: Resolved,
|
|
250
|
+
): Promise<boolean> {
|
|
251
|
+
const breakPath = `${path}.break`
|
|
252
|
+
let held: { identity: Stats; owner: string } | undefined
|
|
253
|
+
for (let attempt = 0; attempt < 2 && !held; attempt++) {
|
|
254
|
+
held = await createOwned(breakPath, env)
|
|
255
|
+
if (held) break
|
|
256
|
+
const breaker = await inspect(breakPath, env)
|
|
257
|
+
if (!breaker) continue
|
|
258
|
+
// A dead breaker's file follows the same rule, once.
|
|
259
|
+
if (breaker.staleReason === undefined || !(await unlinkIfStillStale(breakPath, breaker, env))) {
|
|
260
|
+
return false
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (!held) return false
|
|
264
|
+
try {
|
|
265
|
+
const removed = await unlinkIfStillStale(path, seen, env)
|
|
266
|
+
if (removed) {
|
|
267
|
+
env.logger?.info('Broke an abandoned memory operation lock', {
|
|
268
|
+
'namzu.store.lock_path': path,
|
|
269
|
+
'namzu.store.lock_reason': seen.staleReason,
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
return removed
|
|
273
|
+
} finally {
|
|
274
|
+
await releaseOwned(breakPath, held.identity, held.owner, timeoutMs).catch(() => undefined)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
29
278
|
/**
|
|
30
279
|
* Local-filesystem exclusion for cooperating processes. Atomic rename of the
|
|
31
280
|
* index alone cannot serialize the index/content read-modify-write transaction.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
281
|
+
* A lock whose owner is provably gone (dead pid on this host, or an owner-less
|
|
282
|
+
* file older than the grace period) is broken; a live or unverifiable owner is
|
|
283
|
+
* waited for until the timeout, never replaced on a guessed lease.
|
|
34
284
|
*/
|
|
35
285
|
export async function acquireMemoryOperationLock(
|
|
36
286
|
path: string,
|
|
37
287
|
timeoutMs: number,
|
|
288
|
+
options: MemoryOperationLockOptions = {},
|
|
38
289
|
): Promise<() => Promise<void>> {
|
|
290
|
+
const env: Resolved = {
|
|
291
|
+
logger: options.logger,
|
|
292
|
+
now: options.now ?? Date.now,
|
|
293
|
+
isPidAlive: options.isPidAlive ?? defaultIsPidAlive,
|
|
294
|
+
hostname: options.hostname ?? osHostname(),
|
|
295
|
+
}
|
|
39
296
|
const deadline = performance.now() + timeoutMs
|
|
40
297
|
let attempted = false
|
|
298
|
+
let lastPid: number | undefined
|
|
299
|
+
// The wait bound limits waiting for a live owner, not the attempt that
|
|
300
|
+
// follows a successful break: the lock is free then, and breaking it can
|
|
301
|
+
// itself outlast a short bound on a slow disk.
|
|
302
|
+
let justBroke = false
|
|
41
303
|
for (;;) {
|
|
42
|
-
if (attempted && performance.now() >= deadline) {
|
|
43
|
-
throw
|
|
304
|
+
if (attempted && !justBroke && performance.now() >= deadline) {
|
|
305
|
+
throw busyError(path, lastPid, timeoutMs)
|
|
44
306
|
}
|
|
307
|
+
justBroke = false
|
|
45
308
|
attempted = true
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
} catch (error) {
|
|
50
|
-
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
|
|
51
|
-
const existing = await lstat(path).catch((statError: NodeJS.ErrnoException) => {
|
|
52
|
-
if (statError.code === 'ENOENT') return undefined
|
|
53
|
-
throw statError
|
|
54
|
-
})
|
|
55
|
-
if (!existing) continue
|
|
56
|
-
if (!existing.isFile() || existing.isSymbolicLink()) {
|
|
57
|
-
throw lockError(path, 'the occupied lock is not a regular file', timeoutMs)
|
|
58
|
-
}
|
|
59
|
-
if (performance.now() >= deadline) {
|
|
60
|
-
throw lockError(path, `acquisition timed out after ${timeoutMs} ms`, timeoutMs)
|
|
61
|
-
}
|
|
62
|
-
await delay(Math.min(25, Math.max(1, deadline - performance.now())))
|
|
63
|
-
continue
|
|
309
|
+
const created = await createOwned(path, env)
|
|
310
|
+
if (created) {
|
|
311
|
+
return () => releaseOwned(path, created.identity, created.owner, timeoutMs)
|
|
64
312
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
await handle.writeFile(owner, 'utf8')
|
|
75
|
-
await handle.sync()
|
|
76
|
-
} catch (error) {
|
|
77
|
-
await handle.close()
|
|
78
|
-
const current = await lstat(path).catch(() => undefined)
|
|
79
|
-
if (current?.dev === identity.dev && current.ino === identity.ino) {
|
|
80
|
-
await unlink(path).catch(() => undefined)
|
|
81
|
-
}
|
|
82
|
-
throw error
|
|
313
|
+
const seen = await inspect(path, env)
|
|
314
|
+
if (!seen) continue
|
|
315
|
+
if (!seen.stats.isFile() || seen.stats.isSymbolicLink()) {
|
|
316
|
+
throw lockError(
|
|
317
|
+
path,
|
|
318
|
+
`The stored memory lock ${path} is not a regular file, so Namzu will not use it. Move it aside to continue.`,
|
|
319
|
+
timeoutMs,
|
|
320
|
+
)
|
|
83
321
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
!current.isFile() ||
|
|
89
|
-
current.isSymbolicLink() ||
|
|
90
|
-
current.dev !== identity.dev ||
|
|
91
|
-
current.ino !== identity.ino ||
|
|
92
|
-
(await readFile(path, 'utf8')) !== owner
|
|
93
|
-
) {
|
|
94
|
-
throw lockError(path, 'the lock owner changed before release', timeoutMs)
|
|
95
|
-
}
|
|
96
|
-
await unlink(path)
|
|
322
|
+
lastPid = seen.pid
|
|
323
|
+
if (seen.staleReason !== undefined && (await breakStaleLock(path, seen, timeoutMs, env))) {
|
|
324
|
+
justBroke = true
|
|
325
|
+
continue
|
|
97
326
|
}
|
|
327
|
+
if (performance.now() >= deadline) throw busyError(path, lastPid, timeoutMs)
|
|
328
|
+
await delay(Math.min(25, Math.max(1, deadline - performance.now())))
|
|
98
329
|
}
|
|
99
330
|
}
|