@kapeta/local-cluster-service 0.8.3 → 0.9.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.
Files changed (71) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/src/assetManager.js +7 -4
  3. package/dist/cjs/src/clusterService.js +2 -0
  4. package/dist/cjs/src/codeGeneratorManager.js +3 -3
  5. package/dist/cjs/src/config/routes.js +1 -1
  6. package/dist/cjs/src/configManager.js +13 -1
  7. package/dist/cjs/src/containerManager.d.ts +25 -2
  8. package/dist/cjs/src/containerManager.js +51 -16
  9. package/dist/cjs/src/definitionsManager.d.ts +11 -0
  10. package/dist/cjs/src/definitionsManager.js +44 -0
  11. package/dist/cjs/src/filesystemManager.js +0 -2
  12. package/dist/cjs/src/instanceManager.d.ts +23 -47
  13. package/dist/cjs/src/instanceManager.js +416 -235
  14. package/dist/cjs/src/instances/routes.js +23 -14
  15. package/dist/cjs/src/middleware/kapeta.js +7 -0
  16. package/dist/cjs/src/networkManager.js +6 -0
  17. package/dist/cjs/src/operatorManager.js +8 -4
  18. package/dist/cjs/src/providerManager.js +3 -3
  19. package/dist/cjs/src/repositoryManager.js +7 -3
  20. package/dist/cjs/src/serviceManager.js +5 -0
  21. package/dist/cjs/src/types.d.ts +39 -13
  22. package/dist/cjs/src/types.js +28 -0
  23. package/dist/cjs/src/utils/BlockInstanceRunner.d.ts +3 -3
  24. package/dist/cjs/src/utils/BlockInstanceRunner.js +34 -32
  25. package/dist/cjs/src/utils/utils.d.ts +2 -0
  26. package/dist/cjs/src/utils/utils.js +17 -1
  27. package/dist/esm/src/assetManager.js +7 -4
  28. package/dist/esm/src/clusterService.js +2 -0
  29. package/dist/esm/src/codeGeneratorManager.js +3 -3
  30. package/dist/esm/src/config/routes.js +1 -1
  31. package/dist/esm/src/configManager.js +13 -1
  32. package/dist/esm/src/containerManager.d.ts +25 -2
  33. package/dist/esm/src/containerManager.js +50 -15
  34. package/dist/esm/src/definitionsManager.d.ts +11 -0
  35. package/dist/esm/src/definitionsManager.js +38 -0
  36. package/dist/esm/src/filesystemManager.js +0 -2
  37. package/dist/esm/src/instanceManager.d.ts +23 -47
  38. package/dist/esm/src/instanceManager.js +416 -236
  39. package/dist/esm/src/instances/routes.js +23 -14
  40. package/dist/esm/src/middleware/kapeta.js +7 -0
  41. package/dist/esm/src/networkManager.js +6 -0
  42. package/dist/esm/src/operatorManager.js +8 -4
  43. package/dist/esm/src/providerManager.js +3 -3
  44. package/dist/esm/src/repositoryManager.js +7 -3
  45. package/dist/esm/src/serviceManager.js +5 -0
  46. package/dist/esm/src/types.d.ts +39 -13
  47. package/dist/esm/src/types.js +27 -1
  48. package/dist/esm/src/utils/BlockInstanceRunner.d.ts +3 -3
  49. package/dist/esm/src/utils/BlockInstanceRunner.js +35 -33
  50. package/dist/esm/src/utils/utils.d.ts +2 -0
  51. package/dist/esm/src/utils/utils.js +14 -0
  52. package/package.json +2 -1
  53. package/src/assetManager.ts +7 -4
  54. package/src/clusterService.ts +3 -0
  55. package/src/codeGeneratorManager.ts +3 -2
  56. package/src/config/routes.ts +1 -1
  57. package/src/configManager.ts +13 -1
  58. package/src/containerManager.ts +72 -16
  59. package/src/definitionsManager.ts +54 -0
  60. package/src/filesystemManager.ts +0 -2
  61. package/src/instanceManager.ts +495 -266
  62. package/src/instances/routes.ts +23 -17
  63. package/src/middleware/kapeta.ts +10 -0
  64. package/src/networkManager.ts +6 -0
  65. package/src/operatorManager.ts +11 -6
  66. package/src/providerManager.ts +3 -2
  67. package/src/repositoryManager.ts +7 -3
  68. package/src/serviceManager.ts +6 -0
  69. package/src/types.ts +44 -14
  70. package/src/utils/BlockInstanceRunner.ts +39 -36
  71. package/src/utils/utils.ts +18 -0
@@ -1,117 +1,37 @@
1
1
  import _ from 'lodash';
2
2
  import request from 'request';
3
- import EventEmitter from 'events';
4
3
  import { BlockInstanceRunner } from './utils/BlockInstanceRunner';
5
4
  import { storageService } from './storageService';
6
5
  import { socketManager } from './socketManager';
