@nosana/node 1.1.20-rc → 1.1.21

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.20-rc",
3
+ "version": "1.1.21",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "nosana-node": "./dist/src/index.js"
@@ -73,7 +73,7 @@
73
73
  "@coral-xyz/anchor": "^0.28.1-beta.1",
74
74
  "@inquirer/prompts": "^4.3.0",
75
75
  "@nosana/ipfs": "^2.2.6",
76
- "@nosana/sdk": "^0.4.81",
76
+ "@nosana/sdk": "^0.4.82",
77
77
  "@solana/web3.js": "^1.78.0",
78
78
  "bn.js": "^5.2.1",
79
79
  "chalk": "^5.3.0",
@@ -10,6 +10,7 @@ import { convertFromBytes } from '../../utils/convertFromBytes.js';
10
10
  import { createHFArgs } from '../helpers/createHFArgs.js';
11
11
  import { createS3Args } from '../helpers/createS3Args.js';
12
12
  import { createOllamaArgs } from '../helpers/createOllamaArgs.js';
13
+ import { liveAbortSignal } from '../../../utils/liveAbortSignal.js';
13
14
  export class VolumeManager {
14
15
  constructor(containerOrchestration, repository) {
15
16
  this.containerOrchestration = containerOrchestration;
@@ -105,7 +106,7 @@ export class VolumeManager {
105
106
  stdout: true,
106
107
  stderr: false,
107
108
  follow: true,
108
- abortSignal: controller?.signal,
109
+ abortSignal: liveAbortSignal(controller?.signal),
109
110
  });
110
111
  let start = false;
111
112
  let formatSize = 'kb';
@@ -135,7 +136,7 @@ export class VolumeManager {
135
136
  });
136
137
  const { StatusCode } = await container.wait({
137
138
  condition: 'not-running',
138
- abortSignal: controller?.signal,
139
+ abortSignal: liveAbortSignal(controller?.signal),
139
140
  });
140
141
  // If download failed, remove volume
