@nosana/node 1.1.67-rc → 1.1.68

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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.67-rc",
3
+ "version": "1.1.68",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "nosana-node": "./dist/src/index.js"
@@ -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 = async () => {
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.spinner = ora(renderBar(0)).start();
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
- const duration = (Date.now() - startTime) / 1000;
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(chalk.green(`${chalk.bgGreen.bold(' Checking Specs ')}`)).start();
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,6 +24,9 @@ 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);
@@ -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(chalk.yellow(insufficentSolMessage)).start();
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
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.67-rc",
3
+ "version": "1.1.68",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@nosana/node",
9
- "version": "1.1.67-rc",
9
+ "version": "1.1.68",
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@coral-xyz/anchor": "^0.28.1-beta.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.67-rc",
3
+ "version": "1.1.68",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "nosana-node": "./dist/src/index.js"