7
6
  import { serviceManager } from './serviceManager';
8
7
  import { assetManager } from './assetManager';
9
- import { containerManager } from './containerManager';
8
+ import { containerManager, HEALTH_CHECK_TIMEOUT } from './containerManager';
10
9
  import { configManager } from './configManager';
11
- const CHECK_INTERVAL = 10000;
10
+ import { DesiredInstanceStatus, InstanceOwner, InstanceStatus, InstanceType } from './types';
11
+ import { getBlockInstanceContainerName, normalizeKapetaUri } from './utils/utils';
12
+ const CHECK_INTERVAL = 5000;
12
13
  const DEFAULT_HEALTH_PORT_TYPE = 'rest';
13
14
  const EVENT_STATUS_CHANGED = 'status-changed';
14
15
  const EVENT_INSTANCE_CREATED = 'instance-created';
15
16
  const EVENT_INSTANCE_EXITED = 'instance-exited';
16
17
  const EVENT_INSTANCE_LOG = 'instance-log';
17
- const STATUS_STARTING = 'starting';
18
- const STATUS_READY = 'ready';
19
- const STATUS_UNHEALTHY = 'unhealthy';
20
- const STATUS_STOPPED = 'stopped';
21
18
  const MIN_TIME_RUNNING = 30000; //If something didnt run for more than 30 secs - it failed
