@nosana/node 1.1.66 → 1.1.68-rc
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/README.md +10 -0
- package/dist/package.json +1 -1
- package/dist/src/NodeManager/db/index.d.ts +20 -0
- package/dist/src/NodeManager/db/index.js +6 -0
- package/dist/src/NodeManager/index.d.ts +10 -0
- package/dist/src/NodeManager/index.js +44 -10
- package/dist/src/NodeManager/input/OperatorInput.d.ts +22 -0
- package/dist/src/NodeManager/input/OperatorInput.js +43 -0
- package/dist/src/NodeManager/monitoring/log/NodeLog.js +12 -0
- package/dist/src/NodeManager/monitoring/log/console/ConsoleLogger.d.ts +2 -0
- package/dist/src/NodeManager/monitoring/log/console/ConsoleLogger.js +24 -7
- package/dist/src/NodeManager/node/Node.d.ts +10 -0
- package/dist/src/NodeManager/node/Node.js +21 -1
- package/dist/src/NodeManager/node/balance/balanceHandler.js +4 -1
- package/dist/src/NodeManager/node/resource/resourceManager.d.ts +12 -0
- package/dist/src/NodeManager/node/resource/resourceManager.js +28 -0
- package/dist/src/NodeManager/node/resource/volume/volumeManager.js +8 -3
- package/dist/src/NodeManager/provider/Provider.js +6 -0
- package/dist/src/NodeManager/provider/containerOrchestration/docker/index.d.ts +11 -1
- package/dist/src/NodeManager/provider/containerOrchestration/docker/index.js +27 -1
- package/dist/src/NodeManager/provider/containerOrchestration/interface.d.ts +6 -0
- package/dist/src/NodeManager/provider/containerOrchestration/podman/index.d.ts +2 -1
- package/dist/src/NodeManager/provider/containerOrchestration/podman/index.js +3 -2
- package/dist/src/NodeManager/provider/containerOrchestration/selectContainerOrchestration.d.ts +2 -1
- package/dist/src/NodeManager/provider/containerOrchestration/selectContainerOrchestration.js +3 -3
- package/dist/src/NodeManager/repository/NodeRepository.d.ts +11 -1
- package/dist/src/NodeManager/repository/NodeRepository.js +18 -0
- package/dist/src/cli/commands/prune/action.js +1 -1
- package/dist/src/cli/commands/run/action.js +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,6 +83,16 @@ Options:
|
|
|
83
83
|
-h, --help display help for command
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
### Shutting down after the current job
|
|
87
|
+
|
|
88
|
+
Jobs can follow each other back to back, which leaves no clean moment to take
|
|
89
|
+
the machine down. While a job is running, press `s` in the node's terminal to
|
|
90
|
+
toggle between the auto market loop (default) and shutting the node down once
|
|
91
|
+
that job finishes. The running-job status line shows what pressing the key does
|
|
92
|
+
next: `[s] shutdown after job`, or `[s] cancel shutdown after job` once armed.
|
|
93
|
+
The key is ignored outside a job (setup, benchmarks, queueing) and needs an
|
|
94
|
+
interactive terminal (for example `docker run -it`).
|
|
95
|
+
|
|
86
96
|
## Documentation
|
|
87
97
|
|
|
88
98
|
Please [visit our documentation](https://learn.nosana.com/) for a full list of commands and examples.
|
package/dist/package.json
CHANGED
|
@@ -41,6 +41,26 @@ export type Resources = {
|
|
|
41
41
|
volumes: {
|
|
42
42
|
[key: string]: VolumeResource;
|
|
43
43
|
};
|
|
44
|
+
containers: {
|
|
45
|
+
[id: string]: ContainerResource;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* A container this node started, recorded so a later resync can tear down
|
|
50
|
+
* anything it left behind. Written when the container starts and removed once
|
|
51
|
+
* it is deliberately deleted, so an entry surviving into a resync is a
|
|
52
|
+
* container a crash or restart orphaned.
|
|
53
|
+
*
|
|
54
|
+
* Only job-scoped containers are tracked — op containers, their frpc/bridge
|
|
55
|
+
* sidecars and the resource-download containers. The node's persistent proxies
|
|
56
|
+
* opt out (see `RunContainerArgs.track`), so a resync leaves them running.
|
|
57
|
+
* Tracking by id keeps a node to the containers it owns: a second node on the
|
|
58
|
+
* same host has its own db, and a user's own containers were never recorded.
|
|
59
|
+
*/
|
|
60
|
+
export type ContainerResource = {
|
|
61
|
+
/** Container name when one was assigned; resource-download containers have none. */
|
|
62
|
+
name?: string;
|
|
63
|
+
createdAt: Date;
|
|
44
64
|
};
|
|
45
65
|
export type ResourceHistory = {
|
|
46
66
|
lastUsed: Date;
|
|
@@ -6,6 +6,7 @@ const initial_state = {
|
|
|
6
6
|
resources: {
|
|
7
7
|
images: {},
|
|
8
8
|
volumes: {},
|
|
9
|
+
containers: {},
|
|
9
10
|
},
|
|
10
11
|
flows: {},
|
|
11
12
|
info: {
|
|
@@ -44,6 +45,11 @@ export class DB {
|
|
|
44
45
|
if (!this.db.data.resources) {
|
|
45
46
|
this.db.data.resources = initial_state.resources;
|
|
46
47
|
}
|
|
48
|
+
// Added after the other resource maps, so a db written before it exists
|
|
49
|
+
// reaches here without one.
|
|
50
|
+
if (!this.db.data.resources.containers) {
|
|
51
|
+
this.db.data.resources.containers = {};
|
|
52
|
+
}
|
|
47
53
|
if (!this.db.data.flows) {
|
|
48
54
|
this.db.data.flows = initial_state.flows;
|
|
49
55
|
}
|
|
@@ -5,6 +5,7 @@ export default class NodeManager {
|
|
|
5
5
|
private apiStarted;
|
|
6
6
|
private signalled;
|
|
7
7
|
private configLocation;
|
|
8
|
+
private operatorInput;
|
|
8
9
|
/**
|
|
9
10
|
* Seeded from the environment so the loop survives the process being
|
|
10
11
|
* replaced by an update.
|
|
@@ -26,6 +27,15 @@ export default class NodeManager {
|
|
|
26
27
|
/** Asked to stop, which is not a failure to restart on. */
|
|
27
28
|
isStopping(): boolean;
|
|
28
29
|
error(): Promise<void>;
|
|
30
|
+
/** Stop the node for good and leave the process. */
|
|
31
|
+
private shutdown;
|
|
32
|
+
/**
|
|
33
|
+
* Operator keys. `s` toggles shutting down after the current job, and only
|
|
34
|
+
* counts while a real job executes (the running-job line is on screen);
|
|
35
|
+
* setup, benchmarks, queueing and the result upload ignore it. Listening
|
|
36
|
+
* starts per job cycle in `start()`, see there.
|
|
37
|
+
*/
|
|
38
|
+
private handleOperatorInput;
|
|
29
39
|
/**
|
|
30
40
|
* Set up handling for process exit signals
|
|
31
41
|
*/
|
|
@@ -13,11 +13,13 @@ import { checkWSLStatus } from './utils/wslCheck.js';
|
|
|
13
13
|
import { IN_JOB_LOOP_ENV } from '../exitCodes.js';
|
|
14
14
|
import { reportError } from './monitoring/reportError.js';
|
|
15
15
|
import { reportPodmanDiagnostics } from './monitoring/reportPodmanDiagnostics.js';
|
|
16
|
+
import { OperatorInput } from './input/OperatorInput.js';
|
|
16
17
|
export default class NodeManager {
|
|
17
18
|
constructor(options) {
|
|
18
19
|
this.exiting = false;
|
|
19
20
|
this.apiStarted = false;
|
|
20
21
|
this.signalled = false;
|
|
22
|
+
this.operatorInput = new OperatorInput();
|
|
21
23
|
/**
|
|
22
24
|
* Seeded from the environment so the loop survives the process being
|
|
23
25
|
* replaced by an update.
|
|
@@ -32,6 +34,7 @@ export default class NodeManager {
|
|
|
32
34
|
this.apiHandler = this.node.api();
|
|
33
35
|
this.apiHandler.nodeManager = this;
|
|
34
36
|
this.handleProcessExit();
|
|
37
|
+
this.handleOperatorInput();
|
|
35
38
|
}
|
|
36
39
|
async init() {
|
|
37
40
|
/**
|
|
@@ -173,7 +176,21 @@ export default class NodeManager {
|
|
|
173
176
|
*
|
|
174
177
|
* NT: run() is the primary job execution path. monitorMarket() is a background safeguard.
|
|
175
178
|
*/
|
|
179
|
+
/**
|
|
180
|
+
* Listen for operator keys only for this job cycle. Any interactive
|
|
181
|
+
* prompt (registration, the health check question) has run by now and
|
|
182
|
+
* released stdin; a prompt resets raw mode and pauses stdin, which would
|
|
183
|
+
* silently end a listener armed earlier.
|
|
184
|
+
*/
|
|
185
|
+
this.operatorInput.start();
|
|
176
186
|
await Promise.race([this.node.monitorMarket(), this.node.run()]);
|
|
187
|
+
/**
|
|
188
|
+
* The operator asked (with the `s` key) for a clean stop once this job
|
|
189
|
+
* was done, rather than another cycle.
|
|
190
|
+
*/
|
|
191
|
+
if (this.node.shouldShutdownAfterJob()) {
|
|
192
|
+
await this.shutdown();
|
|
193
|
+
}
|
|
177
194
|
/**
|
|
178
195
|
* start
|
|
179
196
|
*
|
|
@@ -211,6 +228,7 @@ export default class NodeManager {
|
|
|
211
228
|
*/
|
|
212
229
|
async clean() {
|
|
213
230
|
this.exiting = true;
|
|
231
|
+
this.operatorInput.stop();
|
|
214
232
|
/**
|
|
215
233
|
* check if the node exists then clean the processes.
|
|
216
234
|
*/
|
|
@@ -258,20 +276,36 @@ export default class NodeManager {
|
|
|
258
276
|
this.exiting = true;
|
|
259
277
|
this.node.exit();
|
|
260
278
|
}
|
|
279
|
+
/** Stop the node for good and leave the process. */
|
|
280
|
+
async shutdown() {
|
|
281
|
+
LogMonitoringRegistry.getInstance().setLoggable(true);
|
|
282
|
+
this.signalled = true;
|
|
283
|
+
if (this.exiting)
|
|
284
|
+
return;
|
|
285
|
+
this.exiting = true;
|
|
286
|
+
this.operatorInput.stop();
|
|
287
|
+
this.node.exit();
|
|
288
|
+
await this.stop();
|
|
289
|
+
process.exit();
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Operator keys. `s` toggles shutting down after the current job, and only
|
|
293
|
+
* counts while a real job executes (the running-job line is on screen);
|
|
294
|
+
* setup, benchmarks, queueing and the result upload ignore it. Listening
|
|
295
|
+
* starts per job cycle in `start()`, see there.
|
|
296
|
+
*/
|
|
297
|
+
handleOperatorInput() {
|
|
298
|
+
this.operatorInput.on('key', (key) => {
|
|
299
|
+
if (key === 's' && this.node.isRunningJob()) {
|
|
300
|
+
this.node.toggleShutdownAfterJob();
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
261
304
|
/**
|
|
262
305
|
* Set up handling for process exit signals
|
|
263
306
|
*/
|
|
264
307
|
handleProcessExit() {
|
|
265
|
-
const exitHandler =
|
|
266
|
-
LogMonitoringRegistry.getInstance().setLoggable(true);
|
|
267
|
-
this.signalled = true;
|
|
268
|
-
if (this.exiting)
|
|
269
|
-
return;
|
|
270
|
-
this.exiting = true;
|
|
271
|
-
this.node.exit();
|
|
272
|
-
await this.stop();
|
|
273
|
-
process.exit();
|
|
274
|
-
};
|
|
308
|
+
const exitHandler = () => this.shutdown();
|
|
275
309
|
process.on("SIGINT", exitHandler); // Handle Ctrl+C
|
|
276
310
|
process.on("SIGTERM", exitHandler); // Handle termination signals
|
|
277
311
|
// log crashes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
4
|
+
/**
|
|
5
|
+
* Owns the terminal's stdin for the node's lifetime and hands single key
|
|
6
|
+
* presses to whoever listens, so nothing else in the node has to touch stdin
|
|
7
|
+
* (the spinners pass `discardStdin: false` for that reason).
|
|
8
|
+
*
|
|
9
|
+
* Stdin is held in raw mode so keys arrive one at a time and are not echoed.
|
|
10
|
+
* Raw mode disables the terminal's own Ctrl+C, so it is turned back into
|
|
11
|
+
* SIGINT here. Without a TTY there is no operator at a keyboard, and nothing
|
|
12
|
+
* is set up.
|
|
13
|
+
*/
|
|
14
|
+
export declare class OperatorInput extends EventEmitter {
|
|
15
|
+
private readonly stdin;
|
|
16
|
+
private listening;
|
|
17
|
+
constructor(stdin?: NodeJS.ReadStream);
|
|
18
|
+
start(): void;
|
|
19
|
+
/** Hands the terminal back in its normal mode. */
|
|
20
|
+
stop(): void;
|
|
21
|
+
private onData;
|
|
22
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
const CTRL_C = 0x03;
|
|
3
|
+
/**
|
|
4
|
+
* Owns the terminal's stdin for the node's lifetime and hands single key
|
|
5
|
+
* presses to whoever listens, so nothing else in the node has to touch stdin
|
|
6
|
+
* (the spinners pass `discardStdin: false` for that reason).
|
|
7
|
+
*
|
|
8
|
+
* Stdin is held in raw mode so keys arrive one at a time and are not echoed.
|
|
9
|
+
* Raw mode disables the terminal's own Ctrl+C, so it is turned back into
|
|
10
|
+
* SIGINT here. Without a TTY there is no operator at a keyboard, and nothing
|
|
11
|
+
* is set up.
|
|
12
|
+
*/
|
|
13
|
+
export class OperatorInput extends EventEmitter {
|
|
14
|
+
constructor(stdin = process.stdin) {
|
|
15
|
+
super();
|
|
16
|
+
this.stdin = stdin;
|
|
17
|
+
this.listening = false;
|
|
18
|
+
this.onData = (chunk) => {
|
|
19
|
+
if (chunk[0] === CTRL_C) {
|
|
20
|
+
process.emit('SIGINT');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
this.emit('key', chunk.toString());
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
start() {
|
|
27
|
+
if (this.listening || !this.stdin.isTTY)
|
|
28
|
+
return;
|
|
29
|
+
this.listening = true;
|
|
30
|
+
this.stdin.setRawMode(true);
|
|
31
|
+
this.stdin.on('data', this.onData);
|
|
32
|
+
this.stdin.resume();
|
|
33
|
+
}
|
|
34
|
+
/** Hands the terminal back in its normal mode. */
|
|
35
|
+
stop() {
|
|
36
|
+
if (!this.listening)
|
|
37
|
+
return;
|
|
38
|
+
this.listening = false;
|
|
39
|
+
this.stdin.off('data', this.onData);
|
|
40
|
+
this.stdin.pause();
|
|
41
|
+
this.stdin.setRawMode(false);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -132,6 +132,18 @@ class NodeLog {
|
|
|
132
132
|
if (data.class === 'BasicNode' && data.method === 'exit') {
|
|
133
133
|
this.handleExit(data);
|
|
134
134
|
}
|
|
135
|
+
if (data.class === 'BasicNode' &&
|
|
136
|
+
data.method === 'toggleShutdownAfterJob' &&
|
|
137
|
+
data.type === 'return') {
|
|
138
|
+
this.addLog({
|
|
139
|
+
method: `${data.class}.${data.method}`,
|
|
140
|
+
job: this.job,
|
|
141
|
+
log: '',
|
|
142
|
+
timestamp: Date.now(),
|
|
143
|
+
type: 'update',
|
|
144
|
+
payload: { enabled: data.result },
|
|
145
|
+
});
|
|
146
|
+
}
|
|
135
147
|
if (data.class === 'BasicNode' &&
|
|
136
148
|
(data.method === 'restartDelay' || data.method === 'delay')) {
|
|
137
149
|
this.handleRestart(data);
|
|
@@ -15,6 +15,8 @@ export declare class ConsoleLogger implements LogObserver {
|
|
|
15
15
|
private taskManagerStartMethod;
|
|
16
16
|
private benchmarkProgress;
|
|
17
17
|
private running;
|
|
18
|
+
private shutdownAfterJob;
|
|
19
|
+
private renderTaskManagerBar;
|
|
18
20
|
private expiry;
|
|
19
21
|
private taskManagerRunInterval;
|
|
20
22
|
spinner: Ora;
|
|
@@ -25,6 +25,7 @@ export class ConsoleLogger {
|
|
|
25
25
|
this.taskManagerStartMethod = 'TaskManager.start';
|
|
26
26
|
this.benchmarkProgress = { completed: 0, total: 0 };
|
|
27
27
|
this.running = false;
|
|
28
|
+
this.shutdownAfterJob = false;
|
|
28
29
|
}
|
|
29
30
|
addObserver() {
|
|
30
31
|
log().addObserver(this);
|
|
@@ -34,6 +35,15 @@ export class ConsoleLogger {
|
|
|
34
35
|
}
|
|
35
36
|
update(log, isNode = true) {
|
|
36
37
|
const renderBase = `${chalk.bgGreenBright.black.bold(this.taskManagerLabel)} ${chalk.reset('')}`;
|
|
38
|
+
// Handled first so a toggle is never swallowed by the job's quiet window.
|
|
39
|
+
// The running-job line carries the command plate, so a flip re-renders it.
|
|
40
|
+
if (log.method === 'BasicNode.toggleShutdownAfterJob') {
|
|
41
|
+
this.shutdownAfterJob = !!log.payload?.enabled;
|
|
42
|
+
if (this.taskManagerActive && this.renderTaskManagerBar) {
|
|
43
|
+
this.spinner.text = this.renderTaskManagerBar();
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
37
47
|
if (!this.kill && log.type == 'kill-process') {
|
|
38
48
|
if (this.pending) {
|
|
39
49
|
this.spinner.stop();
|
|
@@ -50,7 +60,7 @@ export class ConsoleLogger {
|
|
|
50
60
|
if (isNode && this.taskManagerActive) {
|
|
51
61
|
this.spinner.succeed(renderBase + ' Exited');
|
|
52
62
|
}
|
|
53
|
-
this.spinner = ora(log.log).start();
|
|
63
|
+
this.spinner = ora({ text: log.log, discardStdin: false }).start();
|
|
54
64
|
}
|
|
55
65
|
if (this.kill) {
|
|
56
66
|
if (log.type == 'kill-success') {
|
|
@@ -96,14 +106,18 @@ export class ConsoleLogger {
|
|
|
96
106
|
const isBenchmark = log.method === 'BenchmarkTaskManager.start';
|
|
97
107
|
if (isBenchmark)
|
|
98
108
|
this.benchmarkProgress = { completed: 0, total: 0 };
|
|
109
|
+
// Command plate: what pressing the key does next, which also reads as the mode.
|
|
110
|
+
const modePlate = () => this.shutdownAfterJob
|
|
111
|
+
? `${chalk.red('[s]')} ${chalk.red.bold('cancel')} ${chalk.red('shutdown after job')}`
|
|
112
|
+
: chalk.dim('[s] shutdown after job');
|
|
99
113
|
const renderBar = (duration = 0) => isBenchmark
|
|
100
114
|
? `${activeRenderBase} ${chalk.bgYellow.black.bold(` Ops: ${this.benchmarkProgress.completed}/${this.benchmarkProgress.total} `)} ${chalk.reset('')}`
|
|
101
|
-
: `${activeRenderBase} ${chalk.bgYellow.black.bold(` ⏱ Duration: ${formatTime(duration)} `)} ${chalk.reset('')} ${chalk.bgBlue.black.bold(` ⚡ Max Duration: ${formatTime(this.expiry ?? 0)} `)}`;
|
|
102
|
-
this.
|
|
115
|
+
: `${activeRenderBase} ${chalk.bgYellow.black.bold(` ⏱ Duration: ${formatTime(duration)} `)} ${chalk.reset('')} ${chalk.bgBlue.black.bold(` ⚡ Max Duration: ${formatTime(this.expiry ?? 0)} `)} ${modePlate()}`;
|
|
116
|
+
this.renderTaskManagerBar = () => renderBar((Date.now() - startTime) / 1000);
|
|
117
|
+
this.spinner = ora({ text: renderBar(0), discardStdin: false }).start();
|
|
103
118
|
this.taskManagerRunInterval = setInterval(() => {
|
|
104
119
|
if (this.taskManagerActive) {
|
|
105
|
-
|
|
106
|
-
this.spinner.text = renderBar(duration);
|
|
120
|
+
this.spinner.text = this.renderTaskManagerBar();
|
|
107
121
|
}
|
|
108
122
|
else {
|
|
109
123
|
clearInterval(this.taskManagerRunInterval);
|
|
@@ -141,7 +155,10 @@ export class ConsoleLogger {
|
|
|
141
155
|
log.type == 'process') {
|
|
142
156
|
this.benchmarking = true;
|
|
143
157
|
this.pending = true;
|
|
144
|
-
this.spinner = ora(
|
|
158
|
+
this.spinner = ora({
|
|
159
|
+
text: chalk.green(`${chalk.bgGreen.bold(' Checking Specs ')}`),
|
|
160
|
+
discardStdin: false,
|
|
161
|
+
}).start();
|
|
145
162
|
return;
|
|
146
163
|
}
|
|
147
164
|
if (this.benchmarking) {
|
|
@@ -317,7 +334,7 @@ export class ConsoleLogger {
|
|
|
317
334
|
if (log.pending?.isPending) {
|
|
318
335
|
this.pending = true;
|
|
319
336
|
this.expecting = log.pending?.expecting;
|
|
320
|
-
this.spinner = ora(log.log).start();
|
|
337
|
+
this.spinner = ora({ text: log.log, discardStdin: false }).start();
|
|
321
338
|
}
|
|
322
339
|
else {
|
|
323
340
|
console.log(log.log);
|
|
@@ -16,6 +16,9 @@ export declare class BasicNode {
|
|
|
16
16
|
private provider;
|
|
17
17
|
private containerOrchestration;
|
|
18
18
|
private exiting;
|
|
19
|
+
private shutdownAfterJob;
|
|
20
|
+
/** Set by `run()` while a real job executes; the benchmark never sets it. */
|
|
21
|
+
private jobRunning;
|
|
19
22
|
private sdk;
|
|
20
23
|
constructor(options: {
|
|
21
24
|
[key: string]: any;
|
|
@@ -37,6 +40,13 @@ export declare class BasicNode {
|
|
|
37
40
|
monitorMarket(): Promise<unknown>;
|
|
38
41
|
maintenance(): Promise<void>;
|
|
39
42
|
exit(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Flip whether the node leaves the market loop once the current job
|
|
45
|
+
* finishes, instead of re-queueing for the next one.
|
|
46
|
+
*/
|
|
47
|
+
toggleShutdownAfterJob(): boolean;
|
|
48
|
+
shouldShutdownAfterJob(): boolean;
|
|
49
|
+
isRunningJob(): boolean;
|
|
40
50
|
conclude(): Promise<void>;
|
|
41
51
|
restartDelay(time: number): Promise<void>;
|
|
42
52
|
}
|
|
@@ -24,10 +24,13 @@ import { StopReasons } from './task/TaskManager.js';
|
|
|
24
24
|
export class BasicNode {
|
|
25
25
|
constructor(options) {
|
|
26
26
|
this.exiting = false;
|
|
27
|
+
this.shutdownAfterJob = false;
|
|
28
|
+
/** Set by `run()` while a real job executes; the benchmark never sets it. */
|
|
29
|
+
this.jobRunning = false;
|
|
27
30
|
this.sdk = getSDK();
|
|
28
31
|
const db = new DB(options.config).db;
|
|
29
32
|
this.repository = new NodeRepository(db);
|
|
30
|
-
this.containerOrchestration = selectContainerOrchestrationProvider(options.provider, options.podman, options.gpu, options.trustedExecutionRuntime);
|
|
33
|
+
this.containerOrchestration = selectContainerOrchestrationProvider(options.provider, options.podman, options.gpu, options.trustedExecutionRuntime, this.repository);
|
|
31
34
|
this.resourceManager = new ResourceManager(this.containerOrchestration, this.repository);
|
|
32
35
|
this.provider = new Provider(this.containerOrchestration, this.repository, this.resourceManager);
|
|
33
36
|
this.apiHandler = new ApiHandler(this.sdk, this.repository, this.provider, options.port);
|
|
@@ -189,6 +192,7 @@ export class BasicNode {
|
|
|
189
192
|
*/
|
|
190
193
|
const shouldStart = await this.jobHandler.start(job);
|
|
191
194
|
if (shouldStart) {
|
|
195
|
+
this.jobRunning = true;
|
|
192
196
|
/**
|
|
193
197
|
* Run the flow asynchronously and handle errors via a Promise.
|
|
194
198
|
* This lets us run the job in the background (async) and still get an error in this main process.
|
|
@@ -199,6 +203,7 @@ export class BasicNode {
|
|
|
199
203
|
*/
|
|
200
204
|
// await this.expiryHandler.waitUntilExpired();
|
|
201
205
|
resolvedStatus = await stopReason;
|
|
206
|
+
this.jobRunning = false;
|
|
202
207
|
}
|
|
203
208
|
}
|
|
204
209
|
if (!this.exiting) {
|
|
@@ -212,6 +217,7 @@ export class BasicNode {
|
|
|
212
217
|
}
|
|
213
218
|
}
|
|
214
219
|
catch (error) {
|
|
220
|
+
this.jobRunning = false;
|
|
215
221
|
// Reject the Promise if any errors occur
|
|
216
222
|
reject(error);
|
|
217
223
|
}
|
|
@@ -287,6 +293,20 @@ export class BasicNode {
|
|
|
287
293
|
exit() {
|
|
288
294
|
this.exiting = true;
|
|
289
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Flip whether the node leaves the market loop once the current job
|
|
298
|
+
* finishes, instead of re-queueing for the next one.
|
|
299
|
+
*/
|
|
300
|
+
toggleShutdownAfterJob() {
|
|
301
|
+
this.shutdownAfterJob = !this.shutdownAfterJob;
|
|
302
|
+
return this.shutdownAfterJob;
|
|
303
|
+
}
|
|
304
|
+
shouldShutdownAfterJob() {
|
|
305
|
+
return this.shutdownAfterJob;
|
|
306
|
+
}
|
|
307
|
+
isRunningJob() {
|
|
308
|
+
return this.jobRunning;
|
|
309
|
+
}
|
|
290
310
|
async conclude() {
|
|
291
311
|
const run = this.runHandler.getRun();
|
|
292
312
|
if (run) {
|
|
@@ -41,7 +41,10 @@ export class BalanceHandler {
|
|
|
41
41
|
if (!wait) {
|
|
42
42
|
throw new Error(insufficentSolMessage);
|
|
43
43
|
}
|
|
44
|
-
const spinner = ora(
|
|
44
|
+
const spinner = ora({
|
|
45
|
+
text: chalk.yellow(insufficentSolMessage),
|
|
46
|
+
discardStdin: false,
|
|
47
|
+
}).start();
|
|
45
48
|
await this.waitForSufficentSol();
|
|
46
49
|
spinner.succeed();
|
|
47
50
|
}
|
|
@@ -43,6 +43,18 @@ export declare class ResourceManager {
|
|
|
43
43
|
volumes: VolumeManager;
|
|
44
44
|
constructor(containerOrchestration: ContainerOrchestrationInterface, repository: NodeRepository);
|
|
45
45
|
resyncResourcesDB(): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Tears down containers this node started that are still around at resync.
|
|
48
|
+
*
|
|
49
|
+
* A resync runs between jobs with nothing of ours meant to be running, so any
|
|
50
|
+
* tracked container is one a crash or restart orphaned — a dead resource
|
|
51
|
+
* download still holding its volume, or a job container never cleaned up.
|
|
52
|
+
* Each is stopped, removed and untracked. The record is only ever written for
|
|
53
|
+
* job-scoped containers, so the persistent API proxy is left untouched, and
|
|
54
|
+
* because entries are keyed by containers this node created, a co-tenant
|
|
55
|
+
* node's containers and a user's own are never in scope.
|
|
56
|
+
*/
|
|
57
|
+
resyncContainersDB(): Promise<void>;
|
|
46
58
|
fetchMarketRequiredResources(market: Market | string): Promise<void>;
|
|
47
59
|
prune(): Promise<void>;
|
|
48
60
|
getResourceVolumes(resources: Resource[], controller: AbortController, job?: JobContext, project?: string): Promise<{
|
|
@@ -48,12 +48,40 @@ export class ResourceManager {
|
|
|
48
48
|
this.volumes = new VolumeManager(containerOrchestration, repository);
|
|
49
49
|
}
|
|
50
50
|
async resyncResourcesDB() {
|
|
51
|
+
await this.resyncContainersDB();
|
|
51
52
|
await this.images.resyncImagesDB();
|
|
52
53
|
await this.volumes.resyncResourcesDB();
|
|
53
54
|
if (this.required_market) {
|
|
54
55
|
await this.fetchMarketRequiredResources(this.required_market);
|
|
55
56
|
}
|
|
56
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Tears down containers this node started that are still around at resync.
|
|
60
|
+
*
|
|
61
|
+
* A resync runs between jobs with nothing of ours meant to be running, so any
|
|
62
|
+
* tracked container is one a crash or restart orphaned — a dead resource
|
|
63
|
+
* download still holding its volume, or a job container never cleaned up.
|
|
64
|
+
* Each is stopped, removed and untracked. The record is only ever written for
|
|
65
|
+
* job-scoped containers, so the persistent API proxy is left untouched, and
|
|
66
|
+
* because entries are keyed by containers this node created, a co-tenant
|
|
67
|
+
* node's containers and a user's own are never in scope.
|
|
68
|
+
*/
|
|
69
|
+
async resyncContainersDB() {
|
|
70
|
+
for (const [id, { name }] of Object.entries(this.repository.getContainersResources())) {
|
|
71
|
+
try {
|
|
72
|
+
if (await this.containerOrchestration.doesContainerExist(id)) {
|
|
73
|
+
await this.containerOrchestration.stopAndDeleteContainer(id);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
// A container that will not go stays tracked for the next resync rather
|
|
78
|
+
// than being forgotten; the volume it holds is reclaimed once it does.
|
|
79
|
+
console.error(`Failed to clean up leftover container ${name ?? id}: ${err.message}`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
this.repository.untrackContainer(id);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
57
85
|
async fetchMarketRequiredResources(market) {
|
|
58
86
|
this.required_market = typeof market === 'string' ? market : market.address.toString();
|
|
59
87
|
try {
|
|
@@ -324,8 +324,9 @@ export class VolumeManager {
|
|
|
324
324
|
}
|
|
325
325
|
catch (error) {
|
|
326
326
|
this.progressBarReporter.stop(`${syncing ? 'Syncing' : 'Downloading'} resource ${name} stopped`);
|
|
327
|
-
// Force-removal also stops a download an abort left running
|
|
328
|
-
|
|
327
|
+
// Force-removal also stops a download an abort left running, and
|
|
328
|
+
// untracks it so the next resync does not chase a container already gone.
|
|
329
|
+
await this.containerOrchestration.stopAndDeleteContainer(container.id);
|
|
329
330
|
// The volume goes with the failed download, and its entry goes with the
|
|
330
331
|
// volume. If the deletion itself fails, the entry must stay: a pending
|
|
331
332
|
// entry over a leftover volume is resynced or expired later, which is
|
|
@@ -408,7 +409,11 @@ export class VolumeManager {
|
|
|
408
409
|
this.repository.deleteVolumeResource(resource);
|
|
409
410
|
}
|
|
410
411
|
catch (err) {
|
|
411
|
-
|
|
412
|
+
// The entry is kept so the next resync retries. A leftover container
|
|
413
|
+
// holding the volume is the usual cause, which the container resync
|
|
414
|
+
// that runs first now clears; log rather than swallow so a removal
|
|
415
|
+
// that still fails is visible instead of silently leaking disk.
|
|
416
|
+
console.error(`Failed to remove expired volume ${volume}: ${err.message}`);
|
|
412
417
|
}
|
|
413
418
|
}
|
|
414
419
|
}
|
|
@@ -68,6 +68,9 @@ export class Provider {
|
|
|
68
68
|
if (!doesFrpcExist) {
|
|
69
69
|
await this.containerOrchestration.runFlowContainer(this.frpcImage, {
|
|
70
70
|
name: 'frpc-api-' + address,
|
|
71
|
+
// The node's persistent API proxy, not a job container: a resync
|
|
72
|
+
// between jobs must leave it running rather than reap it.
|
|
73
|
+
track: false,
|
|
71
74
|
cmd: ['-c', '/etc/frp/frpc.toml'],
|
|
72
75
|
networks,
|
|
73
76
|
requires_network_mode: true,
|
|
@@ -91,6 +94,9 @@ export class Provider {
|
|
|
91
94
|
await this.containerOrchestration.stopAndDeleteContainer(frpc_name);
|
|
92
95
|
await this.containerOrchestration.runFlowContainer(this.frpcImage, {
|
|
93
96
|
name: 'frpc-api-' + address,
|
|
97
|
+
// The node's persistent API proxy, not a job container: a resync
|
|
98
|
+
// between jobs must leave it running rather than reap it.
|
|
99
|
+
track: false,
|
|
94
100
|
cmd: ['-c', '/etc/frp/frpc.toml'],
|
|
95
101
|
networks,
|
|
96
102
|
requires_network_mode: true,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Container, ContainerCreateOptions, ImageInfo, Image, Volume, VolumeCreateResponse, VolumeInspectInfo, ContainerInspectInfo } from 'dockerode';
|
|
2
2
|
import { DockerAuth } from '@nosana/sdk';
|
|
3
|
+
import type { NodeRepository } from '../../../repository/NodeRepository.js';
|
|
3
4
|
import { DockerExtended } from './dockerExtended/index.js';
|
|
4
5
|
import { ContainerOrchestrationInterface, RunContainerArgs } from '../interface.js';
|
|
5
6
|
import { ReturnedStatus } from '../../types.js';
|
|
6
7
|
export declare class DockerContainerOrchestration implements ContainerOrchestrationInterface {
|
|
8
|
+
protected repository?: NodeRepository | undefined;
|
|
7
9
|
docker: DockerExtended;
|
|
8
10
|
host: string;
|
|
9
11
|
port: string;
|
|
@@ -12,7 +14,7 @@ export declare class DockerContainerOrchestration implements ContainerOrchestrat
|
|
|
12
14
|
gpu: string;
|
|
13
15
|
teeRuntime?: string;
|
|
14
16
|
listeners: Map<string, (() => Promise<void>)[]>;
|
|
15
|
-
constructor(server: string, gpu: string, teeRuntime?: string);
|
|
17
|
+
constructor(server: string, gpu: string, teeRuntime?: string, repository?: NodeRepository | undefined);
|
|
16
18
|
checkCustomRuntimeAvailability(runtime: string): Promise<boolean>;
|
|
17
19
|
getContainerByName(name: string): Promise<Container | undefined>;
|
|
18
20
|
getContainersByName(names: string[]): Promise<Container[]>;
|
|
@@ -38,6 +40,14 @@ export declare class DockerContainerOrchestration implements ContainerOrchestrat
|
|
|
38
40
|
healthy(): Promise<ReturnedStatus>;
|
|
39
41
|
getProtocol(): string;
|
|
40
42
|
setupContainerAbortListener(containerId: string, controller: AbortController): void;
|
|
43
|
+
/**
|
|
44
|
+
* Records a container we started so a resync can reap it if a crash orphans
|
|
45
|
+
* it. Every started container funnels through here, so a new call site is
|
|
46
|
+
* tracked without the caller doing anything; the persistent proxies opt out
|
|
47
|
+
* with `track: false`. Keyed by container id, matching what `inspect().Id`
|
|
48
|
+
* reports at teardown, so the same key untracks it.
|
|
49
|
+
*/
|
|
50
|
+
protected recordContainer(id: string, name?: string, track?: boolean): void;
|
|
41
51
|
runContainer(args: ContainerCreateOptions, controller?: AbortController): Promise<Container>;
|
|
42
52
|
runFlowContainer(image: string, args: RunContainerArgs): Promise<Container>;
|
|
43
53
|
stopContainer(id: string, controller?: AbortController): Promise<void>;
|
|
@@ -5,7 +5,8 @@ import { checkDeprecationDeadline } from './utils/deadline.js';
|
|
|
5
5
|
import { createDockerRunOptions } from './createDockerRunOptions.js';
|
|
6
6
|
import { liveAbortSignal } from '../../../utils/liveAbortSignal.js';
|
|
7
7
|
export class DockerContainerOrchestration {
|
|
8
|
-
constructor(server, gpu, teeRuntime) {
|
|
8
|
+
constructor(server, gpu, teeRuntime, repository) {
|
|
9
|
+
this.repository = repository;
|
|
9
10
|
this.name = 'docker';
|
|
10
11
|
this.gpu = 'all';
|
|
11
12
|
this.listeners = new Map();
|
|
@@ -244,6 +245,18 @@ export class DockerContainerOrchestration {
|
|
|
244
245
|
current.push(stopFunction);
|
|
245
246
|
this.listeners.set(containerId, current);
|
|
246
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Records a container we started so a resync can reap it if a crash orphans
|
|
250
|
+
* it. Every started container funnels through here, so a new call site is
|
|
251
|
+
* tracked without the caller doing anything; the persistent proxies opt out
|
|
252
|
+
* with `track: false`. Keyed by container id, matching what `inspect().Id`
|
|
253
|
+
* reports at teardown, so the same key untracks it.
|
|
254
|
+
*/
|
|
255
|
+
recordContainer(id, name, track = true) {
|
|
256
|
+
if (!track)
|
|
257
|
+
return;
|
|
258
|
+
this.repository?.trackContainer(id, { name });
|
|
259
|
+
}
|
|
247
260
|
async runContainer(args, controller) {
|
|
248
261
|
try {
|
|
249
262
|
if (controller?.signal.aborted) {
|
|
@@ -253,6 +266,7 @@ export class DockerContainerOrchestration {
|
|
|
253
266
|
...args,
|
|
254
267
|
abortSignal: liveAbortSignal(controller?.signal),
|
|
255
268
|
});
|
|
269
|
+
this.recordContainer(container.id, args.name);
|
|
256
270
|
await container.start();
|
|
257
271
|
if (controller) {
|
|
258
272
|
this.setupContainerAbortListener(container.id, controller);
|
|
@@ -271,6 +285,7 @@ export class DockerContainerOrchestration {
|
|
|
271
285
|
const container = await this.docker.createContainer({
|
|
272
286
|
...createDockerRunOptions(image, args, this.gpu),
|
|
273
287
|
});
|
|
288
|
+
this.recordContainer(container.id, args.name, args.track);
|
|
274
289
|
try {
|
|
275
290
|
if (args.beforeStart)
|
|
276
291
|
await args.beforeStart(container);
|
|
@@ -321,9 +336,20 @@ export class DockerContainerOrchestration {
|
|
|
321
336
|
v: true,
|
|
322
337
|
abortSignal: liveAbortSignal(controller?.signal),
|
|
323
338
|
});
|
|
339
|
+
// Removed, so forget it. Keyed by the resolved id, matching what
|
|
340
|
+
// `recordContainer` wrote.
|
|
341
|
+
this.repository?.untrackContainer(info.Id);
|
|
324
342
|
return info;
|
|
325
343
|
}
|
|
326
344
|
catch (error) {
|
|
345
|
+
// A 404 means the container is already gone — Docker auto-removed it on
|
|
346
|
+
// exit, say, or a prior teardown already took it — so it is removed as
|
|
347
|
+
// far as the caller is concerned and its tracking entry must not linger.
|
|
348
|
+
// Any other failure leaves the container in place, so it stays tracked
|
|
349
|
+
// for the next resync to retry rather than being forgotten while alive.
|
|
350
|
+
if (error.statusCode === 404) {
|
|
351
|
+
this.repository?.untrackContainer(id);
|
|
352
|
+
}
|
|
327
353
|
return undefined;
|
|
328
354
|
}
|
|
329
355
|
}
|
|
@@ -25,6 +25,12 @@ export type RunContainerArgs = {
|
|
|
25
25
|
aliases?: string[] | undefined;
|
|
26
26
|
runtime?: string;
|
|
27
27
|
bind_network_to_container?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Whether a resync may reap this container. Job containers leave it unset and
|
|
30
|
+
* are tracked; the node's persistent proxies pass `false` so they survive a
|
|
31
|
+
* resync that runs between jobs.
|
|
32
|
+
*/
|
|
33
|
+
track?: boolean;
|
|
28
34
|
security?: {
|
|
29
35
|
capDrop?: string[];
|
|
30
36
|
pidsLimit?: number;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Container } from 'dockerode';
|
|
2
|
+
import type { NodeRepository } from '../../../repository/NodeRepository.js';
|
|
2
3
|
import { DockerContainerOrchestration } from '../docker/index.js';
|
|
3
4
|
import { RunContainerArgs } from '../interface.js';
|
|
4
5
|
import { RequestInit, Response } from 'undici';
|
|
5
6
|
export declare class PodmanContainerOrchestration extends DockerContainerOrchestration {
|
|
6
7
|
private api;
|
|
7
8
|
name: string;
|
|
8
|
-
constructor(server: string, gpu: string, teeRuntime?: string);
|
|
9
|
+
constructor(server: string, gpu: string, teeRuntime?: string, repository?: NodeRepository);
|
|
9
10
|
libPodAPICall(path: string, options: RequestInit): Promise<Response>;
|
|
10
11
|
createNetwork(_name: string, controller?: AbortController): Promise<void>;
|
|
11
12
|
runFlowContainer(image: string, args: RunContainerArgs): Promise<Container>;
|
|
@@ -3,8 +3,8 @@ import { createPodmanRunOptions } from './utils/createPodmanRunOptions.js';
|
|
|
3
3
|
import { fetch, Agent } from 'undici';
|
|
4
4
|
import { liveAbortSignal } from '../../../utils/liveAbortSignal.js';
|
|
5
5
|
export class PodmanContainerOrchestration extends DockerContainerOrchestration {
|
|
6
|
-
constructor(server, gpu, teeRuntime) {
|
|
7
|
-
super(server, gpu, teeRuntime);
|
|
6
|
+
constructor(server, gpu, teeRuntime, repository) {
|
|
7
|
+
super(server, gpu, teeRuntime, repository);
|
|
8
8
|
this.name = 'podman';
|
|
9
9
|
if (this.protocol === 'socket') {
|
|
10
10
|
this.api = `http://localhost/v4.5.0/libpod`;
|
|
@@ -60,6 +60,7 @@ export class PodmanContainerOrchestration extends DockerContainerOrchestration {
|
|
|
60
60
|
if (create.status === 201) {
|
|
61
61
|
const createResult = await create.json();
|
|
62
62
|
const container = this.docker.getContainer(createResult.Id);
|
|
63
|
+
this.recordContainer(container.id, args.name, args.track);
|
|
63
64
|
try {
|
|
64
65
|
if (args.beforeStart)
|
|
65
66
|
await args.beforeStart(container);
|
package/dist/src/NodeManager/provider/containerOrchestration/selectContainerOrchestration.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
import type { NodeRepository } from '../../repository/NodeRepository.js';
|
|
1
2
|
import type { ContainerOrchestrationInterface } from './interface.js';
|
|
2
|
-
export declare function selectContainerOrchestrationProvider(provider: string, url: string, gpu: string, teeRuntime: string | undefined): ContainerOrchestrationInterface;
|
|
3
|
+
export declare function selectContainerOrchestrationProvider(provider: string, url: string, gpu: string, teeRuntime: string | undefined, repository: NodeRepository): ContainerOrchestrationInterface;
|
package/dist/src/NodeManager/provider/containerOrchestration/selectContainerOrchestration.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { DockerContainerOrchestration } from './docker/index.js';
|
|
2
2
|
import { PodmanContainerOrchestration } from './podman/index.js';
|
|
3
|
-
export function selectContainerOrchestrationProvider(provider, url, gpu, teeRuntime) {
|
|
3
|
+
export function selectContainerOrchestrationProvider(provider, url, gpu, teeRuntime, repository) {
|
|
4
4
|
if (provider !== 'docker' && teeRuntime === 'SEV-SNP') {
|
|
5
5
|
throw new Error('Custom runtimes are only supported with the Docker provider.');
|
|
6
6
|
}
|
|
7
7
|
switch (provider) {
|
|
8
8
|
case 'podman':
|
|
9
|
-
return new PodmanContainerOrchestration(url, gpu, teeRuntime);
|
|
9
|
+
return new PodmanContainerOrchestration(url, gpu, teeRuntime, repository);
|
|
10
10
|
case 'docker':
|
|
11
11
|
default:
|
|
12
|
-
return new DockerContainerOrchestration(url, gpu, teeRuntime);
|
|
12
|
+
return new DockerContainerOrchestration(url, gpu, teeRuntime, repository);
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { LowSync } from 'lowdb';
|
|
2
2
|
import type { Flow, OpState, FlowState } from '@nosana/sdk';
|
|
3
|
-
import type { NodeDb, ResourceHistory, SshKeyGrant, VolumeResource } from '../db/index.js';
|
|
3
|
+
import type { ContainerResource, NodeDb, ResourceHistory, SshKeyGrant, VolumeResource } from '../db/index.js';
|
|
4
4
|
export declare class NodeRepository {
|
|
5
5
|
private db;
|
|
6
6
|
constructor(db: LowSync<NodeDb>);
|
|
@@ -67,6 +67,16 @@ export declare class NodeRepository {
|
|
|
67
67
|
createVolumeResource(volume: string, fields: VolumeResource): void;
|
|
68
68
|
updateVolumeResource(volume: string, updatedFields: Partial<VolumeResource> | VolumeResource): void;
|
|
69
69
|
deleteVolumeResource(volume: string): void;
|
|
70
|
+
getContainersResources(): {
|
|
71
|
+
[id: string]: ContainerResource;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Record a container this node started, so a later resync can remove it if a
|
|
75
|
+
* crash leaves it behind. Keyed by the runtime's container id.
|
|
76
|
+
*/
|
|
77
|
+
trackContainer(id: string, fields: Omit<ContainerResource, 'createdAt'>): void;
|
|
78
|
+
/** Drop a container's record once it has been deliberately removed. */
|
|
79
|
+
untrackContainer(id: string): void;
|
|
70
80
|
displayLog(log: string): string;
|
|
71
81
|
getFlowOperationName(id: string, index: number): string;
|
|
72
82
|
}
|
|
@@ -260,6 +260,24 @@ export class NodeRepository {
|
|
|
260
260
|
delete this.db.data.resources.volumes[volume];
|
|
261
261
|
this.persist();
|
|
262
262
|
}
|
|
263
|
+
getContainersResources() {
|
|
264
|
+
return this.db.data.resources.containers;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Record a container this node started, so a later resync can remove it if a
|
|
268
|
+
* crash leaves it behind. Keyed by the runtime's container id.
|
|
269
|
+
*/
|
|
270
|
+
trackContainer(id, fields) {
|
|
271
|
+
this.db.data.resources.containers[id] = { ...fields, createdAt: new Date() };
|
|
272
|
+
this.persist();
|
|
273
|
+
}
|
|
274
|
+
/** Drop a container's record once it has been deliberately removed. */
|
|
275
|
+
untrackContainer(id) {
|
|
276
|
+
if (!this.db.data.resources.containers[id])
|
|
277
|
+
return;
|
|
278
|
+
delete this.db.data.resources.containers[id];
|
|
279
|
+
this.persist();
|
|
280
|
+
}
|
|
263
281
|
displayLog(log) {
|
|
264
282
|
return log;
|
|
265
283
|
}
|
|
@@ -8,7 +8,7 @@ export async function pruneResources(options) {
|
|
|
8
8
|
try {
|
|
9
9
|
const db = new DB(options.config).db;
|
|
10
10
|
const repository = new NodeRepository(db);
|
|
11
|
-
const containerOrchestration = selectContainerOrchestrationProvider(options.provider, options.podman, options.gpu, undefined);
|
|
11
|
+
const containerOrchestration = selectContainerOrchestrationProvider(options.provider, options.podman, options.gpu, undefined, repository);
|
|
12
12
|
const resourceManager = new ResourceManager(containerOrchestration, repository);
|
|
13
13
|
await resourceManager.prune();
|
|
14
14
|
console.log('Finished pruning system');
|
|
@@ -29,7 +29,7 @@ export async function runJob(jobDefinitionFile, options) {
|
|
|
29
29
|
log();
|
|
30
30
|
const db = new DB(options.config).db;
|
|
31
31
|
const repository = createLoggingProxy(new NodeRepository(db));
|
|
32
|
-
const containerOrchestration = selectContainerOrchestrationProvider(options.provider, options.podman, options.gpu, options.trustedExecutionRuntime);
|
|
32
|
+
const containerOrchestration = selectContainerOrchestrationProvider(options.provider, options.podman, options.gpu, options.trustedExecutionRuntime, repository);
|
|
33
33
|
const resourceManager = new ResourceManager(containerOrchestration, repository);
|
|
34
34
|
const provider = new Provider(containerOrchestration, repository, resourceManager);
|
|
35
35
|
const logger = new ConsoleLogger(false);
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nosana/node",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.68-rc",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@nosana/node",
|
|
9
|
-
"version": "1.1.
|
|
9
|
+
"version": "1.1.68-rc",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@coral-xyz/anchor": "^0.28.1-beta.1",
|