@kapeta/local-cluster-service 0.12.1 → 0.14.0

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/index.js +4 -0
  3. package/dist/cjs/src/api.d.ts +3 -0
  4. package/dist/cjs/src/api.js +22 -0
  5. package/dist/cjs/src/assetManager.d.ts +3 -1
  6. package/dist/cjs/src/assetManager.js +20 -4
  7. package/dist/cjs/src/assets/routes.js +23 -2
  8. package/dist/cjs/src/containerManager.js +130 -109
  9. package/dist/cjs/src/instanceManager.d.ts +4 -3
  10. package/dist/cjs/src/instanceManager.js +80 -59
  11. package/dist/cjs/src/instances/routes.js +19 -11
  12. package/dist/cjs/src/operatorManager.d.ts +5 -3
  13. package/dist/cjs/src/operatorManager.js +34 -23
  14. package/dist/cjs/src/providerManager.js +1 -1
  15. package/dist/cjs/src/repositoryManager.d.ts +2 -4
  16. package/dist/cjs/src/repositoryManager.js +51 -66
  17. package/dist/cjs/src/socketManager.js +1 -1
  18. package/dist/cjs/src/taskManager.d.ts +64 -0
  19. package/dist/cjs/src/taskManager.js +161 -0
  20. package/dist/cjs/src/tasks/routes.d.ts +3 -0
  21. package/dist/cjs/src/tasks/routes.js +35 -0
  22. package/dist/esm/index.js +4 -0
  23. package/dist/esm/src/api.d.ts +3 -0
  24. package/dist/esm/src/api.js +17 -0
  25. package/dist/esm/src/assetManager.d.ts +3 -1
  26. package/dist/esm/src/assetManager.js +20 -4
  27. package/dist/esm/src/assets/routes.js +23 -2
  28. package/dist/esm/src/containerManager.js +130 -109
  29. package/dist/esm/src/instanceManager.d.ts +4 -3
  30. package/dist/esm/src/instanceManager.js +80 -59
  31. package/dist/esm/src/instances/routes.js +19 -11
  32. package/dist/esm/src/operatorManager.d.ts +5 -3
  33. package/dist/esm/src/operatorManager.js +34 -23
  34. package/dist/esm/src/providerManager.js +1 -1
  35. package/dist/esm/src/repositoryManager.d.ts +2 -4
  36. package/dist/esm/src/repositoryManager.js +51 -66
  37. package/dist/esm/src/socketManager.js +1 -1
  38. package/dist/esm/src/taskManager.d.ts +64 -0
  39. package/dist/esm/src/taskManager.js +157 -0
  40. package/dist/esm/src/tasks/routes.d.ts +3 -0
  41. package/dist/esm/src/tasks/routes.js +30 -0
  42. package/index.ts +9 -0
  43. package/package.json +2 -1
  44. package/src/api.ts +21 -0
  45. package/src/assetManager.ts +28 -4
  46. package/src/assets/routes.ts +24 -2
  47. package/src/containerManager.ts +151 -126
  48. package/src/instanceManager.ts +106 -70
  49. package/src/instances/routes.ts +18 -12
  50. package/src/operatorManager.ts +46 -28
  51. package/src/providerManager.ts +1 -1
  52. package/src/repositoryManager.ts +65 -63
  53. package/src/socketManager.ts +1 -1
  54. package/src/taskManager.ts +223 -0
  55. package/src/tasks/routes.ts +38 -0
  56. package/src/utils/BlockInstanceRunner.ts +0 -2
@@ -62,6 +62,10 @@ class AssetManager {
62
62
  });
63
63
  }
64
64
 
