@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.
- package/CHANGELOG.md +7 -0
- package/dist/cjs/src/instanceManager.d.ts +4 -1
- package/dist/cjs/src/instanceManager.js +215 -191
- package/dist/esm/src/instanceManager.d.ts +4 -1
- package/dist/esm/src/instanceManager.js +215 -191
- package/package.json +3 -1
- package/src/instanceManager.ts +241 -211
package/src/instanceManager.ts
CHANGED
@@ -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
|
146
|
-
|
147
|
-
let instance = this.getInstance(systemId, instanceId);
|
155
|
+
return this.exclusive(systemId, instanceId, async () => {
|
156
|
+
systemId = normalizeKapetaUri(systemId);
|
148
157
|
|
149
|
-
|
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
|
-
|
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
|
-
|
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 (
|
165
|
-
|
166
|
-
|
167
|
-
|
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
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
221
|
+
this.save();
|
211
222
|
|
212
|
-
|
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
|
229
|
-
|
230
|
-
|
231
|
-
instance.status
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
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
|
272
|
-
|
273
|
-
if (!instance) {
|
274
|
-
return;
|
275
|
-
}
|
286
|
+
return this.stopInner(systemId, instanceId, true);
|
287
|
+
}
|
276
288
|
|
277
|
-
|
278
|
-
|
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
|
-
|
282
|
-
|
283
|
-
|
297
|
+
if (instance.status === InstanceStatus.STOPPED) {
|
298
|
+
return;
|
299
|
+
}
|
284
300
|
|
285
|
-
|
301
|
+
if (changeDesired &&
|
302
|
+
instance.desiredStatus !== DesiredInstanceStatus.EXTERNAL) {
|
303
|
+
instance.desiredStatus = DesiredInstanceStatus.STOP;
|
304
|
+
}
|
286
305
|
|
287
|
-
|
288
|
-
console.log('Stopping instance: %s::%s [desired: %s]', systemId, instanceId, instance.desiredStatus);
|
289
|
-
this.save();
|
306
|
+
instance.status = InstanceStatus.STOPPING;
|
290
307
|
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
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
|
-
|
305
|
-
|
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
|
-
|
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
|
-
|
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
|
334
|
-
|
335
|
-
|
336
|
-
|
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
|
-
|
360
|
-
|
361
|
-
|
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
|
-
|
368
|
-
|
369
|
-
|
370
|
-
) {
|
371
|
-
|
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
|
-
|
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
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
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
|
-
|
402
|
-
|
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
|
-
|
405
|
-
|
406
|
-
|
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
|
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
|
-
|
411
|
-
|
412
|
-
|
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
|
-
|
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
|
-
|
441
|
-
|
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
|
-
|
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.
|
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,
|
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.
|
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
|
}
|