@kapeta/local-cluster-service 0.8.3 → 0.9.0
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/assetManager.js +7 -4
- package/dist/cjs/src/clusterService.js +2 -0
- package/dist/cjs/src/codeGeneratorManager.js +3 -3
- package/dist/cjs/src/config/routes.js +1 -1
- package/dist/cjs/src/configManager.js +13 -1
- package/dist/cjs/src/containerManager.d.ts +22 -2
- package/dist/cjs/src/containerManager.js +42 -15
- package/dist/cjs/src/definitionsManager.d.ts +11 -0
- package/dist/cjs/src/definitionsManager.js +44 -0
- package/dist/cjs/src/filesystemManager.js +0 -2
- package/dist/cjs/src/instanceManager.d.ts +23 -47
- package/dist/cjs/src/instanceManager.js +416 -235
- package/dist/cjs/src/instances/routes.js +23 -14
- package/dist/cjs/src/middleware/kapeta.js +7 -0
- package/dist/cjs/src/networkManager.js +6 -0
- package/dist/cjs/src/operatorManager.js +8 -4
- package/dist/cjs/src/providerManager.js +3 -3
- package/dist/cjs/src/repositoryManager.js +7 -3
- package/dist/cjs/src/serviceManager.js +5 -0
- package/dist/cjs/src/types.d.ts +39 -13
- package/dist/cjs/src/types.js +28 -0
- package/dist/cjs/src/utils/BlockInstanceRunner.d.ts +3 -3
- package/dist/cjs/src/utils/BlockInstanceRunner.js +27 -26
- package/dist/cjs/src/utils/utils.d.ts +2 -0
- package/dist/cjs/src/utils/utils.js +17 -1
- package/dist/esm/src/assetManager.js +7 -4
- package/dist/esm/src/clusterService.js +2 -0
- package/dist/esm/src/codeGeneratorManager.js +3 -3
- package/dist/esm/src/config/routes.js +1 -1
- package/dist/esm/src/configManager.js +13 -1
- package/dist/esm/src/containerManager.d.ts +22 -2
- package/dist/esm/src/containerManager.js +41 -14
- package/dist/esm/src/definitionsManager.d.ts +11 -0
- package/dist/esm/src/definitionsManager.js +38 -0
- package/dist/esm/src/filesystemManager.js +0 -2
- package/dist/esm/src/instanceManager.d.ts +23 -47
- package/dist/esm/src/instanceManager.js +416 -236
- package/dist/esm/src/instances/routes.js +23 -14
- package/dist/esm/src/middleware/kapeta.js +7 -0
- package/dist/esm/src/networkManager.js +6 -0
- package/dist/esm/src/operatorManager.js +8 -4
- package/dist/esm/src/providerManager.js +3 -3
- package/dist/esm/src/repositoryManager.js +7 -3
- package/dist/esm/src/serviceManager.js +5 -0
- package/dist/esm/src/types.d.ts +39 -13
- package/dist/esm/src/types.js +27 -1
- package/dist/esm/src/utils/BlockInstanceRunner.d.ts +3 -3
- package/dist/esm/src/utils/BlockInstanceRunner.js +28 -27
- package/dist/esm/src/utils/utils.d.ts +2 -0
- package/dist/esm/src/utils/utils.js +14 -0
- package/package.json +2 -1
- package/src/assetManager.ts +7 -4
- package/src/clusterService.ts +3 -0
- package/src/codeGeneratorManager.ts +3 -2
- package/src/config/routes.ts +1 -1
- package/src/configManager.ts +13 -1
- package/src/containerManager.ts +62 -15
- package/src/definitionsManager.ts +54 -0
- package/src/filesystemManager.ts +0 -2
- package/src/instanceManager.ts +495 -266
- package/src/instances/routes.ts +23 -17
- package/src/middleware/kapeta.ts +10 -0
- package/src/networkManager.ts +6 -0
- package/src/operatorManager.ts +11 -6
- package/src/providerManager.ts +3 -2
- package/src/repositoryManager.ts +7 -3
- package/src/serviceManager.ts +6 -0
- package/src/types.ts +44 -14
- package/src/utils/BlockInstanceRunner.ts +32 -30
- 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
|
-
|
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
|
-
|
39
|
-
this.
|
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
|
-
|
66
|
-
if (
|
67
|
-
|
68
|
-
}
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
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
|
-
*
|
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
|
82
|
+
async registerInstanceFromSDK(systemId, instanceId, info) {
|
83
|
+
systemId = normalizeKapetaUri(systemId);
|
146
84
|
let instance = this.getInstance(systemId, instanceId);
|
147
85
|
//Get target address
|
148
|
-
|
149
|
-
|
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
|
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.
|
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:
|
174
|
-
|
175
|
-
|
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.
|
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
|
-
|
145
|
+
return healthUrl;
|
183
146
|
}
|
184
|
-
|
147
|
+
markAsStopped(systemId, instanceId) {
|
148
|
+
systemId = normalizeKapetaUri(systemId);
|
185
149
|
const instance = _.find(this._instances, { systemId, instanceId });
|
186
|
-
if (instance) {
|
187
|
-
instance.status =
|
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.
|
191
|
-
this.
|
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
|
203
|
-
|
204
|
-
const plan = await assetManager.getPlan(
|
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: ' +
|
162
|
+
throw new Error('Plan not found: ' + systemId);
|
207
163
|
}
|
208
164
|
if (!plan.spec.blocks) {
|
209
|
-
console.warn('No blocks found in plan',
|
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.
|
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
|
229
|
-
|
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 ===
|
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
|
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(
|
255
|
-
|
256
|
-
|
257
|
-
|
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
|
273
|
-
|
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: ' +
|
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
|
-
|
288
|
-
|
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
|
-
|
291
|
-
|
292
|
-
|
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
|
297
|
+
const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
|
296
298
|
//emit stdout/stderr via sockets
|
297
|
-
|
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.
|
306
|
+
this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, payload);
|
305
307
|
});
|
306
|
-
|
308
|
+
processInfo.output.on('exit', (exitCode) => {
|
307
309
|
const timeRunning = Date.now() - startTime;
|
308
|
-
const instance = this.getInstance(
|
309
|
-
if (instance?.status ===
|
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.
|
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
|
-
|
327
|
-
|
328
|
-
|
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:
|
331
|
-
status:
|
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.
|
346
|
-
|
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:
|
363
|
+
status: InstanceStatus.FAILED,
|
351
364
|
});
|
352
|
-
this.
|
353
|
-
this.
|
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
|
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
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
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
|
383
|
-
|
384
|
-
|
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
|
-
|
387
|
-
|
485
|
+
if (changed) {
|
486
|
+
this.save();
|
487
|
+
}
|
488
|
+
//console.log('\n##\n');
|
388
489
|
}
|
389
|
-
async
|
390
|
-
if (
|
391
|
-
|
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 (
|
394
|
-
|
395
|
-
|
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
|
-
|
398
|
-
|
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
|
404
|
-
|
405
|
-
|
406
|
-
|
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
|
-
|
410
|
-
|
411
|
-
|
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.
|
597
|
+
await instanceManager.stopAll();
|
418
598
|
});
|