22
- class InstanceManager {
23
- _interval;
24
- /**
25
- * Contains an array of running instances that have self-registered with this
26
- * cluster service. This is done by the Kapeta SDKs
27
- */
19
+ export class InstanceManager {
20
+ _interval = undefined;
28
21
  _instances = [];
29
- /**
30
- * Contains the process info for the instances started by this manager. In memory only
31
- * so can't be relied on for knowing everything that's running.
32
- *
33
- */
34
- _processes = {};
35
22
  constructor() {
36
- this._interval = setInterval(() => this._checkInstances(), CHECK_INTERVAL);
37
23
  this._instances = storageService.section('instances', []);
38
- this._processes = {};
39
- this._checkInstances();
40
- }
41
- _save() {
42
- storageService.put('instances', this._instances);
43
- }
44
- async _checkInstances() {
45
- let changed = false;
46
- for (let i = 0; i < this._instances.length; i++) {
47
- const instance = this._instances[i];
48
- const newStatus = await this._getInstanceStatus(instance);
49
- if (newStatus === STATUS_UNHEALTHY && instance.status === STATUS_STARTING) {
50
- // If instance is starting we consider unhealthy an indication
51
- // that it is still starting
52
- continue;
53
- }
54
- if (instance.status !== newStatus) {
55
- instance.status = newStatus;
56
- console.log('Instance status changed: %s %s -> %s', instance.systemId, instance.instanceId, instance.status);
57
- this._emit(instance.systemId, EVENT_STATUS_CHANGED, instance);
58
- changed = true;
59
- }
60
- }
61
- if (changed) {
62
- this._save();
63
- }
24
+ // We need to wait a bit before running the first check
25
+ this.checkInstancesLater(1000);
64
26
  }
65
- async _isRunning(instance) {
66
- if (!instance.pid) {
67
- return;
68
- }
69
- if (instance.type === 'docker') {
70
- const container = await containerManager.get(instance.pid);
71
- if (!container) {
72
- console.warn('Container not found: %s', instance.pid);
73
- return false;
74
- }
75
- return await container.isRunning();
76
- }
77
- //Otherwise its just a normal process.
78
- //TODO: Handle for Windows
79
- try {
80
- return process.kill(instance.pid, 0);
81
- }
82
- catch (err) {
83
- return err.code === 'EPERM';
84
- }
85
- }
86
- async _getInstanceStatus(instance) {
87
- if (instance.status === STATUS_STOPPED) {
88
- //Will only change when it reregisters
89
- return STATUS_STOPPED;
90
- }
91
- if (!(await this._isRunning(instance))) {
92
- return STATUS_STOPPED;
93
- }
94
- if (!instance.health) {
95
- //No health url means we assume it's healthy as soon as it's running
96
- return STATUS_READY;
97
- }
98
- return new Promise((resolve) => {
99
- if (!instance.health) {
100
- resolve(STATUS_READY);
101
- return;
102
- }
103
- request(instance.health, (err, response) => {
104
- if (err) {
105
- resolve(STATUS_UNHEALTHY);
106
- return;
107
- }
108
- if (response.statusCode > 399) {
109
- resolve(STATUS_UNHEALTHY);
110
- return;
111
- }
112
- resolve(STATUS_READY);
113
- });
114
- });
27
+ checkInstancesLater(time = CHECK_INTERVAL) {
28
+ if (this._interval) {
29
+ clearTimeout(this._interval);
30
+ }
31
+ this._interval = setTimeout(async () => {
32
+ await this.checkInstances();
33
+ this.checkInstancesLater();
34
+ }, time);
115
35
  }
116
36
  getInstances() {
117
37
  if (!this._instances) {
@@ -123,39 +43,67 @@ class InstanceManager {
123
43
  if (!this._instances) {
124
44
  return [];
125
45
  }
46
+ systemId = normalizeKapetaUri(systemId);
126
47
  return this._instances.filter((instance) => instance.systemId === systemId);
127
48
  }
128
- /**
129
- * Get instance information
130
- *
131
- * @param {string} systemId
132
- * @param {string} instanceId
133
- * @return {*}
134
- */
135
49
  getInstance(systemId, instanceId) {
136
- return _.find(this._instances, { systemId, instanceId });
50
+ systemId = normalizeKapetaUri(systemId);
51
+ return this._instances.find((i) => i.systemId === systemId && i.instanceId === instanceId);
52
+ }
53
+ async saveInternalInstance(instance) {
54
+ instance.systemId = normalizeKapetaUri(instance.systemId);
55
+ if (instance.ref) {
56
+ instance.ref = normalizeKapetaUri(instance.ref);
57
+ }
58
+ //Get target address
59
+ let address = await serviceManager.getProviderAddress(instance.systemId, instance.instanceId, instance.portType ?? DEFAULT_HEALTH_PORT_TYPE);
60
+ const healthUrl = this.getHealthUrl(instance, address);
61
+ instance.address = address;
62
+ if (healthUrl) {
63
+ instance.health = healthUrl;
64
+ }
65
+ let existingInstance = this.getInstance(instance.systemId, instance.instanceId);
66
+ if (existingInstance) {
67
+ const ix = this._instances.indexOf(existingInstance);
68
+ this._instances.splice(ix, 1, instance);
69
+ this.emitSystemEvent(instance.systemId, EVENT_STATUS_CHANGED, instance);
70
+ }
71
+ else {
72
+ this._instances.push(instance);
73
+ this.emitSystemEvent(instance.systemId, EVENT_INSTANCE_CREATED, instance);
74
+ }
75
+ this.save();
76
+ return instance;
137
77
  }
138
78
  /**
139
- *
140
- * @param {string} systemId
141
- * @param {string} instanceId
142
- * @param {InstanceInfo} info
143
- * @return {Promise<void>}
79
+ * Method is called when instance is started from the Kapeta SDKs (e.g. NodeJS SDK)
80
+ * which self-registers with the cluster service locally on startup.
144
81
  */
145
- async registerInstance(systemId, instanceId, info) {
82
+ async registerInstanceFromSDK(systemId, instanceId, info) {
83
+ systemId = normalizeKapetaUri(systemId);
146
84
  let instance = this.getInstance(systemId, instanceId);
147
85
  //Get target address
148
- let address = await serviceManager.getProviderAddress(systemId, instanceId, info.portType ?? DEFAULT_HEALTH_PORT_TYPE);
149
- let healthUrl = null;
150
- let health = info.health;
151
- if (health) {
152
- if (health.startsWith('/')) {
153
- health = health.substring(1);
154
- }
155
- healthUrl = address + health;
156
- }
86
+ const address = await serviceManager.getProviderAddress(systemId, instanceId, info.portType ?? DEFAULT_HEALTH_PORT_TYPE);
87
+ const healthUrl = this.getHealthUrl(info, address);
157
88
  if (instance) {
158
- instance.status = STATUS_STARTING;
89
+ if (instance.status === InstanceStatus.STOPPING && instance.desiredStatus === DesiredInstanceStatus.STOP) {
90
+ //If instance is stopping do not interfere
91
+ return;
92
+ }
93
+ if (info.owner === InstanceOwner.EXTERNAL) {
94
+ //If instance was started externally - then we want to replace the internal instance with that
95
+ if (instance.owner === InstanceOwner.INTERNAL &&
96
+ (instance.status === InstanceStatus.READY ||
97
+ instance.status === InstanceStatus.STARTING ||
98
+ instance.status === InstanceStatus.UNHEALTHY)) {
99
+ throw new Error(`Instance ${instanceId} is already running`);
100
+ }
101
+ instance.desiredStatus = info.desiredStatus;
102
+ instance.owner = info.owner;
103
+ instance.internal = undefined;
104
+ instance.status = InstanceStatus.STARTING;
105
+ instance.startedAt = Date.now();
106
+ }
159
107
  instance.pid = info.pid;
160
108
  instance.address = address;
161
109
  if (info.type) {
@@ -164,56 +112,64 @@ class InstanceManager {
164
112
  if (healthUrl) {
165
113
  instance.health = healthUrl;
166
114
  }
167
- this._emit(systemId, EVENT_STATUS_CHANGED, instance);
115
+ this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
168
116
  }
169
117
  else {
118
+ //If instance was not found - then we're receiving an externally started instance
170
119
  instance = {
120
+ ...info,
171
121
  systemId,
172
122
  instanceId,
173
- status: STATUS_STARTING,
174
- pid: info.pid,
175
- type: info.type,
123
+ status: InstanceStatus.STARTING,
124
+ startedAt: Date.now(),
125
+ desiredStatus: DesiredInstanceStatus.EXTERNAL,
126
+ owner: InstanceOwner.EXTERNAL,
176
127
  health: healthUrl,
177
128
  address,
178
129
  };
179
130
  this._instances.push(instance);
180
- this._emit(systemId, EVENT_INSTANCE_CREATED, instance);
131
+ this.emitSystemEvent(systemId, EVENT_INSTANCE_CREATED, instance);
132
+ }
133
+ this.save();
134
+ return instance;
135
+ }
136
+ getHealthUrl(info, address) {
137
+ let healthUrl = null;
138
+ let health = info.health;
139
+ if (health) {
140
+ if (health.startsWith('/')) {
141
+ health = health.substring(1);
142
+ }
143
+ healthUrl = address + health;
181
144
  }
182
- this._save();
145
+ return healthUrl;
183
146
  }
184
- setInstanceAsStopped(systemId, instanceId) {
147
+ markAsStopped(systemId, instanceId) {
148
+ systemId = normalizeKapetaUri(systemId);
185
149
  const instance = _.find(this._instances, { systemId, instanceId });
186
- if (instance) {
187
- instance.status = STATUS_STOPPED;
150
+ if (instance && instance.owner === InstanceOwner.EXTERNAL && instance.status !== InstanceStatus.STOPPED) {
151
+ instance.status = InstanceStatus.STOPPED;
188
152
  instance.pid = null;
189
153
  instance.health = null;
190
- this._emit(systemId, EVENT_STATUS_CHANGED, instance);
191
- this._save();
192
- }
193
- }
194
- _emit(systemId, type, payload) {
195
- try {
196
- socketManager.emit(`${systemId}/instances`, type, payload);
197
- }
198
- catch (e) {
199
- console.warn('Failed to emit instance event: %s', e.message);
154
+ this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
155
+ this.save();
200
156
  }
201
157
  }
202
- async createProcessesForPlan(planRef) {
203
- await this.stopAllForPlan(planRef);
204
- const plan = await assetManager.getPlan(planRef, true);
158
+ async startAllForPlan(systemId) {
159
+ systemId = normalizeKapetaUri(systemId);
160
+ const plan = await assetManager.getPlan(systemId, true);
205
161
  if (!plan) {
206
- throw new Error('Plan not found: ' + planRef);
162
+ throw new Error('Plan not found: ' + systemId);
207
163
  }
208
164
  if (!plan.spec.blocks) {
209
- console.warn('No blocks found in plan', planRef);
165
+ console.warn('No blocks found in plan', systemId);
210
166
  return [];
211
167
  }
212
168
  let promises = [];
213
169
  let errors = [];
214
170
  for (let blockInstance of Object.values(plan.spec.blocks)) {
215
171
  try {
216
- promises.push(this.createProcess(planRef, blockInstance.id));
172
+ promises.push(this.start(systemId, blockInstance.id));
217
173
  }
218
174
  catch (e) {
219
175
  errors.push(e);
@@ -225,88 +181,134 @@ class InstanceManager {
225
181
  }
226
182
  return settled.map((p) => (p.status === 'fulfilled' ? p.value : null)).filter((p) => !!p);
227
183
  }
228
- async _stopInstance(instance) {
229
- if (!instance.pid) {
184
+ async stop(systemId, instanceId) {
185
+ systemId = normalizeKapetaUri(systemId);
186
+ const instance = this.getInstance(systemId, instanceId);
187
+ if (!instance) {
230
188
  return;
231
189
  }
232
- if (instance.status === 'stopped') {
190
+ if (instance.status === InstanceStatus.STOPPED) {
233
191
  return;
234
192
  }
193
+ if (instance.desiredStatus !== DesiredInstanceStatus.EXTERNAL) {
194
+ instance.desiredStatus = DesiredInstanceStatus.STOP;
195
+ }
196
+ instance.status = InstanceStatus.STOPPING;
197
+ this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
198
+ console.log('Stopping instance: %s::%s [desired: %s]', systemId, instanceId, instance.desiredStatus);
199
+ this.save();
235
200
  try {
236
201
  if (instance.type === 'docker') {
237
- const container = await containerManager.get(instance.pid);
202
+ const containerName = getBlockInstanceContainerName(instance.instanceId);
203
+ const container = await containerManager.getContainerByName(containerName);
238
204
  if (container) {
239
205
  try {
240
206
  await container.stop();
207
+ instance.status = InstanceStatus.STOPPED;
208
+ this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
209
+ this.save();
241
210
  }
242
211
  catch (e) {
243
212
  console.error('Failed to stop container', e);
244
213
  }
245
214
  }
215
+ else {
216
+ console.warn('Container not found', containerName);
217
+ }
218
+ return;
219
+ }
220
+ if (!instance.pid) {
221
+ instance.status = InstanceStatus.STOPPED;
222
+ this.save();
246
223
  return;
247
224
  }
248
225
  process.kill(instance.pid, 'SIGTERM');
226
+ instance.status = InstanceStatus.STOPPED;
227
+ this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
228
+ this.save();
249
229
  }
250
230
  catch (e) {
251
231
  console.error('Failed to stop process', e);
252
232
  }
253
233
  }
254
- async stopAllForPlan(planRef) {
255
- if (this._processes[planRef]) {
256
- const promises = [];
257
- console.log('Stopping all processes for plan', planRef);
258
- for (let instance of Object.values(this._processes[planRef])) {
259
- promises.push(instance.stop());
260
- }
261
- await Promise.all(promises);
262
- this._processes[planRef] = {};
263
- }
264
- //Also stop instances not being maintained by the cluster service
265
- const instancesForPlan = this._instances.filter((instance) => instance.systemId === planRef);
266
- const promises = [];
267
- for (let instance of instancesForPlan) {
268
- promises.push(this._stopInstance(instance));
269
- }
270
- await Promise.all(promises);
234
+ async stopAllForPlan(systemId) {
235
+ systemId = normalizeKapetaUri(systemId);
236
+ const instancesForPlan = this._instances.filter((instance) => instance.systemId === systemId);
237
+ return this.stopInstances(instancesForPlan);
271
238
  }
272
- async createProcess(planRef, instanceId) {
273
- const plan = await assetManager.getPlan(planRef, true);
239
+ async start(systemId, instanceId) {
240
+ systemId = normalizeKapetaUri(systemId);
241
+ const plan = await assetManager.getPlan(systemId, true);
274
242
  if (!plan) {
275
- throw new Error('Plan not found: ' + planRef);
243
+ throw new Error('Plan not found: ' + systemId);
276
244
  }
277
245
  const blockInstance = plan.spec && plan.spec.blocks ? _.find(plan.spec.blocks, { id: instanceId }) : null;
278
246
  if (!blockInstance) {
279
247
  throw new Error('Block instance not found: ' + instanceId);
280
248
  }
281
- const blockRef = blockInstance.block.ref;
249
+ const blockRef = normalizeKapetaUri(blockInstance.block.ref);
282
250
  const blockAsset = await assetManager.getAsset(blockRef, true);
283
- const instanceConfig = await configManager.getConfigForSection(planRef, instanceId);
284
251
  if (!blockAsset) {
285
252
  throw new Error('Block not found: ' + blockRef);
286
253
  }
287
- if (!this._processes[planRef]) {
288
- this._processes[planRef] = {};
254
+ const existingInstance = this.getInstance(systemId, instanceId);
255
+ if (existingInstance) {
256
+ if (existingInstance.status === InstanceStatus.READY) {
257
+ // Instance is already running
258
+ return existingInstance;
259
+ }
260
+ if (existingInstance.desiredStatus === DesiredInstanceStatus.RUN &&
261
+ existingInstance.status === InstanceStatus.STARTING) {
262
+ // Internal instance is already starting - don't start it again
263
+ return existingInstance;
264
+ }
265
+ if (existingInstance.owner === InstanceOwner.EXTERNAL &&
266
+ existingInstance.status === InstanceStatus.STARTING) {
267
+ // External instance is already starting - don't start it again
268
+ return existingInstance;
269
+ }
289
270
  }
290
- await this.stopProcess(planRef, instanceId);
291
- const type = blockAsset.version === 'local' ? 'local' : 'docker';
292
- const runner = new BlockInstanceRunner(planRef);
271
+ let instance = {
272
+ systemId,
273
+ instanceId,
274
+ ref: blockRef,
275
+ name: blockAsset.data.metadata.name,
276
+ desiredStatus: DesiredInstanceStatus.RUN,
277
+ owner: InstanceOwner.INTERNAL,
278
+ type: InstanceType.UNKNOWN,
279
+ status: InstanceStatus.STARTING,
280
+ startedAt: Date.now(),
281
+ };
282
+ console.log('Starting instance: %s::%s [desired: %s]', systemId, instanceId, instance.desiredStatus);
283
+ // Save the instance before starting it, so that we can track the status
284
+ await this.saveInternalInstance(instance);
285
+ if (existingInstance) {
286
+ // Check if the instance is already running - but after we've commmuicated the desired status
287
+ const currentStatus = await this.requestInstanceStatus(existingInstance);
288
+ if (currentStatus === InstanceStatus.READY) {
289
+ // Instance is already running
290
+ return existingInstance;
291
+ }
292
+ }
293
+ const instanceConfig = await configManager.getConfigForSection(systemId, instanceId);
294
+ const runner = new BlockInstanceRunner(systemId);
293
295
  const startTime = Date.now();
294
296
  try {
295
- const process = await runner.start(blockRef, instanceId, instanceConfig);
297
+ const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
296
298
  //emit stdout/stderr via sockets
297
- process.output.on('data', (data) => {
299
+ processInfo.output.on('data', (data) => {
298
300
  const payload = {
299
301
  source: 'stdout',
300
302
  level: 'INFO',
301
303
  message: data.toString(),
302
304
  time: Date.now(),
303
305
  };
304
- this._emit(instanceId, EVENT_INSTANCE_LOG, payload);
306
+ this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, payload);
305
307
  });
306
- process.output.on('exit', (exitCode) => {
308
+ processInfo.output.on('exit', (exitCode) => {
307
309
  const timeRunning = Date.now() - startTime;
308
- const instance = this.getInstance(planRef, instanceId);
309
- if (instance?.status === STATUS_READY) {
310
+ const instance = this.getInstance(systemId, instanceId);
311
+ if (instance?.status === InstanceStatus.READY) {
310
312
  //It's already been running
311
313
  return;
312
314
  }
@@ -316,21 +318,31 @@ class InstanceManager {
316
318
  return;
317
319
  }
318
320
  if (exitCode !== 0 || timeRunning < MIN_TIME_RUNNING) {
319
- this._emit(blockInstance.id, EVENT_INSTANCE_EXITED, {
321
+ const instance = this.getInstance(systemId, instanceId);
322
+ if (instance) {
323
+ instance.status = InstanceStatus.FAILED;
324
+ this.save();
325
+ }
326
+ this.emitSystemEvent(systemId, EVENT_INSTANCE_EXITED, {
320
327
  error: 'Failed to start instance',
321
328
  status: EVENT_INSTANCE_EXITED,
322
329
  instanceId: blockInstance.id,
323
330
  });
324
331
  }
325
332
  });
326
- await this.registerInstance(planRef, instanceId, {
327
- type: process.type,
328
- pid: process.pid ?? -1,
333
+ instance.status = InstanceStatus.READY;
334
+ return this.saveInternalInstance({
335
+ ...instance,
336
+ type: processInfo.type,
337
+ pid: processInfo.pid ?? -1,
329
338
  health: null,
330
- portType: process.portType,
331
- status: STATUS_STARTING,
339
+ portType: processInfo.portType,
340
+ status: InstanceStatus.READY,
341
+ internal: {
342
+ logs: processInfo.logs,
343
+ output: processInfo.output,
344
+ },
332
345
  });
333
- return (this._processes[planRef][instanceId] = process);
334
346
  }
335
347
  catch (e) {
336
348
  console.warn('Failed to start instance', e);
@@ -342,77 +354,245 @@ class InstanceManager {
342
354
  time: Date.now(),
343
355
  },
344
356
  ];
345
- await this.registerInstance(planRef, instanceId, {
346
- type: 'local',
357
+ const out = await this.saveInternalInstance({
358
+ ...instance,
359
+ type: InstanceType.LOCAL,
347
360
  pid: null,
348
361
  health: null,
349
362
  portType: DEFAULT_HEALTH_PORT_TYPE,
350
- status: STATUS_UNHEALTHY,
363
+ status: InstanceStatus.FAILED,
351
364
  });
352
- this._emit(instanceId, EVENT_INSTANCE_LOG, logs[0]);
353
- this._emit(blockInstance.id, EVENT_INSTANCE_EXITED, {
365
+ this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, logs[0]);
366
+ this.emitInstanceEvent(systemId, blockInstance.id, EVENT_INSTANCE_EXITED, {
354
367
  error: `Failed to start instance: ${e.message}`,
355
368
  status: EVENT_INSTANCE_EXITED,
356
369
  instanceId: blockInstance.id,
357
370
  });
358
- return (this._processes[planRef][instanceId] = {
359
- pid: -1,
360
- type,
361
- logs: () => logs,
362
- stop: () => Promise.resolve(),
363
- ref: blockRef,
364
- id: instanceId,
365
- name: blockInstance.name,
366
- output: new EventEmitter(),
367
- });
371
+ return out;
368
372
  }
369
373
  }
370
- /**
371
- *
372
- * @param {string} planRef
373
- * @param {string} instanceId
374
- * @return {ProcessInfo|null}
375
- */
376
- getProcessForInstance(planRef, instanceId) {
377
- if (!this._processes[planRef]) {
378
- return null;
374
+ async restart(systemId, instanceId) {
375
+ systemId = normalizeKapetaUri(systemId);
376
+ await this.stop(systemId, instanceId);
377
+ return this.start(systemId, instanceId);
378
+ }
379
+ async stopAll() {
380
+ return this.stopInstances(this._instances);
381
+ }
382
+ async stopInstances(instances) {
383
+ const promises = instances.map((instance) => this.stop(instance.systemId, instance.instanceId));
384
+ await Promise.allSettled(promises);
385
+ this.save();
386
+ }
387
+ save() {
388
+ try {
389
+ storageService.put('instances', this._instances.map((instance) => {
390
+ const copy = { ...instance };
391
+ delete copy.internal;
392
+ return copy;
393
+ }));
394
+ }
395
+ catch (e) {
396
+ console.error('Failed to save instances', this._instances, e);
379
397
  }
380
- return this._processes[planRef][instanceId];
381
398
  }
382
- async restartIfRunning(planRef, instanceId) {
383
- if (!this._processes[planRef] || !this._processes[planRef][instanceId]) {
384
- return;
399
+ async checkInstances() {
400
+ //console.log('\n## Checking instances:');
401
+ let changed = false;
402
+ const all = [...this._instances];
403
+ while (all.length > 0) {
404
+ // Check a few instances at a time - docker doesn't like too many concurrent requests
405
+ const chunk = all.splice(0, 20);
406
+ const promises = chunk.map(async (instance) => {
407
+ if (!instance.systemId) {
408
+ return;
409
+ }
410
+ instance.systemId = normalizeKapetaUri(instance.systemId);
411
+ if (instance.ref) {
412
+ instance.ref = normalizeKapetaUri(instance.ref);
413
+ }
414
+ const newStatus = await this.requestInstanceStatus(instance);
415
+ /*
416
+ console.log('Check instance %s %s: [current: %s, new: %s, desired: %s]',
417
+ instance.systemId, instance.instanceId, instance.status, newStatus, instance.desiredStatus);
418
+ */
419
+ if (newStatus === InstanceStatus.BUSY) {
420
+ // If instance is busy we skip it
421
+ //console.log('Instance %s %s is busy', instance.systemId, instance.instanceId);
422
+ return;
423
+ }
424
+ if (instance.startedAt !== undefined &&
425
+ newStatus === InstanceStatus.UNHEALTHY &&
426
+ instance.startedAt + HEALTH_CHECK_TIMEOUT < Date.now() &&
427
+ instance.status === InstanceStatus.STARTING) {
428
+ // If instance is starting we consider unhealthy an indication
429
+ // that it is still starting
430
+ //console.log('Instance %s %s is still starting', instance.systemId, instance.instanceId);
431
+ return;
432
+ }
433
+ if (instance.status !== newStatus) {
434
+ const oldStatus = instance.status;
435
+ const skipUpdate = (newStatus === InstanceStatus.STOPPED && instance.status === InstanceStatus.FAILED) ||
436
+ ([InstanceStatus.READY, InstanceStatus.UNHEALTHY].includes(newStatus) &&
437
+ instance.status === InstanceStatus.STOPPING &&
438
+ instance.desiredStatus === DesiredInstanceStatus.STOP) ||
439
+ (newStatus === InstanceStatus.STOPPED &&
440
+ instance.status === InstanceStatus.STARTING &&
441
+ instance.desiredStatus === DesiredInstanceStatus.RUN);
442
+ if (!skipUpdate) {
443
+ const oldStatus = instance.status;
444
+ instance.status = newStatus;
445
+ console.log('Instance status changed: %s %s: %s -> %s', instance.systemId, instance.instanceId, oldStatus, instance.status);
446
+ this.emitSystemEvent(instance.systemId, EVENT_STATUS_CHANGED, instance);
447
+ changed = true;
448
+ }
449
+ }
450
+ if (instance.desiredStatus === DesiredInstanceStatus.RUN && newStatus === InstanceStatus.STOPPED) {
451
+ //If the instance is stopped but we want it to run, start it
452
+ try {
453
+ await this.start(instance.systemId, instance.instanceId);
454
+ }
455
+ catch (e) {
456
+ console.warn('Failed to start instance', instance.systemId, instance.instanceId, e);
457
+ }
458
+ return;
459
+ }
460
+ if (instance.desiredStatus === DesiredInstanceStatus.STOP && newStatus === InstanceStatus.READY) {
461
+ //If the instance is running but we want it to stop, stop it
462
+ try {
463
+ await this.stop(instance.systemId, instance.instanceId);
464
+ }
465
+ catch (e) {
466
+ console.warn('Failed to stop instance', instance.systemId, instance.instanceId, e);
467
+ }
468
+ return;
469
+ }
470
+ if (instance.desiredStatus === DesiredInstanceStatus.RUN &&
471
+ instance.status !== newStatus &&
472
+ newStatus === InstanceStatus.UNHEALTHY) {
473
+ //If the instance is unhealthy, try to restart it
474
+ console.log('Restarting unhealthy instance', instance);
475
+ try {
476
+ await this.restart(instance.systemId, instance.instanceId);
477
+ }
478
+ catch (e) {
479
+ console.warn('Failed to restart instance', instance.systemId, instance.instanceId, e);
480
+ }
481
+ }
482
+ });
483
+ await Promise.allSettled(promises);
385
484
  }
386
- // createProcess will stop the process first if it's running
387
- return this.createProcess(planRef, instanceId);
485
+ if (changed) {
486
+ this.save();
487
+ }
488
+ //console.log('\n##\n');
388
489
  }
389
- async stopProcess(planRef, instanceId) {
390
- if (!this._processes[planRef]) {
391
- return;
490
+ async getExternalStatus(instance) {
491
+ if (instance.type === InstanceType.DOCKER) {
492
+ const containerName = getBlockInstanceContainerName(instance.instanceId);
493
+ const container = await containerManager.getContainerByName(containerName);
494
+ if (!container) {
495
+ // If the container doesn't exist, we consider the instance stopped
496
+ return InstanceStatus.STOPPED;
497
+ }
498
+ const state = await container.status();
499
+ if (state.Status === 'running') {
500
+ if (state.Health?.Status === 'healthy') {
501
+ return InstanceStatus.READY;
502
+ }
503
+ if (state.Health?.Status === 'starting') {
504
+ return InstanceStatus.STARTING;
505
+ }
506
+ if (state.Health?.Status === 'unhealthy') {
507
+ return InstanceStatus.UNHEALTHY;
508
+ }
509
+ return InstanceStatus.READY;
510
+ }
511
+ if (state.Status === 'created') {
512
+ return InstanceStatus.STARTING;
513
+ }
514
+ if (state.Status === 'exited' || state.Status === 'dead') {
515
+ return InstanceStatus.STOPPED;
516
+ }
517
+ if (state.Status === 'removing') {
518
+ return InstanceStatus.BUSY;
519
+ }
520
+ if (state.Status === 'restarting') {
521
+ return InstanceStatus.BUSY;
522
+ }
523
+ if (state.Status === 'paused') {
524
+ return InstanceStatus.BUSY;
525
+ }
526
+ return InstanceStatus.STOPPED;
392
527
  }
393
- if (this._processes[planRef][instanceId]) {
394
- try {
395
- await this._processes[planRef][instanceId].stop();
528
+ if (!instance.pid) {
529
+ return InstanceStatus.STOPPED;
530
+ }
531
+ //Otherwise its just a normal process.
532
+ //TODO: Handle for Windows
533
+ try {
534
+ if (process.kill(instance.pid, 0)) {
535
+ return InstanceStatus.READY;
396
536
  }
397
- catch (e) {
398
- console.error('Failed to stop process for instance: %s -> %s', planRef, instanceId, e);
537
+ }
538
+ catch (err) {
539
+ if (err.code === 'EPERM') {
540
+ return InstanceStatus.READY;
399
541
  }
400
- delete this._processes[planRef][instanceId];
401
542
  }
543
+ return InstanceStatus.STOPPED;
402
544
  }
403
- async stopAllProcesses() {
404
- for (let processesForPlan of Object.values(this._processes)) {
405
- for (let processInfo of Object.values(processesForPlan)) {
406
- await processInfo.stop();
545
+ async requestInstanceStatus(instance) {
546
+ const externalStatus = await this.getExternalStatus(instance);
547
+ if (instance.type === InstanceType.DOCKER) {
548
+ // For docker instances we can rely on docker status
549
+ return externalStatus;
550
+ }
551
+ if (externalStatus === InstanceStatus.STOPPED) {
552
+ return externalStatus;
553
+ }
554
+ if (!instance.health) {
555
+ //No health url means we assume it's healthy as soon as it's running
556
+ return InstanceStatus.READY;
557
+ }
558
+ return new Promise((resolve) => {
559
+ if (!instance.health) {
560
+ resolve(InstanceStatus.READY);
561
+ return;
407
562
  }
563
+ request(instance.health, (err, response) => {
564
+ if (err) {
565
+ resolve(InstanceStatus.UNHEALTHY);
566
+ return;
567
+ }
568
+ if (response.statusCode > 399) {
569
+ resolve(InstanceStatus.UNHEALTHY);
570
+ return;
571
+ }
572
+ resolve(InstanceStatus.READY);
573
+ });
574
+ });
575
+ }
576
+ emitSystemEvent(systemId, type, payload) {
577
+ systemId = normalizeKapetaUri(systemId);
578
+ try {
579
+ socketManager.emit(`${systemId}/instances`, type, payload);
580
+ }
581
+ catch (e) {
582
+ console.warn('Failed to emit instance event: %s', e.message);
408
583
  }
409
- this._processes = {};
410
- for (let instance of this._instances) {
411
- await this._stopInstance(instance);
584
+ }
585
+ emitInstanceEvent(systemId, instanceId, type, payload) {
586
+ systemId = normalizeKapetaUri(systemId);
587
+ try {
588
+ socketManager.emit(`${systemId}/instances/${instanceId}`, type, payload);
589
+ }
590
+ catch (e) {
591
+ console.warn('Failed to emit instance event: %s', e.message);
412
592
  }
413
593
  }
414
594
  }
415
595
  export const instanceManager = new InstanceManager();
416
596
  process.on('exit', async () => {
417
- await instanceManager.stopAllProcesses();
597
+ await instanceManager.stopAll();
418
598
  });