@nosana/node 1.1.31 → 1.1.32-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.31",
3
+ "version": "1.1.32-rc",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "nosana-node": "./dist/src/index.js"
@@ -43,6 +43,14 @@ export type ResourceHistory = {
43
43
  };
44
44
  export type VolumeResource = ResourceHistory & {
45
45
  volume: string;
46
+ /**
47
+ * True from the moment the Docker volume is created until its download
48
+ * completes. The entry is written before the download starts so that a
49
+ * crash mid-download cannot orphan the volume: a pending entry is resynced
50
+ * when its resource is requested again, and expired like any other entry
51
+ * otherwise. Absent on entries written before this field existed.
52
+ */
53
+ pending?: boolean;
46
54
  };
47
55
  export declare class DB {
48
56
  db: LowSync<NodeDb>;
@@ -75,7 +75,10 @@ export class VolumeManager {
75
75
  const controller = new AbortController();
76
76
  const savedVolumes = this.repository.getVolumesResources();
77
77
  for (const resource of this.market_required_volumes) {
78
- if (!savedVolumes[createResourceName(resource)]) {
78
+ // A pending entry is a download that never finished — the volume exists
79
+ // but its contents can't be trusted, so it must be synced, not skipped.
80
+ const saved = savedVolumes[createResourceName(resource)];
81
+ if (!saved || saved.pending) {
79
82
  await this.createRemoteVolume(resource, controller);
80
83
  }
81
84
  }
@@ -93,14 +96,20 @@ export class VolumeManager {
93
96
  if (!volumeName) {
94
97
  sync = false;
95
98
  const response = await this.containerOrchestration.createVolume();
96
- // @ts-ignore **PODMAN returns name not Name**
97
- if (response.name) {
98
- // @ts-ignore **PODMAN returns name not Name**
99
- volumeName = response.name;
100
- }
101
- else {
102
- volumeName = response.Name;
103
- }
99
+ // PODMAN returns `name`, Docker returns `Name`.
100
+ volumeName = response.name ?? response.Name;
101
+ // Recorded before the download starts, marked pending. Recording only
102
+ // after success left every crash mid-download with a full-size volume
103
+ // the DB had never heard of, which nothing ever swept. A pending entry
104
+ // is resynced when the resource is requested again and expires like any
105
+ // other entry if it never is.
106
+ this.repository.updateVolumeResource(resourceName, {
107
+ volume: volumeName,
108
+ required: this.market_required_volumes.some((vol) => createResourceName(vol) === resourceName),
109
+ lastUsed: new Date(),
110
+ usage: 0,
111
+ pending: true,
112
+ });
104
113
  }
105
114
  switch (resource.type) {
106
115
  case 'S3':
@@ -153,68 +162,81 @@ export class VolumeManager {
153
162
  }
154
163
  async runResourceManagerContainer(volume, name, args, controller, syncing = false) {
155
164
  const container = await this.containerOrchestration.runContainer(args);
156
- const logStream = await container.logs({
157
- stdout: true,
158
- stderr: false,
159
- follow: true,
160
- abortSignal: liveAbortSignal(controller?.signal),
161
- });
162
- let start = false;
163
- let formatSize = 'kb';
164
- logStream.on('data', (logBuffer) => {
165
- try {
166
- const logString = logBuffer.toString('utf8');
167
- const logJSON = JSON.parse(logString.slice(8, logString.length - 1));
168
- if (!start && logJSON.event === 'status') {
169
- start = true;
170
- const { value, format } = convertFromBytes(logJSON.size.total);
171
- formatSize = format;
172
- this.progressBarReporter.start(`${syncing ? 'Syncing' : 'Downloading'} resource ${name}`, {
173
- format: `{bar} {percentage}% | {value}/{total}${format} | {valueFiles}/{totalFiles} files`,
174
- }, value, 0, {
175
- valueFiles: 0,
176
- totalFiles: logJSON.count.total,
177
- }, Presets.shades_classic);
178
- }
179
- else if (logJSON.event === 'status') {
180
- const { value } = convertFromBytes(logJSON.size.current, formatSize);
181
- this.progressBarReporter.update(value, {
182
- valueFiles: logJSON.count.current,
183
- });
165
+ try {
166
+ const logStream = await container.logs({
167
+ stdout: true,
168
+ stderr: false,
169
+ follow: true,
170
+ abortSignal: liveAbortSignal(controller?.signal),
171
+ });
172
+ let start = false;
173
+ let formatSize = 'kb';
174
+ logStream.on('data', (logBuffer) => {
175
+ try {
176
+ const logString = logBuffer.toString('utf8');
177
+ const logJSON = JSON.parse(logString.slice(8, logString.length - 1));
178
+ if (!start && logJSON.event === 'status') {
179
+ start = true;
180
+ const { value, format } = convertFromBytes(logJSON.size.total);
181
+ formatSize = format;
182
+ this.progressBarReporter.start(`${syncing ? 'Syncing' : 'Downloading'} resource ${name}`, {
183
+ format: `{bar} {percentage}% | {value}/{total}${format} | {valueFiles}/{totalFiles} files`,
184
+ }, value, 0, {
185
+ valueFiles: 0,
186
+ totalFiles: logJSON.count.total,
187
+ }, Presets.shades_classic);
188
+ }
189
+ else if (logJSON.event === 'status') {
190
+ const { value } = convertFromBytes(logJSON.size.current, formatSize);
191
+ this.progressBarReporter.update(value, {
192
+ valueFiles: logJSON.count.current,
193
+ });
194
+ }
184
195
  }
196
+ catch (error) { }
197
+ });
198
+ const { StatusCode } = await container.wait({
199
+ condition: 'not-running',
200
+ abortSignal: liveAbortSignal(controller?.signal),
201
+ });
202
+ if (StatusCode !== 0) {
203
+ const errrorBuffer = await container.logs({
204
+ follow: false,
205
+ stdout: false,
206
+ stderr: true,
207
+ tail: 24999,
208
+ timestamps: true,
209
+ });
210
+ const { logs } = extractLogsAndResultsFromLogBuffer(errrorBuffer, undefined);
211
+ // Read the state before the catch removes it: a killed process logs
212
+ // nothing, so the runtime's own record is the only account of why.
213
+ const state = await container
214
+ .inspect()
215
+ .then(({ State }) => State)
216
+ .catch(() => undefined);
217
+ // A non-zero exit is a failed download whether or not the container
218
+ // managed to report a reason, so this must throw either way: returning
219
+ // normally here reports success for an unfinished volume, and the run
220
+ // 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)}`);
185
224
  }
186
- catch (error) { }
187
- });
188
- const { StatusCode } = await container.wait({
189
- condition: 'not-running',
190
- abortSignal: liveAbortSignal(controller?.signal),
191
- });
192
- // If download failed, remove volume
193
- if (StatusCode !== 0) {
225
+ }
226
+ catch (error) {
194
227
  this.progressBarReporter.stop(`${syncing ? 'Syncing' : 'Downloading'} resource ${name} stopped`);
195
- const errrorBuffer = await container.logs({
196
- follow: false,
197
- stdout: false,
198
- stderr: true,
199
- tail: 24999,
200
- timestamps: true,
201
- });
202
- const { logs } = extractLogsAndResultsFromLogBuffer(errrorBuffer, undefined);
203
- // Read the state before removing it: a killed process logs nothing, so
204
- // the runtime's own record is the only account of why.
205
- const state = await container
206
- .inspect()
207
- .then(({ State }) => State)
208
- .catch(() => undefined);
209
- await container.remove({ force: true });
210
- await this.containerOrchestration.deleteVolume(volume);
211
- // A non-zero exit is a failed download whether or not the container
212
- // managed to report a reason, so this must throw either way: returning
213
- // normally here reports success for a volume already deleted, and the run
214
- // then fails later with the cause lost.
215
- const reason = findReportedError(logs) ??
216
- `resource download failed (exit code ${StatusCode})`;
217
- throw new Error(`${reason}${describeContainerState(state)}`);
228
+ // Force-removal also stops a download an abort left running.
229
+ await container.remove({ force: true }).catch(() => { });
230
+ // The volume goes with the failed download, and its entry goes with the
231
+ // volume. If the deletion itself fails, the entry must stay: a pending
232
+ // entry over a leftover volume is resynced or expired later, which is
233
+ // the point of writing it up front.
234
+ try {
235
+ await this.containerOrchestration.deleteVolume(volume);
236
+ this.repository.deleteVolumeResource(name);
237
+ }
238
+ catch { }
239
+ throw error;
218
240
  }
219
241
  this.progressBarReporter.stop(`${syncing ? 'Synced' : 'Downloaded'} resource ${name} completed`);
220
242
  await this.containerOrchestration.stopAndDeleteContainer(container.id);
@@ -226,6 +248,8 @@ export class VolumeManager {
226
248
  required: this.market_required_volumes.some((vol) => createResourceName(vol) === bucket),
227
249
  lastUsed: new Date(),
228
250
  usage: volumeObj?.usage + 1 || 1,
251
+ // Only a completed sync reaches here, which is what pending means.
252
+ pending: false,
229
253
  });
230
254
  }
231
255
  async hasVolume(resource) {
@@ -278,7 +302,10 @@ export class VolumeManager {
278
302
  if (hoursSinceLastUsed > 24) {
279
303
  try {
280
304
  await this.containerOrchestration.deleteVolume(volume);
281
- this.repository.deleteVolumeResource(volume);
305
+ // Keyed by resource name — `volume` is the Docker volume ID, which
306
+ // this map is not keyed by, so passing it left the entry behind
307
+ // pointing at a deleted volume.
308
+ this.repository.deleteVolumeResource(resource);
282
309
  }
283
310
  catch (err) {
284
311
  const message = err.json.message;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.31",
3
+ "version": "1.1.32-rc",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@nosana/node",
9
- "version": "1.1.31",
9
+ "version": "1.1.32-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.31",
3
+ "version": "1.1.32-rc",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "nosana-node": "./dist/src/index.js"