@procedures/server 0.17.2 → 0.17.4

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/src/host.ts CHANGED
@@ -14,11 +14,14 @@
14
14
  * removes it after the listeners close.
15
15
  * - The option `declare` gives the builder of the service to the app before `run()`. The app
16
16
  * declares its own parts and dependencies with the vocabulary of the runner (CLI-62).
17
+ * - The probes of the service answer on each listener of the host under `/_probes`, or on a port
18
+ * of their own with the option `probes` (CLI-9, decision D5). The service gives both.
17
19
  */
18
20
 
19
21
  import { daemon, localDev, service, type Lifecycle, type Profile, type RunOptions, type ServiceBuilder } from "@mark1russell7/service";
20
- import { seconds } from "@procedures/client";
21
- import { createProcedureHost, type ProcedureHost, type ProcedureHostOptions } from "@procedures/peer";
22
+ import { probeServer, type ProbeKind } from "@mark1russell7/service/http";
23
+ import { seconds, type Duration } from "@procedures/client";
24
+ import { LOCAL_LISTENER_KNOBS, createProcedureHost, type ProcedureHost, type ProcedureHostOptions } from "@procedures/peer";
22
25
  import { lockfileHandle, type LockfileData } from "@procedures/peer/lockfile";
23
26
 
24
27
  /** The mode of a run: in the foreground of a terminal, or as the background host of `mark`. */
@@ -27,6 +30,28 @@ export type HostRunMode = "foreground" | "background";
27
30
  /** The profile of each mode, as data: `localDev` in the foreground, and `daemon` in the background. */
28
31
  export const HOST_PROFILES: Readonly<Record<HostRunMode, Profile>> = Object.freeze({ foreground: localDev, background: daemon });
29
32
 
33
+ /**
34
+ * A probe server on a port of its own (CLI-9, decision D5): `probeServer()` of
35
+ * `@mark1russell7/service/http`. It starts first and closes last, thus its probes answer through
36
+ * the whole drain. By default, it also answers `/report`, with the whole verdict of the service.
37
+ * The option `kinds` leaves out a kind, for example the report on a port that is not private.
38
+ */
39
+ export interface HostProbeServer {
40
+ /** The port of the probes. 0 gives a free port, and `RunningHost.probes()` gives the real one. */
41
+ readonly port: number;
42
+ /** The address. The default is `127.0.0.1`. */
43
+ readonly host?: string | undefined;
44
+ /** The path before each probe path. The default is none: the port has only the probes. */
45
+ readonly prefix?: string | undefined;
46
+ /** The `Retry-After` of each 503 of a probe. The default is the knob `server_retry_after` of `LOCAL_LISTENER_KNOBS`: 1 s. */
47
+ readonly retryAfter?: Duration | undefined;
48
+ /**
49
+ * The probe kinds that the port answers (SYS-30). The default is all four: `startup`, `readiness`,
50
+ * `liveness` and `report`. The port answers 404 for a kind that is not in the list.
51
+ */
52
+ readonly kinds?: readonly ProbeKind[] | undefined;
53
+ }
54
+
30
55
  /** The configuration of `runProcedureHost()`. */
