@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 +1 -1
- package/dist/src/NodeManager/index.d.ts +3 -0
- package/dist/src/NodeManager/index.js +7 -0
- package/dist/src/NodeManager/node/api/ApiHandler.d.ts +7 -0
- package/dist/src/NodeManager/node/api/ApiHandler.js +5 -2
- package/dist/src/NodeManager/provider/Provider.js +0 -6
- package/dist/src/cli/commands/start/action.js +7 -2
- package/dist/src/exitCodes.d.ts +27 -18
- package/dist/src/exitCodes.js +28 -18
- package/dist/src/version/index.js +20 -8
- package/dist/src/version/registry.d.ts +2 -0
- package/dist/src/version/registry.js +6 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/dist/package.json
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
}
|
package/dist/src/exitCodes.d.ts
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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>;
|
package/dist/src/exitCodes.js
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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,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
|
+
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nosana/node",
|
|
3
|
-
"version": "1.1.
|
|
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.
|
|
9
|
+
"version": "1.1.50",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@coral-xyz/anchor": "^0.28.1-beta.1",
|