rollbridge 0.1.23 → 0.1.24

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rollbridge",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "description": "Zero-downtime process supervisor and local traffic switcher for deploy-managed apps.",
5
5
  "keywords": [
6
6
  "deploy",
package/src/daemon.js CHANGED
@@ -882,12 +882,13 @@ export default class RollbridgeDaemon {
882
882
  }
883
883
 
884
884
  await captureShutdownError(cleanupErrors, "proxy close", async () => this.proxy.close())
885
- const stopResults = await Promise.allSettled([
886
- ...[...this.services.values()].map((processInstance) => processInstance.stop()),
885
+ const dependentStopResults = await Promise.allSettled([
887
886
  ...[...this.singletons.values()].map((processInstance) => processInstance.stop()),
888
887
  ...[...this.startingReleases].map((release) => release.stop()),
889
888
  ...[...this.releases.values()].map((release) => release.stop())
890
889
  ])
890
+ const serviceStopResults = await Promise.allSettled([...this.services.values()].map((processInstance) => processInstance.stop()))
891
+ const stopResults = [...dependentStopResults, ...serviceStopResults]
891
892
  await captureShutdownError(cleanupErrors, "proxy server close", () => this.closeServer(this.proxyServer))
892
893
 
893
894
  // Wait for any in-flight write first so it can't recreate or overwrite the final state (no
@@ -115,6 +115,60 @@ test("shutdown response waits for endpoint and owned-process cleanup before imme
115
115
  }
116
116
  })
117
117
 
118
+ test("shutdown keeps daemon services alive until release-owned dependents stop", async () => {
119
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "rollbridge-shutdown-service-order-"))
120
+ const socketPath = path.join(root, "control.sock")
121
+ const processCommand = `${JSON.stringify(process.execPath)} -e ${JSON.stringify("setInterval(() => {}, 1000)")}`
122
+ const config = buildConfig(socketPath, {
123
+ companion: {command: processCommand, gracefulStopMs: "indefinite", id: "worker", policy: "companion"},
124
+ service: {command: processCommand, id: "coordinator", policy: "service"}
125
+ })
126
+ const daemon = new RollbridgeDaemon({config, logger: () => {}})
127
+ /** @type {() => void} */
128
+ let releaseWorker = () => {}
129
+ const workerGate = new Promise((resolve) => { releaseWorker = () => resolve(undefined) })
130
+ let serviceStopStarted = false
131
+
132
+ try {
133
+ await daemon.start()
134
+ await daemon.deploy({releaseId: "v1", releasePath: root, revision: "v1"})
135
+
136
+ const release = daemon.activeRelease
137
+ const coordinator = daemon.services.get("coordinator")
138
+
139
+ assert.ok(release)
140
+ assert.ok(coordinator)
141
+
142
+ const originalReleaseStop = release.stop.bind(release)
143
+ const originalCoordinatorStop = coordinator.stop.bind(coordinator)
144
+ let signalReleaseStopStarted = () => {}
145
+ const releaseStopStarted = new Promise((resolve) => { signalReleaseStopStarted = () => resolve(undefined) })
146
+
147
+ release.stop = async () => {
148
+ signalReleaseStopStarted()
149
+ await workerGate
150
+ await originalReleaseStop()
151
+ }
152
+ coordinator.stop = async () => {
153
+ serviceStopStarted = true
154
+ await originalCoordinatorStop()
155
+ }
156
+
157
+ const shutdown = daemon.shutdown()
158
+
159
+ await releaseStopStarted
160
+ const serviceStoppedWhileWorkerWasDraining = serviceStopStarted
161
+ releaseWorker()
162
+ await shutdown
163
+
164
+ assert.equal(serviceStoppedWhileWorkerWasDraining, false, "a worker must retain access to daemon services throughout its drain")
165
+ } finally {
166
+ releaseWorker()
167
+ await daemon.shutdown()
168
+ await fs.rm(root, {force: true, recursive: true})
169
+ }
170
+ })
171
+
118
172
  test("external-owner retirement releases listeners before a long-draining companion exits", async () => {
119
173
  const root = await fs.mkdtemp(path.join(os.tmpdir(), "rollbridge-owner-retirement-"))
120
174
  const socketPath = path.join(root, "rollbridge.sock")
@@ -314,13 +368,13 @@ test("shutdown of an already-stopped endpoint fails explicitly", async () => {
314
368
 
315
369
  /**
316
370
  * @param {string} socketPath - Control socket path.
317
- * @param {{companion?: Record<string, import("../src/json.js").JsonValue>}} [options] - Optional companion process.
371
+ * @param {{companion?: Record<string, import("../src/json.js").JsonValue>, service?: Record<string, import("../src/json.js").JsonValue>}} [options] - Optional dependent processes.
318
372
  * @returns {import("../src/config.js").RollbridgeConfig} Normalized config.
319
373
  */
320
- function buildConfig(socketPath, {companion} = {}) {
374
+ function buildConfig(socketPath, {companion, service} = {}) {
321
375
  return normalizeConfig({
322
376
  ...rawConfig(socketPath),
323
- ...(companion ? {processes: [companion, ...rawConfig(socketPath).processes]} : {})
377
+ ...((companion || service) ? {processes: [...(service ? [service] : []), ...(companion ? [companion] : []), ...rawConfig(socketPath).processes]} : {})
324
378
  })
325
379
  }
326
380