rollbridge 0.1.7 → 0.1.8

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.7",
3
+ "version": "0.1.8",
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
@@ -821,7 +821,7 @@ export function releasesToPrune(releases, policy, now) {
821
821
  }
822
822
 
823
823
  /**
824
- * @typedef {{alive: boolean, application?: string, activeReleaseId?: string | null}} ControlSocketInspection
824
+ * @typedef {{alive: boolean, application?: string, activeReleaseId?: string | null, proxy?: {host: string, port: number}}} ControlSocketInspection
825
825
  */
826
826
 
827
827
  /**
@@ -874,7 +874,7 @@ export async function inspectControlSocket(socketPath, timeoutMs = 1000) {
874
874
 
875
875
  const status = parseControlStatus(buffer.slice(0, newlineIndex))
876
876
 
877
- finish(status ? {activeReleaseId: status.activeReleaseId, alive: true, application: status.application} : {alive: true})
877
+ finish(status ? {activeReleaseId: status.activeReleaseId, alive: true, application: status.application, proxy: status.proxy} : {alive: true})
878
878
  })
879
879
  socket.once("error", (error) => {
880
880
  if (settled) return
@@ -896,7 +896,7 @@ export async function inspectControlSocket(socketPath, timeoutMs = 1000) {
896
896
  /**
897
897
  * Parses a control status response line into a Rollbridge identity, if it is one.
898
898
  * @param {string} line - JSON response line.
899
- * @returns {{application: string, activeReleaseId: string | null} | undefined} Identity, or undefined when unrecognized.
899
+ * @returns {{application: string, activeReleaseId: string | null, proxy: {host: string, port: number} | undefined} | undefined} Identity, or undefined when unrecognized.
900
900
  */
