@jeffusion/bungee-core 3.2.1 → 3.3.1

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/dist/master.js CHANGED
@@ -16534,6 +16534,228 @@ var init_plugin_permissions = __esm(() => {
16534
16534
  };
16535
16535
  });
16536
16536
 
16537
+ // src/plugin-runtime-multi-worker-convergence.ts
16538
+ class PluginRuntimeMultiWorkerConvergenceTracker {
16539
+ convergenceDeadlineMs;
16540
+ workers = new Map;
16541
+ targetGeneration = null;
16542
+ startedAt = null;
16543
+ deadlineAt = null;
16544
+ constructor(convergenceDeadlineMs = 5000) {
16545
+ this.convergenceDeadlineMs = convergenceDeadlineMs;
16546
+ }
16547
+ upsertWorker(workerId, pid = null) {
16548
+ const existing = this.workers.get(workerId);
16549
+ if (existing) {
16550
+ existing.pid = pid;
16551
+ if (existing.baseStatus === "stopped") {
16552
+ existing.baseStatus = "starting";
16553
+ }
16554
+ existing.updatedAt = Date.now();
16555
+ return;
16556
+ }
16557
+ this.workers.set(workerId, {
16558
+ workerId,
16559
+ pid,
16560
+ baseStatus: "starting",
16561
+ servingGeneration: null,
16562
+ lastRequestedGeneration: null,
16563
+ lastAppliedGeneration: null,
16564
+ failureGeneration: null,
16565
+ error: null,
16566
+ updatedAt: Date.now()
16567
+ });
16568
+ }
16569
+ recordWorkerReady(workerId, generation, pid = null) {
16570
+ const worker = this.getOrCreateWorker(workerId, pid);
16571
+ worker.pid = pid ?? worker.pid;
16572
+ worker.baseStatus = "ready";
16573
+ worker.servingGeneration = generation;
16574
+ worker.lastAppliedGeneration = generation;
16575
+ worker.error = null;
16576
+ worker.updatedAt = Date.now();
16577
+ }
16578
+ beginGeneration(targetGeneration, workerIds) {
16579
+ this.targetGeneration = targetGeneration;
16580
+ this.startedAt = Date.now();
16581
+ this.deadlineAt = this.startedAt + Math.max(0, this.convergenceDeadlineMs);
16582
+ for (const workerId of workerIds ?? Array.from(this.workers.keys())) {
16583
+ const worker = this.getOrCreateWorker(workerId);
16584
+ worker.lastRequestedGeneration = targetGeneration;
16585
+ worker.failureGeneration = worker.failureGeneration === targetGeneration ? null : worker.failureGeneration;
16586
+ worker.error = worker.failureGeneration === targetGeneration ? null : worker.error;
16587
+ if (worker.baseStatus !== "stopped") {
16588
+ worker.baseStatus = "reconciling";
16589
+ }
16590
+ worker.updatedAt = Date.now();
16591
+ }
16592
+ }
16593
+ recordWorkerSuccess(workerId, generation, servingGeneration = generation, pid = null) {
16594
+ const worker = this.getOrCreateWorker(workerId, pid);
16595
+ worker.pid = pid ?? worker.pid;
16596
+ worker.baseStatus = "ready";
16597
+ worker.lastAppliedGeneration = generation;
16598
+ worker.servingGeneration = servingGeneration;
16599
+ worker.failureGeneration = null;
16600
+ worker.error = null;
16601
+ worker.updatedAt = Date.now();
16602
+ }
16603
+ recordWorkerFailure(workerId, generation, error, servingGeneration, pid = null) {
16604
+ const worker = this.getOrCreateWorker(workerId, pid);
16605
+ worker.pid = pid ?? worker.pid;
16606
+ worker.baseStatus = "failed";
16607
+ worker.lastAppliedGeneration = generation;
16608
+ worker.failureGeneration = generation;
16609
+ worker.servingGeneration = servingGeneration;
16610
+ worker.error = error;
16611
+ worker.updatedAt = Date.now();
16612
+ }
16613
+ recordWorkerStopped(workerId) {
16614
+ const worker = this.getOrCreateWorker(workerId);
16615
+ worker.baseStatus = "stopped";
16616
+ worker.updatedAt = Date.now();
16617
+ }
16618
+ getStatusReport(now = Date.now()) {
16619
+ const targetGeneration = this.targetGeneration;
16620
+ const deadlineExceeded = this.deadlineAt !== null && now > this.deadlineAt;
16621
+ const workers = Array.from(this.workers.values()).sort((left, right) => left.workerId - right.workerId).map((worker) => {
16622
+ const status2 = deriveWorkerStatus(worker, targetGeneration);
16623
+ return {
16624
+ workerId: worker.workerId,
16625
+ pid: worker.pid,
16626
+ status: status2,
16627
+ servingGeneration: worker.servingGeneration,
16628
+ lastRequestedGeneration: worker.lastRequestedGeneration,
16629
+ lastAppliedGeneration: worker.lastAppliedGeneration,
16630
+ failureGeneration: worker.failureGeneration,
16631
+ error: worker.error,
16632
+ updatedAt: worker.updatedAt ? new Date(worker.updatedAt).toISOString() : null
16633
+ };
16634
+ });
16635
+ const failedWorkers = workers.filter((worker) => worker.status === "failed").map((worker) => worker.workerId);
16636
+ const staleWorkers = workers.filter((worker) => worker.status === "stale" || worker.status === "failed" && targetGeneration !== null && worker.servingGeneration !== null && worker.servingGeneration < targetGeneration).map((worker) => worker.workerId);
16637
+ const pendingWorkers = workers.filter((worker) => worker.status === "reconciling" || worker.status === "starting" || worker.status === "ready").map((worker) => worker.workerId);
16638
+ const convergedWorkers = workers.filter((worker) => worker.status === "converged").map((worker) => worker.workerId);
16639
+ let status = "idle";
16640
+ let convergedGeneration = null;
16641
+ if (targetGeneration !== null) {
16642
+ if (failedWorkers.length > 0 || deadlineExceeded && (staleWorkers.length > 0 || pendingWorkers.length > 0)) {
16643
+ status = "failed";
16644
+ } else if (workers.length > 0 && convergedWorkers.length === workers.length) {
16645
+ status = "converged";
16646
+ convergedGeneration = targetGeneration;
16647
+ } else {
16648
+ status = "pending";
16649
+ }
16650
+ }
16651
+ return {
16652
+ targetGeneration,
16653
+ convergedGeneration,
16654
+ startedAt: this.startedAt ? new Date(this.startedAt).toISOString() : null,
16655
+ deadlineAt: this.deadlineAt ? new Date(this.deadlineAt).toISOString() : null,
16656
+ status,
16657
+ failedWorkers,
16658
+ staleWorkers,
16659
+ pendingWorkers,
16660
+ convergedWorkers,
16661
+ workers
16662
+ };
16663
+ }
16664
+ getOrCreateWorker(workerId, pid = null) {
16665
+ const existing = this.workers.get(workerId);
16666
+ if (existing) {
16667
+ if (pid !== null) {
16668
+ existing.pid = pid;
16669
+ }
16670
+ return existing;
16671
+ }
16672
+ const worker = {
16673
+ workerId,
16674
+ pid,
16675
+ baseStatus: "starting",
16676
+ servingGeneration: null,
16677
+ lastRequestedGeneration: null,
16678
+ lastAppliedGeneration: null,
16679
+ failureGeneration: null,
16680
+ error: null,
16681
+ updatedAt: null
16682
+ };
16683
+ this.workers.set(workerId, worker);
16684
+ return worker;
16685
+ }
16686
+ }
16687
+
16688
+ class PluginRuntimeMultiWorkerCoordinator {
16689
+ tracker;
16690
+ pollIntervalMs;
16691
+ requestedGeneration = 0;
16692
+ constructor(tracker = new PluginRuntimeMultiWorkerConvergenceTracker, pollIntervalMs = 50) {
16693
+ this.tracker = tracker;
16694
+ this.pollIntervalMs = pollIntervalMs;
16695
+ }
16696
+ noteWorkerReady(workerId, generation, pid = null) {
16697
+ this.requestedGeneration = Math.max(this.requestedGeneration, generation ?? 0);
16698
+ this.tracker.recordWorkerReady(workerId, generation, pid);
16699
+ }
16700
+ noteWorkerStarting(workerId, pid = null) {
16701
+ this.tracker.upsertWorker(workerId, pid);
16702
+ }
16703
+ noteWorkerStopped(workerId) {
16704
+ this.tracker.recordWorkerStopped(workerId);
16705
+ }
16706
+ noteWorkerReconcileSuccess(workerId, generation, servingGeneration = generation, pid = null) {
16707
+ this.requestedGeneration = Math.max(this.requestedGeneration, generation);
16708
+ this.tracker.recordWorkerSuccess(workerId, generation, servingGeneration, pid);
16709
+ }
16710
+ noteWorkerReconcileFailure(workerId, generation, error, servingGeneration, pid = null) {
16711
+ this.requestedGeneration = Math.max(this.requestedGeneration, generation);
16712
+ this.tracker.recordWorkerFailure(workerId, generation, error, servingGeneration, pid);
16713
+ }
16714
+ getStatusReport(now = Date.now()) {
16715
+ return this.tracker.getStatusReport(now);
16716
+ }
16717
+ async dispatchReconcile(workers) {
16718
+ const activeWorkers = workers.filter((worker) => worker.status === "ready");
16719
+ const targetGeneration = this.requestedGeneration + 1;
16720
+ this.requestedGeneration = targetGeneration;
16721
+ this.tracker.beginGeneration(targetGeneration, activeWorkers.map((worker) => worker.workerId));
16722
+ const command = {
16723
+ command: "reconcile-plugin-runtime",
16724
+ generation: targetGeneration
16725
+ };
16726
+ for (const worker of activeWorkers) {
16727
+ worker.send?.(command);
16728
+ }
16729
+ if (activeWorkers.length === 0) {
16730
+ return this.tracker.getStatusReport();
16731
+ }
16732
+ return await new Promise((resolve) => {
16733
+ const poller = setInterval(() => {
16734
+ const report = this.tracker.getStatusReport();
16735
+ if (report.status === "converged" || report.status === "failed") {
16736
+ clearInterval(poller);
16737
+ resolve(report);
16738
+ }
16739
+ }, this.pollIntervalMs);
16740
+ });
16741
+ }
16742
+ }
16743
+ function deriveWorkerStatus(worker, targetGeneration) {
16744
+ if (worker.baseStatus === "stopped" || worker.baseStatus === "failed") {
16745
+ return worker.baseStatus;
16746
+ }
16747
+ if (targetGeneration === null) {
16748
+ return worker.baseStatus;
16749
+ }
16750
+ if (worker.servingGeneration === targetGeneration) {
16751
+ return "converged";
16752
+ }
16753
+ if (worker.servingGeneration !== null && worker.servingGeneration < targetGeneration) {
16754
+ return "stale";
16755
+ }
16756
+ return worker.baseStatus;
16757
+ }
16758
+
16537
16759
  // src/master.ts
