@amritk/nish-x86_64-linux 0.29.0 → 0.31.0

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.
@@ -51,6 +51,26 @@
51
51
  * gone longer than that. With `restart` off nothing is signalled; a worker sees
52
52
  * its peer's exit as a hang-up.
53
53
  *
54
+ * **Listeners and pinning.** With `listenPort` set (0 or above), the
55
+ * supervisor binds one `SO_REUSEPORT` listener per worker itself, in index
56
+ * order, keeps them for its whole run and hands listener `k` to every start of
57
+ * worker `k` after the channel ends; `preforkListener(args)` answers it in the
58
+ * worker (`--nish-prefork-listener=`, before the channels flag), so a worker
59
+ * serves that descriptor instead of binding its own. Port 0 binds the first
60
+ * listener anywhere and the rest to its port. With `pin` on as well, each
61
+ * listener steers by CPU (`tcpListenFlags`' flag 4): the kernel hands a
62
+ * connection that arrived on CPU `k` to listener `k`, and a worker that is
63
+ * restarted takes the same listener, so the order holds across restarts,
64
+ * which it would not if each worker bound its own. `pin` alone makes each
65
+ * worker call `setCpuAffinity(k)` before `preforkWith` answers it, so worker
66
+ * `k` runs on CPU `k` only; a worker the kernel refuses (CPU `k` outside the
67
+ * process's mask) says so on stderr and runs unpinned. More workers than
68
+ * `availableParallelism()` is refused with a panic, since a worker past the
69
+ * last CPU would be pinned nowhere and, steered, handed nothing. Pinning and
70
+ * steering are Linux's: elsewhere the supervisor says once that the workers
71
+ * run unpinned, and the listeners are plain `SO_REUSEPORT` ones. Both are off
72
+ * by default.
73
+ *
54
74
  * **How many.** `NISH_WORKERS` when it is a positive whole number, else
55
75
  * `PreforkConfig.workers` when that is positive, else
56
76
  * `availableParallelism()` on Linux and 1 elsewhere: Darwin shares a
@@ -83,10 +103,12 @@ import {
83
103
  NET_UNIX_SEQPACKET,
84
104
  NET_UNIX_STREAM,
85
105
  netClose,
106
+ netLocalPort,
86
107
  netRead,
87
108
  pollAdd,
88
109
  pollCreate,
89
110
  pollWait,
111
+ tcpListenFlags,
90
112
  unixSocketPair,
91
113
  } from "nish:net"
92
114
  import {
@@ -96,6 +118,7 @@ import {
96
118
  monotonicNanos,
97
119
  platform,
98
120
  readSignal,
121
+ setCpuAffinity,
99
122
  signalFd,
100
123
  spawn,
101
124
  spawnWithFds,
@@ -111,6 +134,12 @@ export const PREFORK_WORKER_FLAG: string = "--nish-prefork-worker="
111
134
  * for none.
112
135
  */
113
136
  export const PREFORK_CHANNELS_FLAG: string = "--nish-prefork-channels="
137
+ /**
138
+ * The argument before the channels flag, or before the worker flag when there
139
+ * is none, when the supervisor holds the listeners: the worker's listener's
140
+ * descriptor.
141
+ */
142
+ export const PREFORK_LISTENER_FLAG: string = "--nish-prefork-listener="
114
143
 
115
144
  /** What `preforkWith` answers in the supervisor once its workers are gone. */
116
145
  export const PREFORK_STOPPED: i32 = -1
@@ -127,6 +156,8 @@ export const PREFORK_WORKERS_EXITED: i32 = -3
127
156
 
128
157
  /** The supervisor's numbers; every one is checked by `preforkWith`. */
129
158
  export class PreforkConfig {
159
+ /** The address the listeners of `listenPort` bind. */
160
+ listenHost: string = "0.0.0.0"
130
161
  /** Workers to start; 0 takes `NISH_WORKERS` or the platform's default. */
131
162
  workers: i32 = 0
132
163
  /** How often, in milliseconds, the supervisor reaps when nothing wakes it. */
@@ -139,6 +170,23 @@ export class PreforkConfig {
139
170
  quickExitMs: i32 = 1000
140
171
  /** How long the workers get to exit after SIGTERM before SIGKILL. */
141
172
  graceMs: i32 = 5000
173
+ /** The worker every other worker gets a socketpair to; -1 for none. */
174
+ serviceWorker: i32 = -1
175
+ /**
176
+ * What every channel is: `NET_UNIX_STREAM` (1), or `NET_UNIX_SEQPACKET` (2),
177
+ * which keeps each write one message but only Linux has. Stream is the
178
+ * default because it works on Darwin too.
179
+ */
180
+ channelKind: i32 = 1
181
+ /**
182
+ * The port the supervisor binds one listener per worker on, 0 for any; -1,
183
+ * the default, leaves each worker to bind its own.
184
+ */
185
+ listenPort: i32 = -1
186
+ /** Their backlog. */
187
+ listenBacklog: i32 = 1024
188
+ /** Their `tcpListenFlags` beyond `SO_REUSEPORT`, which is always set: 0, or 2 for `TCP_NODELAY`. */
189
+ listenFlags: i32 = 0
142
190
  /**
143
191
  * Start a worker that exited again. Off, a worker that exits stays gone,
144
192
  * and the supervisor returns once none is left.
@@ -146,14 +194,14 @@ export class PreforkConfig {
146
194
  restart: boolean = true
147
195
  /** Give each worker a socketpair to the supervisor (`preforkSupervise`). */
148
196
  supervisorChannels: boolean = false
149
- /** The worker every other worker gets a socketpair to; -1 for none. */
150
- serviceWorker: i32 = -1
151
197
  /**
152
- * What every channel is: `NET_UNIX_STREAM` (1), or `NET_UNIX_SEQPACKET` (2),
153
- * which keeps each write one message but only Linux has. Stream is the
154
- * default because it works on Darwin too.
198
+ * Pin worker `k` to CPU `k` (`setCpuAffinity`), and with `listenPort`, steer
199
+ * each connection to the listener of the CPU that received it. Linux only;
200
+ * elsewhere the workers run unpinned. `k` is the CPU number, not the `k`-th
201
+ * allowed CPU: under a mask that skips CPU `k`, worker `k` runs unpinned and
202
+ * a connection received past the listeners falls back to the kernel's hash.
155
203
  */
156
- channelKind: i32 = 1
204
+ pin: boolean = false
157
205
  }
158
206
 
159
207
  /** True when `s` is one or more ASCII digits and nothing else. */
@@ -206,8 +254,8 @@ export const preforkIndex = (args: string[]): i32 => {
206
254
  }
207
255
 
208
256
  /**
209
- * `args` (`process.argv`) without the worker flag and the channels flag before
210
- * it, the same in every process.
257
+ * `args` (`process.argv`) without the worker flag and the channels and
258
+ * listener flags before it, the same in every process.
211
259
  */
212
260
  export const preforkArgs = (args: string[]): string[] => {
213
261
  let keep: i32 = toI32(args.length)
@@ -216,6 +264,9 @@ export const preforkArgs = (args: string[]): string[] => {
216
264
  if (keep > 1 && args[keep - 1].startsWith(PREFORK_CHANNELS_FLAG)) {
217
265
  keep = keep - 1
218
266
  }
267
+ if (keep > 1 && args[keep - 1].startsWith(PREFORK_LISTENER_FLAG)) {
268
+ keep = keep - 1
269
+ }
219
270
  }
220
271
  const out: string[] = []
221
272
  for (let i: i32 = 0; i < toI32(args.length); i++) {
@@ -258,6 +309,21 @@ export const preforkServiceChannel = (args: string[]): i32 => preforkChannel(arg
258
309
  export const preforkServiceChannelOf = (args: string[], index: i32): i32 =>
259
310
  index < 0 || index > 2147483645 ? -1 : preforkChannel(args, 2 + index)
260
311
 
312
+ /** In a worker, the listener the supervisor bound for it (`listenPort`); -1 when none. */
313
+ export const preforkListener = (args: string[]): i32 => {
314
+ if (preforkIndex(args) < 0) {
315
+ return -1
316
+ }
317
+ for (let back: i32 = 2; back <= 3 && back < toI32(args.length); back++) {
318
+ const flag: string = args[toI32(args.length) - back]
319
+ if (flag.startsWith(PREFORK_LISTENER_FLAG)) {
320
+ const digits: string = flag.substring(PREFORK_LISTENER_FLAG.length)
321
+ return preforkDigits(digits) ? parseInt(digits) : -1
322
+ }
323
+ }
324
+ return -1
325
+ }
326
+
261
327
  /** Milliseconds on the monotonic clock. */
262
328
  const preforkNowMs = (): i64 => monotonicNanos() / 1000000
263
329
 
@@ -298,6 +364,8 @@ class PreforkSupervisor {
298
364
  service: i32
299
365
  /** The group is being stopped, to be started again once none of it is left. */
300
366
  cascading: boolean = false
367
+ /** Worker i's listener at i, held for the whole run; empty without `listenPort`. */
368
+ listeners: i32[]
301
369
 
302
370
  constructor(config: PreforkConfig, args: string[], count: i32, loop: i32) {
303
371
  this.config = config
@@ -305,6 +373,7 @@ class PreforkSupervisor {
305
373
  this.service = config.serviceWorker
306
374
  this.toService = []
307
375
  this.fromWorkers = []
376
+ this.listeners = []
308
377
  this.args = preforkArgs(args)
309
378
  const self: string | null = platform === "linux" ? realpathSync("/proc/self/exe") : null
310
379
  this.program = self !== null ? self : this.args[0]
@@ -359,7 +428,12 @@ class PreforkSupervisor {
359
428
  for (let i: i32 = 1; i < toI32(this.args.length); i++) {
360
429
  argv.push(this.args[i])
361
430
  }
362
- if (fds.length > 0) {
431
+ const channels: boolean = fds.length > 0
432
+ if (this.listeners.length > 0) {
433
+ fds.push(this.listeners[index])
434
+ argv.push(`${PREFORK_LISTENER_FLAG}${2 + fds.length}`)
435
+ }
436
+ if (channels) {
363
437
  argv.push(`${PREFORK_CHANNELS_FLAG}${fields.join(",")}`)
364
438
  }
365
439
  argv.push(`${PREFORK_WORKER_FLAG}${index}`)
@@ -576,6 +650,37 @@ class PreforkSupervisor {
576
650
  return true
577
651
  }
578
652
 
653
+ /**
654
+ * One listener per worker, in index order, which is the order steering
655
+ * counts CPUs in; false, with the ones made closed, when one cannot bind.
656
+ */
657
+ listen(steer: boolean): boolean {
658
+ const config: PreforkConfig = this.config
659
+ const flags: i32 = config.listenFlags | 1 | (steer ? 4 : 0)
660
+ let port: i32 = config.listenPort
661
+ for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
662
+ const fd: i32 = tcpListenFlags(config.listenHost, port, config.listenBacklog, flags)
663
+ if (fd < 0) {
664
+ console.error(`nish/net/prefork: cannot listen on ${config.listenHost}:${port} (${fd})`)
665
+ this.closeListeners()
666
+ return false
667
+ }
668
+ this.listeners.push(fd)
669
+ if (port === 0) {
670
+ port = netLocalPort(fd)
671
+ }
672
+ }
673
+ return true
674
+ }
675
+
676
+ /** Close the listeners `listen` made. */
677
+ closeListeners(): void {
678
+ for (let i: i32 = 0; i < toI32(this.listeners.length); i++) {
679
+ netClose(this.listeners[i])
680
+ }
681
+ this.listeners = []
682
+ }
683
+
579
684
  /** Send `sig` to every running worker. */
580
685
  signal(sig: i32): void {
581
686
  for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
@@ -644,6 +749,15 @@ const preforkInvalid = (config: PreforkConfig): string => {
644
749
  if (config.serviceWorker < -1) {
645
750
  return "serviceWorker below -1"
646
751
  }
752
+ if (config.listenPort < -1 || config.listenPort > 65535) {
753
+ return "listenPort not -1 or 0..65535"
754
+ }
755
+ if (config.listenBacklog <= 0) {
756
+ return "listenBacklog not above 0"
757
+ }
758
+ if (config.listenFlags !== 0 && config.listenFlags !== 2) {
759
+ return "listenFlags not 0 or 2"
760
+ }
647
761
  return ""
648
762
  }
649
763
 
@@ -679,8 +793,11 @@ export const preforkWith = (args: string[], config: PreforkConfig): i32 => {
679
793
  * supervisor closes it after a hang-up, when the worker is reaped and when it
680
794
  * stops, and a restarted worker comes with a new one. Panics, besides, when
681
795
  * `serviceWorker` is not below the worker count or the count is above 64 with
682
- * a service worker, the most `spawnWithFds` hands one child; and, through
683
- * `preforkInvalid`, on seqpacket channels off Linux.
796
+ * a service worker (63 with `listenPort`), the most `spawnWithFds` hands one
797
+ * child; when `pin`, on Linux, asks for more workers than
798
+ * `availableParallelism()`; and, through `preforkInvalid`, on seqpacket
799
+ * channels off Linux. A listener that cannot bind answers
800
+ * `PREFORK_SPAWN_FAILED` before any worker starts.
684
801
  */
685
802
  export const preforkSupervise = <S>(
686
803
  args: string[],
@@ -692,13 +809,27 @@ export const preforkSupervise = <S>(
692
809
  if (invalid !== "") {
693
810
  panic(`nish/net/prefork: ${invalid}`)
694
811
  }
812
+ const pin: boolean = config.pin && platform === "linux"
695
813
  const index: i32 = preforkIndex(args)
696
814
  if (index >= 0) {
815
+ const pinned: i32 = pin ? setCpuAffinity(index) : 0
816
+ if (pinned < 0) {
817
+ console.error(`nish/net/prefork: worker ${index} runs unpinned (${pinned})`)
818
+ }
697
819
  return index
698
820
  }
699
821
  const count: i32 = preforkWorkerCount(config.workers, getenv("NISH_WORKERS"))
700
- if (config.serviceWorker >= count || (config.serviceWorker >= 0 && count > 64)) {
701
- panic(`nish/net/prefork: serviceWorker not below ${count} workers, or above 64 workers`)
822
+ // A held listener is one more descriptor for every worker, the service one too.
823
+ const fdMax: i32 = config.listenPort >= 0 ? 63 : 64
824
+ if (config.serviceWorker >= count || (config.serviceWorker >= 0 && count > fdMax)) {
825
+ panic(`nish/net/prefork: serviceWorker not below ${count} workers, or above ${fdMax} workers`)
826
+ }
827
+ const cpus: i32 = pin ? availableParallelism() : 0
828
+ if (pin && count > cpus) {
829
+ panic(`nish/net/prefork: pin with ${count} workers on ${cpus} CPUs`)
830
+ }
831
+ if (config.pin && !pin) {
832
+ console.error("nish/net/prefork: pinning needs Linux; the workers run unpinned")
702
833
  }
703
834
  const signals: i32 = signalFd()
704
835
  const loop: i32 = pollCreate()
@@ -712,6 +843,10 @@ export const preforkSupervise = <S>(
712
843
  return PREFORK_SPAWN_FAILED
713
844
  }
714
845
  const supervisor = new PreforkSupervisor(config, args, count, loop)
846
+ if (config.listenPort >= 0 && !supervisor.listen(pin)) {
847
+ netClose(loop)
848
+ return PREFORK_SPAWN_FAILED
849
+ }
715
850
  // Token 0 is the signal pipe, token i + 1 worker i's channel.
716
851
  const ready: i32[] = new Array<i32>(2 * count + 2)
717
852
  let result: i32 = PREFORK_STOPPED
@@ -748,6 +883,7 @@ export const preforkSupervise = <S>(
748
883
  }
749
884
  }
750
885
  supervisor.stop(loop, signals, ready)
886
+ supervisor.closeListeners()
751
887
  netClose(loop)
752
888
  return result
753
889
  }
package/std/net/router.ts CHANGED
@@ -69,7 +69,7 @@ export const ROUTER_BAD_ESCAPE: i32 = -10
69
69
  /** `routerDecode`: the decoded bytes do not fit `out`; nothing was written. */
70
70
  export const ROUTER_NO_ROOM: i32 = -11
71
71
 
72
- /** The longest method `add` takes, as `nish/net/http1`'s `HTTP1_MAX_METHOD`. */
72
+ /** The longest method `add` takes, as `nish/net/http1-codec`'s `HTTP1_MAX_METHOD`. */
73
73
  const ROUTER_MAX_METHOD: i32 = 32
74
74
  /** No node or route: an `i32` spelled once, so that `--number-mode f64` does not read a bare `-1` as a double. */
75
75
  const ROUTER_NONE: i32 = -1