@nosana/node 1.1.49 → 1.1.50

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.49",
3
+ "version": "1.1.50",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "nosana-node": "./dist/src/index.js"
@@ -3,6 +3,7 @@ export default class NodeManager {
3
3
  private apiHandler;
4
4
  private exiting;
5
5
  private apiStarted;
6
+ private signalled;
6
7
  /**
7
8
  * Seeded from the environment so the loop survives the process being
8
9
  * replaced by an update.
@@ -21,6 +22,8 @@ export default class NodeManager {
21
22
  clean(): Promise<void>;
22
23
  restart(marketArg?: string): Promise<void>;
23
24
  delay(sec: number): Promise<void>;
25
+ /** Asked to stop, which is not a failure to restart on. */
26
+ isStopping(): boolean;
24
27
  error(): Promise<void>;
25
28
  /**
26
29
  * Set up handling for process exit signals
@@ -16,6 +16,7 @@ export default class NodeManager {
16
16
  constructor(options) {
17
17
  this.exiting = false;
18
18
  this.apiStarted = false;
19
+ this.signalled = false;
19
20
  /**
20
21
  * Seeded from the environment so the loop survives the process being
21
22
  * replaced by an update.
@@ -27,6 +28,7 @@ export default class NodeManager {
27
28
  * because we want the api to be independent from nodes restarts
28
29
  */
29
30
  this.apiHandler = this.node.api();
31
+ this.apiHandler.nodeManager = this;
30
32
  this.handleProcessExit();
31
33
  }
