@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';
@@ -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 = normalizeKapetaUri(systemId);
109
- let instance = this.getInstance(systemId, instanceId);
110
- //Get target address
111
- const address = await serviceManager.getProviderAddress(systemId, instanceId, info.portType ?? DEFAULT_HEALTH_PORT_TYPE);
112
- const healthUrl = this.getHealthUrl(info, address);
113
- if (instance) {
114
- if (instance.status === InstanceStatus.STOPPING && instance.desiredStatus === DesiredInstanceStatus.STOP) {
115
- //If instance is stopping do not interfere
116
- return;
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
- instance.desiredStatus = info.desiredStatus;
127
- instance.owner = info.owner;
128
- instance.status = InstanceStatus.STARTING;
129
- instance.startedAt = Date.now();
130
- }
131
- instance.pid = info.pid;
132
- instance.address = address;
133
- if (info.type) {
134
- instance.type = info.type;
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
- if (healthUrl) {
137
- instance.health = healthUrl;
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.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
140
- }
141
- else {
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 = normalizeKapetaUri(systemId);
173
- const instance = _.find(this._instances, { systemId, instanceId });
174
- if (instance && instance.owner === InstanceOwner.EXTERNAL && instance.status !== InstanceStatus.STOPPED) {
175
- instance.status = InstanceStatus.STOPPED;
176
- instance.pid = null;
177
- instance.health = null;
178
- this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
179
- this.save();
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 = normalizeKapetaUri(systemId);
210
- const instance = this.getInstance(systemId, instanceId);
211
- if (!instance) {
212
- return;
213
- }
214
- if (instance.status === InstanceStatus.STOPPED) {
215
- return;
216
- }
217
- if (instance.desiredStatus !== DesiredInstanceStatus.EXTERNAL) {
218
- instance.desiredStatus = DesiredInstanceStatus.STOP;
219
- }
220
- instance.status = InstanceStatus.STOPPING;
221
- this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
222
- console.log('Stopping instance: %s::%s [desired: %s]', systemId, instanceId, instance.desiredStatus);
223
- this.save();
224
- try {
225
- if (instance.type === 'docker') {
226
- const containerName = getBlockInstanceContainerName(instance.systemId, instance.instanceId);
227
- const container = await containerManager.getContainerByName(containerName);
228
- if (container) {
229
- try {
230
- await container.stop();
231
- instance.status = InstanceStatus.STOPPED;
232
- this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
233
- this.save();
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
- catch (e) {
236
- console.error('Failed to stop container', e);
255
+ else {
256
+ console.warn('Container not found', containerName);
237
257
  }
258
+ return;
238
259
  }
239
- else {
240
- console.warn('Container not found', containerName);
260
+ if (!instance.pid) {
261
+ instance.status = InstanceStatus.STOPPED;
262
+ this.save();
263
+ return;
241
264
  }
242
- return;
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
- process.kill(instance.pid, 'SIGTERM');
250
- instance.status = InstanceStatus.STOPPED;
251
- this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
252
- this.save();
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 = normalizeKapetaUri(systemId);
265
- const plan = await assetManager.getPlan(systemId, true);
266
- if (!plan) {
267
- throw new Error('Plan not found: ' + systemId);
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
- if (existingInstance.desiredStatus === DesiredInstanceStatus.RUN &&
285
- existingInstance.status === InstanceStatus.STARTING) {
286
- // Internal instance is already starting - don't start it again
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
- if (existingInstance.owner === InstanceOwner.EXTERNAL &&
290
- existingInstance.status === InstanceStatus.STARTING) {
291
- // External instance is already starting - don't start it again
292
- return existingInstance;
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
- let instance = {
296
- systemId,
297
- instanceId,
298
- ref: blockRef,
299
- name: blockAsset.data.metadata.name,
300
- desiredStatus: DesiredInstanceStatus.RUN,
301
- owner: InstanceOwner.INTERNAL,
302
- type: existingInstance?.type ?? InstanceType.UNKNOWN,
303
- status: InstanceStatus.STARTING,
304
- startedAt: Date.now(),
305
- };
306
- console.log('Starting instance: %s::%s [desired: %s]', systemId, instanceId, instance.desiredStatus);
307
- // Save the instance before starting it, so that we can track the status
308
- await this.saveInternalInstance(instance);
309
- if (existingInstance) {
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
- const instanceConfig = await configManager.getConfigForSection(systemId, instanceId);
318
- const runner = new BlockInstanceRunner(systemId);
319
- const startTime = Date.now();
320
- try {
321
- const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
322
- instance.status = InstanceStatus.READY;
323
- return this.saveInternalInstance({
324
- ...instance,
325
- type: processInfo.type,
326
- pid: processInfo.pid ?? -1,
327
- health: null,
328
- portType: processInfo.portType,
329
- status: InstanceStatus.READY,
330
- });
331
- }
332
- catch (e) {
333
- console.warn('Failed to start instance', e);
334
- const logs = [
335
- {
336
- source: 'stdout',
337
- level: 'ERROR',
338
- message: e.message,
339
- time: Date.now(),
340
- },
341
- ];
342
- const out = await this.saveInternalInstance({
343
- ...instance,
344
- type: InstanceType.LOCAL,
345
- pid: null,
346
- health: null,
347
- portType: DEFAULT_HEALTH_PORT_TYPE,
348
- status: InstanceStatus.FAILED,
349
- });
350
- this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, logs[0]);
351
- this.emitInstanceEvent(systemId, blockInstance.id, EVENT_INSTANCE_EXITED, {
352
- error: `Failed to start instance: ${e.message}`,
353
- status: EVENT_INSTANCE_EXITED,
354
- instanceId: blockInstance.id,
355
- });
356
- return out;
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.stop(systemId, instanceId);
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, 20);
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.stop(instance.systemId, instance.instanceId);
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.0",
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",