@nosana/node 1.1.33 → 1.1.34-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/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.33",
3
+ "version": "1.1.34-rc",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "nosana-node": "./dist/src/index.js"
@@ -1,2 +1,2 @@
1
- export declare const s3HelperImage = "registry.hub.docker.com/nosana/resource-manager:1.1.10";
1
+ export declare const s3HelperImage = "registry.hub.docker.com/nosana/resource-manager:1.1.12";
2
2
  export declare const nosanaBucket = "https://models.nosana.io";
@@ -1,2 +1,2 @@
1
- export const s3HelperImage = 'registry.hub.docker.com/nosana/resource-manager:1.1.10';
1
+ export const s3HelperImage = 'registry.hub.docker.com/nosana/resource-manager:1.1.12';
2
2
  export const nosanaBucket = 'https://models.nosana.io';
@@ -3,7 +3,7 @@ import { Resource } from '@nosana/sdk/dist/types/resources.js';
3
3
  import { ContainerOrchestrationInterface } from '../../provider/containerOrchestration/interface.js';
4
4
  import { NodeRepository } from '../../repository/NodeRepository.js';
5
5
  import { ImageManager } from './image/imageManager.js';
6
- import { VolumeManager } from './volume/volumeManager.js';
6
+ import { JobContext, VolumeManager } from './volume/volumeManager.js';
7
7
  /**
8
8
  * The market endpoint reports an HF resource's model as `url`, the field it
9
9
  * also uses for S3, while everywhere else in the node — job definitions
@@ -24,7 +24,7 @@ export declare class ResourceManager {
24
24
  resyncResourcesDB(): Promise<void>;
25
25
  fetchMarketRequiredResources(market: Market | string): Promise<void>;
26
26
  prune(): Promise<void>;
27
- getResourceVolumes(resources: Resource[], controller: AbortController): Promise<{
27
+ getResourceVolumes(resources: Resource[], controller: AbortController, job?: JobContext): Promise<{
28
28
  dest: string;
29
29
  name: string;
30
30
  readonly?: boolean;
@@ -53,10 +53,10 @@ export class ResourceManager {
53
53
  await this.images.pruneImages();
54
54
  await this.volumes.pruneVolumes();
55
55
  }
56
- async getResourceVolumes(resources, controller) {
56
+ async getResourceVolumes(resources, controller, job) {
57
57
  const volumes = [];
58
58
  for (const resource of resources) {
59
- await this.volumes.createRemoteVolume(resource, controller);
59
+ await this.volumes.createRemoteVolume(resource, controller, job);
60
60
  if ((await this.volumes.hasVolume(resource)) === false) {
61
61
  const error = new Error(`Missing required resource ${createResourceName(resource)}.`);
62
62
  throw error;
@@ -1,17 +1,45 @@
1
+ import { ContainerInspectInfo } from 'dockerode';
1
2
  import { Resource } from '@nosana/sdk/dist/types/resources.js';
2
3
  import { RequiredResource } from '@nosana/sdk';
3
4
  import { ContainerOrchestrationInterface } from '../../../provider/containerOrchestration/interface.js';
4
5
  import { NodeRepository } from '../../../repository/NodeRepository.js';
5
6
  /**
6
- * The reason the resource manager gave for failing, if it gave one.
7
+ * Renders the runtime state that says why the download stopped, for inclusion in
8
+ * the error message. A process that is killed rather than failing logs nothing,
9
+ * so this is the only evidence of an OOM; a container the runtime could not
10
+ * create or start logs nothing either, and `State.Error` is Docker's only
11
+ * account of that. The run duration separates a download that died on contact
12
+ * from one the network dropped an hour in.
13
+ *
14
+ * Optional because the caller inspects on a best-effort basis; the fields
15
+ * themselves are always present when it succeeds.
16
+ */
17
+ export declare function describeContainerState(state?: ContainerInspectInfo['State']): string;
18
+ /**
19
+ * The reasons the resource manager gave for failing, if it gave any.
7
20
  *
8
21
  * It reports failures as a JSON line on stderr — `{"event":"error","message":
9
- * "..."}` — from every service it implements, then exits non-zero. Read the
10
- * last such line, since a run may log several before giving up.
22
+ * "..."}` — from every service it implements, then exits non-zero. Parallel
23
+ * download workers can each report their own failure before the run dies, so
24
+ * every such line is collected, in the order logged; the last is the one the
25
+ * run gave up on.
11
26
  */