65
+ public clearCache() {
66
+ this.cache.flushAll();
67
+ }
68
+
65
69
  /**
66
70
  *
67
71
  * @param {string[]} [assetKinds]
@@ -98,23 +102,32 @@ class AssetManager {
98
102
  return asset.data;
99
103
  }
100
104
 
101
- async getAsset(ref: string, noCache: boolean = false): Promise<EnrichedAsset | undefined> {
105
+ async getAsset(
106
+ ref: string,
107
+ noCache: boolean = false,
108
+ autoFetch: boolean = true
109
+ ): Promise<EnrichedAsset | undefined> {
102
110
  ref = normalizeKapetaUri(ref);
103
111
  const cacheKey = `getAsset:${ref}`;
104
112
  if (!noCache && this.cache.has(cacheKey)) {
105
113
  return this.cache.get(cacheKey);
106
114
  }
107
115
  const uri = parseKapetaUri(ref);
108
- await repositoryManager.ensureAsset(uri.handle, uri.name, uri.version);
116
+ if (autoFetch) {
117
+ await repositoryManager.ensureAsset(uri.handle, uri.name, uri.version, true);
118
+ }
109
119
 
110
120
  let asset = definitionsManager
111
121
  .getDefinitions()
112
122
  .map(enrichAsset)
113
123
  .find((a) => parseKapetaUri(a.ref).equals(uri));
114
- if (!asset) {
124
+ if (autoFetch && !asset) {
115
125
  throw new Error('Asset not found: ' + ref);
116
126
  }
117
- this.cache.set(cacheKey, asset);
127
+ if (asset) {
128
+ this.cache.set(cacheKey, asset);
129
+ }
130
+
118
131
  return asset;
119
132
  }
120
133
 
@@ -189,6 +202,17 @@ class AssetManager {
189
202
  this.cache.flushAll();
190
203
  await Actions.uninstall(progressListener, [asset.ref]);
191
204
  }
205
+
206
+ async installAsset(ref: string) {
207
+ const asset = await this.getAsset(ref, true, false);
208
+ if (asset) {
209
+ throw new Error('Asset already installed: ' + ref);
210
+ }
211
+ const uri = parseKapetaUri(ref);
212
+ console.log('Installing %s', ref);
213
+
214
+ return await repositoryManager.ensureAsset(uri.handle, uri.name, uri.version, false);
215
+ }
192
216
  }
193
217
 
194
218
  export const assetManager = new AssetManager();
@@ -32,7 +32,7 @@ router.use('/', stringBody);
32
32
  * Get all local assets available
33
33
  */
34
34
  router.get('/', (req: Request, res: Response) => {
35
- res.send(assetManager.getAssets());
35
+ res.send(assetManager.getAssets([]));
36
36
  });
37
37
 
