@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
@@ -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';
|
@@ -19,6 +20,7 @@ const MIN_TIME_RUNNING = 30000; //If something didnt run for more than 30 secs -
|
|
19
20
|
export class InstanceManager {
|
20
21
|
_interval = undefined;
|
21
22
|
_instances = [];
|
23
|
+
instanceLocks = new AsyncLock();
|
22
24
|
constructor() {
|
23
25
|
this._instances = storageService.section('instances', []);
|
24
26
|
// We need to wait a bit before running the first check
|
@@ -50,6 +52,11 @@ export class InstanceManager {
|
|
50
52
|
systemId = normalizeKapetaUri(systemId);
|
51
53
|
return this._instances.find((i) => i.systemId === systemId && i.instanceId === instanceId);
|
52
54
|
}
|
55
|
+
async exclusive(systemId, instanceId, fn) {
|
56
|
+
systemId = normalizeKapetaUri(systemId);
|
57
|
+
const key = `${systemId}/${instanceId}`;
|
58
|
+
return this.instanceLocks.acquire(key, fn);
|
59
|
+
}
|
53
60
|
async getLogs(systemId, instanceId) {
|
54
61
|
const instance = this.getInstance(systemId, instanceId);
|
55
62
|
if (!instance) {
|
@@ -105,57 +112,59 @@ export class InstanceManager {
|
|
105
112
|
* which self-registers with the cluster service locally on startup.
|
106
113
|
*/
|
107
114
|
async registerInstanceFromSDK(systemId, instanceId, info) {
|
108
|
-
systemId
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
if (instance
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
if (info.owner === InstanceOwner.EXTERNAL) {
|
119
|
-
//If instance was started externally - then we want to replace the internal instance with that
|
120
|
-
if (instance.owner === InstanceOwner.INTERNAL &&
|
121
|
-
(instance.status === InstanceStatus.READY ||
|
122
|
-
instance.status === InstanceStatus.STARTING ||
|
123
|
-
instance.status === InstanceStatus.UNHEALTHY)) {
|
124
|
-
throw new Error(`Instance ${instanceId} is already running`);
|
115
|
+
return this.exclusive(systemId, instanceId, async () => {
|
116
|
+
systemId = normalizeKapetaUri(systemId);
|
117
|
+
let instance = this.getInstance(systemId, instanceId);
|
118
|
+
//Get target address
|
119
|
+
const address = await serviceManager.getProviderAddress(systemId, instanceId, info.portType ?? DEFAULT_HEALTH_PORT_TYPE);
|
120
|
+
const healthUrl = this.getHealthUrl(info, address);
|
121
|
+
if (instance) {
|
122
|
+
if (instance.status === InstanceStatus.STOPPING && instance.desiredStatus === DesiredInstanceStatus.STOP) {
|
123
|
+
//If instance is stopping do not interfere
|
124
|
+
return;
|
125
125
|
}
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
126
|
+
if (info.owner === InstanceOwner.EXTERNAL) {
|
127
|
+
//If instance was started externally - then we want to replace the internal instance with that
|
128
|
+
if (instance.owner === InstanceOwner.INTERNAL &&
|
129
|
+
(instance.status === InstanceStatus.READY ||
|
130
|
+
instance.status === InstanceStatus.STARTING ||
|
131
|
+
instance.status === InstanceStatus.UNHEALTHY)) {
|
132
|
+
throw new Error(`Instance ${instanceId} is already running`);
|
133
|
+
}
|
134
|
+
instance.desiredStatus = info.desiredStatus;
|
135
|
+
instance.owner = info.owner;
|
136
|
+
instance.status = InstanceStatus.STARTING;
|
137
|
+
instance.startedAt = Date.now();
|
138
|
+
}
|
139
|
+
instance.pid = info.pid;
|
140
|
+
instance.address = address;
|
141
|
+
if (info.type) {
|
142
|
+
instance.type = info.type;
|
143
|
+
}
|
144
|
+
if (healthUrl) {
|
145
|
+
instance.health = healthUrl;
|
146
|
+
}
|
147
|
+
this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
|
135
148
|
}
|
136
|
-
|
137
|
-
instance
|
149
|
+
else {
|
150
|
+
//If instance was not found - then we're receiving an externally started instance
|
151
|
+
instance = {
|
152
|
+
...info,
|
153
|
+
systemId,
|
154
|
+
instanceId,
|
155
|
+
status: InstanceStatus.STARTING,
|
156
|
+
startedAt: Date.now(),
|
157
|
+
desiredStatus: DesiredInstanceStatus.EXTERNAL,
|
158
|
+
owner: InstanceOwner.EXTERNAL,
|
159
|
+
health: healthUrl,
|
160
|
+
address,
|
161
|
+
};
|
162
|
+
this._instances.push(instance);
|
163
|
+
this.emitSystemEvent(systemId, EVENT_INSTANCE_CREATED, instance);
|
138
164
|
}
|
139
|
-
this.
|
140
|
-
|
141
|
-
|
142
|
-
//If instance was not found - then we're receiving an externally started instance
|
143
|
-
instance = {
|
144
|
-
...info,
|
145
|
-
systemId,
|
146
|
-
instanceId,
|
147
|
-
status: InstanceStatus.STARTING,
|
148
|
-
startedAt: Date.now(),
|
149
|
-
desiredStatus: DesiredInstanceStatus.EXTERNAL,
|
150
|
-
owner: InstanceOwner.EXTERNAL,
|
151
|
-
health: healthUrl,
|
152
|
-
address,
|
153
|
-
};
|
154
|
-
this._instances.push(instance);
|
155
|
-
this.emitSystemEvent(systemId, EVENT_INSTANCE_CREATED, instance);
|
156
|
-
}
|
157
|
-
this.save();
|
158
|
-
return instance;
|
165
|
+
this.save();
|
166
|
+
return instance;
|
167
|
+
});
|
159
168
|
}
|
160
169
|
getHealthUrl(info, address) {
|
161
170
|
let healthUrl = null;
|
@@ -169,15 +178,17 @@ export class InstanceManager {
|
|
169
178
|
return healthUrl;
|
170
179
|
}
|
171
180
|
markAsStopped(systemId, instanceId) {
|
172
|
-
systemId
|
173
|
-
|
174
|
-
|
175
|
-
instance.status
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
+
return this.exclusive(systemId, instanceId, async () => {
|
182
|
+
systemId = normalizeKapetaUri(systemId);
|
183
|
+
const instance = _.find(this._instances, { systemId, instanceId });
|
184
|
+
if (instance && instance.owner === InstanceOwner.EXTERNAL && instance.status !== InstanceStatus.STOPPED) {
|
185
|
+
instance.status = InstanceStatus.STOPPED;
|
186
|
+
instance.pid = null;
|
187
|
+
instance.health = null;
|
188
|
+
this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
|
189
|
+
this.save();
|
190
|
+
}
|
191
|
+
});
|
181
192
|
}
|
182
193
|
async startAllForPlan(systemId) {
|
183
194
|
systemId = normalizeKapetaUri(systemId);
|
@@ -206,54 +217,60 @@ export class InstanceManager {
|
|
206
217
|
return settled.map((p) => (p.status === 'fulfilled' ? p.value : null)).filter((p) => !!p);
|
207
218
|
}
|
208
219
|
async stop(systemId, instanceId) {
|
209
|
-
systemId
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
instance.
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
220
|
+
return this.stopInner(systemId, instanceId, true);
|
221
|
+
}
|
222
|
+
async stopInner(systemId, instanceId, changeDesired = false) {
|
223
|
+
return this.exclusive(systemId, instanceId, async () => {
|
224
|
+
systemId = normalizeKapetaUri(systemId);
|
225
|
+
const instance = this.getInstance(systemId, instanceId);
|
226
|
+
if (!instance) {
|
227
|
+
return;
|
228
|
+
}
|
229
|
+
if (instance.status === InstanceStatus.STOPPED) {
|
230
|
+
return;
|
231
|
+
}
|
232
|
+
if (changeDesired &&
|
233
|
+
instance.desiredStatus !== DesiredInstanceStatus.EXTERNAL) {
|
234
|
+
instance.desiredStatus = DesiredInstanceStatus.STOP;
|
235
|
+
}
|
236
|
+
instance.status = InstanceStatus.STOPPING;
|
237
|
+
this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
|
238
|
+
console.log('Stopping instance: %s::%s [desired: %s]', systemId, instanceId, instance.desiredStatus);
|
239
|
+
this.save();
|
240
|
+
try {
|
241
|
+
if (instance.type === 'docker') {
|
242
|
+
const containerName = getBlockInstanceContainerName(instance.systemId, instance.instanceId);
|
243
|
+
const container = await containerManager.getContainerByName(containerName);
|
244
|
+
if (container) {
|
245
|
+
try {
|
246
|
+
await container.stop();
|
247
|
+
instance.status = InstanceStatus.STOPPED;
|
248
|
+
this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
|
249
|
+
this.save();
|
250
|
+
}
|
251
|
+
catch (e) {
|
252
|
+
console.error('Failed to stop container', e);
|
253
|
+
}
|
234
254
|
}
|
235
|
-
|
236
|
-
console.
|
255
|
+
else {
|
256
|
+
console.warn('Container not found', containerName);
|
237
257
|
}
|
258
|
+
return;
|
238
259
|
}
|
239
|
-
|
240
|
-
|
260
|
+
if (!instance.pid) {
|
261
|
+
instance.status = InstanceStatus.STOPPED;
|
262
|
+
this.save();
|
263
|
+
return;
|
241
264
|
}
|
242
|
-
|
243
|
-
}
|
244
|
-
if (!instance.pid) {
|
265
|
+
process.kill(instance.pid, 'SIGTERM');
|
245
266
|
instance.status = InstanceStatus.STOPPED;
|
267
|
+
this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
|
246
268
|
this.save();
|
247
|
-
return;
|
248
269
|
}
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
}
|
254
|
-
catch (e) {
|
255
|
-
console.error('Failed to stop process', e);
|
256
|
-
}
|
270
|
+
catch (e) {
|
271
|
+
console.error('Failed to stop process', e);
|
272
|
+
}
|
273
|
+
});
|
257
274
|
}
|
258
275
|
async stopAllForPlan(systemId) {
|
259
276
|
systemId = normalizeKapetaUri(systemId);
|
@@ -261,104 +278,111 @@ export class InstanceManager {
|
|
261
278
|
return this.stopInstances(instancesForPlan);
|
262
279
|
}
|
263
280
|
async start(systemId, instanceId) {
|
264
|
-
systemId
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
const blockInstance = plan.spec && plan.spec.blocks ? _.find(plan.spec.blocks, { id: instanceId }) : null;
|
270
|
-
if (!blockInstance) {
|
271
|
-
throw new Error('Block instance not found: ' + instanceId);
|
272
|
-
}
|
273
|
-
const blockRef = normalizeKapetaUri(blockInstance.block.ref);
|
274
|
-
const blockAsset = await assetManager.getAsset(blockRef, true);
|
275
|
-
if (!blockAsset) {
|
276
|
-
throw new Error('Block not found: ' + blockRef);
|
277
|
-
}
|
278
|
-
const existingInstance = this.getInstance(systemId, instanceId);
|
279
|
-
if (existingInstance) {
|
280
|
-
if (existingInstance.status === InstanceStatus.READY) {
|
281
|
-
// Instance is already running
|
282
|
-
return existingInstance;
|
281
|
+
return this.exclusive(systemId, instanceId, async () => {
|
282
|
+
systemId = normalizeKapetaUri(systemId);
|
283
|
+
const plan = await assetManager.getPlan(systemId, true);
|
284
|
+
if (!plan) {
|
285
|
+
throw new Error('Plan not found: ' + systemId);
|
283
286
|
}
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
return existingInstance;
|
287
|
+
const blockInstance = plan.spec && plan.spec.blocks ? _.find(plan.spec.blocks, { id: instanceId }) : null;
|
288
|
+
if (!blockInstance) {
|
289
|
+
throw new Error('Block instance not found: ' + instanceId);
|
288
290
|
}
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
291
|
+
const blockRef = normalizeKapetaUri(blockInstance.block.ref);
|
292
|
+
const blockAsset = await assetManager.getAsset(blockRef, true);
|
293
|
+
if (!blockAsset) {
|
294
|
+
throw new Error('Block not found: ' + blockRef);
|
293
295
|
}
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
// Check if the instance is already running - but after we've commmuicated the desired status
|
311
|
-
const currentStatus = await this.requestInstanceStatus(existingInstance);
|
312
|
-
if (currentStatus === InstanceStatus.READY) {
|
313
|
-
// Instance is already running
|
314
|
-
return existingInstance;
|
296
|
+
const existingInstance = this.getInstance(systemId, instanceId);
|
297
|
+
if (existingInstance) {
|
298
|
+
if (existingInstance.status === InstanceStatus.READY) {
|
299
|
+
// Instance is already running
|
300
|
+
return existingInstance;
|
301
|
+
}
|
302
|
+
if (existingInstance.desiredStatus === DesiredInstanceStatus.RUN &&
|
303
|
+
existingInstance.status === InstanceStatus.STARTING) {
|
304
|
+
// Internal instance is already starting - don't start it again
|
305
|
+
return existingInstance;
|
306
|
+
}
|
307
|
+
if (existingInstance.owner === InstanceOwner.EXTERNAL &&
|
308
|
+
existingInstance.status === InstanceStatus.STARTING) {
|
309
|
+
// External instance is already starting - don't start it again
|
310
|
+
return existingInstance;
|
311
|
+
}
|
315
312
|
}
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
313
|
+
let instance = {
|
314
|
+
systemId,
|
315
|
+
instanceId,
|
316
|
+
ref: blockRef,
|
317
|
+
name: blockAsset.data.metadata.name,
|
318
|
+
desiredStatus: DesiredInstanceStatus.RUN,
|
319
|
+
owner: InstanceOwner.INTERNAL,
|
320
|
+
type: existingInstance?.type ?? InstanceType.UNKNOWN,
|
321
|
+
status: InstanceStatus.STARTING,
|
322
|
+
startedAt: Date.now(),
|
323
|
+
};
|
324
|
+
console.log('Starting instance: %s::%s [desired: %s]', systemId, instanceId, instance.desiredStatus);
|
325
|
+
// Save the instance before starting it, so that we can track the status
|
326
|
+
await this.saveInternalInstance(instance);
|
327
|
+
if (existingInstance) {
|
328
|
+
// Check if the instance is already running - but after we've commmuicated the desired status
|
329
|
+
const currentStatus = await this.requestInstanceStatus(existingInstance);
|
330
|
+
if (currentStatus === InstanceStatus.READY) {
|
331
|
+
// Instance is already running
|
332
|
+
return existingInstance;
|
333
|
+
}
|
334
|
+
}
|
335
|
+
const instanceConfig = await configManager.getConfigForSection(systemId, instanceId);
|
336
|
+
const runner = new BlockInstanceRunner(systemId);
|
337
|
+
const startTime = Date.now();
|
338
|
+
try {
|
339
|
+
const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
|
340
|
+
instance.status = InstanceStatus.READY;
|
341
|
+
return this.saveInternalInstance({
|
342
|
+
...instance,
|
343
|
+
type: processInfo.type,
|
344
|
+
pid: processInfo.pid ?? -1,
|
345
|
+
health: null,
|
346
|
+
portType: processInfo.portType,
|
347
|
+
status: InstanceStatus.READY,
|
348
|
+
});
|
349
|
+
}
|
350
|
+
catch (e) {
|
351
|
+
console.warn('Failed to start instance', e);
|
352
|
+
const logs = [
|
353
|
+
{
|
354
|
+
source: 'stdout',
|
355
|
+
level: 'ERROR',
|
356
|
+
message: e.message,
|
357
|
+
time: Date.now(),
|
358
|
+
},
|
359
|
+
];
|
360
|
+
const out = await this.saveInternalInstance({
|
361
|
+
...instance,
|
362
|
+
type: InstanceType.LOCAL,
|
363
|
+
pid: null,
|
364
|
+
health: null,
|
365
|
+
portType: DEFAULT_HEALTH_PORT_TYPE,
|
366
|
+
status: InstanceStatus.FAILED,
|
367
|
+
});
|
368
|
+
this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, logs[0]);
|
369
|
+
this.emitInstanceEvent(systemId, blockInstance.id, EVENT_INSTANCE_EXITED, {
|
370
|
+
error: `Failed to start instance: ${e.message}`,
|
371
|
+
status: EVENT_INSTANCE_EXITED,
|
372
|
+
instanceId: blockInstance.id,
|
373
|
+
});
|
374
|
+
return out;
|
375
|
+
}
|
376
|
+
});
|
358
377
|
}
|
359
378
|
async restart(systemId, instanceId) {
|
360
379
|
systemId = normalizeKapetaUri(systemId);
|
361
|
-
await this.
|
380
|
+
await this.stopInner(systemId, instanceId);
|
381
|
+
const existingInstance = this.getInstance(systemId, instanceId);
|
382
|
+
if (existingInstance?.desiredStatus === DesiredInstanceStatus.STOP) {
|
383
|
+
// Internal instance was marked as stopped - abort restart
|
384
|
+
return existingInstance;
|
385
|
+
}
|
362
386
|
return this.start(systemId, instanceId);
|
363
387
|
}
|
364
388
|
async stopAll() {
|
@@ -385,7 +409,7 @@ export class InstanceManager {
|
|
385
409
|
const all = [...this._instances];
|
386
410
|
while (all.length > 0) {
|
387
411
|
// Check a few instances at a time - docker doesn't like too many concurrent requests
|
388
|
-
const chunk = all.splice(0,
|
412
|
+
const chunk = all.splice(0, 30);
|
389
413
|
const promises = chunk.map(async (instance) => {
|
390
414
|
if (!instance.systemId) {
|
391
415
|
return;
|
@@ -445,7 +469,7 @@ export class InstanceManager {
|
|
445
469
|
[InstanceStatus.READY, InstanceStatus.STARTING, InstanceStatus.UNHEALTHY].includes(newStatus)) {
|
446
470
|
//If the instance is running but we want it to stop, stop it
|
447
471
|
try {
|
448
|
-
await this.
|
472
|
+
await this.stopInner(instance.systemId, instance.instanceId);
|
449
473
|
}
|
450
474
|
catch (e) {
|
451
475
|
console.warn('Failed to stop instance', instance.systemId, instance.instanceId, e);
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@kapeta/local-cluster-service",
|
3
|
-
"version": "0.11.
|
3
|
+
"version": "0.11.1",
|
4
4
|
"description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
|
5
5
|
"type": "commonjs",
|
6
6
|
"exports": {
|
@@ -50,6 +50,7 @@
|
|
50
50
|
"@kapeta/nodejs-utils": "<2",
|
51
51
|
"@kapeta/schemas": "^0.0.58",
|
52
52
|
"@kapeta/sdk-config": "<2",
|
53
|
+
"async-lock": "^1.4.0",
|
53
54
|
"express": "4.17.1",
|
54
55
|
"express-promise-router": "^4.1.1",
|
55
56
|
"fs-extra": "^11.1.0",
|
@@ -70,6 +71,7 @@
|
|
70
71
|
"@kapeta/eslint-config": "^0.6.1",
|
71
72
|
"@kapeta/prettier-config": "^0.6.0",
|
72
73
|
"@tsconfig/node18": "^18.2.0",
|
74
|
+
"@types/async-lock": "^1.4.0",
|
73
75
|
"@types/express": "^4.17.17",
|
74
76
|
"@types/fs-extra": "^11.0.1",
|
75
77
|
"@types/lodash": "^4.14.195",
|