12
- export declare function findReportedError(logs: {
27
+ export declare function findReportedErrors(logs: {
13
28
  log?: string;
14
- }[]): string | undefined;
29
+ }[]): string[];
30
+ /**
31
+ * A resource rendered for an error message. Resources carry credentials (`IAM`,
32
+ * HF `accessToken`), and errors reach the logs and the support channel, so
33
+ * those fields are replaced rather than reported.
34
+ */
35
+ /**
36
+ * The op a download is running for. Passed only for job resources — a market
37
+ * preload has no op state to write errors to.
38
+ */
39
+ export interface JobContext {
40
+ id: string;
41
+ opIndex: number;
42
+ }
15
43
  export declare class VolumeManager {
16
44
  private containerOrchestration;
17
45
  private repository;
@@ -20,7 +48,7 @@ export declare class VolumeManager {
20
48
  private progressBarReporter;
21
49
  constructor(containerOrchestration: ContainerOrchestrationInterface, repository: NodeRepository);
22
50
  pullMarketRequiredVolumes(remoteResources: RequiredResource[]): Promise<void>;
23
- createRemoteVolume(resource: RequiredResource, controller: AbortController): Promise<string>;
51
+ createRemoteVolume(resource: RequiredResource, controller: AbortController, job?: JobContext): Promise<string>;
24
52
  private runResourceManagerContainer;
25
53
  setVolume(bucket: string, volume: string): Promise<void>;
26
54
  hasVolume(resource: Resource): Promise<boolean>;
@@ -11,48 +11,60 @@ import { createHFArgs } from '../helpers/createHFArgs.js';
11
11
  import { createS3Args } from '../helpers/createS3Args.js';
12
12
  import { createOllamaArgs } from '../helpers/createOllamaArgs.js';
13
13
  import { liveAbortSignal } from '../../../utils/liveAbortSignal.js';
14
+ import { formatTime } from '../../../utils/formatTime.js';
14
15
  /**
15
16
  * Renders the runtime state that says why the download stopped, for inclusion in
16
17
  * the error message. A process that is killed rather than failing logs nothing,
17
- * so this is the only evidence of an OOM.
18
+ * so this is the only evidence of an OOM; a container the runtime could not
19
+ * create or start logs nothing either, and `State.Error` is Docker's only
20
+ * account of that. The run duration separates a download that died on contact
21
+ * from one the network dropped an hour in.
18
22
  *
19
23
  * Optional because the caller inspects on a best-effort basis; the fields
20
24
  * themselves are always present when it succeeds.
21
25
  */
22
- function describeContainerState(state) {
26
+ export function describeContainerState(state) {
23
27
  if (!state)
24
28
  return '';
25
29
  const parts = [
26
30
  `exitCode: ${state.ExitCode}`,
27
31
  `OOMKilled: ${state.OOMKilled}`
28
32
  ];
33
+ if (state.Error) {
34
+ parts.push(`error: ${state.Error}`);
35
+ }
36
+ const started = Date.parse(state.StartedAt);
37
+ const finished = Date.parse(state.FinishedAt);
38
+ // Docker reports a container that never started or never finished with the
39
+ // zero timestamp "0001-01-01T00:00:00Z"; a duration across one is noise.
40
+ if (started > 0 && finished >= started) {
41
+ parts.push(`ran for ${formatTime((finished - started) / 1000)}`);
42
+ }
29
43
  return ` [${parts.join(', ')}]`;
30
44
  }
31
45
  /**
32
- * The reason the resource manager gave for failing, if it gave one.
46
+ * The reasons the resource manager gave for failing, if it gave any.
33
47
  *
34
48
  * It reports failures as a JSON line on stderr — `{"event":"error","message":
35
- * "..."}` — from every service it implements, then exits non-zero. Read the
36
- * last such line, since a run may log several before giving up.
49
+ * "..."}` — from every service it implements, then exits non-zero. Parallel
50
+ * download workers can each report their own failure before the run dies, so
51
+ * every such line is collected, in the order logged; the last is the one the
52
+ * run gave up on.
37
53
  */
38
- export function findReportedError(logs) {
39
- for (const log of [...logs].reverse()) {
54
+ export function findReportedErrors(logs) {
55
+ const messages = [];
56
+ for (const log of logs) {
40
57
  if (log.log?.includes('{"event":"error"')) {
41
58
  try {
42
59
  const parsed = JSON.parse(log.log.slice(log.log.indexOf('{')));
43
60
  if (typeof parsed.message === 'string')
44
- return parsed.message;
61
+ messages.push(parsed.message);
45
62
  }
46
63
  catch { }
47
64
  }
48
65
  }
49
- return undefined;
66
+ return messages;
50
67
  }
51
- /**
52
- * A resource rendered for an error message. Resources carry credentials (`IAM`,
53
- * HF `accessToken`), and errors reach the logs and the support channel, so
54
- * those fields are replaced rather than reported.
55
- */
56
68
  function describeResource(resource) {
57
69
  return JSON.stringify(resource, (key, value) => key === 'IAM' || key === 'accessToken' ? '[hidden]' : value);
58
70
  }
@@ -83,7 +95,7 @@ export class VolumeManager {
83
95
  }
84
96
  }
85
97
  }
86
- async createRemoteVolume(resource, controller) {
98
+ async createRemoteVolume(resource, controller, job) {
87
99
  const resourceName = createResourceName(resource);
88
100
  // Checked before anything is allocated. Left to the download, an empty repo
89
101
  // becomes a request for `/api/models//tree/main` and comes back as a bare
@@ -121,12 +133,12 @@ export class VolumeManager {
121
133
  files: s3Resource.files,
122
134
  bucket: s3Resource.bucket,
123
135
  }, s3Resource.IAM);
124
- await this.runResourceManagerContainer(volumeName, resourceName, args, controller, sync);
136
+ await this.runResourceManagerContainer(volumeName, resourceName, args, controller, sync, job);
125
137
  }
126
138
  else {
127
139
  for (const bucket of s3Resource.buckets) {
128
140
  const args = createS3Args(volumeName, { url: bucket.url, files: bucket.files }, s3Resource.IAM);
129
- await this.runResourceManagerContainer(volumeName, resourceName, args, controller, sync);
141
+ await this.runResourceManagerContainer(volumeName, resourceName, args, controller, sync, job);
130
142
  }
131
143
  }
132
144
  this.setVolume(resourceName, volumeName);
@@ -139,7 +151,7 @@ export class VolumeManager {
139
151
  try {
140
152
  const { repo, revision, files: hfFiles, accessToken, } = resource;
141
153
  const args = createHFArgs(volumeName, { repo, revision, files: hfFiles }, accessToken);
142
- await this.runResourceManagerContainer(volumeName, resourceName, args, controller, sync);
154
+ await this.runResourceManagerContainer(volumeName, resourceName, args, controller, sync, job);
143
155
  this.setVolume(resourceName, volumeName);
144
156
  }
145
157
  catch (err) {
@@ -150,7 +162,7 @@ export class VolumeManager {
150
162
  try {
151
163
  const { model } = resource;
152
164
  const args = createOllamaArgs(volumeName, model);
153
- await this.runResourceManagerContainer(volumeName, resourceName, args, controller, sync);
165
+ await this.runResourceManagerContainer(volumeName, resourceName, args, controller, sync, job);
154
166
  this.setVolume(resourceName, volumeName);
155
167
  }
156
168
  catch (err) {
@@ -160,7 +172,7 @@ export class VolumeManager {
160
172
  }
161
173
  return volumeName;
162
174
  }
163
- async runResourceManagerContainer(volume, name, args, controller, syncing = false) {
175
+ async runResourceManagerContainer(volume, name, args, controller, syncing = false, job) {
164
176
  const container = await this.containerOrchestration.runContainer(args);
165
177
  try {
166
178
  const logStream = await container.logs({
@@ -214,13 +226,24 @@ export class VolumeManager {
214
226
  .inspect()
215
227
  .then(({ State }) => State)
216
228
  .catch(() => undefined);
229
+ const reported = findReportedErrors(logs);
230
+ // Written straight to the op state: the summary thrown below quotes
231
+ // only the last reported failure, and parallel download workers can
232
+ // each report their own before the run dies.
233
+ if (job) {
234
+ for (const message of reported) {
235
+ this.repository.updateOpStateError(job.id, job.opIndex, {
236
+ event: 'resource-error',
237
+ message,
238
+ });
239
+ }
240
+ }
217
241
  // A non-zero exit is a failed download whether or not the container
218
242
  // managed to report a reason, so this must throw either way: returning
219
243
  // normally here reports success for an unfinished volume, and the run
220
244
  // then fails later with the cause lost.
221
- const reason = findReportedError(logs) ??
222
- `resource download failed (exit code ${StatusCode})`;
223
- throw new Error(`${reason}${describeContainerState(state)}`);
245
+ const reason = reported.at(-1) ?? `no reason reported (exit code ${StatusCode})`;
246
+ throw new Error(`resource download failed for ${name}: ${reason}${describeContainerState(state)}`);
224
247
  }
225
248
  }
226
249
  catch (error) {
@@ -213,7 +213,7 @@ export class Provider {
213
213
  await this.containerOrchestration.pullImage(s3HelperImage, undefined, controller);
214
214
  this.resourceManager.images.setImage(s3HelperImage);
215
215
  // transformCollections has already resolved any SpreadMarker objects by this point
216
- const resourceVolumes = await this.resourceManager.getResourceVolumes(op.args.resources ?? [], controller);
216
+ const resourceVolumes = await this.resourceManager.getResourceVolumes(op.args.resources ?? [], controller, { id: flow.id, opIndex: index });
217
217
  volumes.push(...resourceVolumes);
218
218
  }
219
219
  // The op only declares that it wants a TEE; the node's configured
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.33",
3
+ "version": "1.1.34-rc",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@nosana/node",
9
- "version": "1.1.33",
9
+ "version": "1.1.34-rc",
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.33",
3
+ "version": "1.1.34-rc",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "nosana-node": "./dist/src/index.js"