141
142
  if (StatusCode !== 0) {
@@ -0,0 +1,18 @@
1
+ /// <reference types="node" />
2
+ import type { EventEmitter } from 'events';
3
+ /**
4
+ * Enforces a maximum running time for a single operation.
5
+ *
6
+ * The clock starts when the operation emits `start` (i.e. the container is
7
+ * actually running) — the same lifecycle point the rest of
8
+ * `runTaskManagerOperation` keys its timeout enforcement off. If the operation
9
+ * is still running `timeoutSeconds` later, `onTimeout` is invoked; callers use
10
+ * this to abort the op. The timer is cleared as soon as the operation settles
11
+ * (`exit`/`error`) or is torn down (`end`), so a normally completing op never
12
+ * triggers it.
13
+ *
14
+ * The unit is seconds, matching the on-chain `job.timeout` convention.
15
+ *
16
+ * A missing, zero, or negative timeout disables enforcement (no-op).
17
+ */
18
+ export declare function enforceOperationTimeout(emitter: EventEmitter, timeoutSeconds: number | undefined, onTimeout: () => void): void;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Enforces a maximum running time for a single operation.
3
+ *
4
+ * The clock starts when the operation emits `start` (i.e. the container is
5
+ * actually running) — the same lifecycle point the rest of
6
+ * `runTaskManagerOperation` keys its timeout enforcement off. If the operation
7
+ * is still running `timeoutSeconds` later, `onTimeout` is invoked; callers use
8
+ * this to abort the op. The timer is cleared as soon as the operation settles
9
+ * (`exit`/`error`) or is torn down (`end`), so a normally completing op never
10
+ * triggers it.
11
+ *
12
+ * The unit is seconds, matching the on-chain `job.timeout` convention.
13
+ *
14
+ * A missing, zero, or negative timeout disables enforcement (no-op).
15
+ */
16
+ export function enforceOperationTimeout(emitter, timeoutSeconds, onTimeout) {
17
+ if (!timeoutSeconds || timeoutSeconds <= 0)
18
+ return;
19
+ let handle;
20
+ const clear = () => {
21
+ if (handle) {
22
+ clearTimeout(handle);
23
+ handle = undefined;
24
+ }
25
+ };
26
+ emitter.on('start', () => {
27
+ clear();
28
+ handle = setTimeout(() => {
29
+ handle = undefined;
30
+ onTimeout();
31
+ }, timeoutSeconds * 1000);
32
+ });
33
+ emitter.on('exit', clear);
34
+ emitter.on('error', clear);
35
+ emitter.on('end', clear);
36
+ }
@@ -4,6 +4,7 @@ import { finalizeEnvOnOperation } from '../globalStore/finalizeEnv.js';
4
4
  import { createResultsObject, extractResultFromLog, } from '../../utils/extractResultsFromLogs.js';
5
5
  import { stanatizeArrays } from '../globalStore/stanatizeArrays.js';
6
6
  import { logEmitter } from '../../../monitoring/proxy/loggingProxy.js';
7
+ import { enforceOperationTimeout } from './enforceOperationTimeout.js';
7
8
  /**
8
9
  * Executes a full lifecycle of a container-based operation using internal class state.
9
10
  *
@@ -70,6 +71,21 @@ export async function runTaskManagerOperation(flow, op, dependent) {
70
71
  };
71
72
  this.mainAbortController.signal.addEventListener('abort', handleMainAbort);
72
73
  this.abortControllerMap.set(op.id, abort);
74
+ /**
75
+ * Enforce the operation's maximum run time, if one was supplied on its
76
+ * execution. The clock starts when the op emits `start`; if it's still
77
+ * running when the timeout elapses, we abort it with EXPIRED — the same
78
+ * clean (non-failure) stop used for job-level expiry — and record the reason
79
+ * for diagnostics/auditing.
80
+ */
81
+ const opTimeoutSeconds = op.execution?.timeout;
82
+ enforceOperationTimeout(emitter, opTimeoutSeconds, () => {
83
+ this.operationStatus.set(op.id, OperationProgressStatuses.STOPPING);
84
+ this.repository.setOpStateDiagnosticsReason(this.job, index, {
85
+ reason: `Operation killed after exceeding its timeout of ${opTimeoutSeconds}s`,
86
+ });
87
+ abort.abort(StopReasons.EXPIRED);
88
+ });
73
89
  /**
74
90
  * quit the operation if the main controller is already emmited
75
91
  */
@@ -2,6 +2,7 @@ import { Readable } from 'stream';
2
2
  import { createInterface } from 'readline';
3
3
  import { parseBuffer } from './utils/parseBuffer.js';
4
4
  import { parseDockerStat } from './utils/parseDockerStat.js';
5
+ import { liveAbortSignal } from '../utils/liveAbortSignal.js';
5
6
  function destroyStream(stream) {
6
7
  if (!stream)
7
8
  return;
@@ -32,9 +33,15 @@ export class ContainerStateManager {
32
33
  await Promise.all([this.attachLogStream(), this.attachStatsStream()]);
33
34
  if (!this.restartPolicy) {
34
35
  this.container
35
- .wait({ abortSignal: this.controller.signal })
36
+ .wait({ abortSignal: liveAbortSignal(this.controller.signal) })
36
37
  .finally(() => {
37
38
  this.state = 'exited';
39
+ })
40
+ .catch(() => {
41
+ // `wait` rejects with an AbortError when the op is aborted (timeout,
42
+ // stop, expiry) while the container is still running. The exit is
43
+ // already reflected via `state` above and observed by `waitForExit`,
44
+ // so swallow the rejection to avoid an unhandled promise rejection.
38
45
  });
39
46
  return;
40
47
  }
@@ -66,6 +73,11 @@ export class ContainerStateManager {
66
73
  }, 1000);
67
74
  }
68
75
  async attachLogStream() {
76
+ // A re-attach can be scheduled (on stream close) before an abort lands;
77
+ // bail if the op is already gone so we never open a stream against a
78
+ // container we're tearing down. Mirrors `attachStatsStream`.
79
+ if (this.controller.signal.aborted)
80
+ return;
69
81
  try {
70
82
  this.currentLogStream = await this.container.logs({
71
83
  stdout: true,
@@ -73,7 +85,7 @@ export class ContainerStateManager {
73
85
  follow: true,
74
86
  timestamps: true,
75
87
  since: this.lastLogTimestamp + 1,
76
- abortSignal: this.controller.signal,
88
+ abortSignal: liveAbortSignal(this.controller.signal),
77
89
  });
78
90
  this.currentLogStream.on('data', (data) => {
79
91
  const { log, type, timestamp } = parseBuffer(data);
@@ -5,6 +5,7 @@ import { ExposedPortHealthCheck } from './ExposedPortHealthCheck.js';
5
5
  import { s3HelperImage } from '../node/resource/definition/index.js';
6
6
  import { applyLoggingProxyToClass } from '../monitoring/proxy/loggingProxy.js';
7
7
  import { promiseTimeoutWrapper } from '../utils/timeoutPromiseWrapper.js';
8
+ import { liveAbortSignal } from '../utils/liveAbortSignal.js';
8
9
  import { generateProxies, generateUrlSecretObject, } from '../utils/expose-util.js';
9
10
  import { ContainerStateManager } from './ContainerStateManager.js';
10
11
  export class Provider {
@@ -260,7 +261,7 @@ export class Provider {
260
261
  await stateManager.waitForExit();
261
262
  }
262
263
  const info = await promiseTimeoutWrapper(container.inspect({
263
- abortSignal: controller.signal,
264
+ abortSignal: liveAbortSignal(controller.signal),
264
265
  }), 360, controller);
265
266
  emitter.emit('exit', { exitCode: info.State.ExitCode });
266
267
  }
@@ -3,6 +3,7 @@ import { DockerExtended } from './dockerExtended/index.js';
3
3
  import { createSeverObject } from './utils/createServerObject.js';
4
4
  import { checkDeprecationDeadline } from './utils/deadline.js';
5
5
  import { createDockerRunOptions } from './createDockerRunOptions.js';
6
+ import { liveAbortSignal } from '../../../utils/liveAbortSignal.js';
6
7
  export class DockerContainerOrchestration {
7
8
  constructor(server, gpu, teeRuntime) {
8
9
  this.name = 'docker';
@@ -103,7 +104,7 @@ export class DockerContainerOrchestration {
103
104
  if (await this.docker.hasImage(image)) {
104
105
  await this.docker
105
106
  .getImage(image)
106
- .remove({ force: true, abortSignal: controller?.signal });
107
+ .remove({ force: true, abortSignal: liveAbortSignal(controller?.signal) });
107
108
  }
108
109
  }
109
110
  async createNetwork(_name, controller) {
@@ -117,7 +118,7 @@ export class DockerContainerOrchestration {
117
118
  Driver: 'default',
118
119
  Config: [{ Subnet: '192.168.101.0/24', Gateway: '192.168.101.1' }],
119
120
  },
120
- abortSignal: controller?.signal,
121
+ abortSignal: liveAbortSignal(controller?.signal),
121
122
  });
122
123
  }
123
124
  catch (error) {
@@ -167,7 +168,7 @@ export class DockerContainerOrchestration {
167
168
  const volume = this.docker.getVolume(name);
168
169
  try {
169
170
  await volume.inspect(); // This will throw if the volume doesn't exist
170
- await volume.remove({ force: true, abortSignal: controller?.signal });
171
+ await volume.remove({ force: true, abortSignal: liveAbortSignal(controller?.signal) });
171
172
  }
172
173
  catch (error) {
173
174
  if (error.statusCode === 404) {
@@ -218,7 +219,7 @@ export class DockerContainerOrchestration {
218
219
  }
219
220
  const container = await this.docker.createContainer({
220
221
  ...args,
221
- abortSignal: controller?.signal,
222
+ abortSignal: liveAbortSignal(controller?.signal),
222
223
  });
223
224
  await container.start();
224
225
  if (controller) {
@@ -258,7 +259,7 @@ export class DockerContainerOrchestration {
258
259
  try {
259
260
  const info = await container.inspect();
260
261
  if (info.State.Status !== 'exited')
261
- await container.stop({ abortSignal: controller?.signal });
262
+ await container.stop({ abortSignal: liveAbortSignal(controller?.signal) });
262
263
  }
263
264
  catch { }
264
265
  }
@@ -271,12 +272,12 @@ export class DockerContainerOrchestration {
271
272
  const container = await this.docker.getContainer(id);
272
273
  try {
273
274
  // Need to catch in case container is already stopped
274
- await container.stop({ abortSignal: controller?.signal }).catch(() => { });
275
+ await container.stop({ abortSignal: liveAbortSignal(controller?.signal) }).catch(() => { });
275
276
  const info = await container.inspect();
276
277
  await container.remove({
277
278
  force: true,
278
279
  v: true,
279
- abortSignal: controller?.signal,
280
+ abortSignal: liveAbortSignal(controller?.signal),
280
281
  });
281
282
  return info;
282
283
  }
@@ -1,6 +1,7 @@
1
1
  import { DockerContainerOrchestration } from '../docker/index.js';
2
2
  import { createPodmanRunOptions } from './utils/createPodmanRunOptions.js';
3
3
  import { fetch, Agent } from 'undici';
4
+ import { liveAbortSignal } from '../../../utils/liveAbortSignal.js';
4
5
  export class PodmanContainerOrchestration extends DockerContainerOrchestration {
5
6
  constructor(server, gpu, _teeRuntime) {
6
7
  super(server, gpu);
@@ -32,7 +33,7 @@ export class PodmanContainerOrchestration extends DockerContainerOrchestration {
32
33
  Driver: 'bridge',
33
34
  Config: [{ Subnet: '192.168.101.0/24', Gateway: '192.168.101.1' }],
34
35
  },
35
- abortSignal: controller?.signal,
36
+ abortSignal: liveAbortSignal(controller?.signal),
36
37
  });
37
38
  }
38
39
  catch (error) {
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Returns the signal only while it is still active, otherwise `undefined`.
3
+ *
4
+ * docker-modem crashes with an uncaught `AbortError` when handed an
5
+ * already-aborted `AbortSignal`: with a socket path the request is built in a
6
+ * microtask (`getSocketPath().then(...)`), where `addAbortSignal` synchronously
7
+ * destroys the freshly-created `ClientRequest` and emits `'error'` before any
8
+ * handler is attached — an unhandled `'error'` event that takes down the
9
+ * process.
10
+ *
11
+ * Teardown calls (inspect/stop/remove) legitimately run after the op's signal
12
+ * has aborted, so every Docker call must be guarded with this: an active signal
13
+ * is passed through (cancellation still works), while an already-aborted one is
14
+ * dropped so the call runs to completion instead of crashing.
15
+ */
16
+ export declare function liveAbortSignal(signal: AbortSignal | undefined): AbortSignal | undefined;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Returns the signal only while it is still active, otherwise `undefined`.
3
+ *
4
+ * docker-modem crashes with an uncaught `AbortError` when handed an
5
+ * already-aborted `AbortSignal`: with a socket path the request is built in a
6
+ * microtask (`getSocketPath().then(...)`), where `addAbortSignal` synchronously
7
+ * destroys the freshly-created `ClientRequest` and emits `'error'` before any
8
+ * handler is attached — an unhandled `'error'` event that takes down the
9
+ * process.
10
+ *
11
+ * Teardown calls (inspect/stop/remove) legitimately run after the op's signal
12
+ * has aborted, so every Docker call must be guarded with this: an active signal
13
+ * is passed through (cancellation still works), while an already-aborted one is
14
+ * dropped so the call runs to completion instead of crashing.
15
+ */
16
+ export function liveAbortSignal(signal) {
17
+ return signal?.aborted ? undefined : signal;
18
+ }
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.20-rc",
3
+ "version": "1.1.21",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@nosana/node",
9
- "version": "1.1.20-rc",
9
+ "version": "1.1.21",
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@coral-xyz/anchor": "^0.28.1-beta.1",
13
13
  "@inquirer/prompts": "^4.3.0",
14
14
  "@nosana/ipfs": "^2.2.6",
15
- "@nosana/sdk": "^0.4.81",
15
+ "@nosana/sdk": "^0.4.82",
16
16
  "@solana/web3.js": "^1.78.0",
17
17
  "bn.js": "^5.2.1",
18
18
  "chalk": "^5.3.0",
@@ -1609,9 +1609,9 @@
1609
1609
  }
1610
1610
  },
1611
1611
  "node_modules/@nosana/sdk": {
1612
- "version": "0.4.81",
1613
- "resolved": "https://registry.npmjs.org/@nosana/sdk/-/sdk-0.4.81.tgz",
1614
- "integrity": "sha512-xviLJyO/MPYKnMHDDMum/t2yXrsVzS8lC325Ru/HfRc4GXz7IkD7/UoahmD8YNGGDZV570zdmasTnaT9N9BkIQ==",
1612
+ "version": "0.4.82",
1613
+ "resolved": "https://registry.npmjs.org/@nosana/sdk/-/sdk-0.4.82.tgz",
1614
+ "integrity": "sha512-s964bc25mLM/Jb5ctSttZ58lbB3SR3KcxECF/BKlOWiN8VUqUEY/GLG6NAzkQ97r23IhenXSewOhNWMbY405Ig==",
1615
1615
  "license": "ISC",
1616
1616
  "dependencies": {
1617
1617
  "@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.20-rc",
3
+ "version": "1.1.21",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "nosana-node": "./dist/src/index.js"
@@ -73,7 +73,7 @@
73
73
  "@coral-xyz/anchor": "^0.28.1-beta.1",
74
74
  "@inquirer/prompts": "^4.3.0",
75
75
  "@nosana/ipfs": "^2.2.6",
76
- "@nosana/sdk": "^0.4.81",
76
+ "@nosana/sdk": "^0.4.82",
77
77
  "@solana/web3.js": "^1.78.0",
78
78
  "bn.js": "^5.2.1",
79
79
  "chalk": "^5.3.0",
@@ -105,4 +105,4 @@
105
105
  "util": "^0.12.5",
106
106
  "yaml": "^2.8.0"
107
107
  }
108
- }
108
+ }