31
56
  export interface ProcedureHostRunConfig {
32
57
  /** The name of the service of the process. */
@@ -42,6 +67,12 @@ export interface ProcedureHostRunConfig {
42
67
  readonly log?: ((line: string) => void) | undefined;
43
68
  /** The options of `run()`: a process port and a clock, for a test. The host gets the same clock. */
44
69
  readonly run?: RunOptions | undefined;
70
+ /**
71
+ * The probe server of the process on a port of its own (CLI-9, decision D5). Without it, each
72
+ * listener of the host answers the probes under `/_probes` (the option `probes` of the host).
73
+ * With it, the listeners do not answer them, unless the option `probes` of the host is given.
74
+ */
75
+ readonly probes?: HostProbeServer | undefined;
45
76
  /**
46
77
  * This function declares the parts of the app on the service of the process, before `run()`
47
78
  * (CLI-62). It gets the builder of the service and the host.
@@ -66,6 +97,11 @@ export interface RunningHost {
66
97
  readonly service: ServiceBuilder;
67
98
  /** The lifecycle. Its state is `running` after a startup that ended, and `draining` or `exited` after a startup that failed. */
68
99
  readonly life: Lifecycle;
100
+ /**
101
+ * The base URL of each place that answers the probes (CLI-9): the probe server, or each listener
102
+ * of the host, for example `http://127.0.0.1:3000/_probes`. Add `/startup`, `/readiness` or `/liveness`.
103
+ */
104
+ probes(): string[];
69
105
  }
70
106
 
71
107
  /**
@@ -75,9 +111,25 @@ export interface RunningHost {
75
111
  */
76
112
  export async function runProcedureHost(config: ProcedureHostRunConfig): Promise<RunningHost> {
77
113
  const clock = config.run?.clock;
78
- const host = createProcedureHost({ ...config.host, ...(clock ? { clock } : {}) });
114
+ // A probe server of its own: the listeners of the host then answer no probes, unless the host options say so (D5)
115
+ const dedicated = config.probes;
116
+ const host = createProcedureHost({
117
+ ...config.host,
118
+ ...(clock ? { clock } : {}),
119
+ ...(dedicated && config.host.probes === undefined ? { probes: false } : {}),
120
+ });
79
121
  const profile = config.profile ?? HOST_PROFILES[config.mode];
80
122
  const svc = service(config.name).profile(profile);
123
+ const probes = dedicated
124
+ ? probeServer({
125
+ port: dedicated.port,
126
+ host: dedicated.host ?? "127.0.0.1",
127
+ ...(dedicated.prefix !== undefined ? { prefix: dedicated.prefix } : {}),
128
+ ...(dedicated.kinds !== undefined ? { kinds: dedicated.kinds } : {}),
129
+ server_retry_after: dedicated.retryAfter ?? LOCAL_LISTENER_KNOBS.server_retry_after,
130
+ })
131
+ : undefined;
132
+ if (probes) svc.healthServer(probes);
81
133
  // The profiles of the computer of a developer leave the policies to the service. No check gates them.
82
134
  if (!profile.readiness) svc.readiness({ type: "debounce", interval: seconds(5), failAfter: 3, recoverAfter: 1 });
83
135
  if (!profile.liveness) svc.liveness({ type: "window", failAfter: seconds(45) });
@@ -93,5 +145,12 @@ export async function runProcedureHost(config: ProcedureHostRunConfig): Promise<
93
145
  const log = config.log;
94
146
  if (log && profile.phases?.some((phase) => phase.name === "stop intake")) svc.onPhase("stop intake", () => log("\nShutting down..."));
95
147
  const life = await svc.run(config.run);
96
- return { host, service: svc, life };
148
+ const probeBase = (): string[] => {
149
+ const address = probes?.server.address();
150
+ if (typeof address !== "object" || address === null) return [];
151
+ const bound = dedicated?.host ?? "127.0.0.1";
152
+ const shown = bound === "0.0.0.0" || bound === "::" ? "localhost" : bound;
153
+ return [`http://${shown}:${address.port}${dedicated?.prefix ?? ""}`];
154
+ };
155
+ return { host, service: svc, life, probes: () => (probes ? probeBase() : host.info.probes()) };
97
156
  }
@@ -200,7 +200,8 @@ export const serverStartProcedure: Procedure<
200
200
  }
201
201
  }
202
202
 
