@kapeta/local-cluster-service 0.11.0 → 0.11.1

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.
@@ -1,5 +1,6 @@
1
1
  import _ from 'lodash';
2
2
  import request from 'request';
3
+ import AsyncLock from 'async-lock';
3
4
  import { BlockInstanceRunner } from './utils/BlockInstanceRunner';
4
5
  import { storageService } from './storageService';
5
6
  import { socketManager } from './socketManager';
@@ -25,6 +26,8 @@ export class InstanceManager {
25
26
 
26
27
  private readonly _instances: InstanceInfo[] = [];
27
28
 
29
+ private readonly instanceLocks:AsyncLock = new AsyncLock();
30
+
28
31
  constructor() {
29
32
  this._instances = storageService.section('instances', []);
30
33
 
@@ -68,6 +71,13 @@ export class InstanceManager {
68
71
  }
69
72
 
70
73
 
74
+ private async exclusive<T = any>(systemId: string, instanceId: string, fn: () => Promise<T>) {
75
+ systemId = normalizeKapetaUri(systemId);
76
+ const key = `${systemId}/${instanceId}`;
77
+ return this.instanceLocks.acquire(key, fn);
78
+ }
79
+
80
+
71
81
  public async getLogs(systemId: string, instanceId: string):Promise<LogEntry[]> {
72
82
  const instance = this.getInstance(systemId, instanceId);
73
83
  if (!instance) {
@@ -142,74 +152,76 @@ export class InstanceManager {
142
152
  instanceId: string,
143
153
  info: Omit<InstanceInfo, 'systemId' | 'instanceId'>
144
154
  ) {
145
- systemId = normalizeKapetaUri(systemId);
146
-
147
- let instance = this.getInstance(systemId, instanceId);
155
+ return this.exclusive(systemId, instanceId, async () => {
156
+ systemId = normalizeKapetaUri(systemId);
148
157
 
149
- //Get target address
150
- const address = await serviceManager.getProviderAddress(
151
- systemId,
152
- instanceId,
153
- info.portType ?? DEFAULT_HEALTH_PORT_TYPE
154
- );
158
+ let instance = this.getInstance(systemId, instanceId);
155
159
 
156
- const healthUrl = this.getHealthUrl(info, address);
160
+ //Get target address
161
+ const address = await serviceManager.getProviderAddress(
162
+ systemId,
163
+ instanceId,
164
+ info.portType ?? DEFAULT_HEALTH_PORT_TYPE
165
+ );
157
166
 
158
- if (instance) {
159
- if (instance.status === InstanceStatus.STOPPING && instance.desiredStatus === DesiredInstanceStatus.STOP) {
160
- //If instance is stopping do not interfere
161
- return;
162
- }
167
+ const healthUrl = this.getHealthUrl(info, address);
163
168
 
164
- if (info.owner === InstanceOwner.EXTERNAL) {
165
- //If instance was started externally - then we want to replace the internal instance with that
166
- if (
167
- instance.owner === InstanceOwner.INTERNAL &&
168
- (instance.status === InstanceStatus.READY ||
169
- instance.status === InstanceStatus.STARTING ||
170
- instance.status === InstanceStatus.UNHEALTHY)
171
- ) {
172
- throw new Error(`Instance ${instanceId} is already running`);
169
+ if (instance) {
170
+ if (instance.status === InstanceStatus.STOPPING && instance.desiredStatus === DesiredInstanceStatus.STOP) {
171
+ //If instance is stopping do not interfere
172
+ return;
173
173
  }
174
174
 
175
- instance.desiredStatus = info.desiredStatus;
176
- instance.owner = info.owner;
177
- instance.status = InstanceStatus.STARTING;
178
- instance.startedAt = Date.now();
179
- }
180
-
181
- instance.pid = info.pid;
182
- instance.address = address;
183
- if (info.type) {
184
- instance.type = info.type;
185
- }
186
- if (healthUrl) {
187
- instance.health = healthUrl;
188
- }
175
+ if (info.owner === InstanceOwner.EXTERNAL) {
176
+ //If instance was started externally - then we want to replace the internal instance with that
177
+ if (
178
+ instance.owner === InstanceOwner.INTERNAL &&
179
+ (instance.status === InstanceStatus.READY ||
180
+ instance.status === InstanceStatus.STARTING ||
181
+ instance.status === InstanceStatus.UNHEALTHY)
182
+ ) {
183
+ throw new Error(`Instance ${instanceId} is already running`);
184
+ }
189
185
 
190
- this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
191
- } else {
192
- //If instance was not found - then we're receiving an externally started instance
193
- instance = {
194
- ...info,
195
- systemId,
196
- instanceId,
197
- status: InstanceStatus.STARTING,
198
- startedAt: Date.now(),
199
- desiredStatus: DesiredInstanceStatus.EXTERNAL,
200
- owner: InstanceOwner.EXTERNAL,
201
- health: healthUrl,
202
- address,
203
- };
186
+ instance.desiredStatus = info.desiredStatus;
187
+ instance.owner = info.owner;
188
+ instance.status = InstanceStatus.STARTING;
189
+ instance.startedAt = Date.now();
190
+ }
204
191
 
205
- this._instances.push(instance);
192
+ instance.pid = info.pid;
193
+ instance.address = address;
194
+ if (info.type) {
195
+ instance.type = info.type;
196
+ }
197
+ if (healthUrl) {
198
+ instance.health = healthUrl;
199
+ }
206
200
 
207
- this.emitSystemEvent(systemId, EVENT_INSTANCE_CREATED, instance);
208
- }
201
+ this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
202
+ } else {
203
+ //If instance was not found - then we're receiving an externally started instance
204
+ instance = {
205
+ ...info,
206
+ systemId,
207
+ instanceId,
208
+ status: InstanceStatus.STARTING,
209
+ startedAt: Date.now(),
210
+ desiredStatus: DesiredInstanceStatus.EXTERNAL,
211
+ owner: InstanceOwner.EXTERNAL,
212
+ health: healthUrl,
213
+ address,
214
+ };
215
+
216
+ this._instances.push(instance);
217
+
218
+ this.emitSystemEvent(systemId, EVENT_INSTANCE_CREATED, instance);
219
+ }
209
220
 
210
- this.save();
221
+ this.save();
211
222
 
212
- return instance;
223
+ return instance;
224
+ });
213
225
  }
214
226
 
215
227
  private getHealthUrl(info: Omit<InstanceInfo, 'systemId' | 'instanceId'>, address: string) {
@@ -225,15 +237,17 @@ export class InstanceManager {
225
237
  }
226
238
 
227
239
  public markAsStopped(systemId: string, instanceId: string) {
228
- systemId = normalizeKapetaUri(systemId);
229
- const instance = _.find(this._instances, { systemId, instanceId });
230
- if (instance && instance.owner === InstanceOwner.EXTERNAL && instance.status !== InstanceStatus.STOPPED) {
231
- instance.status = InstanceStatus.STOPPED;
232
- instance.pid = null;
233
- instance.health = null;
234
- this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
235
- this.save();
236
- }
240
+ return this.exclusive(systemId, instanceId, async () => {
241
+ systemId = normalizeKapetaUri(systemId);
242
+ const instance = _.find(this._instances, {systemId, instanceId});
243
+ if (instance && instance.owner === InstanceOwner.EXTERNAL && instance.status !== InstanceStatus.STOPPED) {
244
+ instance.status = InstanceStatus.STOPPED;
245
+ instance.pid = null;
246
+ instance.health = null;
247
+ this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
248
+ this.save();
249
+ }
250
+ })
237
251
  }
238
252
 
239
253
  public async startAllForPlan(systemId: string): Promise<InstanceInfo[]> {
@@ -267,59 +281,67 @@ export class InstanceManager {
267
281
  return settled.map((p) => (p.status === 'fulfilled' ? p.value : null)).filter((p) => !!p) as InstanceInfo[];
268
282
  }
269
283
 
284
+
270
285
  public async stop(systemId: string, instanceId: string) {
271
- systemId = normalizeKapetaUri(systemId);
272
- const instance = this.getInstance(systemId, instanceId);
273
- if (!instance) {
274
- return;
275
- }
286
+ return this.stopInner(systemId, instanceId, true);
287
+ }
276
288
 
277
- if (instance.status === InstanceStatus.STOPPED) {
278
- return;
279
- }
289
+ private async stopInner(systemId: string, instanceId: string, changeDesired:boolean = false) {
290
+ return this.exclusive(systemId, instanceId, async () => {
291
+ systemId = normalizeKapetaUri(systemId);
292
+ const instance = this.getInstance(systemId, instanceId);
293
+ if (!instance) {
294
+ return;
295
+ }
280
296
 
281
- if (instance.desiredStatus !== DesiredInstanceStatus.EXTERNAL) {
282
- instance.desiredStatus = DesiredInstanceStatus.STOP;
283
- }
297
+ if (instance.status === InstanceStatus.STOPPED) {
298
+ return;
299
+ }
284
300
 
285
- instance.status = InstanceStatus.STOPPING;
301
+ if (changeDesired &&
302
+ instance.desiredStatus !== DesiredInstanceStatus.EXTERNAL) {
303
+ instance.desiredStatus = DesiredInstanceStatus.STOP;
304
+ }
286
305
 
287
- this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
288
- console.log('Stopping instance: %s::%s [desired: %s]', systemId, instanceId, instance.desiredStatus);
289
- this.save();
306
+ instance.status = InstanceStatus.STOPPING;
290
307
 
291
- try {
292
- if (instance.type === 'docker') {
293
- const containerName = getBlockInstanceContainerName(instance.systemId, instance.instanceId);
294
- const container = await containerManager.getContainerByName(containerName);
295
- if (container) {
296
- try {
297
- await container.stop();
298
- instance.status = InstanceStatus.STOPPED;
299
- this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
300
- this.save();
301
- } catch (e) {
302
- console.error('Failed to stop container', e);
308
+ this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
309
+ console.log('Stopping instance: %s::%s [desired: %s]', systemId, instanceId, instance.desiredStatus);
310
+ this.save();
311
+
312
+ try {
313
+ if (instance.type === 'docker') {
314
+ const containerName = getBlockInstanceContainerName(instance.systemId, instance.instanceId);
315
+ const container = await containerManager.getContainerByName(containerName);
316
+ if (container) {
317
+ try {
318
+ await container.stop();
319
+ instance.status = InstanceStatus.STOPPED;
320
+ this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
321
+ this.save();
322
+ } catch (e) {
323
+ console.error('Failed to stop container', e);
324
+ }
325
+ } else {
326
+ console.warn('Container not found', containerName);
303
327
  }
304
- } else {
305
- console.warn('Container not found', containerName);
328
+ return;
329
+ }
330
+
331
+ if (!instance.pid) {
332
+ instance.status = InstanceStatus.STOPPED;
333
+ this.save();
334
+ return;
306
335
  }
307
- return;
308
- }
309
336
 
310
- if (!instance.pid) {
337
+ process.kill(instance.pid as number, 'SIGTERM');
311
338
  instance.status = InstanceStatus.STOPPED;
339
+ this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
312
340
  this.save();
313
- return;
341
+ } catch (e) {
342
+ console.error('Failed to stop process', e);
314
343
  }
315
-
316
- process.kill(instance.pid as number, 'SIGTERM');
317
- instance.status = InstanceStatus.STOPPED;
318
- this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
319
- this.save();
320
- } catch (e) {
321
- console.error('Failed to stop process', e);
322
- }
344
+ });
323
345
  }
324
346
 
325
347
  public async stopAllForPlan(systemId: string) {
@@ -330,126 +352,134 @@ export class InstanceManager {
330
352
  }
331
353
 
332
354
  public async start(systemId: string, instanceId: string): Promise<InstanceInfo> {
333
- systemId = normalizeKapetaUri(systemId);
334
- const plan = await assetManager.getPlan(systemId, true);
335
- if (!plan) {
336
- throw new Error('Plan not found: ' + systemId);
337
- }
338
-
339
- const blockInstance = plan.spec && plan.spec.blocks ? _.find(plan.spec.blocks, { id: instanceId }) : null;
340
- if (!blockInstance) {
341
- throw new Error('Block instance not found: ' + instanceId);
342
- }
343
-
344
- const blockRef = normalizeKapetaUri(blockInstance.block.ref);
345
-
346
- const blockAsset = await assetManager.getAsset(blockRef, true);
347
- if (!blockAsset) {
348
- throw new Error('Block not found: ' + blockRef);
349
- }
350
-
351
- const existingInstance = this.getInstance(systemId, instanceId);
352
-
353
- if (existingInstance) {
354
- if (existingInstance.status === InstanceStatus.READY) {
355
- // Instance is already running
356
- return existingInstance;
355
+ return this.exclusive(systemId, instanceId, async () => {
356
+ systemId = normalizeKapetaUri(systemId);
357
+ const plan = await assetManager.getPlan(systemId, true);
358
+ if (!plan) {
359
+ throw new Error('Plan not found: ' + systemId);
357
360
  }
358
361
 
359
- if (
360
- existingInstance.desiredStatus === DesiredInstanceStatus.RUN &&
361
- existingInstance.status === InstanceStatus.STARTING
362
- ) {
363
- // Internal instance is already starting - don't start it again
364
- return existingInstance;
362
+ const blockInstance = plan.spec && plan.spec.blocks ? _.find(plan.spec.blocks, {id: instanceId}) : null;
363
+ if (!blockInstance) {
364
+ throw new Error('Block instance not found: ' + instanceId);
365
365
  }
366
366
 
367
- if (
368
- existingInstance.owner === InstanceOwner.EXTERNAL &&
369
- existingInstance.status === InstanceStatus.STARTING
370
- ) {
371
- // External instance is already starting - don't start it again
372
- return existingInstance;
367
+ const blockRef = normalizeKapetaUri(blockInstance.block.ref);
368
+
369
+ const blockAsset = await assetManager.getAsset(blockRef, true);
370
+ if (!blockAsset) {
371
+ throw new Error('Block not found: ' + blockRef);
373
372
  }
374
- }
375
373
 
376
- let instance: InstanceInfo = {
377
- systemId,
378
- instanceId,
379
- ref: blockRef,
380
- name: blockAsset.data.metadata.name,
381
- desiredStatus: DesiredInstanceStatus.RUN,
382
- owner: InstanceOwner.INTERNAL,
383
- type: existingInstance?.type ?? InstanceType.UNKNOWN,
384
- status: InstanceStatus.STARTING,
385
- startedAt: Date.now(),
386
- };
387
-
388
- console.log('Starting instance: %s::%s [desired: %s]', systemId, instanceId, instance.desiredStatus);
389
- // Save the instance before starting it, so that we can track the status
390
- await this.saveInternalInstance(instance);
374
+ const existingInstance = this.getInstance(systemId, instanceId);
391
375
 
392
- if (existingInstance) {
393
- // Check if the instance is already running - but after we've commmuicated the desired status
394
- const currentStatus = await this.requestInstanceStatus(existingInstance);
395
- if (currentStatus === InstanceStatus.READY) {
396
- // Instance is already running
397
- return existingInstance;
398
- }
399
- }
376
+ if (existingInstance) {
377
+ if (existingInstance.status === InstanceStatus.READY) {
378
+ // Instance is already running
379
+ return existingInstance;
380
+ }
400
381
 
401
- const instanceConfig = await configManager.getConfigForSection(systemId, instanceId);
402
- const runner = new BlockInstanceRunner(systemId);
382
+ if (
383
+ existingInstance.desiredStatus === DesiredInstanceStatus.RUN &&
384
+ existingInstance.status === InstanceStatus.STARTING
385
+ ) {
386
+ // Internal instance is already starting - don't start it again
387
+ return existingInstance;
388
+ }
403
389
 
404
- const startTime = Date.now();
405
- try {
406
- const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
390
+ if (
391
+ existingInstance.owner === InstanceOwner.EXTERNAL &&
392
+ existingInstance.status === InstanceStatus.STARTING
393
+ ) {
394
+ // External instance is already starting - don't start it again
395
+ return existingInstance;
396
+ }
397
+ }
407
398
 
408
- instance.status = InstanceStatus.READY;
399
+ let instance: InstanceInfo = {
400
+ systemId,
401
+ instanceId,
402
+ ref: blockRef,
403
+ name: blockAsset.data.metadata.name,
404
+ desiredStatus: DesiredInstanceStatus.RUN,
405
+ owner: InstanceOwner.INTERNAL,
406
+ type: existingInstance?.type ?? InstanceType.UNKNOWN,
407
+ status: InstanceStatus.STARTING,
408
+ startedAt: Date.now(),
409
+ };
409
410
 
410
- return this.saveInternalInstance({
411
- ...instance,
412
- type: processInfo.type,
413
- pid: processInfo.pid ?? -1,
414
- health: null,
415
- portType: processInfo.portType,
416
- status: InstanceStatus.READY,
417
- });
418
- } catch (e: any) {
419
- console.warn('Failed to start instance', e);
420
- const logs: LogEntry[] = [
421
- {
422
- source: 'stdout',
423
- level: 'ERROR',
424
- message: e.message,
425
- time: Date.now(),
426
- },
427
- ];
428
-
429
- const out = await this.saveInternalInstance({
430
- ...instance,
431
- type: InstanceType.LOCAL,
432
- pid: null,
433
- health: null,
434
- portType: DEFAULT_HEALTH_PORT_TYPE,
435
- status: InstanceStatus.FAILED,
436
- });
411
+ console.log('Starting instance: %s::%s [desired: %s]', systemId, instanceId, instance.desiredStatus);
412
+ // Save the instance before starting it, so that we can track the status
413
+ await this.saveInternalInstance(instance);
437
414
 
438
- this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, logs[0]);
415
+ if (existingInstance) {
416
+ // Check if the instance is already running - but after we've commmuicated the desired status
417
+ const currentStatus = await this.requestInstanceStatus(existingInstance);
418
+ if (currentStatus === InstanceStatus.READY) {
419
+ // Instance is already running
420
+ return existingInstance;
421
+ }
422
+ }
439
423
 
440
- this.emitInstanceEvent(systemId, blockInstance.id, EVENT_INSTANCE_EXITED, {
441
- error: `Failed to start instance: ${e.message}`,
442
- status: EVENT_INSTANCE_EXITED,
443
- instanceId: blockInstance.id,
444
- });
424
+ const instanceConfig = await configManager.getConfigForSection(systemId, instanceId);
425
+ const runner = new BlockInstanceRunner(systemId);
445
426
 
446
- return out;
447
- }
427
+ const startTime = Date.now();
428
+ try {
429
+ const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
430
+
431
+ instance.status = InstanceStatus.READY;
432
+
433
+ return this.saveInternalInstance({
434
+ ...instance,
435
+ type: processInfo.type,
436
+ pid: processInfo.pid ?? -1,
437
+ health: null,
438
+ portType: processInfo.portType,
439
+ status: InstanceStatus.READY,
440
+ });
441
+ } catch (e: any) {
442
+ console.warn('Failed to start instance', e);
443
+ const logs: LogEntry[] = [
444
+ {
445
+ source: 'stdout',
446
+ level: 'ERROR',
447
+ message: e.message,
448
+ time: Date.now(),
449
+ },
450
+ ];
451
+
452
+ const out = await this.saveInternalInstance({
453
+ ...instance,
454
+ type: InstanceType.LOCAL,
455
+ pid: null,
456
+ health: null,
457
+ portType: DEFAULT_HEALTH_PORT_TYPE,
458
+ status: InstanceStatus.FAILED,
459
+ });
460
+
461
+ this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, logs[0]);
462
+
463
+ this.emitInstanceEvent(systemId, blockInstance.id, EVENT_INSTANCE_EXITED, {
464
+ error: `Failed to start instance: ${e.message}`,
465
+ status: EVENT_INSTANCE_EXITED,
466
+ instanceId: blockInstance.id,
467
+ });
468
+
469
+ return out;
470
+ }
471
+ });
448
472
  }
449
473
 
450
474
  public async restart(systemId: string, instanceId: string) {
451
475
  systemId = normalizeKapetaUri(systemId);
452
- await this.stop(systemId, instanceId);
476
+ await this.stopInner(systemId, instanceId);
477
+
478
+ const existingInstance = this.getInstance(systemId, instanceId);
479
+ if (existingInstance?.desiredStatus === DesiredInstanceStatus.STOP) {
480
+ // Internal instance was marked as stopped - abort restart
481
+ return existingInstance;
482
+ }
453
483
 
454
484
  return this.start(systemId, instanceId);
455
485
  }
@@ -483,7 +513,7 @@ export class InstanceManager {
483
513
  const all = [...this._instances];
484
514
  while (all.length > 0) {
485
515
  // Check a few instances at a time - docker doesn't like too many concurrent requests
486
- const chunk = all.splice(0, 20);
516
+ const chunk = all.splice(0, 30);
487
517
  const promises = chunk.map(async (instance) => {
488
518
  if (!instance.systemId) {
489
519
  return;
@@ -563,7 +593,7 @@ export class InstanceManager {
563
593
  ) {
564
594
  //If the instance is running but we want it to stop, stop it
565
595
  try {
566
- await this.stop(instance.systemId, instance.instanceId);
596
+ await this.stopInner(instance.systemId, instance.instanceId);
567
597
  } catch (e) {
568
598
  console.warn('Failed to stop instance', instance.systemId, instance.instanceId, e);
569
599
  }