901
901
  function parseControlStatus(line) {
902
902
  /** @type {JsonValue} */
@@ -911,5 +911,7 @@ function parseControlStatus(line) {
911
911
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined
912
912
  if (typeof parsed.application !== "string") return undefined
913
913
 
914
- return {activeReleaseId: typeof parsed.activeReleaseId === "string" ? parsed.activeReleaseId : null, application: parsed.application}
914
+ const proxy = "proxy" in parsed && parsed.proxy && typeof parsed.proxy === "object" && !Array.isArray(parsed.proxy) && typeof parsed.proxy.host === "string" && typeof parsed.proxy.port === "number" ? {host: parsed.proxy.host, port: parsed.proxy.port} : undefined
915
+
916
+ return {activeReleaseId: typeof parsed.activeReleaseId === "string" ? parsed.activeReleaseId : null, application: parsed.application, proxy}
915
917
  }
package/src/doctor.js CHANGED
@@ -26,7 +26,7 @@ export async function runEnvironmentChecks(config) {
26
26
 
27
27
  checks.push(controlSocketCheck(config, socketInspection))
28
28
  checks.push(await controlSocketDirectoryCheck(config))
29
- checks.push(await proxyPortCheck(config))
29
+ checks.push(await proxyPortCheck(config, socketInspection))
30
30
 
31
31
  if (config.statePath !== undefined) {
32
32
  // A live daemon persists its own (live) pids into the state file, so they are not orphans.
@@ -79,7 +79,7 @@ async function orphanCheck(statePath, daemonRunning) {
79
79
 
80
80
  /**
81
81
  * @param {string} socketPath - Control socket path.
82
- * @returns {Promise<{alive: boolean, application?: string} | {error: string}>} Probe result, or the probe error.
82
+ * @returns {Promise<import("./daemon.js").ControlSocketInspection | {error: string}>} Probe result, or the probe error.
83
83
  */
84
84
  async function inspectControlSocketSafely(socketPath) {
85
85
  try {
@@ -91,7 +91,7 @@ async function inspectControlSocketSafely(socketPath) {
91
91
 
92
92
  /**
93
93
  * @param {import("./config.js").RollbridgeConfig} config - Normalized config.
94
- * @param {{alive: boolean, application?: string} | {error: string}} inspection - Control socket probe result.
94
+ * @param {import("./daemon.js").ControlSocketInspection | {error: string}} inspection - Control socket probe result.
95
95
  * @returns {DoctorCheck} Control socket reachability check.
96
96
  */
97
97
  function controlSocketCheck(config, inspection) {
@@ -109,6 +109,10 @@ function controlSocketCheck(config, inspection) {
109
109
  return {detail: `another process is already listening on ${socketPath}; the daemon would fail to bind it`, name: "control socket", ok: false}
110
110
  }
111
111
 
112
+ if (inspection.application === config.application) {
113
+ return {detail: `Rollbridge daemon for "${inspection.application}" is running on ${socketPath}`, name: "control socket", ok: true}
114
+ }
115
+
112
116
  return {detail: `a Rollbridge daemon for "${inspection.application}" is already running on ${socketPath}; stop it before starting another`, name: "control socket", ok: false}
113
117
  }
114
118
 
@@ -131,9 +135,10 @@ async function controlSocketDirectoryCheck(config) {
131
135
 
132
136
  /**
133
137
  * @param {import("./config.js").RollbridgeConfig} config - Normalized config.
134
- * @returns {Promise<DoctorCheck>} Whether the proxy port can be bound.
138
+ * @param {import("./daemon.js").ControlSocketInspection | {error: string}} inspection - Control socket probe result.
139
+ * @returns {Promise<DoctorCheck>} Whether the proxy port can be bound or is owned by the running daemon.
135
140
  */
136
- async function proxyPortCheck(config) {
141
+ async function proxyPortCheck(config, inspection) {
137
142
  const address = `${config.proxy.host}:${config.proxy.port}`
138
143
  const bind = await canBindPort(config.proxy.host, config.proxy.port)
139
144
 
@@ -141,6 +146,10 @@ async function proxyPortCheck(config) {
141
146
  return {detail: `${address} is available`, name: "proxy port", ok: true}
142
147
  }
143
148
 
149
+ if (!("error" in inspection) && inspection.application === config.application && inspection.proxy?.host === config.proxy.host && inspection.proxy.port === config.proxy.port) {
150
+ return {detail: `${address} is already held by the running Rollbridge daemon`, name: "proxy port", ok: true}
151
+ }
152
+
144
153
  return {detail: `${address} is unavailable (${bind.code})`, name: "proxy port", ok: false}
145
154
  }
146
155
 
@@ -188,7 +188,7 @@ test("runEnvironmentChecks reports a missing control socket directory", async ()
188
188
  assert.match(directoryCheck.detail, /missing or not writable/)
189
189
  })
190
190
 
191
- test("runEnvironmentChecks fails when a Rollbridge daemon already holds the socket and port", async () => {
191
+ test("runEnvironmentChecks passes when the running Rollbridge daemon holds the socket and port", async () => {
192
192
  const root = await fs.mkdtemp(path.join(os.tmpdir(), "rollbridge-doctor-"))
193
193
  const config = buildConfig({controlPath: path.join(root, "rollbridge.sock"), proxyPort: await freePort()})
194
194
  const daemon = new RollbridgeDaemon({config, logger: () => {}})
@@ -199,11 +199,33 @@ test("runEnvironmentChecks fails when a Rollbridge daemon already holds the sock
199
199
  const checks = await runEnvironmentChecks(config)
200
200
  const socketCheck = checkNamed(checks, "control socket")
201
201
 
202
- // A daemon already running means `rollbridge daemon` would fail to bind, so doctor must fail too.
203
- assert.equal(socketCheck.ok, false)
204
- assert.match(socketCheck.detail, /a Rollbridge daemon for "doctor-test" is already running/)
202
+ assert.equal(socketCheck.ok, true)
203
+ assert.match(socketCheck.detail, /Rollbridge daemon for "doctor-test" is running/)
204
+ assert.equal(checkNamed(checks, "proxy port").ok, true)
205
+ } finally {
206
+ await daemon.shutdown()
207
+ await fs.rm(root, {force: true, recursive: true})
208
+ }
209
+ })
210
+
211
+ test("runEnvironmentChecks fails when the running daemon does not own the configured proxy port", async () => {
212
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "rollbridge-doctor-"))
213
+ const config = buildConfig({controlPath: path.join(root, "rollbridge.sock"), proxyPort: await freePort()})
214
+ const changedConfigPort = await freePort()
215
+ const changedConfig = buildConfig({controlPath: config.control.path, proxyPort: changedConfigPort})
216
+ const server = net.createServer()
217
+ const daemon = new RollbridgeDaemon({config, logger: () => {}})
218
+
219
+ await daemon.start()
220
+ await new Promise((resolve) => server.listen(changedConfigPort, "127.0.0.1", () => resolve(undefined)))
221
+
222
+ try {
223
+ const checks = await runEnvironmentChecks(changedConfig)
224
+
225
+ assert.equal(checkNamed(checks, "control socket").ok, true)
205
226
  assert.equal(checkNamed(checks, "proxy port").ok, false)
206
227
  } finally {
228
+ await new Promise((resolve) => server.close(() => resolve(undefined)))
207
229
  await daemon.shutdown()
208
230
  await fs.rm(root, {force: true, recursive: true})
209
231
  }