203
- // Timed out waiting for lockfile, but child may still be alive
203
+ // Timed out waiting for lockfile, but child may still be alive. Deprecated (CLI-9): from the
204
+ // next MINOR of the client group, this result is `success: false`, with the PID of the child.
204
205
  if (child.pid && isProcessAlive(child.pid)) {
205
206
  return {
206
207
  success: true,
package/src/run.ts CHANGED
@@ -247,6 +247,11 @@ export async function startServer(
247
247
  };
248
248
  }
249
249
 
250
+ /** The address of the probe server of `--probe-port`: the address of the first network transport, as `--host` gives it. */
251
+ function probeHostOf(config: ServerConfig): string {
252
+ return config.transports.find((transport) => transport.type !== "local")?.host ?? "127.0.0.1";
253
+ }
254
+
250
255
  /**
251
256
  * The `server` CLI: parse the options, start the server under `run()` of `@mark1russell7/service`,
252
257
  * and print where it listens. `mark serve` calls it with its own arguments. The runner owns the
@@ -262,11 +267,13 @@ export async function runServer(argv: string[], options: ServerRunOptions = {}):
262
267
  }
263
268
 
264
269
  const { registry, policy, token, generated, confined } = await prepareServer(config, console.log, options);
265
- const { host, life } = await runProcedureHost({
270
+ const { host, life, probes } = await runProcedureHost({
266
271
  name: "procedure-server",
267
272
  mode: "foreground",
268
273
  host: peerHostOptions({ id: newPeerId(), transports: config.transports, registry, policy }, "procedures"),
269
274
  log: console.log,
275
+ // --probe-port: the probes answer on a port of their own, on the address of --host (CLI-9, D5)
276
+ ...(config.probePort !== undefined ? { probes: { port: config.probePort, host: probeHostOf(config) } } : {}),
270
277
  ...(options.run ? { run: options.run } : {}),
271
278
  });
272
279
  if (life.state !== "running") {
@@ -284,6 +291,9 @@ export async function runServer(argv: string[], options: ServerRunOptions = {}):
284
291
  for (const endpoint of host.info.endpoints()) {
285
292
  console.log(` [${endpoint.type}] ${endpoint.address}`);
286
293
  }
294
+ for (const base of probes()) {
295
+ console.log(` [probes] ${base}/startup, ${base}/readiness, ${base}/liveness (no token)`);
296
+ }
287
297
  if (token === undefined) {
288
298
  console.log("\nNo token: any program on this computer can call the server.");
289
299
  } else {
package/src/schemas.ts CHANGED
@@ -43,7 +43,11 @@ export const serverStatusInputSchema: ZodLikeSchema<ServerStatusInput> = zodAdap
43
43
  );
44
44
 
45
45
  export const ServerStartOutputSchema: z.ZodType<ServerStartOutput> = z.looseObject({
46
- success: z.boolean().describe("True when the server started"),
46
+ success: z
47
+ .boolean()
48
+ .describe(
49
+ "True when the server started. Deprecated (CLI-9): at the timeout, a server that still starts gives true. From the next MINOR, it gives false."
50
+ ),
47
51
  pid: z.number().optional().describe("The process id of the server"),
48
52
  port: z.number().optional().describe("The port of the server"),
49
53
  endpoint: z.string().optional().describe("The URL of the server"),
package/src/types.ts CHANGED
@@ -30,6 +30,11 @@ export interface ServerConfig {
30
30
  events?: string[] | undefined;
31
31
  /** The root of the files (`--fs-root`): each procedure keeps its paths in it, or it is refused. Undefined: no root. */
32
32
  fsRoot?: string | undefined;
33
+ /**
34
+ * The port of the probe server (`--probe-port`, CLI-9). Undefined: each listener answers the probes
35
+ * under `/_probes`, and the server has no probe server.
36
+ */
37
+ probePort?: number | undefined;
33
38
  }
34
39
 
35
40
  export interface ServerCreateResult {
@@ -52,7 +57,14 @@ export interface ServerStartInput {
52
57
  }
53
58
 
54
59
  export interface ServerStartOutput {
55
- /** Whether start succeeded */
60
+ /**
61
+ * True when the server started.
62
+ *
63
+ * @deprecated Only the value at the timeout is deprecated, not this field (CLI-9). At the
64
+ * timeout, a server that still starts gives `true` and the message "still loading". From the next
65
+ * MINOR of the client group, a start that is not ready at the timeout gives `success: false`, with
66
+ * the PID of the process that still starts. Read the readiness probe or `mark server status` for the state.
67
+ */
56
68
  success: boolean;
57
69
  /** Process ID of server */
58
70
  pid?: number | undefined;