rollbridge 0.1.32 → 0.1.33
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
CHANGED
package/src/daemon.js
CHANGED
|
@@ -412,7 +412,10 @@ export default class RollbridgeDaemon {
|
|
|
412
412
|
|
|
413
413
|
if (!staged.committed) {
|
|
414
414
|
if (retiredIncumbentControl) {
|
|
415
|
-
|
|
415
|
+
const processKey = this.guardian.processes.keys().next().value
|
|
416
|
+
|
|
417
|
+
if (!processKey) throw new Error("Retired owner replacement requires an exact recovered guardian process registration")
|
|
418
|
+
await this.guardian.commitRetiredOwnerReplacement(prepared.replacementId, processKey)
|
|
416
419
|
} else {
|
|
417
420
|
try {
|
|
418
421
|
if (!incumbentControl) throw new Error("Owner replacement incumbent control session is unavailable")
|
package/src/guardian-client.js
CHANGED
|
@@ -194,9 +194,12 @@ export default class GuardianClient {
|
|
|
194
194
|
await this.request({command: "commit-owner-replacement", replacementId})
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
/**
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
/**
|
|
198
|
+
* @param {string} replacementId - Same-authority transaction whose incumbent listener is absent.
|
|
199
|
+
* @param {string} key - Exact recovered guardian process proving candidate reconstruction.
|
|
200
|
+
*/
|
|
201
|
+
async commitRetiredOwnerReplacement(replacementId, key) {
|
|
202
|
+
await this.request({command: "commit-retired-owner-replacement", key, replacementId})
|
|
200
203
|
}
|
|
201
204
|
|
|
202
205
|
/** @param {string} replacementId - Committed transaction awaiting incumbent retirement. */
|
package/src/process-guardian.js
CHANGED
|
@@ -300,6 +300,7 @@ async function execute(request, socket) {
|
|
|
300
300
|
|
|
301
301
|
if (request.command === "commit-retired-owner-replacement") {
|
|
302
302
|
requireReplacement(socket, request)
|
|
303
|
+
requireProcess(request)
|
|
303
304
|
if (!replacementOwnerState) throw new Error("Retired owner replacement transaction is not staged")
|
|
304
305
|
if (!isDeepStrictEqual(ownerAuthority(ownerState), replacementAuthority)) throw new Error("Retired owner replacement requires unchanged owner authority")
|
|
305
306
|
const controlPath = ownerControlPath(ownerState)
|
|
@@ -408,10 +409,7 @@ async function execute(request, socket) {
|
|
|
408
409
|
return managedProcess.status()
|
|
409
410
|
}
|
|
410
411
|
|
|
411
|
-
|
|
412
|
-
const record = processes.get(request.key)
|
|
413
|
-
|
|
414
|
-
if (!record) throw new Error(`Guardian process ${request.key} is not registered`)
|
|
412
|
+
const record = requireProcess(request)
|
|
415
413
|
|
|
416
414
|
if (request.command !== "status") requireOwner(socket, request.command)
|
|
417
415
|
|
|
@@ -530,6 +528,18 @@ function requireReplacement(socket, request) {
|
|
|
530
528
|
if (replacementClient !== socket || request.replacementId !== replacementId) throw new Error("Owner replacement transaction is not the prepared candidate")
|
|
531
529
|
}
|
|
532
530
|
|
|
531
|
+
/**
|
|
532
|
+
* @param {GuardianRequest} request - Keyed guardian request.
|
|
533
|
+
* @returns {{desired: boolean, process: ManagedProcess, provenance: string}} Exact registered process.
|
|
534
|
+
*/
|
|
535
|
+
function requireProcess(request) {
|
|
536
|
+
if (!request.key) throw new Error(`Guardian ${request.command} requires a process key`)
|
|
537
|
+
const record = processes.get(request.key)
|
|
538
|
+
|
|
539
|
+
if (!record) throw new Error(`Guardian process ${request.key} is not registered`)
|
|
540
|
+
return record
|
|
541
|
+
}
|
|
542
|
+
|
|
533
543
|
/**
|
|
534
544
|
* @param {import("./json.js").JsonValue} state - Transfer state.
|
|
535
545
|
* @returns {import("./json.js").JsonValue} Embedded authority fence.
|
|
@@ -210,32 +210,68 @@ test("replacement staging rejects owner state published after prepare", async ()
|
|
|
210
210
|
}
|
|
211
211
|
})
|
|
212
212
|
|
|
213
|
+
test("retired owner replacement commit carries its exact recovered process key", async () => {
|
|
214
|
+
const client = new GuardianClient({socketPath: "/unused", token: "authenticated-capability"})
|
|
215
|
+
const replacementId = "prepared-replacement"
|
|
216
|
+
const processKey = "release:v1:worker"
|
|
217
|
+
|
|
218
|
+
client.request = async (request) => {
|
|
219
|
+
if (!request.key) throw new Error(`Guardian ${request.command} requires a process key`)
|
|
220
|
+
assert.deepEqual(request, {command: "commit-retired-owner-replacement", key: processKey, replacementId})
|
|
221
|
+
return {committed: true}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await client.commitRetiredOwnerReplacement(replacementId, processKey)
|
|
225
|
+
})
|
|
226
|
+
|
|
213
227
|
test("retired owner replacement requires unchanged authority and the exact control path absent", async () => {
|
|
214
228
|
const fixture = await createGuardian()
|
|
215
229
|
const candidate = new GuardianClient({socketPath: fixture.client.socketPath, token: fixture.token})
|
|
230
|
+
const contender = new GuardianClient({socketPath: fixture.client.socketPath, token: fixture.token})
|
|
216
231
|
const controlPath = path.join(fixture.root, "rollbridge.sock")
|
|
232
|
+
const processKey = "release:v1:worker"
|
|
217
233
|
const authority = {configDigest: "incumbent", runtime: null}
|
|
218
234
|
const nextAuthority = {configDigest: "candidate", runtime: null}
|
|
219
235
|
const snapshot = {activeReleaseId: "v1", control: {path: controlPath}}
|
|
220
236
|
|
|
221
237
|
try {
|
|
238
|
+
await fixture.client.process(processKey, definition("worker")).recover()
|
|
222
239
|
await fixture.client.publishOwnerState({authority, snapshot})
|
|
223
240
|
await candidate.connect()
|
|
224
241
|
const changed = await candidate.prepareOwnerReplacement(authority, nextAuthority)
|
|
225
242
|
|
|
226
243
|
await candidate.stageOwnerReplacement(changed.replacementId, {authority: nextAuthority, snapshot})
|
|
227
|
-
await assert.rejects(() => candidate.commitRetiredOwnerReplacement(changed.replacementId), /unchanged owner authority/)
|
|
244
|
+
await assert.rejects(() => candidate.commitRetiredOwnerReplacement(changed.replacementId, processKey), /unchanged owner authority/)
|
|
228
245
|
await candidate.abortOwnerReplacement(changed.replacementId)
|
|
229
246
|
|
|
230
247
|
const occupied = await candidate.prepareOwnerReplacement(authority, authority)
|
|
231
248
|
|
|
232
249
|
await candidate.stageOwnerReplacement(occupied.replacementId, {authority, snapshot})
|
|
233
250
|
await fs.writeFile(controlPath, "occupied\n")
|
|
234
|
-
await assert.rejects(() => candidate.commitRetiredOwnerReplacement(occupied.replacementId), /control socket .* still exists/)
|
|
251
|
+
await assert.rejects(() => candidate.commitRetiredOwnerReplacement(occupied.replacementId, processKey), /control socket .* still exists/)
|
|
235
252
|
await candidate.abortOwnerReplacement(occupied.replacementId)
|
|
236
|
-
|
|
253
|
+
|
|
254
|
+
await fs.rm(controlPath)
|
|
255
|
+
const ready = await candidate.prepareOwnerReplacement(authority, authority)
|
|
256
|
+
|
|
257
|
+
await candidate.stageOwnerReplacement(ready.replacementId, {authority, snapshot})
|
|
258
|
+
await contender.connect()
|
|
259
|
+
await assert.rejects(
|
|
260
|
+
() => contender.request({command: "commit-retired-owner-replacement", key: processKey, replacementId: ready.replacementId}),
|
|
261
|
+
/not the prepared candidate/
|
|
262
|
+
)
|
|
263
|
+
await assert.rejects(
|
|
264
|
+
() => candidate.commitRetiredOwnerReplacement(ready.replacementId, "release:v1:wrong"),
|
|
265
|
+
/process .* is not registered/
|
|
266
|
+
)
|
|
267
|
+
const committed = candidate.waitForEvent("replacement-committed")
|
|
268
|
+
|
|
269
|
+
await candidate.commitRetiredOwnerReplacement(ready.replacementId, processKey)
|
|
270
|
+
await committed
|
|
271
|
+
await candidate.shutdown()
|
|
237
272
|
await fixture.client.guardianExit()
|
|
238
273
|
} finally {
|
|
274
|
+
contender.disconnect()
|
|
239
275
|
candidate.disconnect()
|
|
240
276
|
await cleanupGuardian(fixture)
|
|
241
277
|
}
|