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