16538
16760
  var exports_master = {};
16539
16761
  import path7 from "path";
@@ -16547,6 +16769,7 @@ class Master {
16547
16769
  isReloading = false;
16548
16770
  reloadTimeout = null;
16549
16771
  pluginStorageCleanupService = null;
16772
+ pluginRuntimeConvergenceCoordinator = new PluginRuntimeMultiWorkerCoordinator;
16550
16773
  constructor() {
16551
16774
  this.loadAndStart();
16552
16775
  this.watchConfig();
@@ -16601,6 +16824,14 @@ class Master {
16601
16824
  await new Promise((resolve) => setTimeout(resolve, 100));
16602
16825
  }
16603
16826
  }
16827
+ const startupConvergence = await this.reconcilePluginRuntimeAcrossWorkers();
16828
+ logger.info({
16829
+ targetGeneration: startupConvergence.targetGeneration,
16830
+ convergedGeneration: startupConvergence.convergedGeneration,
16831
+ status: startupConvergence.status,
16832
+ failedWorkers: startupConvergence.failedWorkers,
16833
+ staleWorkers: startupConvergence.staleWorkers
16834
+ }, "Initial multi-worker plugin runtime convergence completed");
16604
16835
  logger.info("All workers have been started.");
16605
16836
  }
16606
16837
  forkWorker(workerId) {
@@ -16626,10 +16857,12 @@ class Master {
16626
16857
  process: workerProcess,
16627
16858
  workerId,
16628
16859
  status: "starting",
16860
+ pluginRuntimeGeneration: null,
16629
16861
  exitPromise,
16630
16862
  exitResolve
16631
16863
  };
16632
16864
  this.workers.set(workerId, workerInfo);
16865
+ this.pluginRuntimeConvergenceCoordinator.noteWorkerStarting(workerId, workerProcess.pid ?? null);
16633
16866
  const startupTimeout = setTimeout(() => {
16634
16867
  logger.error(`Worker #${workerId} (PID: ${workerProcess.pid}) failed to start within timeout`);
16635
16868
  workerProcess.kill();
@@ -16646,6 +16879,7 @@ class Master {
16646
16879
  }
16647
16880
  this.workers.delete(workerId);
16648
16881
  workerInfo.status = "stopped";
16882
+ this.pluginRuntimeConvergenceCoordinator.noteWorkerStopped(workerId);
16649
16883
  if (exitResolve) {
16650
16884
  exitResolve();
16651
16885
  }
@@ -16655,12 +16889,30 @@ class Master {
16655
16889
  });
16656
16890
  workerProcess.on("message", (message) => {
16657
16891
  clearTimeout(startupTimeout);
16658
- if (message.status === "ready") {
16892
+ if ("command" in message && message.command === "cluster-reconcile-plugin-runtime") {
16893
+ this.handleClusterPluginRuntimeReconcileRequest(workerInfo, message);
16894
+ return;
16895
+ }
16896
+ if (!("status" in message)) {
16897
+ return;
16898
+ }
16899
+ const workerMessage = message;
16900
+ if (workerMessage.status === "ready") {
16659
16901
  logger.info(`Worker #${workerId} (PID: ${workerProcess.pid}) reported ready.`);
16660
16902
  workerInfo.status = "ready";
16903
+ workerInfo.pluginRuntimeGeneration = workerMessage.pluginRuntime.generation;
16904
+ this.pluginRuntimeConvergenceCoordinator.noteWorkerReady(workerId, workerMessage.pluginRuntime.generation, workerMessage.pid);
16661
16905
  resolve(true);
16662
- } else if (message.status === "error") {
16663
- const errorMsg = message.error || "Unknown error";
16906
+ } else if (workerMessage.status === "plugin-runtime-reconcile-complete") {
16907
+ workerInfo.pluginRuntimeGeneration = workerMessage.servingGeneration;
16908
+ this.pluginRuntimeConvergenceCoordinator.noteWorkerReconcileSuccess(workerId, workerMessage.generation, workerMessage.servingGeneration, workerMessage.pid);
16909
+ this.logPluginRuntimeConvergenceStatus();
16910
+ } else if (workerMessage.status === "plugin-runtime-reconcile-failed") {
16911
+ workerInfo.pluginRuntimeGeneration = workerMessage.servingGeneration;
16912
+ this.pluginRuntimeConvergenceCoordinator.noteWorkerReconcileFailure(workerId, workerMessage.generation, workerMessage.error, workerMessage.servingGeneration, workerMessage.pid);
16913
+ this.logPluginRuntimeConvergenceStatus();
16914
+ } else if (workerMessage.status === "error") {
16915
+ const errorMsg = workerMessage.error || "Unknown error";
16664
16916
  logger.error(`Worker #${workerId} (PID: ${workerProcess.pid}) reported an error: ${errorMsg}`);
16665
16917
  workerProcess.kill();
16666
16918
  this.workers.delete(workerId);
@@ -16680,6 +16932,42 @@ class Master {
16680
16932
  }
16681
16933
  });
16682
16934
  }
16935
+ async reconcilePluginRuntimeAcrossWorkers() {
16936
+ return await this.pluginRuntimeConvergenceCoordinator.dispatchReconcile(Array.from(this.workers.values()).map((workerInfo) => ({
16937
+ workerId: workerInfo.workerId,
16938
+ status: workerInfo.status,
16939
+ send: workerInfo.process.send?.bind(workerInfo.process)
16940
+ })));
16941
+ }
16942
+ async handleClusterPluginRuntimeReconcileRequest(requester, message) {
16943
+ try {
16944
+ const convergence = await this.reconcilePluginRuntimeAcrossWorkers();
16945
+ requester.process.send?.({
16946
+ status: "cluster-plugin-runtime-reconcile-result",
16947
+ requestId: message.requestId,
16948
+ convergence
16949
+ });
16950
+ } catch (error) {
16951
+ requester.process.send?.({
16952
+ status: "cluster-plugin-runtime-reconcile-result",
16953
+ requestId: message.requestId,
16954
+ convergence: null,
16955
+ error: error instanceof Error ? error.message : String(error)
16956
+ });
16957
+ }
16958
+ }
16959
+ logPluginRuntimeConvergenceStatus() {
16960
+ const report = this.pluginRuntimeConvergenceCoordinator.getStatusReport();
16961
+ logger.info({
16962
+ targetGeneration: report.targetGeneration,
16963
+ convergedGeneration: report.convergedGeneration,
16964
+ status: report.status,
16965
+ failedWorkers: report.failedWorkers,
16966
+ staleWorkers: report.staleWorkers,
16967
+ pendingWorkers: report.pendingWorkers,
16968
+ workers: report.workers
16969
+ }, "Plugin runtime multi-worker convergence status updated");
16970
+ }
16683
16971
  watchConfig() {
16684
16972
  fs7.watch(CONFIG_PATH, (eventType) => {
16685
16973
  if (eventType === "change") {