@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.
- package/CHANGELOG.md +14 -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 +25 -2
- package/dist/cjs/src/containerManager.js +51 -16
- 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 +34 -32
- 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 +25 -2
- package/dist/esm/src/containerManager.js +50 -15
- 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 +35 -33
- 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 +72 -16
- 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 +39 -36
- 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
|
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
|
-
|
45
|
-
this.
|
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
|
-
|
72
|
-
if (
|
73
|
-
|
74
|
-
}
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
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
|
-
*
|
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
|
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
|
-
|
155
|
-
|
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
|
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.
|
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:
|
180
|
-
|
181
|
-
|
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.
|
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
|
-
|
151
|
+
return healthUrl;
|
189
152
|
}
|
190
|
-
|
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 =
|
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.
|
197
|
-
this.
|
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
|
209
|
-
|
210
|
-
const plan = await assetManager_1.assetManager.getPlan(
|
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: ' +
|
168
|
+
throw new Error('Plan not found: ' + systemId);
|
213
169
|
}
|
214
170
|
if (!plan.spec.blocks) {
|
215
|
-
console.warn('No blocks found in plan',
|
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.
|
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
|
235
|
-
|
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 ===
|
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
|
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(
|
261
|
-
|
262
|
-
|
263
|
-
|
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
|
279
|
-
|
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: ' +
|
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
|
-
|
294
|
-
|
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
|
-
|
297
|
-
|
298
|
-
|
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
|
303
|
+
const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
|
302
304
|
//emit stdout/stderr via sockets
|
303
|
-
|
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.
|
312
|
+
this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, payload);
|
311
313
|
});
|
312
|
-
|
314
|
+
processInfo.output.on('exit', (exitCode) => {
|
313
315
|
const timeRunning = Date.now() - startTime;
|
314
|
-
const instance = this.getInstance(
|
315
|
-
if (instance?.status ===
|
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.
|
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
|
-
|
333
|
-
|
334
|
-
|
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:
|
337
|
-
status:
|
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.
|
352
|
-
|
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:
|
369
|
+
status: types_1.InstanceStatus.FAILED,
|
357
370
|
});
|
358
|
-
this.
|
359
|
-
this.
|
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
|
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
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
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
|
389
|
-
|
390
|
-
|
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
|
-
|
393
|
-
|
491
|
+
if (changed) {
|
492
|
+
this.save();
|
493
|
+
}
|
494
|
+
//console.log('\n##\n');
|
394
495
|
}
|
395
|
-
async
|
396
|
-
if (
|
397
|
-
|
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 (
|
400
|
-
|
401
|
-
|
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
|
-
|
404
|
-
|
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
|
410
|
-
|
411
|
-
|
412
|
-
|
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
|
-
|
416
|
-
|
417
|
-
|
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.
|
604
|
+
await exports.instanceManager.stopAll();
|
424
605
|
});
|