rollbridge 0.1.47 → 0.1.49
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 +1 -1
- package/src/daemon.js +12 -6
- package/test/rollbridge.test.js +11 -1
package/package.json
CHANGED
package/src/daemon.js
CHANGED
|
@@ -1895,7 +1895,7 @@ export default class RollbridgeDaemon {
|
|
|
1895
1895
|
if (transition.previousReleaseId !== identity.previousReleaseId) {
|
|
1896
1896
|
throw new Error(`Generation transition previous release is ${transition.previousReleaseId}; refusing stale recovery for ${identity.previousReleaseId}`)
|
|
1897
1897
|
}
|
|
1898
|
-
if (transition.phase !== "activating_candidate" && transition.phase !== "restoring_previous" && transition.phase !== "retiring_failed_candidate" && transition.phase !== "degraded_active") {
|
|
1898
|
+
if (transition.phase !== "activating_candidate" && transition.phase !== "retiring_previous" && transition.phase !== "restoring_previous" && transition.phase !== "retiring_failed_candidate" && transition.phase !== "degraded_active") {
|
|
1899
1899
|
throw new Error(`Generation transition ${transition.candidateReleaseId} at ${transition.phase} is not a safe failed pre-commit transition`)
|
|
1900
1900
|
}
|
|
1901
1901
|
if (transition.phase === "degraded_active" && !identity.acceptRetiredIncumbent) {
|
|
@@ -1923,8 +1923,10 @@ export default class RollbridgeDaemon {
|
|
|
1923
1923
|
* @returns {Promise<Record<string, JsonValue>>} Explicit degraded recovery result.
|
|
1924
1924
|
*/
|
|
1925
1925
|
async acceptRetiredIncumbentTransition(transition) {
|
|
1926
|
-
if (transition.phase !== "restoring_previous" && transition.phase !== "degraded_active") throw new Error(`Retired-incumbent recovery requires
|
|
1927
|
-
|
|
1926
|
+
if (transition.phase !== "retiring_previous" && transition.phase !== "restoring_previous" && transition.phase !== "degraded_active") throw new Error(`Retired-incumbent recovery requires retiring_previous or restoring_previous; transition ${transition.candidateReleaseId} is at ${transition.phase}`)
|
|
1927
|
+
const retirementFailure = transition.phase === "retiring_previous" ? transition.error : transition.compensationError
|
|
1928
|
+
|
|
1929
|
+
if (!terminalRetirementFailure(retirementFailure)) throw new Error("Retired-incumbent recovery requires a recorded restoration failure compatible with terminal retirement")
|
|
1928
1930
|
if (!transition.previousReleaseId) throw new Error("Retired-incumbent recovery requires a retained previous release")
|
|
1929
1931
|
const candidate = this.releases.get(transition.candidateReleaseId)
|
|
1930
1932
|
const previous = this.releases.get(transition.previousReleaseId)
|
|
@@ -1934,7 +1936,9 @@ export default class RollbridgeDaemon {
|
|
|
1934
1936
|
if (candidate.releasePath !== transition.candidateReleasePath || candidate.revision !== transition.candidateRevision || ownerConfigDigest(candidate.config) !== transition.configDigest) {
|
|
1935
1937
|
throw new Error(`Generation transition candidate ${candidate.releaseId} does not retain its exact path, revision, and config authority`)
|
|
1936
1938
|
}
|
|
1937
|
-
|
|
1939
|
+
const recoverableCandidateStates = transition.phase === "retiring_previous" ? ["starting", "draining", "stopped"] : ["draining", "stopped"]
|
|
1940
|
+
|
|
1941
|
+
if (!recoverableCandidateStates.includes(candidate.state)) throw new Error(`Failed candidate ${candidate.releaseId} must be retiring or retired before accepting a retired incumbent`)
|
|
1938
1942
|
if (this.activeRelease !== previous) throw new Error(`Previous release ${previous.releaseId} is not the authoritative proxy target`)
|
|
1939
1943
|
const activationConfig = previous.config.processes.find((processConfig) => processConfig.lifecycle.activateCommand !== undefined)
|
|
1940
1944
|
const coordinator = activationConfig ? previous.getProcesses(activationConfig.id)[0]?.process : undefined
|
|
@@ -1958,6 +1962,7 @@ export default class RollbridgeDaemon {
|
|
|
1958
1962
|
|
|
1959
1963
|
if (transition.phase === "degraded_active") return result
|
|
1960
1964
|
const previousError = transition.error
|
|
1965
|
+
const previousPhase = transition.phase
|
|
1961
1966
|
const previousJournalRevision = transition.journalRevision
|
|
1962
1967
|
const previousUpdatedAt = transition.updatedAt
|
|
1963
1968
|
|
|
@@ -1968,7 +1973,7 @@ export default class RollbridgeDaemon {
|
|
|
1968
1973
|
try {
|
|
1969
1974
|
await this.checkpointGenerationTransition()
|
|
1970
1975
|
} catch (error) {
|
|
1971
|
-
transition.phase =
|
|
1976
|
+
transition.phase = previousPhase
|
|
1972
1977
|
transition.error = previousError
|
|
1973
1978
|
transition.journalRevision = previousJournalRevision
|
|
1974
1979
|
transition.updatedAt = previousUpdatedAt
|
|
@@ -2962,7 +2967,8 @@ export function ownerReplacementTransitionAuthorityMatches(transfer, transition,
|
|
|
2962
2967
|
function terminalRetirementFailure(failure) {
|
|
2963
2968
|
return typeof failure === "string" && (
|
|
2964
2969
|
/^Cannot activate .+ generation from retired$/iu.test(failure) ||
|
|
2965
|
-
/^activate command exited non-zero with status \d+$/u.test(failure)
|
|
2970
|
+
/^activate command exited non-zero with status \d+$/u.test(failure) ||
|
|
2971
|
+
/^Release .+ retirement quiescence failed: quiet command exited non-zero with status \d+$/u.test(failure)
|
|
2966
2972
|
)
|
|
2967
2973
|
}
|
|
2968
2974
|
|
package/test/rollbridge.test.js
CHANGED
|
@@ -799,7 +799,7 @@ test("explicit recovery stops the exact failed candidate and fences degraded inc
|
|
|
799
799
|
await assert.rejects(() => exactRecovery({previousReleaseId: "wrong-v1"}), /refusing stale recovery/i)
|
|
800
800
|
await assert.rejects(() => exactRecovery({revision: "wrong-v2"}), /exact same release, path, revision, and config authority/i)
|
|
801
801
|
transition.phase = "retiring_failed_candidate"
|
|
802
|
-
await assert.rejects(() => exactRecovery(), /
|
|
802
|
+
await assert.rejects(() => exactRecovery(), /requires retiring_previous or restoring_previous/i)
|
|
803
803
|
transition.phase = "restoring_previous"
|
|
804
804
|
const terminalFailure = transition.compensationError
|
|
805
805
|
|
|
@@ -826,6 +826,16 @@ test("explicit recovery stops the exact failed candidate and fences degraded inc
|
|
|
826
826
|
const persisted = /** @type {{generationTransition?: import("../src/json.js").JsonValue} | undefined} */ (await readState(fixture.statePath))
|
|
827
827
|
|
|
828
828
|
assert.equal(/** @type {{phase?: string} | undefined} */ (persisted?.generationTransition)?.phase, "degraded_active")
|
|
829
|
+
|
|
830
|
+
transition.phase = "retiring_previous"
|
|
831
|
+
transition.error = "Release v1 retirement quiescence failed: quiet command exited non-zero with status 1"
|
|
832
|
+
transition.compensationError = undefined
|
|
833
|
+
candidate.state = "starting"
|
|
834
|
+
await daemon.checkpointGenerationTransition()
|
|
835
|
+
const legacyRecovery = await exactRecovery()
|
|
836
|
+
|
|
837
|
+
assert.equal(legacyRecovery.recoveryStatus, "retired_incumbent_accepted")
|
|
838
|
+
assert.equal(daemon.status().generationTransition?.phase, "degraded_active", "guarded recovery migrates a legacy terminal retirement fence")
|
|
829
839
|
await assert.rejects(() => daemon.deploy({releaseId: "bad-v3", releasePath: fixture.root, revision: "bad-v3"}), /health check failed/i)
|
|
830
840
|
assert.equal(daemon.status().generationTransition?.phase, "degraded_active")
|
|
831
841
|
assert.equal(statusRelease(daemon, "v1").processes.find(({id}) => id === "web")?.pid, incumbentWebPid)
|