38
38
  /**
@@ -44,8 +44,15 @@ router.get('/read', async (req: Request, res: Response) => {
44
44
  return;
45
45
  }
46
46
 
47
+ const ensure = req.query.ensure !== 'false';
48
+
47
49
  try {
48
- res.send(await assetManager.getAsset(req.query.ref as string, true));
50
+ const asset = await assetManager.getAsset(req.query.ref as string, true, ensure);
51
+ if (asset) {
52
+ res.send(asset);
53
+ } else {
54
+ res.status(404).send({ error: 'Asset not found' });
55
+ }
49
56
  } catch (err: any) {
50
57
  res.status(400).send({ error: err.message });
51
58
  }
@@ -129,4 +136,19 @@ router.put('/import', async (req: Request, res: Response) => {
129
136
  }
130
137
  });
131
138
 
139
+ router.put('/install', async (req: Request, res: Response) => {
140
+ if (!req.query.ref) {
141
+ res.status(400).send({ error: 'Query parameter "ref" is missing' });
142
+ return;
143
+ }
144
+
145
+ try {
146
+ const tasks = await assetManager.installAsset(req.query.ref as string);
147
+ const taskIds = tasks?.map((t) => t.id) ?? [];
148
+ res.status(200).send(taskIds);
149
+ } catch (err: any) {
150
+ res.status(400).send({ error: err.message });
151
+ }
152
+ });
153
+
132
154
  export default router;
@@ -15,6 +15,7 @@ import { socketManager } from './socketManager';
15
15
  import { handlers as ArtifactHandlers } from '@kapeta/nodejs-registry-utils';
16
16
  import { progressListener } from './progressListener';
17
17
  import { KapetaAPI } from '@kapeta/nodejs-api-client';
18
+ import { taskManager, Task } from './taskManager';
18
19
 
19
20
  const EVENT_IMAGE_PULL = 'docker-image-pull';
20
21
 
@@ -67,7 +68,6 @@ const NANO_SECOND = 1000000;
67
68
  const HEALTH_CHECK_INTERVAL = 3000;
68
69
  const HEALTH_CHECK_MAX = 20;
69
70
 
70
-
71
71
  export const HEALTH_CHECK_TIMEOUT = HEALTH_CHECK_INTERVAL * HEALTH_CHECK_MAX * 2;
72
72
 
73
73
  const promisifyStream = (stream: ReadStream, handler: (d: string | Buffer) => void) =>
@@ -238,151 +238,176 @@ class ContainerManager {
238
238
  return false;
239
239
  }
240
240
 
241
- const timeStarted = Date.now();
242
- socketManager.emitGlobal(EVENT_IMAGE_PULL, { image, percent: -1 });
241
+ let friendlyImageName = image;
242
+ const imageParts = imageName.split('/');
243
+ if (imageParts.length > 2) {
244
+ //Strip the registry to make the name shorter
245
+ friendlyImageName = `${imageParts.slice(1).join('/')}:${tag}`;
246
+ }
243
247
 
244
- const api = new KapetaAPI();
245
- const accessToken = await api.getAccessToken();
248
+ const taskName = `Pulling image ${friendlyImageName}`;
249
+
250
+ const processor = async (task: Task) => {
251
+ const timeStarted = Date.now();
252
+ const api = new KapetaAPI();
253
+ const accessToken = await api.getAccessToken();
254
+
255
+ const auth = image.startsWith('docker.kapeta.com/')
256
+ ? {
257
+ username: 'kapeta',
258
+ password: accessToken,
259
+ serveraddress: 'docker.kapeta.com',
260
+ }
261
+ : {};
262
+
263
+ const stream = (await this.docker().image.create(auth, {
264
+ fromImage: imageName,
265
+ tag: tag,
266
+ })) as ReadStream;
267
+
268
+ const chunks: {
269
+ [p: string]: {
270
+ downloading: {
271
+ total: number;
272
+ current: number;
273
+ };
274
+ extracting: {
275
+ total: number;
276
+ current: number;
277
+ };
278
+ done: boolean;
279
+ };
280
+ } = {};
281
+
282
+ let lastEmitted = Date.now();
283
+ await promisifyStream(stream, (rawData) => {
284
+ const lines = rawData.toString().trim().split('\n');
285
+ lines.forEach((line) => {
286
+ const data = JSON.parse(line);
287
+ if (
288
+ ![
289
+ 'Waiting',
290
+ 'Downloading',
291
+ 'Extracting',
292
+ 'Download complete',
293
+ 'Pull complete',
294
+ 'Already exists',
295
+ ].includes(data.status)
296
+ ) {
297
+ return;
298
+ }
246
299
 
247
- const auth = image.startsWith('docker.kapeta.com/')
248
- ? {
249
- username: 'kapeta',
250
- password: accessToken,
251
- serveraddress: 'docker.kapeta.com',
252
- }
253
- : {};
300
+ if (!chunks[data.id]) {
301
+ chunks[data.id] = {
302
+ downloading: {
303
+ total: 0,
304
+ current: 0,
305
+ },
306
+ extracting: {
307
+ total: 0,
308
+ current: 0,
309
+ },
310
+ done: false,
311
+ };
312
+ }
254
313
 
255
- const stream = (await this.docker().image.create(auth, {
256
- fromImage: imageName,
257
- tag: tag,
258
- })) as ReadStream;
314
+ const chunk = chunks[data.id];
315
+
316
+ switch (data.status) {
317
+ case 'Downloading':
318
+ chunk.downloading = data.progressDetail;
319
+ break;
320
+ case 'Extracting':
321
+ chunk.extracting = data.progressDetail;
322
+ break;
323
+ case 'Download complete':
324
+ chunk.downloading.current = chunks[data.id].downloading.total;
325
+ break;
326
+ case 'Pull complete':
327
+ chunk.extracting.current = chunks[data.id].extracting.total;
328
+ chunk.done = true;
329
+ break;
330
+ case 'Already exists':
331
+ // Force layer to be done
332
+ chunk.downloading.current = 1;
333
+ chunk.downloading.total = 1;
334
+ chunk.extracting.current = 1;
335
+ chunk.extracting.total = 1;
336
+ chunk.done = true;
337
+ break;
338
+ }
339
+ });
259
340
 
260
- const chunks: {
261
- [p: string]: {
262
- downloading: {
263
- total: number;
264
- current: number;
265
- };
266
- extracting: {
267
- total: number;
268
- current: number;
269
- };
270
- done: boolean;
271
- };
272
- } = {};
273
-
274
- let lastEmitted = Date.now();
275
- await promisifyStream(stream, (rawData) => {
276
- const lines = rawData.toString().trim().split('\n');
277
- lines.forEach((line) => {
278
- const data = JSON.parse(line);
279
- if (
280
- ![
281
- 'Waiting',
282
- 'Downloading',
283
- 'Extracting',
284
- 'Download complete',
285
- 'Pull complete',
286
- 'Already exists',
287
- ].includes(data.status)
288
- ) {
341
+ if (Date.now() - lastEmitted < 1000) {
289
342
  return;
290
343
  }
291
344
 
292
- if (!chunks[data.id]) {
293
- chunks[data.id] = {
294
- downloading: {
295
- total: 0,
296
- current: 0,
297
- },
298
- extracting: {
299
- total: 0,
300
- current: 0,
301
- },
302
- done: false,
303
- };
304
- }
305
-
306
- const chunk = chunks[data.id];
307
-
308
- switch (data.status) {
309
- case 'Downloading':
310
- chunk.downloading = data.progressDetail;
311
- break;
312
- case 'Extracting':
313
- chunk.extracting = data.progressDetail;
314
- break;
315
- case 'Download complete':
316
- chunk.downloading.current = chunks[data.id].downloading.total;
317
- break;
318
- case 'Pull complete':
319
- chunk.extracting.current = chunks[data.id].extracting.total;
320
- chunk.done = true;
321
- break;
322
- case 'Already exists':
323
- // Force layer to be done
324
- chunk.downloading.current = 1;
325
- chunk.downloading.total = 1;
326
- chunk.extracting.current = 1;
327
- chunk.extracting.total = 1;
328
- chunk.done = true;
329
- break;
330
- }
331
- });
345
+ const chunkList = Object.values(chunks);
346
+ let totals = {
347
+ downloading: {
348
+ total: 0,
349
+ current: 0,
350
+ },
351
+ extracting: {
352
+ total: 0,
353
+ current: 0,
354
+ },
355
+ total: chunkList.length,
356
+ done: 0,
357
+ };
332
358
 
333
- if (Date.now() - lastEmitted < 1000) {
334
- return;
335
- }
359
+ chunkList.forEach((chunk) => {
360
+ if (chunk.downloading.current > 0) {
361
+ totals.downloading.current += chunk.downloading.current;
362
+ }
336
363
 
337
- const chunkList = Object.values(chunks);
338
- let totals = {
339
- downloading: {
340
- total: 0,
341
- current: 0,
342
- },
343
- extracting: {
344
- total: 0,
345
- current: 0,
346
- },
347
- total: chunkList.length,
348
- done: 0,
349
- };
364
+ if (chunk.downloading.total > 0) {
365
+ totals.downloading.total += chunk.downloading.total;
366
+ }
350
367
 
351
- chunkList.forEach((chunk) => {
352
- if (chunk.downloading.current > 0) {
353
- totals.downloading.current += chunk.downloading.current;
354
- }
368
+ if (chunk.extracting.current > 0) {
369
+ totals.extracting.current += chunk.extracting.current;
370
+ }
355
371
 
356
- if (chunk.downloading.total > 0) {
357
- totals.downloading.total += chunk.downloading.total;
358
- }
372
+ if (chunk.extracting.total > 0) {
373
+ totals.extracting.total += chunk.extracting.total;
374
+ }
359
375
 
360
- if (chunk.extracting.current > 0) {
361
- totals.extracting.current += chunk.extracting.current;
362
- }
376
+ if (chunk.done) {
377
+ totals.done++;
378
+ }
379
+ });
363
380
 
364
- if (chunk.extracting.total > 0) {
365
- totals.extracting.total += chunk.extracting.total;
366
- }
381
+ const progress = totals.total > 0 ? (totals.done / totals.total) * 100 : 0;
367
382
 
368
- if (chunk.done) {
369
- totals.done++;
370
- }
383
+ task.metadata = {
384
+ ...task.metadata,
385
+ image,
386
+ progress,
387
+ status: totals,
388
+ timeTaken: Date.now() - timeStarted,
389
+ };
390
+ task.emitUpdate();
391
+ lastEmitted = Date.now();
392
+ //console.log('Pulling image %s: %s % [done: %s, total: %s]', image, Math.round(percent), totals.done, totals.total);
371
393
  });
372
394
 
373
- const percent = totals.total > 0 ? (totals.done / totals.total) * 100 : 0;
374
- //We emit at most every second to not spam the client
375
- socketManager.emitGlobal(EVENT_IMAGE_PULL, {
395
+ task.metadata = {
396
+ ...task.metadata,
376
397
  image,
377
- percent,
378
- status: totals,
398
+ progress: 100,
379
399
  timeTaken: Date.now() - timeStarted,
380
- });
381
- lastEmitted = Date.now();
382
- //console.log('Pulling image %s: %s % [done: %s, total: %s]', image, Math.round(percent), totals.done, totals.total);
400
+ };
401
+ task.emitUpdate();
402
+ };
403
+
404
+ const task = taskManager.add(`docker:image:pull:${image}`, processor, {
405
+ name: taskName,
406
+ image,
407
+ progress: -1,
383
408
  });
384
409
 
385
- socketManager.emitGlobal(EVENT_IMAGE_PULL, { image, percent: 100, timeTaken: Date.now() - timeStarted });
410
+ await task.wait();
386
411
 
387
412
  return true;
388
413
  }
@@ -9,11 +9,12 @@ import { assetManager } from './assetManager';
9
9
  import { containerManager, HEALTH_CHECK_TIMEOUT } from './containerManager';
10
10
  import { configManager } from './configManager';
11
11
  import { DesiredInstanceStatus, InstanceInfo, InstanceOwner, InstanceStatus, InstanceType, LogEntry } from './types';
12
- import {BlockDefinitionSpec, BlockInstance, Plan} from '@kapeta/schemas';
12
+ import { BlockDefinitionSpec, BlockInstance, Plan } from '@kapeta/schemas';
13
13
  import { getBlockInstanceContainerName, normalizeKapetaUri } from './utils/utils';
14
14
  import { KIND_OPERATOR, operatorManager } from './operatorManager';
15
15
  import { parseKapetaUri } from '@kapeta/nodejs-utils';
16
16
  import { definitionsManager } from './definitionsManager';
17
+ import { Task, taskManager } from './taskManager';
17
18
 
18
19
  const CHECK_INTERVAL = 5000;
19
20
  const DEFAULT_HEALTH_PORT_TYPE = 'rest';
@@ -74,7 +75,9 @@ export class InstanceManager {
74
75
 
75
76
  const instanceIds = plan.spec.blocks.map((block) => block.id);
76
77
 
77
- return this._instances.filter((instance) => instance.systemId === systemId && instanceIds.includes(instance.instanceId));
78
+ return this._instances.filter(
79
+ (instance) => instance.systemId === systemId && instanceIds.includes(instance.instanceId)
80
+ );
78
81
  }
79
82
 
80
83
  public getInstance(systemId: string, instanceId: string) {
@@ -271,35 +274,51 @@ export class InstanceManager {
271
274
  });
272
275
  }
273
276
 
274
- public async startAllForPlan(systemId: string): Promise<InstanceInfo[]> {
277
+ public async startAllForPlan(systemId: string): Promise<Task<InstanceInfo[]>> {
275
278
  systemId = normalizeKapetaUri(systemId);
276
279
  const plan = await assetManager.getPlan(systemId, true);
277
280
  if (!plan) {
278
- throw new Error('Plan not found: ' + systemId);
281
+ throw new Error(`Plan not found: ${systemId}`);
279
282
  }
280
283
 
281
284
  if (!plan.spec.blocks) {
282
- console.warn('No blocks found in plan', systemId);
283
- return [];
285
+ throw new Error(`No blocks found in plan: ${systemId}`);
284
286
  }
285
287
 
286
- let promises: Promise<InstanceInfo>[] = [];
287
- let errors = [];
288
- for (let blockInstance of Object.values(plan.spec.blocks as BlockInstance[])) {
289
- try {
290
- promises.push(this.start(systemId, blockInstance.id));
291
- } catch (e) {
292
- errors.push(e);
293
- }
294
- }
288
+ return taskManager.add(
289
+ `plan:start:${systemId}`,
290
+ async () => {
291
+ let promises: Promise<InstanceInfo>[] = [];
292
+ let errors = [];
293
+ for (let blockInstance of Object.values(plan.spec.blocks as BlockInstance[])) {
294
+ try {
295
+ promises.push(
296
+ this.start(systemId, blockInstance.id).then((taskOrInstance) => {
297
+ if (taskOrInstance instanceof Task) {
298
+ return taskOrInstance.wait();
299
+ }
300
+ return taskOrInstance;
301
+ })
302
+ );
303
+ } catch (e) {
304
+ errors.push(e);
305
+ }
306
+ }
295
307
 
296
- const settled = await Promise.allSettled(promises);
308
+ const settled = await Promise.allSettled(promises);
297
309
 
298
- if (errors.length > 0) {
299
- throw errors[0];
300
- }
310
+ if (errors.length > 0) {
311
+ throw errors[0];
312
+ }
301
313
 
302
- return settled.map((p) => (p.status === 'fulfilled' ? p.value : null)).filter((p) => !!p) as InstanceInfo[];
314
+ return settled
315
+ .map((p) => (p.status === 'fulfilled' ? p.value : null))
316
+ .filter((p) => !!p) as InstanceInfo[];
317
+ },
318
+ {
319
+ name: `Starting plan ${systemId}`,
320
+ }
321
+ );
303
322
  }
304
323
 
305
324
  public async stop(systemId: string, instanceId: string) {
@@ -363,14 +382,21 @@ export class InstanceManager {
363
382
  });
364
383
  }
365
384
 
366
- public async stopAllForPlan(systemId: string) {
385
+ public stopAllForPlan(systemId: string) {
367
386
  systemId = normalizeKapetaUri(systemId);
368
387
  const instancesForPlan = this._instances.filter((instance) => instance.systemId === systemId);
369
-
370
- return this.stopInstances(instancesForPlan);
388
+ return taskManager.add(
389
+ `plan:stop:${systemId}`,
390
+ async () => {
391
+ return this.stopInstances(instancesForPlan);
392
+ },
393
+ {
394
+ name: `Stopping plan ${systemId}`,
395
+ }
396
+ );
371
397
  }
372
398
 
373
- public async start(systemId: string, instanceId: string): Promise<InstanceInfo> {
399
+ public async start(systemId: string, instanceId: string): Promise<InstanceInfo | Task<InstanceInfo>> {
374
400
  return this.exclusive(systemId, instanceId, async () => {
375
401
  systemId = normalizeKapetaUri(systemId);
376
402
  const plan = await assetManager.getPlan(systemId, true);
@@ -463,53 +489,63 @@ export class InstanceManager {
463
489
  }
464
490
 
465
491
  const instanceConfig = await configManager.getConfigForSection(systemId, instanceId);
466
- const runner = new BlockInstanceRunner(systemId);
467
-
468
- const startTime = Date.now();
469
- try {
470
- const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
471
-
472
- instance.status = InstanceStatus.READY;
473
-
474
- return this.saveInternalInstance({
475
- ...instance,
476
- type: processInfo.type,
477
- pid: processInfo.pid ?? -1,
478
- health: null,
479
- portType: processInfo.portType,
480
- status: InstanceStatus.READY,
481
- });
482
- } catch (e: any) {
483
- console.warn('Failed to start instance: ', systemId, instanceId, blockRef, e.message);
484
- const logs: LogEntry[] = [
485
- {
486
- source: 'stdout',
487
- level: 'ERROR',
488
- message: e.message,
489
- time: Date.now(),
490
- },
491
- ];
492
-
493
- const out = await this.saveInternalInstance({
494
- ...instance,
495
- type: InstanceType.UNKNOWN,
496
- pid: null,
497
- health: null,
498
- portType: DEFAULT_HEALTH_PORT_TYPE,
499
- status: InstanceStatus.FAILED,
500
- errorMessage: e.message ?? 'Failed to start - Check logs for details.',
501
- });
502
-
503
- this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, logs[0]);
504
-
505
- this.emitInstanceEvent(systemId, blockInstance.id, EVENT_INSTANCE_EXITED, {
506
- error: `Failed to start instance: ${e.message}`,
507
- status: EVENT_INSTANCE_EXITED,
508
- instanceId: blockInstance.id,
509
- });
492
+ const task = taskManager.add(
493
+ `instance:start:${systemId}:${instanceId}`,
494
+ async () => {
495
+ const runner = new BlockInstanceRunner(systemId);
496
+ const startTime = Date.now();
497
+ try {
498
+ const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
499
+
500
+ instance.status = InstanceStatus.READY;
501
+
502
+ return this.saveInternalInstance({
503
+ ...instance,
504
+ type: processInfo.type,
505
+ pid: processInfo.pid ?? -1,
506
+ health: null,
507
+ portType: processInfo.portType,
508
+ status: InstanceStatus.READY,
509
+ });
510
+ } catch (e: any) {
511
+ console.warn('Failed to start instance: ', systemId, instanceId, blockRef, e.message);
512
+ const logs: LogEntry[] = [
513
+ {
514
+ source: 'stdout',
515
+ level: 'ERROR',
516
+ message: e.message,
517
+ time: Date.now(),
518
+ },
519
+ ];
520
+
521
+ const out = await this.saveInternalInstance({
522
+ ...instance,
523
+ type: InstanceType.UNKNOWN,
524
+ pid: null,
525
+ health: null,
526
+ portType: DEFAULT_HEALTH_PORT_TYPE,
527
+ status: InstanceStatus.FAILED,
528
+ errorMessage: e.message ?? 'Failed to start - Check logs for details.',
529
+ });
530
+
531
+ this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, logs[0]);
532
+
533
+ this.emitInstanceEvent(systemId, blockInstance.id, EVENT_INSTANCE_EXITED, {
534
+ error: `Failed to start instance: ${e.message}`,
535
+ status: EVENT_INSTANCE_EXITED,
536
+ instanceId: blockInstance.id,
537
+ });
538
+
539
+ return out;
540
+ }
541
+ },
542
+ {
543
+ name: `Starting instance: ${instance.name}`,
544
+ systemId,
545
+ }
546
+ );
510
547
 
511
- return out;
512
- }
548
+ return task;
513
549
  });
514
550
  }
515
551