nebula-notebook 0.1.1 → 0.2.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.
Files changed (78) hide show
  1. package/README.md +90 -11
  2. package/dist/assets/errorwidget-C4r2j2DQ.js +5 -0
  3. package/dist/assets/fa-brands-400-CEJbCg16.woff +0 -0
  4. package/dist/assets/fa-brands-400-CSYNqBb_.ttf +0 -0
  5. package/dist/assets/fa-brands-400-DnkPfk3o.eot +0 -0
  6. package/dist/assets/fa-brands-400-UxlILjvJ.woff2 +0 -0
  7. package/dist/assets/fa-brands-400-cH1MgKbP.svg +3717 -0
  8. package/dist/assets/fa-regular-400-BhTwtT8w.eot +0 -0
  9. package/dist/assets/fa-regular-400-D1vz6WBx.ttf +0 -0
  10. package/dist/assets/fa-regular-400-DFnMcJPd.woff +0 -0
  11. package/dist/assets/fa-regular-400-DGzu1beS.woff2 +0 -0
  12. package/dist/assets/fa-regular-400-gwj8Pxq-.svg +801 -0
  13. package/dist/assets/fa-solid-900-B4ZZ7kfP.svg +5034 -0
  14. package/dist/assets/fa-solid-900-B6Axprfb.eot +0 -0
  15. package/dist/assets/fa-solid-900-BUswJgRo.woff2 +0 -0
  16. package/dist/assets/fa-solid-900-DOXgCApm.woff +0 -0
  17. package/dist/assets/fa-solid-900-mxuxnBEa.ttf +0 -0
  18. package/dist/assets/index-7-YBurka.js +716 -0
  19. package/dist/assets/index-BtWv4MIT.css +7 -0
  20. package/dist/assets/index-CFBUnxSZ.css +32 -0
  21. package/dist/assets/index-CsHoPQy-.js +1 -0
  22. package/dist/assets/index-D5w21_Z8.js +81 -0
  23. package/dist/assets/index-Day3QcNs.js +1 -0
  24. package/dist/assets/services-shim-D6p_A67v.js +33 -0
  25. package/dist/assets/viewlist-uomDf7I7.js +1 -0
  26. package/dist/assets/widgets-X7J3NxEn.css +1 -0
  27. package/dist/index.html +2 -2
  28. package/node-server/dist/cluster/client-registration.js +3 -0
  29. package/node-server/dist/cluster/kernel-proxy.js +24 -9
  30. package/node-server/dist/cluster/server-registry.d.ts +8 -0
  31. package/node-server/dist/cluster/server-registry.js +31 -7
  32. package/node-server/dist/fs/fs-service.d.ts +55 -7
  33. package/node-server/dist/fs/fs-service.js +489 -80
  34. package/node-server/dist/fs/notebook-formats/percent.d.ts +25 -0
  35. package/node-server/dist/fs/notebook-formats/percent.js +286 -0
  36. package/node-server/dist/fs/notebook-formats/qmd.d.ts +29 -0
  37. package/node-server/dist/fs/notebook-formats/qmd.js +307 -0
  38. package/node-server/dist/fs/notebook-formats/registry.d.ts +12 -0
  39. package/node-server/dist/fs/notebook-formats/registry.js +77 -0
  40. package/node-server/dist/fs/notebook-formats/types.d.ts +37 -0
  41. package/node-server/dist/fs/notebook-formats/types.js +13 -0
  42. package/node-server/dist/idle-exit.d.ts +52 -0
  43. package/node-server/dist/idle-exit.js +83 -0
  44. package/node-server/dist/index.js +129 -6
  45. package/node-server/dist/kernel/kernel-service.d.ts +113 -2
  46. package/node-server/dist/kernel/kernel-service.js +762 -60
  47. package/node-server/dist/notebook/headless-handler.d.ts +2 -0
  48. package/node-server/dist/notebook/headless-handler.js +70 -24
  49. package/node-server/dist/notebook/operation-router.js +7 -2
  50. package/node-server/dist/notebook/undoRedoManager.d.ts +4 -1
  51. package/node-server/dist/notebook/undoRedoManager.js +10 -2
  52. package/node-server/dist/output/display-data.js +2 -0
  53. package/node-server/dist/routes/cluster.js +2 -2
  54. package/node-server/dist/routes/compute.d.ts +8 -0
  55. package/node-server/dist/routes/compute.js +136 -0
  56. package/node-server/dist/routes/fs.js +2 -2
  57. package/node-server/dist/routes/kernel.js +111 -1
  58. package/node-server/dist/routes/notebook.js +29 -1
  59. package/node-server/dist/scheduler/allocation-service.d.ts +43 -0
  60. package/node-server/dist/scheduler/allocation-service.js +169 -0
  61. package/node-server/dist/scheduler/job-template.d.ts +30 -0
  62. package/node-server/dist/scheduler/job-template.js +85 -0
  63. package/node-server/dist/scheduler/mock-scheduler.d.ts +30 -0
  64. package/node-server/dist/scheduler/mock-scheduler.js +121 -0
  65. package/node-server/dist/scheduler/slurm-scheduler.d.ts +31 -0
  66. package/node-server/dist/scheduler/slurm-scheduler.js +393 -0
  67. package/node-server/dist/scheduler/types.d.ts +117 -0
  68. package/node-server/dist/scheduler/types.js +8 -0
  69. package/node-server/dist/scheduler/util.d.ts +7 -0
  70. package/node-server/dist/scheduler/util.js +20 -0
  71. package/node-server/dist/terminal/pty-manager.js +8 -0
  72. package/node-server/dist/terminal/server.js +43 -2
  73. package/node-server/dist/update-check.d.ts +20 -0
  74. package/node-server/dist/update-check.js +114 -0
  75. package/node-server/package.json +1 -0
  76. package/package.json +2 -1
  77. package/dist/assets/index-BvrHu37J.js +0 -648
  78. package/dist/assets/index-Dfj_xsKU.css +0 -32
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ /**
3
+ * Allocation service — owns the lifecycle a compute allocation has *before* and
4
+ * *around* the cluster registry: submit a job, follow it through the queue, and
5
+ * correlate the client-server's registration (by one-time token) back to the
6
+ * allocation. Once correlated, the allocation is a normal registered server and
7
+ * kernels run on it through the existing cluster path.
8
+ *
9
+ * Phase-1 MVP: in-memory allocations, direct transport (no SSH tunnel — not
10
+ * needed where compute↔login is directly reachable).
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.allocationService = void 0;
47
+ const crypto_1 = require("crypto");
48
+ const fs = __importStar(require("fs"));
49
+ const path = __importStar(require("path"));
50
+ const job_template_1 = require("./job-template");
51
+ const server_registry_1 = require("../cluster/server-registry");
52
+ const POLL_INTERVAL_MS = 5_000;
53
+ const TERMINAL = ['ended', 'failed', 'cancelled'];
54
+ class AllocationService {
55
+ scheduler = null;
56
+ ctx = null;
57
+ allocations = new Map();
58
+ pollTimer = null;
59
+ enabled = false;
60
+ init(scheduler, ctx) {
61
+ this.scheduler = scheduler;
62
+ this.ctx = ctx;
63
+ this.enabled = true;
64
+ fs.mkdirSync(ctx.stateDir, { recursive: true });
65
+ if (this.pollTimer)
66
+ clearInterval(this.pollTimer);
67
+ this.pollTimer = setInterval(() => {
68
+ this.poll().catch((err) => console.error('[Scheduler] poll error:', err));
69
+ }, POLL_INTERVAL_MS);
70
+ }
71
+ isEnabled() {
72
+ return this.enabled;
73
+ }
74
+ getScheduler() {
75
+ return this.scheduler;
76
+ }
77
+ list() {
78
+ return [...this.allocations.values()].sort((a, b) => b.createdAt - a.createdAt);
79
+ }
80
+ get(id) {
81
+ return this.allocations.get(id);
82
+ }
83
+ async create(spec) {
84
+ if (!this.scheduler || !this.ctx)
85
+ throw new Error('scheduler not initialized');
86
+ const id = (0, crypto_1.randomUUID)().slice(0, 8);
87
+ const token = (0, crypto_1.randomUUID)();
88
+ const alloc = { id, token, spec, state: 'pending', createdAt: Date.now() };
89
+ const script = (0, job_template_1.renderJobScript)(spec, this.ctx, id, token);
90
+ const scriptPath = path.join(this.ctx.stateDir, `${id}.sh`);
91
+ fs.writeFileSync(scriptPath, script, { mode: 0o700 });
92
+ const { jobId } = await this.scheduler.submit(scriptPath);
93
+ alloc.jobId = jobId;
94
+ this.allocations.set(id, alloc);
95
+ console.log(`[Scheduler] Allocation ${id} submitted as job ${jobId} (${spec.partition}${spec.qos ? '/' + spec.qos : ''})`);
96
+ // Kick an immediate poll so a fast-starting job doesn't sit at "pending".
97
+ void this.poll().catch(() => { });
98
+ return alloc;
99
+ }
100
+ async cancel(id) {
101
+ const alloc = this.allocations.get(id);
102
+ if (!alloc)
103
+ return false;
104
+ if (alloc.jobId && this.scheduler) {
105
+ try {
106
+ await this.scheduler.cancel(alloc.jobId);
107
+ }
108
+ catch (err) {
109
+ console.error(`[Scheduler] scancel failed for ${alloc.jobId}:`, err);
110
+ }
111
+ }
112
+ if (alloc.serverId)
113
+ server_registry_1.serverRegistry.unregister(alloc.serverId);
114
+ alloc.state = 'cancelled';
115
+ return true;
116
+ }
117
+ async poll() {
118
+ if (!this.scheduler)
119
+ return;
120
+ for (const alloc of this.allocations.values()) {
121
+ if (TERMINAL.includes(alloc.state))
122
+ continue;
123
+ // Correlate: has the client-server for this allocation registered yet?
124
+ if (!alloc.serverId) {
125
+ const server = server_registry_1.serverRegistry.getServerByAllocationToken(alloc.token);
126
+ if (server) {
127
+ alloc.serverId = server.id;
128
+ alloc.state = 'active';
129
+ alloc.nodes = [server.host];
130
+ if (!alloc.walltimeEndsAt) {
131
+ alloc.walltimeEndsAt = Date.now() + alloc.spec.walltimeMinutes * 60_000;
132
+ }
133
+ console.log(`[Scheduler] Allocation ${alloc.id} active — registered as ${server.id}`);
134
+ }
135
+ }
136
+ // Follow the job through the scheduler.
137
+ if (!alloc.jobId)
138
+ continue;
139
+ let status;
140
+ try {
141
+ status = await this.scheduler.query(alloc.jobId);
142
+ }
143
+ catch {
144
+ continue;
145
+ }
146
+ if (status.state === 'running' && alloc.state === 'pending') {
147
+ alloc.state = 'running';
148
+ alloc.nodes = status.nodes.length ? status.nodes : alloc.nodes;
149
+ if (!alloc.walltimeEndsAt) {
150
+ alloc.walltimeEndsAt = Date.now() + alloc.spec.walltimeMinutes * 60_000;
151
+ }
152
+ }
153
+ else if (['completed', 'cancelled', 'failed'].includes(status.state)) {
154
+ alloc.state = status.state === 'failed' ? 'failed' : status.state === 'cancelled' ? 'cancelled' : 'ended';
155
+ alloc.reason = status.reason;
156
+ if (alloc.serverId)
157
+ server_registry_1.serverRegistry.unregister(alloc.serverId);
158
+ console.log(`[Scheduler] Allocation ${alloc.id} ${alloc.state} (job ${alloc.jobId})`);
159
+ }
160
+ }
161
+ }
162
+ shutdown() {
163
+ if (this.pollTimer) {
164
+ clearInterval(this.pollTimer);
165
+ this.pollTimer = null;
166
+ }
167
+ }
168
+ }
169
+ exports.allocationService = new AllocationService();
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Renders the sbatch script that launches a Nebula client-server inside a job.
3
+ *
4
+ * The rendered job:
5
+ * - re-invokes this very Nebula install (same node binary + entry) with --client
6
+ * from the same working directory (all visible on the shared filesystem),
7
+ * - points it at the login-node main server,
8
+ * - carries a one-time allocation token so the main server can correlate the
9
+ * resulting registration back to this allocation.
10
+ *
11
+ * The client spawns its kernels locally (ZeroMQ on 127.0.0.1 on the compute
12
+ * node); nothing here touches the kernel transport.
13
+ */
14
+ import type { JobSpec } from './types';
15
+ export interface LaunchContext {
16
+ /** URL the compute node uses to reach the main server, e.g. http://login:3000 */
17
+ mainUrl: string;
18
+ secret?: string;
19
+ /** Absolute node binary path (shared FS), e.g. process.execPath */
20
+ nodeBin: string;
21
+ /** Node exec args, e.g. ['--import', 'tsx'] */
22
+ execArgv: string[];
23
+ /** Server entry script, e.g. process.argv[1] */
24
+ scriptPath: string;
25
+ /** Working directory to launch from (the nebula checkout) */
26
+ cwd: string;
27
+ /** Directory (on shared storage) for job scripts + logs */
28
+ stateDir: string;
29
+ }
30
+ export declare function renderJobScript(spec: JobSpec, ctx: LaunchContext, allocId: string, token: string): string;
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ /**
3
+ * Renders the sbatch script that launches a Nebula client-server inside a job.
4
+ *
5
+ * The rendered job:
6
+ * - re-invokes this very Nebula install (same node binary + entry) with --client
7
+ * from the same working directory (all visible on the shared filesystem),
8
+ * - points it at the login-node main server,
9
+ * - carries a one-time allocation token so the main server can correlate the
10
+ * resulting registration back to this allocation.
11
+ *
12
+ * The client spawns its kernels locally (ZeroMQ on 127.0.0.1 on the compute
13
+ * node); nothing here touches the kernel transport.
14
+ */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.renderJobScript = renderJobScript;
50
+ const util_1 = require("./util");
51
+ const path = __importStar(require("path"));
52
+ function renderJobScript(spec, ctx, allocId, token) {
53
+ const walltime = (0, util_1.formatWalltime)(spec.walltimeMinutes);
54
+ const launch = [ctx.nodeBin, ...ctx.execArgv, ctx.scriptPath, '--client']
55
+ .map(util_1.shellQuote)
56
+ .join(' ');
57
+ const directives = [
58
+ `#SBATCH --job-name=${spec.jobName}`,
59
+ `#SBATCH --partition=${spec.partition}`,
60
+ spec.qos ? `#SBATCH --qos=${spec.qos}` : null,
61
+ spec.account ? `#SBATCH --account=${spec.account}` : null,
62
+ `#SBATCH --cpus-per-task=${spec.cpus}`,
63
+ `#SBATCH --mem=${spec.memGb}G`,
64
+ spec.gpus ? `#SBATCH --gres=gpu:${spec.gpuType ? `${spec.gpuType}:` : ''}${spec.gpus}` : null,
65
+ `#SBATCH --time=${walltime}`,
66
+ `#SBATCH --output=${path.join(ctx.stateDir, `${allocId}.log`)}`,
67
+ ].filter(Boolean).join('\n');
68
+ const secretLine = ctx.secret
69
+ ? `export NEBULA_CLUSTER_SECRET=${(0, util_1.shellQuote)(ctx.secret)}\n`
70
+ : '';
71
+ // PORT=0 → the client binds an ephemeral port and registers the *actual* bound
72
+ // port with the main server (see index.ts client-registration wiring). Binding
73
+ // 0.0.0.0 makes it reachable from the login node for the kernel proxy.
74
+ return `#!/bin/bash
75
+ ${directives}
76
+
77
+ cd ${(0, util_1.shellQuote)(ctx.cwd)}
78
+ export PATH=${(0, util_1.shellQuote)(path.dirname(ctx.nodeBin))}:"$PATH"
79
+ export NEBULA_MAIN_SERVER=${(0, util_1.shellQuote)(ctx.mainUrl)}
80
+ ${secretLine}export NEBULA_ALLOCATION_TOKEN=${(0, util_1.shellQuote)(token)}
81
+ export NEBULA_SERVER_NAME=${(0, util_1.shellQuote)(spec.jobName)}
82
+ ${spec.idleTimeoutMinutes ? `export NEBULA_IDLE_EXIT_MINUTES=${Math.floor(spec.idleTimeoutMinutes)}\n` : ''}export PORT=0
83
+ exec ${launch}
84
+ `;
85
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * MockScheduler — a fake batch scheduler for demos and tests, with no real
3
+ * cluster behind it. Enabled with `NEBULA_SCHEDULER=mock`.
4
+ *
5
+ * It implements the full `Scheduler` interface with fabricated (but plausibly
6
+ * shaped) partition / QoS / load data, so the real compute UI — allocation modal,
7
+ * cluster-load panel, server list — renders exactly as it would against a live
8
+ * cluster. The names here are generic on purpose (no real site's queues, hosts,
9
+ * accounts, or GPU SKUs) so nothing site-specific leaks into screenshots or the repo.
10
+ *
11
+ * `submit()` runs the *real* rendered client-launch script locally (via `bash`)
12
+ * instead of `sbatch`, so the allocation still registers, flips to an online
13
+ * server, and runs real kernels — the only thing faked is the scheduler itself.
14
+ */
15
+ import type { Scheduler, Associations, QueueLoad, JobStatus, StartEstimate } from './types';
16
+ export declare class MockScheduler implements Scheduler {
17
+ readonly name = "mock";
18
+ private jobs;
19
+ private nextJobId;
20
+ detect(): Promise<boolean>;
21
+ associations(_user: string): Promise<Associations>;
22
+ load(): Promise<QueueLoad>;
23
+ allowedQos(partition: string): Promise<string[] | null>;
24
+ estimateStart(): Promise<StartEstimate>;
25
+ submit(scriptPath: string): Promise<{
26
+ jobId: string;
27
+ }>;
28
+ query(jobId: string): Promise<JobStatus>;
29
+ cancel(jobId: string): Promise<void>;
30
+ }
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ /**
3
+ * MockScheduler — a fake batch scheduler for demos and tests, with no real
4
+ * cluster behind it. Enabled with `NEBULA_SCHEDULER=mock`.
5
+ *
6
+ * It implements the full `Scheduler` interface with fabricated (but plausibly
7
+ * shaped) partition / QoS / load data, so the real compute UI — allocation modal,
8
+ * cluster-load panel, server list — renders exactly as it would against a live
9
+ * cluster. The names here are generic on purpose (no real site's queues, hosts,
10
+ * accounts, or GPU SKUs) so nothing site-specific leaks into screenshots or the repo.
11
+ *
12
+ * `submit()` runs the *real* rendered client-launch script locally (via `bash`)
13
+ * instead of `sbatch`, so the allocation still registers, flips to an online
14
+ * server, and runs real kernels — the only thing faked is the scheduler itself.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.MockScheduler = void 0;
18
+ const child_process_1 = require("child_process");
19
+ // A visible queue wait before the job "starts", so the UI shows "Queued · waiting…"
20
+ // before flipping to online. Tunable for capture.
21
+ const QUEUE_DELAY_MS = Number(process.env.NEBULA_MOCK_QUEUE_MS ?? 5_000);
22
+ // Generic, non-identifying cluster shape. Two idle CPU queues, a couple of GPU
23
+ // queues with different cards, a big-memory queue, and a preemptible lab queue.
24
+ function partitions() {
25
+ const p = (name, total, idle, timeLimit, jobs, gpus, nodes) => ({
26
+ name, up: true, timeLimit,
27
+ cpus: { idle, alloc: total - idle - 0, other: 0, total },
28
+ gpus,
29
+ nodes: nodes ?? { idle: Math.round(idle / 32), mixed: 4, alloc: 3, down: 0, total: Math.round(total / 32) },
30
+ jobs,
31
+ });
32
+ return [
33
+ p('cpu', 512, 236, '1-00:00:00', { pending: 5, running: 44 }),
34
+ p('cpu-long', 256, 14, '7-00:00:00', { pending: 71, running: 58 }),
35
+ p('gpu', 128, 46, '1-00:00:00', { pending: 3, running: 12 }, { type: 'nvidia_l40s', total: 16, idle: 7 }),
36
+ p('gpu-a100', 96, 22, '1-00:00:00', { pending: 6, running: 9 }, { type: 'nvidia_a100_80gb', total: 8, idle: 3 }),
37
+ p('bigmem', 192, 104, '2-00:00:00', { pending: 1, running: 6 }),
38
+ p('lab', 64, 28, '30-00:00:00', { pending: 0, running: 3 }, { type: 'nvidia_h100_80gb', total: 8, idle: 5 }),
39
+ ];
40
+ }
41
+ function qoses() {
42
+ return [
43
+ { name: 'normal', priority: 100, preemptible: false, preempts: [], jobs: { running: 58, pending: 40 } },
44
+ { name: 'priority', priority: 1000, preemptible: false, preempts: ['opportunistic'], maxWall: '12:00:00', jobs: { running: 9, pending: 2 } },
45
+ { name: 'opportunistic', priority: 1, preemptible: true, preempts: [], jobs: { running: 27, pending: 14 } },
46
+ ];
47
+ }
48
+ class MockScheduler {
49
+ name = 'mock';
50
+ jobs = new Map();
51
+ nextJobId = 480217;
52
+ async detect() {
53
+ return true;
54
+ }
55
+ async associations(_user) {
56
+ return {
57
+ account: 'demo-lab',
58
+ partitions: ['cpu', 'cpu-long', 'gpu', 'gpu-a100', 'bigmem', 'lab'],
59
+ qoses: ['normal', 'priority', 'opportunistic'],
60
+ defaultQos: 'normal',
61
+ };
62
+ }
63
+ async load() {
64
+ return { partitions: partitions(), qoses: qoses(), fetchedAt: Date.now() };
65
+ }
66
+ async allowedQos(partition) {
67
+ // The lab-owned queues require an explicit QoS; the open queues accept any.
68
+ if (partition === 'lab' || partition === 'gpu-a100')
69
+ return ['priority', 'opportunistic'];
70
+ return null;
71
+ }
72
+ async estimateStart() {
73
+ // The launcher uses capacity-based availability, not this dry-run estimate.
74
+ return {};
75
+ }
76
+ async submit(scriptPath) {
77
+ const jobId = String(this.nextJobId++);
78
+ const now = Date.now();
79
+ const job = { child: null, startedAt: now, spawnAt: now + QUEUE_DELAY_MS, cancelled: false, exited: false };
80
+ this.jobs.set(jobId, job);
81
+ // After a short "queue wait", run the real client-launch script locally so the
82
+ // allocation registers and becomes a usable online server. Advertise a generic
83
+ // NEBULA_HOST so the (real, local) machine's hostname never surfaces in the UI —
84
+ // the whole allocation reads as a fabricated compute node.
85
+ const nodeName = `node-${String(this.nextJobId % 90 + 10)}`;
86
+ setTimeout(() => {
87
+ if (job.cancelled)
88
+ return;
89
+ const child = (0, child_process_1.spawn)('bash', [scriptPath], {
90
+ stdio: 'ignore',
91
+ env: { ...process.env, NEBULA_HOST: nodeName },
92
+ });
93
+ job.child = child;
94
+ job.node = nodeName;
95
+ child.on('exit', () => { job.exited = true; });
96
+ child.on('error', () => { job.exited = true; });
97
+ }, QUEUE_DELAY_MS);
98
+ return { jobId };
99
+ }
100
+ async query(jobId) {
101
+ const job = this.jobs.get(jobId);
102
+ if (!job)
103
+ return { state: 'unknown', nodes: [] };
104
+ if (job.cancelled)
105
+ return { state: 'cancelled', nodes: [] };
106
+ if (job.exited)
107
+ return { state: 'completed', nodes: [] };
108
+ if (Date.now() < job.spawnAt)
109
+ return { state: 'pending', nodes: [], reason: 'Resources' };
110
+ return { state: 'running', nodes: job.node ? [job.node] : [] };
111
+ }
112
+ async cancel(jobId) {
113
+ const job = this.jobs.get(jobId);
114
+ if (!job)
115
+ return;
116
+ job.cancelled = true;
117
+ if (job.child && job.child.exitCode === null)
118
+ job.child.kill('SIGTERM');
119
+ }
120
+ }
121
+ exports.MockScheduler = MockScheduler;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * SLURM implementation of the Scheduler interface.
3
+ *
4
+ * Everything shells out to the standard SLURM client CLIs (sbatch, squeue,
5
+ * scancel, sinfo, sacctmgr, sacct). The Nebula main server runs on the login
6
+ * node where these are available; detection is capability-based.
7
+ */
8
+ import type { Scheduler, JobSpec, JobStatus, StartEstimate, Associations, QueueLoad } from './types';
9
+ export declare class SlurmScheduler implements Scheduler {
10
+ readonly name = "slurm";
11
+ /** Cache of per-partition required-QoS (site job-submit filters change rarely). */
12
+ private qosCache;
13
+ private static readonly LOAD_TTL_MS;
14
+ private static readonly ASSOC_TTL_MS;
15
+ private loadCache;
16
+ private loadInflight;
17
+ private assocCache;
18
+ private assocInflight;
19
+ detect(): Promise<boolean>;
20
+ associations(user: string): Promise<Associations>;
21
+ private associationsFresh;
22
+ load(): Promise<QueueLoad>;
23
+ private loadFresh;
24
+ allowedQos(partition: string): Promise<string[] | null>;
25
+ estimateStart(spec: JobSpec): Promise<StartEstimate>;
26
+ submit(scriptPath: string): Promise<{
27
+ jobId: string;
28
+ }>;
29
+ query(jobId: string): Promise<JobStatus>;
30
+ cancel(jobId: string): Promise<void>;
31
+ }