@nosana/node 1.1.27 → 1.1.29-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.27",
3
+ "version": "1.1.29-rc",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "nosana-node": "./dist/src/index.js"
@@ -15,7 +15,7 @@ export declare class MarketHandler {
15
15
  clear(): void;
16
16
  isInMarket(): boolean;
17
17
  setInMarket(): void;
18
- request(requestedMarket?: string, session?: string): Promise<Market>;
18
+ request(requestedMarket?: string, session?: string, benchmarkRetries?: number): Promise<Market>;
19
19
  private logFeedbackReport;
20
20
  private logLatestMeasurements;
21
21
  check(market: string): Promise<Market>;
@@ -6,6 +6,8 @@ import { NodeBannedError } from "../../errors/NodeBannedError.js";
6
6
  import { NodeNotQualifiedError } from "../../errors/NodeNotQualifiedError.js";
7
7
  import { MarketAccessHandler } from "./marketAccess.js";
8
8
  import { sleep } from "../../utils/utils.js";
9
+ const RETEST_BUFFER_S = 60;
10
+ const MAX_BENCHMARK_RETRIES = 3;
9
11
  export class MarketHandler {
10
12
  constructor(sdk, provider, repository) {
11
13
  this.sdk = sdk;
@@ -24,7 +26,7 @@ export class MarketHandler {
24
26
  setInMarket() {
25
27
  this.inMarket = true;
26
28
  }
27
- async request(requestedMarket, session) {
29
+ async request(requestedMarket, session, benchmarkRetries = 0) {
28
30
  const result = await HostManager.requestMarket(requestedMarket, session);
29
31
  // Not registered - caller will handle registration and retry
30
32
  if (result.notRegistered) {
@@ -40,7 +42,7 @@ export class MarketHandler {
40
42
  await benchmark.run();
41
43
  // Re-poll within the same request-market cycle, echoing the session the
42
44
  // host issued so lifecycle metrics aren't re-requested on the retry.
43
- return await this.request(requestedMarket, result.session ?? session);
45
+ return await this.request(requestedMarket, result.session ?? session, benchmarkRetries);
44
46
  }
45
47
  // Sign and send SFT mint/burn transaction if provided
46
48
  if (result.market?.sftTx) {
@@ -53,6 +55,14 @@ export class MarketHandler {
53
55
  this.logFeedbackReport(result.feedbackReport);
54
56
  }
55
57
  if (!result.market?.address) {
58
+ const nextTestAt = parseFutureDate(result.nextTestAt);
59
+ if (nextTestAt && benchmarkRetries < MAX_BENCHMARK_RETRIES) {
60
+ const waitSeconds = Math.ceil((nextTestAt.getTime() - Date.now()) / 1000) +
61
+ RETEST_BUFFER_S;
62
+ console.log(chalk.yellow(`Node does not qualify for a market yet. Next eligible retest at ${nextTestAt.toISOString()}, retrying in ~${Math.ceil(waitSeconds / 60)} minutes.`));
63
+ await sleep(waitSeconds);
64
+ return await this.request(requestedMarket, result.session ?? session, benchmarkRetries + 1);
65
+ }
56
66
  throw new NodeNotQualifiedError(result.nextTestAt);
57
67
  }
58
68
  const onchainMarket = await this.sdk.jobs.getMarket(result.market.address);
@@ -263,6 +273,15 @@ export class MarketHandler {
263
273
  this.clear();
264
274
  }
265
275
  }
276
+ function parseFutureDate(value) {
277
+ if (!value)
278
+ return undefined;
279
+ const date = new Date(value);
280
+ if (Number.isNaN(date.getTime()) || date.getTime() <= Date.now()) {
281
+ return undefined;
282
+ }
283
+ return date;
284
+ }
266
285
  function flattenMetrics(obj, prefix = "") {
267
286
  const result = [];
268
287
  for (const [key, value] of Object.entries(obj)) {
@@ -2,6 +2,16 @@ import { Resource } from '@nosana/sdk/dist/types/resources.js';
2
2
  import { RequiredResource } from '@nosana/sdk';
3
3
  import { ContainerOrchestrationInterface } from '../../../provider/containerOrchestration/interface.js';
4
4
  import { NodeRepository } from '../../../repository/NodeRepository.js';
5
+ /**
6
+ * The reason the resource manager gave for failing, if it gave one.
7
+ *
8
+ * 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.
11
+ */
12
+ export declare function findReportedError(logs: {
13
+ log?: string;
14
+ }[]): string | undefined;
5
15
  export declare class VolumeManager {
6
16
  private containerOrchestration;
7
17
  private repository;
@@ -11,6 +11,43 @@ 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
+ /**
15
+ * Renders the runtime state that says why the download stopped, for inclusion in
16
+ * the error message. A process that is killed rather than failing logs nothing,
17
+ * so this is the only evidence of an OOM.
18
+ *
19
+ * Optional because the caller inspects on a best-effort basis; the fields
20
+ * themselves are always present when it succeeds.
21
+ */
22
+ function describeContainerState(state) {
23
+ if (!state)
24
+ return '';
25
+ const parts = [
26
+ `exitCode: ${state.ExitCode}`,
27
+ `OOMKilled: ${state.OOMKilled}`
28
+ ];
29
+ return ` [${parts.join(', ')}]`;
30
+ }
31
+ /**
32
+ * The reason the resource manager gave for failing, if it gave one.
33
+ *
34
+ * 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.
37
+ */
38
+ export function findReportedError(logs) {
39
+ for (const log of logs.reverse()) {
40
+ if (log.log?.includes('{"event":"error"')) {
41
+ try {
42
+ const parsed = JSON.parse(log.log.slice(log.log.indexOf('{')));
43
+ if (typeof parsed.message === 'string')
44
+ return parsed.message;
45
+ }
46
+ catch { }
47
+ }
48
+ }
49
+ return undefined;
50
+ }
14
51
  export class VolumeManager {
15
52
  constructor(containerOrchestration, repository) {
16
53
  this.containerOrchestration = containerOrchestration;
@@ -149,12 +186,21 @@ export class VolumeManager {
149
186
  timestamps: true,
150
187
  });
151
188
  const { logs } = extractLogsAndResultsFromLogBuffer(errrorBuffer, undefined);
189
+ // Read the state before removing it: a killed process logs nothing, so
190
+ // the runtime's own record is the only account of why.
191
+ const state = await container
192
+ .inspect()
193
+ .then(({ State }) => State)
194
+ .catch(() => undefined);
152
195
  await container.remove({ force: true });
153
196
  await this.containerOrchestration.deleteVolume(volume);
154
- const errorLog = logs.find(({ log }) => log?.startsWith('Error:'));
155
- if (errorLog) {
156
- throw new Error(errorLog.log?.replace('Error: ', ''));
157
- }
197
+ // A non-zero exit is a failed download whether or not the container
198
+ // managed to report a reason, so this must throw either way: returning
199
+ // normally here reports success for a volume already deleted, and the run
200
+ // then fails later with the cause lost.
201
+ const reason = findReportedError(logs) ??
202
+ `resource download failed (exit code ${StatusCode})`;
203
+ throw new Error(`${reason}${describeContainerState(state)}`);
158
204
  }
159
205
  this.progressBarReporter.stop(`${syncing ? 'Synced' : 'Downloaded'} resource ${name} completed`);
160
206
  await this.containerOrchestration.stopAndDeleteContainer(container.id);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.27",
3
+ "version": "1.1.29-rc",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@nosana/node",
9
- "version": "1.1.27",
9
+ "version": "1.1.29-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.27",
3
+ "version": "1.1.29-rc",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "nosana-node": "./dist/src/index.js"