32
34
  async init() {
@@ -234,6 +236,10 @@ export default class NodeManager {
234
236
  async delay(sec) {
235
237
  await this.node.restartDelay(sec);
236
238
  }
239
+ /** Asked to stop, which is not a failure to restart on. */
240
+ isStopping() {
241
+ return this.signalled;
242
+ }
237
243
  async error() {
238
244
  if (this.exiting)
239
245
  return;
@@ -246,6 +252,7 @@ export default class NodeManager {
246
252
  handleProcessExit() {
247
253
  const exitHandler = async () => {
248
254
  LogMonitoringRegistry.getInstance().setLoggable(true);
255
+ this.signalled = true;
249
256
  if (this.exiting)
250
257
  return;
251
258
  this.exiting = true;
@@ -1,6 +1,7 @@
1
1
  import { Client as SDK } from '@nosana/sdk';
2
2
  import { Provider } from '../../provider/Provider.js';
3
3
  import { NodeRepository } from '../../repository/NodeRepository.js';
4
+ import type NodeManager from '../../index.js';
4
5
  export declare class ApiHandler {
5
6
  private sdk;
6
7
  private repository;
@@ -13,6 +14,12 @@ export declare class ApiHandler {
13
14
  private eventEmitter;
14
15
  private apiCheckTimer;
15
16
  private failedProxyRestarts;
17
+ /**
18
+ * Where the node's progress into the job loop is kept. The API outlives the
19
+ * node's own restarts, and the process that replaces this one on a restart
20
+ * resumes there.
21
+ */
22
+ nodeManager?: NodeManager;
16
23
  constructor(sdk: SDK, repository: NodeRepository, provider: Provider, port: number);
17
24
  start(): Promise<string>;
18
25
  preventMultipleApiStarts(): Promise<void>;
@@ -8,9 +8,9 @@ import { stateStreaming } from '../../monitoring/streaming/StateStreamer.js';
8
8
  import { applyLoggingProxyToClass } from '../../monitoring/proxy/loggingProxy.js';
9
9
  import { verifyJobOwnerSignatureMiddleware, verifyWSJobOwnerSignatureMiddleware, verifyWSNodeOrJobOwnerSignatureMiddleware, } from './middlewares/index.js';
10
10
  import { getNodeInfoRoute, getJobResultsRoute, getServiceUrlRoute, postJobDefinitionRoute, postServiceStopRoute, wssLogRoute, wssStatusRoute, getCurrentGroupStatusHandler, getGroupStatusHandler, getOperationsStatusHandler, getOperationStatusHandler, restartGroupOperationHandler, restartOperationHandler, stopGroupOperationHandler, stopOperationHandler, wssTaskManagerLogRoute, getJobDefinitionRoute, getJobInfoRoute, getJobStatsRoute, getJobStatsStreamRoute, } from './routes/index.js';
11
+ import { requestExit } from '../../../exitCodes.js';
11
12
  import { NodeAlreadyActiveError } from '../../errors/NodeAlreadyActiveError.js';
12
13
  import { reportError } from '../../monitoring/reportError.js';
13
- import { EXIT_CODES } from '../../../exitCodes.js';
14
14
  import { TaskManagerRegistry } from '../task/TaskManagerRegistry.js';
15
15
  /** Rebuilds to fail before the process is replaced. */
16
16
  const MAX_PROXY_RESTART_FAILURES = 3;
@@ -190,7 +190,10 @@ export class ApiHandler {
190
190
  return;
191
191
  }
192
192
  console.error('The API proxy could not be rebuilt, restarting the node.');
193
- process.exit(EXIT_CODES.RESTART);
193
+ void requestExit({
194
+ type: 'restart',
195
+ inJobLoop: this.nodeManager?.inJobLoop ?? false,
196
+ });
194
197
  }
195
198
  async registerRoutes() {
196
199
  // Attach require objects to routes
@@ -119,12 +119,6 @@ export class Provider {
119
119
  });
120
120
  }
121
121
  }
122
- // The runtime carries a create through to the end, so a setup stopped
123
- // during one takes the container back off rather than leaving it up.
124
- if (startup.signal.aborted) {
125
- await this.containerOrchestration.stopAndDeleteContainer(frpc_name);
126
- throw new Error('API proxy setup was stopped');
127
- }
128
122
  }
129
123
  catch (error) {
130
124
  throw error;
@@ -1,6 +1,6 @@
1
1
  import 'rpc-websockets/dist/lib/client.js';
2
2
  import NodeManager from '../../../NodeManager/index.js';
3
- import { EXIT_CODES } from '../../../exitCodes.js';
3
+ import { requestExit } from '../../../exitCodes.js';
4
4
  import { validateCLIVersion } from '../../../version/index.js';
5
5
  /** Failures no restart resolves: the node reports them and stops. */
6
6
  const TERMINAL_ERRORS = ['NodeBannedError', 'NodeNotQualifiedError'];
@@ -13,6 +13,9 @@ export async function startNode(market, options) {
13
13
  await nodeManager.start(market);
14
14
  }
15
15
  catch (error) {
16
+ // Failed because the node is stopping, and that shutdown owns the exit.
17
+ if (nodeManager.isStopping())
18
+ return;
16
19
  if (error.name === 'WSLBlockedError') {
17
20
  await nodeManager.stop();
18
21
  process.exit(1);
@@ -50,7 +53,9 @@ export async function startNode(market, options) {
50
53
  else {
51
54
  await nodeManager.error();
52
55
  await nodeManager.stop();
53
- process.exit(TERMINAL_ERRORS.includes(error.name) ? 0 : EXIT_CODES.RESTART);
56
+ if (TERMINAL_ERRORS.includes(error.name))
57
+ process.exit(0);
58
+ await requestExit({ type: 'restart', inJobLoop: nodeManager.inJobLoop });
54
59
  }
55
60
  }
56
61
  }
@@ -1,23 +1,32 @@
1
1
  /**
2
- * Exit codes the auto-update wrapper acts on; it stops on any other code.
3
- * Keep in sync with `auto-update/src/index.ts`.
2
+ * How the node asks the auto-update wrapper for another process. Keep in sync
3
+ * with `auto-update/src/index.ts`.
4
4
  *
5
- * Wrappers already deployed only know UPDATE (129); every other code ends
6
- * them, and with them the node. Until those wrappers are replaced,
7
- * UPDATE_IN_JOB_LOOP deliberately aliases UPDATE so a node updating from the
8
- * job loop is respawned rather than dropped. The cost is that the newer
9
- * wrapper cannot tell the two apart, so the replacement starts fresh instead
10
- * of resuming the job loop. Restore 76 once old wrappers are gone.
11
- *
12
- * RESTART stays distinct: old wrappers exit on it, as they did before it
13
- * existed, so the outside supervisor takes over; the newer wrapper restarts
14
- * with backoff. Aliasing it to UPDATE would reinstall on every failure with
15
- * no delay, and kill pinned wrappers outright.
5
+ * The node always leaves with RESPAWN_EXIT_CODE. Every wrapper ever deployed
6
+ * reinstalls and respawns on it, and stops on anything else, so it is the one
7
+ * code that never takes the node down. What the node wanted (an update or a
8
+ * plain restart, in the job loop or not, which version) travels as an
9
+ * `ExitRequest` over IPC when the wrapper opened a channel; wrappers without
10
+ * one treat every exit as an update, which is the old behaviour.
16
11
  */
17
- export declare const EXIT_CODES: {
18
- readonly UPDATE: 129;
19
- readonly UPDATE_IN_JOB_LOOP: 129;
20
- readonly RESTART: 75;
21
- };
12
+ export declare const RESPAWN_EXIT_CODE = 129;
22
13
  /** Set by the wrapper on the process that replaces a node in the job loop. */
23
14
  export declare const IN_JOB_LOOP_ENV = "NOSANA_NODE_IN_JOB_LOOP";
15
+ /**
16
+ * Sent to the wrapper before exiting. Add fields freely; a wrapper ignores
17
+ * what it does not know. A wrapper that does not know a `type` treats the
18
+ * exit as an update, so a new type must be safe to handle that way.
19
+ */
20
+ export type ExitRequest = {
21
+ type: 'update';
22
+ inJobLoop: boolean;
23
+ requestedVersion: string;
24
+ } | {
25
+ type: 'restart';
26
+ inJobLoop: boolean;
27
+ };
28
+ /**
29
+ * Ask the wrapper for another process. Tells it over IPC when there is a
30
+ * channel, then exits with RESPAWN_EXIT_CODE either way. Never resolves.
31
+ */
32
+ export declare function requestExit(request: ExitRequest): Promise<never>;
@@ -1,23 +1,33 @@
1
1
  /**
2
- * Exit codes the auto-update wrapper acts on; it stops on any other code.
3
- * Keep in sync with `auto-update/src/index.ts`.
2
+ * How the node asks the auto-update wrapper for another process. Keep in sync
3
+ * with `auto-update/src/index.ts`.
4
4
  *
5
- * Wrappers already deployed only know UPDATE (129); every other code ends
6
- * them, and with them the node. Until those wrappers are replaced,
7
- * UPDATE_IN_JOB_LOOP deliberately aliases UPDATE so a node updating from the
8
- * job loop is respawned rather than dropped. The cost is that the newer
9
- * wrapper cannot tell the two apart, so the replacement starts fresh instead
10
- * of resuming the job loop. Restore 76 once old wrappers are gone.
11
- *
12
- * RESTART stays distinct: old wrappers exit on it, as they did before it
13
- * existed, so the outside supervisor takes over; the newer wrapper restarts
14
- * with backoff. Aliasing it to UPDATE would reinstall on every failure with
15
- * no delay, and kill pinned wrappers outright.
5
+ * The node always leaves with RESPAWN_EXIT_CODE. Every wrapper ever deployed
6
+ * reinstalls and respawns on it, and stops on anything else, so it is the one
7
+ * code that never takes the node down. What the node wanted (an update or a
8
+ * plain restart, in the job loop or not, which version) travels as an
9
+ * `ExitRequest` over IPC when the wrapper opened a channel; wrappers without
10
+ * one treat every exit as an update, which is the old behaviour.
16
11
  */
17
- export const EXIT_CODES = {
18
- UPDATE: 129,
19
- UPDATE_IN_JOB_LOOP: 129,
20
- RESTART: 75,
21
- };
12
+ export const RESPAWN_EXIT_CODE = 129;
13
+ /** How long the wrapper is given to take the message before leaving anyway. */
14
+ const SEND_DEADLINE_MS = 2 * 1000;
22
15
  /** Set by the wrapper on the process that replaces a node in the job loop. */
23
16
  export const IN_JOB_LOOP_ENV = 'NOSANA_NODE_IN_JOB_LOOP';
17
+ /**
18
+ * Ask the wrapper for another process. Tells it over IPC when there is a
19
+ * channel, then exits with RESPAWN_EXIT_CODE either way. Never resolves.
20
+ */
21
+ export function requestExit(request) {
22
+ return new Promise(() => {
23
+ // Nothing calls back when the channel went with the wrapper.
24
+ setTimeout(() => process.exit(RESPAWN_EXIT_CODE), SEND_DEADLINE_MS);
25
+ if (process.send) {
26
+ // Exiting before the message is flushed can drop it.
27
+ process.send(request, undefined, undefined, () => process.exit(RESPAWN_EXIT_CODE));
28
+ }
29
+ else {
30
+ process.exit(RESPAWN_EXIT_CODE);
31
+ }
32
+ });
33
+ }
@@ -1,15 +1,28 @@
1
1
  import chalk from 'chalk';
2
2
  import { pkg } from '../static/index.js';
3
- import { EXIT_CODES } from '../exitCodes.js';
3
+ import { requestExit } from '../exitCodes.js';
4
+ import { fetchDistTags } from './registry.js';
4
5
  function requiresNewVersion(required, current) {
5
6
  return parseInt(required) > parseInt(current);
6
7
  }
7
8
  function exitCleanly(currentVersion, latestVersion, inJobLoop) {
8
9
  console.log(chalk.red(`You are currently running Nosana CLI version ${currentVersion}. Version ${latestVersion} has been released, please update your CLI to the latest version using npm install ${pkg.name}.`));
9
- process.exit(inJobLoop ? EXIT_CODES.UPDATE_IN_JOB_LOOP : EXIT_CODES.UPDATE);
10
+ return requestExit({
11
+ type: 'update',
12
+ inJobLoop,
13
+ requestedVersion: latestVersion,
14
+ });
10
15
  }
11
16
  /** @param inJobLoop the process that replaces this one stays in the job loop. */
12
17
  export async function validateCLIVersion(inJobLoop = false) {
18
+ // The version the wrapper was told to seed with, for exercising the update
19
+ // cycle: let it reach the job loop, whose check still runs and requests the
20
+ // update from there. The version installed then no longer matches, so its
21
+ // startup check runs as normal.
22
+ if (!inJobLoop && pkg.version === process.env.NOSANA_NODE_INITIAL_VERSION) {
23
+ console.warn(chalk.yellow(`Running seeded version ${pkg.version}, deferring the version check to the job loop`));
24
+ return;
25
+ }
13
26
  try {
14
27
  const [current_major, current_minor, current_patch] = pkg.version.split('.');
15
28
  if (parseInt(current_major) === 0 &&
@@ -18,10 +31,9 @@ export async function validateCLIVersion(inJobLoop = false) {
18
31
  console.warn(chalk.yellow('Running in dev mode, skipping version check'));
19
32
  return;
20
33
  }
21
- const packageData = await fetch(`https://registry.npmjs.com/${pkg.name}`);
22
- const packageJSON = await packageData.json();
34
+ const distTags = await fetchDistTags(pkg.name);
23
35
  const distTag = pkg.version.includes('-rc') ? 'next' : 'latest';
24
- const registryLatestVersion = packageJSON['dist-tags']?.[distTag];
36
+ const registryLatestVersion = distTags?.[distTag];
25
37
  if (!registryLatestVersion || typeof registryLatestVersion !== 'string') {
26
38
  throw new Error('Could not retrieve valid package information from npm');
27
39
  }
@@ -29,18 +41,18 @@ export async function validateCLIVersion(inJobLoop = false) {
29
41
  if (parseInt(required_major) === parseInt(current_major)) {
30
42
  if (parseInt(required_minor) === parseInt(current_minor)) {
31
43
  if (requiresNewVersion(required_patch, current_patch)) {
32
- exitCleanly(pkg.version, registryLatestVersion, inJobLoop);
44
+ await exitCleanly(pkg.version, registryLatestVersion, inJobLoop);
33
45
  }
34
46
  }
35
47
  else {
36
48
  if (requiresNewVersion(required_minor, current_minor)) {
37
- exitCleanly(pkg.version, registryLatestVersion, inJobLoop);
49
+ await exitCleanly(pkg.version, registryLatestVersion, inJobLoop);
38
50
  }
39
51
  }
40
52
  }
41
53
  else {
42
54
  if (requiresNewVersion(required_major, current_major)) {
43
- exitCleanly(pkg.version, registryLatestVersion, inJobLoop);
55
+ await exitCleanly(pkg.version, registryLatestVersion, inJobLoop);
44
56
  }
45
57
  }
46
58
  }
@@ -0,0 +1,2 @@
1
+ /** The package's dist-tags (`latest`, `next`, ...) as the npm registry has them. */
2
+ export declare function fetchDistTags(name: string): Promise<Record<string, string> | undefined>;
@@ -0,0 +1,6 @@
1
+ /** The package's dist-tags (`latest`, `next`, ...) as the npm registry has them. */
2
+ export async function fetchDistTags(name) {
3
+ const response = await fetch(`https://registry.npmjs.com/${name}`);
4
+ const json = await response.json();
5
+ return json['dist-tags'];
6
+ }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.49",
3
+ "version": "1.1.50",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@nosana/node",
9
- "version": "1.1.49",
9
+ "version": "1.1.50",
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.49",
3
+ "version": "1.1.50",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "nosana-node": "./dist/src/index.js"