@andersbakken/fisk 5.0.14 → 5.0.17

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": "@andersbakken/fisk",
3
- "version": "5.0.14",
3
+ "version": "5.0.17",
4
4
  "description": "Fisk, a distributed compile system",
5
5
  "scripts": {
6
6
  "lint": "eslint . --ext .ts",
@@ -4426,10 +4426,9 @@ class Client extends require$$0__default$2["default"] {
4426
4426
  this.npmVersion = "";
4427
4427
  this.user = "";
4428
4428
  this.port = 0;
4429
- this.ws.on("pong", () => {
4430
- // console.log("got pong", this.name);
4431
- this.pingSent = undefined;
4432
- });
4429
+ }
4430
+ notePong() {
4431
+ this.pingSent = undefined;
4433
4432
  }
4434
4433
  send(type, msg) {
4435
4434
  try {
@@ -4472,8 +4471,12 @@ class Client extends require$$0__default$2["default"] {
4472
4471
  error(message) {
4473
4472
  try {
4474
4473
  this.ws.send(`{"error": "${message}"}`);
4475
- this.ws.close();
4474
+ // Emit before closing. A real websocket closes asynchronously, but a
4475
+ // DaemonJobSocket does it synchronously and tears the job down -- and
4476
+ // removes these listeners -- so closing first means nobody ever hears
4477
+ // this, including whoever releases the job's counters.
4476
4478
  this.emit("error", message);
4479
+ this.ws.close();
4477
4480
  }
4478
4481
  catch (err) {
4479
4482
  /* */
@@ -4506,6 +4509,115 @@ class Compile extends Client {
4506
4509
  this.environment = environment;
4507
4510
  this.sourcePath = sourcePath;
4508
4511
  this.sha1 = sha1;
4512
+ // A job relayed by a daemon cannot stream an environment tarball to us: it is
4513
+ // one multiplexed stream on a connection shared with every other compile on
4514
+ // that host. Such a client reconnects directly to upload instead.
4515
+ this.canUploadEnvironment = true;
4516
+ }
4517
+ }
4518
+
4519
+ // One fiskc job on a daemon's shared connection. Everything the scheduler sends
4520
+ // to "its client" is framed onto that connection by relay(), and closing it (a
4521
+ // version mismatch, Client.error()) has to be reported to the daemon rather than
4522
+ // dropping a TCP connection, or the fiskc process would wait for its watchdog.
4523
+ class DaemonJobSocket {
4524
+ constructor(relay, closed) {
4525
+ this.relay = relay;
4526
+ this.closed = closed;
4527
+ this.open = true;
4528
+ }
4529
+ send(data) {
4530
+ if (!this.open) {
4531
+ return;
4532
+ }
4533
+ if (typeof data === "string") {
4534
+ this.relay(data);
4535
+ }
4536
+ else if (data instanceof Buffer) {
4537
+ this.relay(data.toString("utf8"));
4538
+ }
4539
+ else {
4540
+ console.error("Unexpected payload for a daemon job", typeof data);
4541
+ }
4542
+ }
4543
+ close(code, reason) {
4544
+ if (!this.open) {
4545
+ return;
4546
+ }
4547
+ this.open = false;
4548
+ this.closed(String(reason || `closed with ${code === undefined ? 1000 : code}`));
4549
+ }
4550
+ // eslint-disable-next-line class-methods-use-this
4551
+ ping() {
4552
+ // Liveness is a property of the daemon's connection, not of a single job.
4553
+ }
4554
+ terminate() {
4555
+ this.close(1006, "terminated");
4556
+ }
4557
+ }
4558
+
4559
+ function decode$3(message) {
4560
+ try {
4561
+ return JSON.parse(message);
4562
+ }
4563
+ catch (err) {
4564
+ return { error: `Unserializable scheduler message: ${message}` };
4565
+ }
4566
+ }
4567
+ class DaemonConnection extends Client {
4568
+ constructor(ws, ip, option) {
4569
+ super(5 /* ClientType.Daemon */, ws, ip, option);
4570
+ this.jobs = new Map();
4571
+ }
4572
+ get activeJobs() {
4573
+ return this.jobs.size;
4574
+ }
4575
+ createJob(request) {
4576
+ var _a;
4577
+ if (this.jobs.has(request.id)) {
4578
+ return undefined;
4579
+ }
4580
+ const socket = new DaemonJobSocket((message) => {
4581
+ this.send({ type: "jobMessage", id: request.id, message: decode$3(message) });
4582
+ }, (reason) => {
4583
+ this.finishJob(request.id, reason, true);
4584
+ });
4585
+ const compile = new Compile(socket, this.ip, request.environment, request.sourcePath, request.sha1, this.option);
4586
+ compile.canUploadEnvironment = false;
4587
+ compile.npmVersion = request.npmVersion || "";
4588
+ compile.name = request.name || this.name;
4589
+ compile.hostname = request.hostname || this.hostname;
4590
+ compile.user = request.user || "";
4591
+ if (request.builder) {
4592
+ compile.builder = request.builder;
4593
+ }
4594
+ if ((_a = request.labels) === null || _a === void 0 ? void 0 : _a.length) {
4595
+ compile.labels = request.labels;
4596
+ }
4597
+ this.jobs.set(request.id, compile);
4598
+ return compile;
4599
+ }
4600
+ // The end of a job is a message now, not a socket close, so every exit has to
4601
+ // funnel through here: the daemon saying the fiskc process is gone, the
4602
+ // scheduler closing the job itself, or this whole connection dropping. Missing
4603
+ // one leaks a builder's activeClients slot and the scheduler's activeJobs
4604
+ // count for as long as the scheduler runs.
4605
+ finishJob(id, reason, notifyDaemon) {
4606
+ const compile = this.jobs.get(id);
4607
+ if (!compile) {
4608
+ return;
4609
+ }
4610
+ this.jobs.delete(id);
4611
+ if (notifyDaemon) {
4612
+ this.send({ type: "jobClosed", id, reason });
4613
+ }
4614
+ compile.emit("close", { code: 1000, reason });
4615
+ compile.removeAllListeners();
4616
+ }
4617
+ finishAllJobs(reason) {
4618
+ for (const id of Array.from(this.jobs.keys())) {
4619
+ this.finishJob(id, reason, false);
4620
+ }
4509
4621
  }
4510
4622
  }
4511
4623
 
@@ -56206,17 +56318,20 @@ class Server extends require$$0__default$2["default"] {
56206
56318
  }
56207
56319
  this.emit("compile", client);
56208
56320
  const remaining = {};
56209
- client.ws.on("error", (err) => client.emit("error", err));
56210
- client.ws.on("close", (code, reason) => {
56321
+ ws.on("pong", () => {
56322
+ client.notePong();
56323
+ });
56324
+ ws.on("error", (err) => client.emit("error", err));
56325
+ ws.on("close", (code, reason) => {
56211
56326
  if (remaining.bytes) {
56212
56327
  client.emit("error", "Got close while reading a binary message");
56213
56328
  }
56214
56329
  if (client) {
56215
56330
  client.emit("close", { code: code, reason: reason });
56216
56331
  }
56217
- client.ws.removeAllListeners();
56332
+ ws.removeAllListeners();
56218
56333
  });
56219
- client.ws.on("message", (msg) => {
56334
+ ws.on("message", (msg) => {
56220
56335
  switch (typeof msg) {
56221
56336
  case "string": {
56222
56337
  if (remaining.bytes) {
@@ -56284,14 +56399,18 @@ class Server extends require$$0__default$2["default"] {
56284
56399
  }
56285
56400
  });
56286
56401
  }
56287
- _handleBuilder(req, client) {
56288
- client.ws.on("close", (code, reason) => {
56402
+ _handleBuilder(req, ws, ip) {
56403
+ const client = new Builder(ws, ip);
56404
+ ws.on("pong", () => {
56405
+ client.notePong();
56406
+ });
56407
+ ws.on("close", (code, reason) => {
56289
56408
  client.emit("close", { code: code, reason: reason });
56290
- client.ws.removeAllListeners();
56409
+ ws.removeAllListeners();
56291
56410
  });
56292
- client.ws.on("error", () => {
56411
+ ws.on("error", () => {
56293
56412
  client.emit("close", { code: 1005, reason: "unknown" });
56294
- client.ws.removeAllListeners();
56413
+ ws.removeAllListeners();
56295
56414
  });
56296
56415
  if (!("x-fisk-port" in req.headers)) {
56297
56416
  client.error("No x-fisk-port header");
@@ -56328,7 +56447,7 @@ class Server extends require$$0__default$2["default"] {
56328
56447
  client.environments[env] = true;
56329
56448
  }
56330
56449
  });
56331
- client.ws.on("message", (msg) => {
56450
+ ws.on("message", (msg) => {
56332
56451
  // console.log("Got message from builder", typeof msg, msg.length);
56333
56452
  switch (typeof msg) {
56334
56453
  case "string": {
@@ -56364,28 +56483,128 @@ class Server extends require$$0__default$2["default"] {
56364
56483
  // console.log("Got dude", client);
56365
56484
  this.emit("builder", client);
56366
56485
  }
56367
- _handleMonitor(req, client) {
56486
+ _handleMonitor(req, ws, ip) {
56487
+ const client = new Client(3 /* ClientType.Monitor */, ws, ip);
56368
56488
  client.nonce = this.nonces.get(req);
56369
56489
  // console.log("Got nonce", req.nonce);
56370
- client.ws.on("message", (message) => client.emit("message", message));
56490
+ ws.on("pong", () => {
56491
+ client.notePong();
56492
+ });
56493
+ ws.on("message", (message) => client.emit("message", message));
56371
56494
  this.emit("monitor", client);
56372
- client.ws.on("close", (code, reason) => {
56373
- client.ws.removeAllListeners();
56495
+ ws.on("close", (code, reason) => {
56496
+ ws.removeAllListeners();
56374
56497
  client.emit("close", { code: code, reason: reason });
56375
56498
  });
56376
- client.ws.on("error", (err) => client.emit("error", err));
56499
+ ws.on("error", (err) => client.emit("error", err));
56377
56500
  }
56378
- _handleClientVerify(req, client) {
56501
+ _handleClientVerify(req, ws, ip) {
56502
+ const client = new Client(4 /* ClientType.ClientVerify */, ws, ip);
56379
56503
  Object.assign(client, { npmVersion: header(req, "x-fisk-npm-version") });
56380
56504
  this.emit("clientVerify", client);
56381
- client.ws.on("close", (code, reason) => {
56382
- client.ws.removeAllListeners();
56505
+ ws.on("pong", () => {
56506
+ client.notePong();
56507
+ });
56508
+ ws.on("close", (code, reason) => {
56509
+ ws.removeAllListeners();
56383
56510
  client.emit("close", { code: code, reason: reason });
56384
56511
  });
56385
- client.ws.on("error", (err) => client.emit("error", err));
56512
+ ws.on("error", (err) => client.emit("error", err));
56513
+ }
56514
+ // One connection per host, shared by every fiskc process on it. fiskc used to
56515
+ // open its own websocket per translation unit, which is a TCP connect plus an
56516
+ // HTTP upgrade per compile; when this scheduler's event loop stalls the listen
56517
+ // backlog fills, SYNs get dropped and those clients time out and compile
56518
+ // locally instead. Jobs are multiplexed here as messages tagged with the
56519
+ // daemon's request id, and each one gets a Compile that behaves like any other
56520
+ // client so the scheduling and accounting code below stays untouched.
56521
+ _handleDaemon(req, ws, ip) {
56522
+ const configVersion = parseInt(header(req, "x-fisk-config-version") || "");
56523
+ if (configVersion !== this.configVersion) {
56524
+ ws.send(`{"error": "Bad config version, expected ${this.configVersion}, got ${configVersion}"}`);
56525
+ ws.close();
56526
+ return;
56527
+ }
56528
+ const connection = new DaemonConnection(ws, ip, this.option);
56529
+ connection.name = header(req, "x-fisk-daemon-name") || "";
56530
+ connection.hostname = header(req, "x-fisk-daemon-hostname") || "";
56531
+ connection.npmVersion = header(req, "x-fisk-npm-version") || "";
56532
+ ws.on("pong", () => {
56533
+ connection.notePong();
56534
+ });
56535
+ ws.on("close", (code, reason) => {
56536
+ connection.finishAllJobs(`daemon ${ip} disconnected: ${code} ${String(reason)}`);
56537
+ ws.removeAllListeners();
56538
+ connection.emit("close", { code: code, reason: reason });
56539
+ });
56540
+ ws.on("error", (err) => {
56541
+ connection.finishAllJobs(`daemon ${ip} error: ${err.message}`);
56542
+ connection.emit("error", err);
56543
+ });
56544
+ ws.on("message", (msg) => {
56545
+ if (typeof msg !== "string") {
56546
+ console.error("Unexpected binary message from daemon", ip);
56547
+ return;
56548
+ }
56549
+ let json;
56550
+ try {
56551
+ json = JSON.parse(msg);
56552
+ }
56553
+ catch (err) {
56554
+ console.error(`Unable to parse message from daemon ${ip} as JSON`, err);
56555
+ return;
56556
+ }
56557
+ if (!json || typeof json.id !== "number") {
56558
+ console.error("Message from daemon without a job id", ip, json);
56559
+ return;
56560
+ }
56561
+ const id = json.id;
56562
+ switch (json.type) {
56563
+ case "compileRequest": {
56564
+ if (typeof json.environment !== "string" || !json.environment) {
56565
+ connection.send({ type: "jobMessage", id, message: { error: "No environment" } });
56566
+ return;
56567
+ }
56568
+ if (typeof json.sourceFile !== "string" || !json.sourceFile) {
56569
+ connection.send({ type: "jobMessage", id, message: { error: "No sourceFile" } });
56570
+ return;
56571
+ }
56572
+ const sha1 = typeof json.sha1 === "string" ? json.sha1 : undefined;
56573
+ if (sha1 && sha1.length !== 40) {
56574
+ connection.send({ type: "jobMessage", id, message: { error: `Bad sha1 sum: ${sha1}` } });
56575
+ return;
56576
+ }
56577
+ const labels = typeof json.labels === "string" ? json.labels.split(/ +/).filter((x) => x) : undefined;
56578
+ const compile = connection.createJob({
56579
+ id,
56580
+ environment: json.environment,
56581
+ sourcePath: json.sourceFile,
56582
+ sha1,
56583
+ name: typeof json.name === "string" ? json.name : undefined,
56584
+ user: typeof json.user === "string" ? json.user : undefined,
56585
+ hostname: typeof json.hostname === "string" ? json.hostname : undefined,
56586
+ builder: typeof json.builder === "string" ? json.builder : undefined,
56587
+ labels,
56588
+ npmVersion: typeof json.npmVersion === "string" ? json.npmVersion : undefined
56589
+ });
56590
+ if (!compile) {
56591
+ connection.send({ type: "jobMessage", id, message: { error: `Duplicate job id ${id}` } });
56592
+ return;
56593
+ }
56594
+ this.emit("compile", compile);
56595
+ break;
56596
+ }
56597
+ case "compileDone":
56598
+ connection.finishJob(id, typeof json.reason === "string" ? json.reason : "done", false);
56599
+ break;
56600
+ default:
56601
+ console.error("Unexpected message type from daemon", ip, json.type);
56602
+ break;
56603
+ }
56604
+ });
56605
+ this.emit("daemon", connection);
56386
56606
  }
56387
56607
  _handleConnection(ws, req) {
56388
- let client = undefined;
56389
56608
  let ip = req.connection.remoteAddress;
56390
56609
  // console.log("_handleConnection", ip);
56391
56610
  if (!ip) {
@@ -56402,16 +56621,16 @@ class Server extends require$$0__default$2["default"] {
56402
56621
  this._handleCompile(req, ws, ip);
56403
56622
  break;
56404
56623
  case "/builder":
56405
- client = new Builder(ws, ip);
56406
- this._handleBuilder(req, client);
56624
+ this._handleBuilder(req, ws, ip);
56407
56625
  break;
56408
56626
  case "/monitor":
56409
- client = new Client(3 /* ClientType.Monitor */, ws, ip);
56410
- this._handleMonitor(req, client);
56627
+ this._handleMonitor(req, ws, ip);
56411
56628
  break;
56412
56629
  case "/client_verify":
56413
- client = new Client(4 /* ClientType.ClientVerify */, ws, ip);
56414
- this._handleClientVerify(req, client);
56630
+ this._handleClientVerify(req, ws, ip);
56631
+ break;
56632
+ case "/daemon":
56633
+ this._handleDaemon(req, ws, ip);
56415
56634
  break;
56416
56635
  default:
56417
56636
  console.error(`Invalid pathname ${url.pathname} from: ${ip}`);
@@ -58095,9 +58314,9 @@ catch (err) {
58095
58314
  console.log("Couldn't parse package json", err);
58096
58315
  process.exit();
58097
58316
  }
58098
- const builders = {};
58317
+ const builders = new Set();
58318
+ const daemons = new Set();
58099
58319
  const monitors = [];
58100
- let builderCount = 0;
58101
58320
  let activeJobs = 0;
58102
58321
  let capacity = 0;
58103
58322
  let jobsFailed = 0;
@@ -58245,12 +58464,6 @@ function jobFinished(builder, job) {
58245
58464
  });
58246
58465
  }
58247
58466
  }
58248
- function builderKey(ip, port) {
58249
- if (typeof ip === "object") {
58250
- return ip.ip + " " + ip.port;
58251
- }
58252
- return ip + " " + port;
58253
- }
58254
58467
  function builderToMonitorInfo(builder, type) {
58255
58468
  return {
58256
58469
  type: type,
@@ -58270,8 +58483,7 @@ function builderToMonitorInfo(builder, type) {
58270
58483
  };
58271
58484
  }
58272
58485
  function insertBuilder(builder) {
58273
- builders[builderKey(builder)] = builder;
58274
- ++builderCount;
58486
+ builders.add(builder);
58275
58487
  assert__default["default"](typeof builder.slots === "number");
58276
58488
  capacity += builder.slots;
58277
58489
  if (monitors.length) {
@@ -58285,8 +58497,8 @@ function insertBuilder(builder) {
58285
58497
  }
58286
58498
  }
58287
58499
  function forEachBuilder(cb) {
58288
- for (const key in builders) {
58289
- cb(builders[key]);
58500
+ for (const builder of builders) {
58501
+ cb(builder);
58290
58502
  }
58291
58503
  }
58292
58504
  function onObjectCacheCleared() {
@@ -58319,10 +58531,9 @@ if (option("object-cache")) {
58319
58531
  setObjectCacheEnabled(true);
58320
58532
  }
58321
58533
  function removeBuilder(builder) {
58322
- --builderCount;
58323
58534
  assert__default["default"](typeof builder.slots === "number");
58324
58535
  capacity -= builder.slots;
58325
- delete builders[builderKey(builder)];
58536
+ builders.delete(builder);
58326
58537
  if (monitors.length) {
58327
58538
  const info = builderToMonitorInfo(builder, "builderRemoved");
58328
58539
  if (monitorsLog) {
@@ -58468,27 +58679,44 @@ server.on("listen", (app) => {
58468
58679
  app.get("/builders", (req, res) => {
58469
58680
  const ret = [];
58470
58681
  const now = Date.now();
58471
- for (const bKey in builders) {
58472
- const s = builders[bKey];
58682
+ for (const builder of builders) {
58683
+ ret.push({
58684
+ ip: builder.ip,
58685
+ name: builder.name,
58686
+ labels: builder.labels,
58687
+ slots: builder.slots,
58688
+ port: builder.port,
58689
+ activeClients: builder.activeClients,
58690
+ jobsScheduled: builder.jobsScheduled,
58691
+ lastJob: builder.lastJob ? new Date(builder.lastJob).toString() : "",
58692
+ jobsPerformed: builder.jobsPerformed,
58693
+ compileSpeed: builder.jobsPerformed / builder.totalCompileSpeed || 0,
58694
+ uploadSpeed: builder.jobsPerformed / builder.totalUploadSpeed || 0,
58695
+ hostname: builder.hostname,
58696
+ system: builder.system,
58697
+ created: builder.created,
58698
+ load: builder.load,
58699
+ uptime: now - builder.created.valueOf(),
58700
+ npmVersion: builder.npmVersion,
58701
+ environments: Object.keys(builder.environments)
58702
+ });
58703
+ }
58704
+ const pretty = req.query && req.query.unpretty ? undefined : 4;
58705
+ res.send(JSON.stringify(ret, null, pretty) + "\n");
58706
+ });
58707
+ app.get("/daemons", (req, res) => {
58708
+ const ret = [];
58709
+ const now = Date.now();
58710
+ for (const daemon of daemons) {
58473
58711
  ret.push({
58474
- ip: s.ip,
58475
- name: s.name,
58476
- labels: s.labels,
58477
- slots: s.slots,
58478
- port: s.port,
58479
- activeClients: s.activeClients,
58480
- jobsScheduled: s.jobsScheduled,
58481
- lastJob: s.lastJob ? new Date(s.lastJob).toString() : "",
58482
- jobsPerformed: s.jobsPerformed,
58483
- compileSpeed: s.jobsPerformed / s.totalCompileSpeed || 0,
58484
- uploadSpeed: s.jobsPerformed / s.totalUploadSpeed || 0,
58485
- hostname: s.hostname,
58486
- system: s.system,
58487
- created: s.created,
58488
- load: s.load,
58489
- uptime: now - s.created.valueOf(),
58490
- npmVersion: s.npmVersion,
58491
- environments: Object.keys(s.environments)
58712
+ ip: daemon.ip,
58713
+ name: daemon.name,
58714
+ labels: daemon.labels,
58715
+ port: daemon.port,
58716
+ hostname: daemon.hostname,
58717
+ created: daemon.created,
58718
+ uptime: now - daemon.created.valueOf(),
58719
+ npmVersion: daemon.npmVersion
58492
58720
  });
58493
58721
  }
58494
58722
  const pretty = req.query && req.query.unpretty ? undefined : 4;
@@ -58501,17 +58729,18 @@ server.on("listen", (app) => {
58501
58729
  return { count: count, percentage: (count ? (count * 100) / jobs : 0).toFixed(1) + "%" };
58502
58730
  }
58503
58731
  const obj = {
58504
- builderCount: Object.keys(builders).length,
58732
+ builderCount: builders.size,
58733
+ daemonCount: daemons.size,
58505
58734
  npmVersion: schedulerNpmVersion,
58506
58735
  environments: environmentsInfo(),
58507
58736
  configVersion: common.Version,
58508
- capacity: capacity,
58509
- activeJobs: activeJobs,
58737
+ capacity,
58738
+ activeJobs,
58510
58739
  peaks: peakData(),
58511
58740
  jobsFailed: percentage(jobsFailed),
58512
- jobsStarted: jobsStarted,
58513
- jobs: jobs,
58514
- jobsScheduled: jobsScheduled,
58741
+ jobsStarted,
58742
+ jobs,
58743
+ jobsScheduled,
58515
58744
  jobsFinished: percentage(jobsFinished),
58516
58745
  cacheHits: percentage(objectCache ? objectCache.hits : 0),
58517
58746
  uptimeMS: now - serverStartTime,
@@ -58560,9 +58789,9 @@ server.on("listen", (app) => {
58560
58789
  code: req.query.code || 0,
58561
58790
  purgeEnvironments: "purge_environments" in req.query
58562
58791
  };
58563
- console.log("Sending quit message to builders", msg, Object.keys(builders));
58564
- for (const ip in builders) {
58565
- builders[ip].send(msg);
58792
+ console.log("Sending quit message to builders", msg, builders.size);
58793
+ for (const builder of builders) {
58794
+ builder.send(msg);
58566
58795
  }
58567
58796
  });
58568
58797
  app.get("/environment/*", (req, res) => {
@@ -58606,8 +58835,7 @@ server.on("listen", (app) => {
58606
58835
  return;
58607
58836
  }
58608
58837
  let found;
58609
- for (const key in builders) {
58610
- const builder = builders[key];
58838
+ for (const builder of builders) {
58611
58839
  console.log(builder.ip, builder.name, builder.hostname, req.body.builder);
58612
58840
  if (builder.ip === req.body.builder ||
58613
58841
  builder.name === req.body.builder ||
@@ -58742,7 +58970,7 @@ server.on("builder", (builder) => {
58742
58970
  }
58743
58971
  builder.activeClients = 0;
58744
58972
  insertBuilder(builder);
58745
- console.log("builder connected", builder.npmVersion, builder.ip, builder.name || "", builder.hostname || "", Object.keys(builder.environments), "builderCount is", builderCount);
58973
+ console.log("builder connected", builder.npmVersion, builder.ip, builder.name || "", builder.hostname || "", Object.keys(builder.environments), "builderCount is", builders.size);
58746
58974
  syncEnvironments(builder);
58747
58975
  builder.on("environments", (message) => {
58748
58976
  builder.environments = {};
@@ -58775,7 +59003,7 @@ server.on("builder", (builder) => {
58775
59003
  if (objectCache) {
58776
59004
  objectCache.removeNode(builder);
58777
59005
  }
58778
- console.log(`builder disconnected ${builder.ip}:${builder.port} ${builder.name} ${builder.hostname} builderCount is ${builderCount}`);
59006
+ console.log(`builder disconnected ${builder.ip}:${builder.port} ${builder.name} ${builder.hostname} builderCount is ${builders.size}`);
58779
59007
  builder.removeAllListeners();
58780
59008
  });
58781
59009
  builder.on("load", (message) => {
@@ -58810,6 +59038,20 @@ function requestEnvironment(compile) {
58810
59038
  if (compile.environment in pendingEnvironments) {
58811
59039
  return false;
58812
59040
  }
59041
+ if (!compile.canUploadEnvironment) {
59042
+ // Tell it what we are missing but leave the pending slot open: this client
59043
+ // is going to reconnect with a websocket of its own to do the upload, and
59044
+ // that connection is the one that has to claim the slot and get the
59045
+ // listeners below.
59046
+ console.log(`Asking ${compile.name} ${compile.ip} to upload ${compile.environment} over a direct connection`);
59047
+ compile.send({ type: "needsEnvironment" });
59048
+ // Nothing further will ever happen on this job -- the upload arrives on a
59049
+ // new connection -- so release it now rather than holding it for the whole
59050
+ // local compile fiskc does in the meantime. The direct branch below must
59051
+ // NOT do this: that client uploads over this very socket.
59052
+ compile.close();
59053
+ return true;
59054
+ }
58813
59055
  pendingEnvironments[compile.environment] = true;
58814
59056
  console.log(`Asking ${compile.name} ${compile.ip} to upload ${compile.environment}`);
58815
59057
  compile.send({ type: "needsEnvironment" });
@@ -58893,13 +59135,35 @@ server.on("clientVerify", (clientVerify) => {
58893
59135
  clientVerify.send("version_verified", { minimum_version: `${clientMinimumVersion}` });
58894
59136
  }
58895
59137
  });
59138
+ // Also gives the connection an "error" listener: EventEmitter throws on an
59139
+ // unhandled "error" event, and this one would take the scheduler down with it.
59140
+ server.on("daemon", (daemon) => {
59141
+ console.log(`daemon connected ${daemon.ip} ${daemon.name} ${daemon.hostname} version ${daemon.npmVersion}`);
59142
+ daemon.on("error", (err) => {
59143
+ console.error(`daemon error ${daemon.ip}: ${typeof err === "string" ? err : err.message}`);
59144
+ // ### do I get a close if I get an error?
59145
+ daemons.delete(daemon);
59146
+ });
59147
+ daemon.on("close", () => {
59148
+ console.log(`daemon disconnected ${daemon.ip} ${daemon.name}`);
59149
+ daemon.removeAllListeners();
59150
+ daemons.delete(daemon);
59151
+ });
59152
+ daemons.add(daemon);
59153
+ });
58896
59154
  server.on("compile", (compile) => {
58897
59155
  compile.on("log", (event) => {
58898
59156
  addLogFile({ source: "client", ip: compile.ip, contents: event.message });
58899
59157
  });
59158
+ // Every dead end below closes the client. On a real websocket fiskc did that
59159
+ // for us by disconnecting; a job relayed by a daemon has no socket to drop, so
59160
+ // without this it sits in DaemonConnection.jobs holding a Compile until the
59161
+ // daemon notices fiskc exit -- which for these paths means all the way through
59162
+ // the local compile fiskc falls back to.
58900
59163
  if (clientTooOld(compile.npmVersion)) {
58901
59164
  ++jobsFailed;
58902
59165
  compile.send("version_mismatch", { minimum_version: `${clientMinimumVersion}` });
59166
+ compile.close();
58903
59167
  return;
58904
59168
  }
58905
59169
  // console.log("request", compile.hostname, compile.ip, compile.environment);
@@ -58912,6 +59176,7 @@ server.on("compile", (compile) => {
58912
59176
  if (!usableEnvs.length) {
58913
59177
  console.log(`We're already waiting for ${compile.environment} and we don't have any compatible ones`);
58914
59178
  compile.send("builder", {});
59179
+ compile.close();
58915
59180
  ++jobsFailed;
58916
59181
  return;
58917
59182
  }
@@ -58994,17 +59259,20 @@ server.on("compile", (compile) => {
58994
59259
  ++jobsFailed;
58995
59260
  console.log(`Specific builder "${compile.builder}" was requested and we couldn't find a builder with that ${compile.environment}`);
58996
59261
  compile.send("builder", {});
59262
+ compile.close();
58997
59263
  return;
58998
59264
  }
58999
59265
  if (compile.labels) {
59000
59266
  ++jobsFailed;
59001
59267
  console.log(`Specific labels "${compile.labels}" were specified we couldn't match ${compile.environment} with any builder with those labels`);
59002
59268
  compile.send("builder", {});
59269
+ compile.close();
59003
59270
  return;
59004
59271
  }
59005
59272
  ++jobsFailed;
59006
59273
  console.log("No builder for you", compile.ip);
59007
59274
  compile.send("builder", {});
59275
+ compile.close();
59008
59276
  return;
59009
59277
  }
59010
59278
  const data = {};
@@ -59437,10 +59705,15 @@ Environments.instance
59437
59705
  .then(() => {
59438
59706
  setInterval(() => {
59439
59707
  // console.log("sending pings");
59440
- for (const key in builders) {
59441
- const builder = builders[key];
59708
+ for (const builder of builders) {
59442
59709
  builder.ping();
59443
59710
  }
59711
+ // A daemon holds every job on its host, so a silently dead one pins
59712
+ // activeJobs and activeClients until the kernel gives up on the TCP
59713
+ // connection. Pinging is what makes Client.ping close it instead.
59714
+ for (const daemon of daemons) {
59715
+ daemon.ping();
59716
+ }
59444
59717
  }, option.int("ping-interval", 20000));
59445
59718
  })
59446
59719
  .catch((e) => {