kuzzle 2.60.0-beta.2 → 2.60.0-beta.4
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/dist/bin/start-kuzzle-server +2 -1
- package/dist/lib/api/controllers/documentController.d.ts +1 -1
- package/dist/lib/cluster/command.d.ts +7 -0
- package/dist/lib/cluster/command.js +67 -28
- package/dist/lib/cluster/idCardHandler.d.ts +4 -0
- package/dist/lib/cluster/idCardHandler.js +23 -2
- package/dist/lib/cluster/node.d.ts +28 -5
- package/dist/lib/cluster/node.js +159 -76
- package/dist/lib/cluster/subscriber.d.ts +2 -0
- package/dist/lib/cluster/subscriber.js +6 -0
- package/dist/lib/cluster/workers/IDCardRenewer.d.ts +6 -0
- package/dist/lib/cluster/workers/IDCardRenewer.js +14 -8
- package/dist/lib/core/auth/passportWrapper.js +3 -1
- package/dist/lib/core/backend/backend.d.ts +1 -1
- package/dist/lib/core/backend/backend.js +15 -15
- package/dist/lib/core/backend/backendCluster.d.ts +1 -1
- package/dist/lib/core/shared/store.js +4 -0
- package/dist/lib/core/storage/clientAdapter.d.ts +6 -0
- package/dist/lib/core/storage/clientAdapter.js +35 -5
- package/dist/lib/kerror/codes/8-cluster.json +23 -0
- package/dist/lib/kuzzle/exitOnFailure.d.ts +13 -0
- package/dist/lib/kuzzle/exitOnFailure.js +60 -0
- package/dist/lib/kuzzle/internalIndexHandler.js +24 -6
- package/dist/lib/kuzzle/kuzzle.d.ts +7 -0
- package/dist/lib/kuzzle/kuzzle.js +45 -2
- package/dist/lib/model/security/profile.d.ts +1 -1
- package/dist/lib/model/security/role.d.ts +1 -1
- package/dist/lib/model/security/user.d.ts +1 -1
- package/dist/lib/service/storage/7/elasticsearch.d.ts +12 -0
- package/dist/lib/service/storage/7/elasticsearch.js +32 -1
- package/dist/lib/service/storage/7/esWrapper.js +31 -2
- package/dist/lib/service/storage/8/elasticsearch.d.ts +12 -0
- package/dist/lib/service/storage/8/elasticsearch.js +32 -1
- package/dist/lib/service/storage/8/esWrapper.js +31 -2
- package/dist/lib/types/events/EventGenericDocument.d.ts +2 -1
- package/dist/lib/types/events/EventProtocol.d.ts +1 -1
- package/dist/lib/util/mutex.d.ts +2 -1
- package/dist/lib/util/mutex.js +7 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
|
@@ -104,18 +104,24 @@ class IDCardRenewer {
|
|
|
104
104
|
// Unable to refresh the key in time before it expires
|
|
105
105
|
// => this node is too slow, we need to remove it from the cluster
|
|
106
106
|
if (refreshed === 0) {
|
|
107
|
-
await this.
|
|
108
|
-
process.send?.({
|
|
109
|
-
error: "Node too slow: ID card expired",
|
|
110
|
-
});
|
|
107
|
+
await this.reportFailure("Node too slow: ID card expired");
|
|
111
108
|
}
|
|
112
109
|
}
|
|
113
110
|
catch (error) {
|
|
114
|
-
await this.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
111
|
+
await this.reportFailure(`Failed to refresh ID Card: ${messageOf(error)}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Disposes the worker and tells the parent why, once: the renewals queued
|
|
116
|
+
* behind a stalled redis all fail together, and each report evicts the node
|
|
117
|
+
* again (#3017)
|
|
118
|
+
*/
|
|
119
|
+
async reportFailure(error) {
|
|
120
|
+
if (this.disposed) {
|
|
121
|
+
return;
|
|
118
122
|
}
|
|
123
|
+
await this.dispose();
|
|
124
|
+
process.send?.({ error });
|
|
119
125
|
}
|
|
120
126
|
async dispose() {
|
|
121
127
|
if (this.disposed) {
|
|
@@ -99,7 +99,9 @@ class PassportWrapper {
|
|
|
99
99
|
fail(err);
|
|
100
100
|
}
|
|
101
101
|
else if (!user) {
|
|
102
|
-
const message =
|
|
102
|
+
const message =
|
|
103
|
+
// `typeof null` is "object" too: a strategy may fail with no info
|
|
104
|
+
typeof info === "object" && info !== null && "message" in info
|
|
103
105
|
? info.message
|
|
104
106
|
: undefined;
|
|
105
107
|
reject(kerror.get("plugin", "strategy", "missing_user", message));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { JSONObject } from "../../types/JSONObject";
|
|
2
2
|
import { EmbeddedSDK } from "../shared/sdk/embeddedSdk";
|
|
3
|
-
import type { EventDefinition } from "
|
|
3
|
+
import type { EventDefinition } from "../../types/EventHandler";
|
|
4
4
|
import { BackendCluster, BackendConfig, BackendController, BackendHook, BackendImport, BackendPipe, BackendPlugin, BackendStorage, BackendVault, BackendOpenApi, BackendErrors, BackendSubscription } from "./index";
|
|
5
5
|
import { Logger } from "../../kuzzle/Logger";
|
|
6
6
|
import Plugin from "../plugin/plugin";
|
|
@@ -61,8 +61,8 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
61
61
|
const kuzzle_1 = __importDefault(require("../../kuzzle"));
|
|
62
62
|
const embeddedSdk_1 = require("../shared/sdk/embeddedSdk");
|
|
63
63
|
const kerror = __importStar(require("../../kerror"));
|
|
64
|
-
const
|
|
65
|
-
const
|
|
64
|
+
const name_generator_1 = require("../../util/name-generator");
|
|
65
|
+
const index_1 = require("./index");
|
|
66
66
|
const Logger_1 = require("../../kuzzle/Logger");
|
|
67
67
|
const plugin_1 = __importDefault(require("../plugin/plugin"));
|
|
68
68
|
const assertionError = kerror.wrap("plugin", "assert");
|
|
@@ -130,7 +130,7 @@ class Backend {
|
|
|
130
130
|
throw assertionError.get("invalid_application_name", name);
|
|
131
131
|
}
|
|
132
132
|
this._name = name;
|
|
133
|
-
this.nodeId =
|
|
133
|
+
this.nodeId = name_generator_1.NameGenerator.generateRandomName({ prefix: "knode" });
|
|
134
134
|
Reflect.defineProperty(this, "_kuzzle", {
|
|
135
135
|
writable: true,
|
|
136
136
|
});
|
|
@@ -162,18 +162,18 @@ class Backend {
|
|
|
162
162
|
// Silent if no version can be found
|
|
163
163
|
}
|
|
164
164
|
global.app = this;
|
|
165
|
-
this.pipe = new
|
|
166
|
-
this.hook = new
|
|
167
|
-
this.config = new
|
|
168
|
-
this.vault = new
|
|
169
|
-
this.controller = new
|
|
170
|
-
this.plugin = new
|
|
171
|
-
this.storage = new
|
|
172
|
-
this.import = new
|
|
173
|
-
this.cluster = new
|
|
174
|
-
this.openApi = new
|
|
175
|
-
this.errors = new
|
|
176
|
-
this.subscription = new
|
|
165
|
+
this.pipe = new index_1.BackendPipe(this);
|
|
166
|
+
this.hook = new index_1.BackendHook(this);
|
|
167
|
+
this.config = new index_1.BackendConfig(this);
|
|
168
|
+
this.vault = new index_1.BackendVault(this);
|
|
169
|
+
this.controller = new index_1.BackendController(this);
|
|
170
|
+
this.plugin = new index_1.BackendPlugin(this);
|
|
171
|
+
this.storage = new index_1.BackendStorage(this);
|
|
172
|
+
this.import = new index_1.BackendImport(this);
|
|
173
|
+
this.cluster = new index_1.BackendCluster();
|
|
174
|
+
this.openApi = new index_1.BackendOpenApi(this);
|
|
175
|
+
this.errors = new index_1.BackendErrors(this);
|
|
176
|
+
this.subscription = new index_1.BackendSubscription(this);
|
|
177
177
|
this.log = new Logger_1.Logger(this.config.content, "kuzzle:app");
|
|
178
178
|
this.kerror = kerror;
|
|
179
179
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EventDefinition, ClusterEventHandler } from "
|
|
1
|
+
import type { EventDefinition, ClusterEventHandler } from "../../types/EventHandler";
|
|
2
2
|
export declare class BackendCluster {
|
|
3
3
|
/**
|
|
4
4
|
* Broadcasts an event to listeners across all registered Kuzzle nodes
|
|
@@ -156,6 +156,10 @@ class Store {
|
|
|
156
156
|
}
|
|
157
157
|
const exist = await global.kuzzle.ask(`core:storage:${this.scope}:collection:exist`, this.index, collection);
|
|
158
158
|
if (exist) {
|
|
159
|
+
// Another node created it, maybe so recently that its primary
|
|
160
|
+
// shards are still initializing: a document request on it would
|
|
161
|
+
// get a 503 until they are active
|
|
162
|
+
await global.kuzzle.ask(`core:storage:${this.scope}:collection:shards:wait`, this.index, collection);
|
|
159
163
|
// @deprecated
|
|
160
164
|
const dynamicSettings = isConfigDeprecated
|
|
161
165
|
? null
|
|
@@ -41,6 +41,12 @@ declare class ClientAdapter {
|
|
|
41
41
|
* Also checks for duplicated index names.
|
|
42
42
|
*/
|
|
43
43
|
populateCache(): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Adds the indexes and collections found in storage to the index cache,
|
|
46
|
+
* then reloads the collection write locks. Unlike `populateCache`, it never
|
|
47
|
+
* writes to storage.
|
|
48
|
+
*/
|
|
49
|
+
reloadCache(): Promise<void>;
|
|
44
50
|
/**
|
|
45
51
|
* Replaces the cached collection write locks with the cluster-wide ones,
|
|
46
52
|
* stored in Redis.
|
|
@@ -56,6 +56,7 @@ const Elasticsearch_1 = require("../../service/storage/Elasticsearch");
|
|
|
56
56
|
const indexCache_1 = require("./indexCache");
|
|
57
57
|
const safeObject_1 = require("../../util/safeObject");
|
|
58
58
|
const kerror = __importStar(require("../../kerror"));
|
|
59
|
+
const kuzzleError_1 = require("../../kerror/errors/kuzzleError");
|
|
59
60
|
const distributedLock_1 = require("../../util/distributedLock");
|
|
60
61
|
require("../../types/Global");
|
|
61
62
|
const servicesError = kerror.wrap("services", "storage");
|
|
@@ -93,10 +94,11 @@ class ClientAdapter {
|
|
|
93
94
|
this.registerMappingEvents();
|
|
94
95
|
this.registerCacheEvents();
|
|
95
96
|
this.registerLockEvents();
|
|
96
|
-
// The locks were first loaded
|
|
97
|
-
// cluster:
|
|
98
|
-
// it. Reading them again once the
|
|
99
|
-
|
|
97
|
+
// The cache and the locks were first loaded before this node joined the
|
|
98
|
+
// cluster: an index, a collection or a lock a peer added in between was
|
|
99
|
+
// broadcast before this node could hear it. Reading them again once the
|
|
100
|
+
// cluster is joined closes that gap.
|
|
101
|
+
global.kuzzle.onPipe("kuzzle:state:live", () => this.reloadCache());
|
|
100
102
|
// Global store events registration
|
|
101
103
|
/**
|
|
102
104
|
* Manually refresh the index cache (e.g. after alias creation)
|
|
@@ -120,7 +122,20 @@ class ClientAdapter {
|
|
|
120
122
|
throw servicesError.get("index_already_exists", this.scope, index);
|
|
121
123
|
}
|
|
122
124
|
if (!indexCacheOnly) {
|
|
123
|
-
|
|
125
|
+
try {
|
|
126
|
+
await this.client.createIndex(index);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
// The index exists in storage but not in this node's cache: a peer
|
|
130
|
+
// created it and its cache broadcast never reached this node (e.g. the
|
|
131
|
+
// peer stopped before this node joined the cluster). Cache it so that
|
|
132
|
+
// the next request to that index finds it, then report the conflict.
|
|
133
|
+
if (error instanceof kuzzleError_1.KuzzleError &&
|
|
134
|
+
error.id === "services.storage.index_already_exists") {
|
|
135
|
+
this.cache.addIndex(index);
|
|
136
|
+
}
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
124
139
|
}
|
|
125
140
|
this.cache.addIndex(index);
|
|
126
141
|
if (propagate) {
|
|
@@ -191,6 +206,14 @@ class ClientAdapter {
|
|
|
191
206
|
if (global.kuzzle.config.services.storageEngine.generateMissingAliases) {
|
|
192
207
|
await this.client.generateMissingAliases();
|
|
193
208
|
}
|
|
209
|
+
await this.reloadCache();
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Adds the indexes and collections found in storage to the index cache,
|
|
213
|
+
* then reloads the collection write locks. Unlike `populateCache`, it never
|
|
214
|
+
* writes to storage.
|
|
215
|
+
*/
|
|
216
|
+
async reloadCache() {
|
|
194
217
|
// `client` is untyped (see the field's comment), so the schema shape is
|
|
195
218
|
// stated here: index name -> its collection names.
|
|
196
219
|
const schema = await this.client.getSchema();
|
|
@@ -311,6 +334,13 @@ class ClientAdapter {
|
|
|
311
334
|
this.cache.assertCollectionExists(index, collection);
|
|
312
335
|
return this.client.getSettings(index, collection);
|
|
313
336
|
});
|
|
337
|
+
/**
|
|
338
|
+
* Wait, for a bounded time, until the primary shards of a collection
|
|
339
|
+
* another node has just created are active
|
|
340
|
+
* @param {string} index
|
|
341
|
+
* @param {string} collection
|
|
342
|
+
*/
|
|
343
|
+
global.kuzzle.onAsk(`core:storage:${this.scope}:collection:shards:wait`, (index, collection) => this.client.waitForCollectionShards(index, collection));
|
|
314
344
|
/**
|
|
315
345
|
* Check a collection existence
|
|
316
346
|
* @param {string} index
|
|
@@ -11,6 +11,29 @@
|
|
|
11
11
|
"class": "InternalError"
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
},
|
|
15
|
+
"join": {
|
|
16
|
+
"code": 1,
|
|
17
|
+
"errors": {
|
|
18
|
+
"duplicate_ip": {
|
|
19
|
+
"description": "Another node of the cluster has the same IP address as the one joining it. The joining node does not start.",
|
|
20
|
+
"code": 1,
|
|
21
|
+
"message": "Another node shares the same IP address as this one (%s): %s.",
|
|
22
|
+
"class": "InternalError"
|
|
23
|
+
},
|
|
24
|
+
"full_state_unavailable": {
|
|
25
|
+
"description": "None of the cluster nodes discovered could give the joining node the cluster state, twice in a row (network split). The joining node does not start.",
|
|
26
|
+
"code": 2,
|
|
27
|
+
"message": "Could not connect to the discovered cluster nodes (network split detected).",
|
|
28
|
+
"class": "InternalError"
|
|
29
|
+
},
|
|
30
|
+
"timeout": {
|
|
31
|
+
"description": "The node did not join the cluster within the configured cluster.joinTimeout. It does not start.",
|
|
32
|
+
"code": 3,
|
|
33
|
+
"message": "Failed to join the cluster: timed out (joinTimeout: %sms).",
|
|
34
|
+
"class": "InternalError"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
14
37
|
}
|
|
15
38
|
}
|
|
16
39
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ends the process with exit code 1, for a start script whose Kuzzle failed
|
|
3
|
+
* to start or to run.
|
|
4
|
+
*
|
|
5
|
+
* A bare `process.exit(1)` cuts Kuzzle's graceful shutdown short: the locks
|
|
6
|
+
* the node holds and its ID card stay in Redis until they expire (#3022).
|
|
7
|
+
* Once a Kuzzle instance exists, this goes through `shutdown(1)` instead,
|
|
8
|
+
* which joins the one a failed start already began (#3020). Before that,
|
|
9
|
+
* there is nothing to release.
|
|
10
|
+
*
|
|
11
|
+
* Internal to Kuzzle: not exported by the package.
|
|
12
|
+
*/
|
|
13
|
+
export declare function exitOnFailure(): Promise<void>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Kuzzle, a backend software, self-hostable and ready to use
|
|
4
|
+
* to power modern apps
|
|
5
|
+
*
|
|
6
|
+
* Copyright 2015-2022 Kuzzle
|
|
7
|
+
* mailto: support AT kuzzle.io
|
|
8
|
+
* website: http://kuzzle.io
|
|
9
|
+
*
|
|
10
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
11
|
+
* you may not use this file except in compliance with the License.
|
|
12
|
+
* You may obtain a copy of the License at
|
|
13
|
+
*
|
|
14
|
+
* https://www.apache.org/licenses/LICENSE-2.0
|
|
15
|
+
*
|
|
16
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
17
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
18
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
19
|
+
* See the License for the specific language governing permissions and
|
|
20
|
+
* limitations under the License.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.exitOnFailure = exitOnFailure;
|
|
24
|
+
const node_util_1 = require("node:util");
|
|
25
|
+
/**
|
|
26
|
+
* Ends the process with exit code 1, for a start script whose Kuzzle failed
|
|
27
|
+
* to start or to run.
|
|
28
|
+
*
|
|
29
|
+
* A bare `process.exit(1)` cuts Kuzzle's graceful shutdown short: the locks
|
|
30
|
+
* the node holds and its ID card stay in Redis until they expire (#3022).
|
|
31
|
+
* Once a Kuzzle instance exists, this goes through `shutdown(1)` instead,
|
|
32
|
+
* which joins the one a failed start already began (#3020). Before that,
|
|
33
|
+
* there is nothing to release.
|
|
34
|
+
*
|
|
35
|
+
* Internal to Kuzzle: not exported by the package.
|
|
36
|
+
*/
|
|
37
|
+
async function exitOnFailure() {
|
|
38
|
+
const kuzzle = currentKuzzle();
|
|
39
|
+
if (kuzzle) {
|
|
40
|
+
try {
|
|
41
|
+
// Ends with `process.exit`
|
|
42
|
+
await kuzzle.shutdown(1);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
kuzzle.log.error(`Unable to shut down: ${error instanceof Error ? error.message : (0, node_util_1.inspect)(error)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
function currentKuzzle() {
|
|
52
|
+
try {
|
|
53
|
+
return global.kuzzle;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// The `global.kuzzle` getter throws until an instance is built
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=exitOnFailure.js.map
|
|
@@ -132,8 +132,11 @@ class InternalIndexHandler extends store_1.Store {
|
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
134
134
|
await bluebird_1.default.resolve(this._bootstrapSequence()).timeout(this.timeout);
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
// A node that stopped while holding the lock may still have its
|
|
136
|
+
// own bootstrap in flight, and write this marker after the check
|
|
137
|
+
// above: replacing it keeps this node from failing on it
|
|
138
|
+
await this.createOrReplace("config", this._BOOTSTRAP_DONE_ID, {
|
|
139
|
+
timestamp: Date.now(),
|
|
137
140
|
});
|
|
138
141
|
}
|
|
139
142
|
catch (error) {
|
|
@@ -154,9 +157,11 @@ class InternalIndexHandler extends store_1.Store {
|
|
|
154
157
|
await this.createInitialValidations();
|
|
155
158
|
debug("Bootstrapping JWT secret");
|
|
156
159
|
await this._initSecret();
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
// Replaced rather than created: a bootstrap interrupted after this write
|
|
161
|
+
// but before the `.done` marker leaves it behind, and the next attempt
|
|
162
|
+
// must not fail on it (every step above is idempotent too)
|
|
163
|
+
await this.createOrReplace("config", this._DATAMODEL_VERSION_ID, {
|
|
164
|
+
version: dataModelVersion,
|
|
160
165
|
});
|
|
161
166
|
}
|
|
162
167
|
/**
|
|
@@ -195,7 +200,20 @@ class InternalIndexHandler extends store_1.Store {
|
|
|
195
200
|
if (!configSeed) {
|
|
196
201
|
if (!storedSeed) {
|
|
197
202
|
storedSeed = node_crypto_1.default.randomBytes(512).toString("hex");
|
|
198
|
-
|
|
203
|
+
try {
|
|
204
|
+
await this.create("config", { seed: storedSeed }, { id: this._JWT_SECRET_ID });
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
// An interrupted bootstrap stored one in the meantime: it is the
|
|
208
|
+
// seed other nodes may already sign with, so keep it, and read it
|
|
209
|
+
// back below
|
|
210
|
+
const alreadyStored = error instanceof Error &&
|
|
211
|
+
"id" in error &&
|
|
212
|
+
error.id === "services.storage.document_already_exists";
|
|
213
|
+
if (!alreadyStored) {
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
199
217
|
}
|
|
200
218
|
this.logger.warn("[!] Kuzzle is using a generated seed for authentication. This is suitable for development but should NEVER be used in production. See https://docs.kuzzle.io/core/2/guides/getting-started/deploy-your-application/");
|
|
201
219
|
}
|
|
@@ -17,6 +17,12 @@ import { Logger } from "./Logger";
|
|
|
17
17
|
declare class Kuzzle extends KuzzleEventEmitter {
|
|
18
18
|
config: IKuzzleConfiguration;
|
|
19
19
|
private _state;
|
|
20
|
+
/** The running shutdown, which later `shutdown()` calls wait for (#3017) */
|
|
21
|
+
private shutdownPromise;
|
|
22
|
+
/** The highest exit code asked for by the `shutdown()` calls so far */
|
|
23
|
+
private shutdownExitCode;
|
|
24
|
+
/** Ends a failure's shutdown that does not finish in time */
|
|
25
|
+
private shutdownWatchdog;
|
|
20
26
|
log: Logger;
|
|
21
27
|
/** Read by `PluginsManager` to locate the plugins directories. */
|
|
22
28
|
rootPath: string;
|
|
@@ -117,6 +123,7 @@ declare class Kuzzle extends KuzzleEventEmitter {
|
|
|
117
123
|
* @returns {Promise}
|
|
118
124
|
*/
|
|
119
125
|
shutdown(exitCode?: number): Promise<void>;
|
|
126
|
+
private runShutdown;
|
|
120
127
|
/**
|
|
121
128
|
* Execute multiple handlers only once on any given environment
|
|
122
129
|
*
|
|
@@ -93,6 +93,12 @@ const Logger_1 = require("./Logger");
|
|
|
93
93
|
const vault_1 = __importDefault(require("./vault"));
|
|
94
94
|
const name_generator_1 = require("../util/name-generator");
|
|
95
95
|
let _kuzzle = null;
|
|
96
|
+
/**
|
|
97
|
+
* How long a shutdown caused by a failure may run before the process exits
|
|
98
|
+
* anyway: a `kuzzle:shutdown` pipe or a lock release that never settles must
|
|
99
|
+
* not keep a failed node alive, which a bare `process.exit(1)` never did
|
|
100
|
+
*/
|
|
101
|
+
const FAILURE_SHUTDOWN_TIMEOUT = 30000;
|
|
96
102
|
Reflect.defineProperty(global, "kuzzle", {
|
|
97
103
|
configurable: true,
|
|
98
104
|
enumerable: false,
|
|
@@ -113,6 +119,12 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
|
|
|
113
119
|
constructor(config) {
|
|
114
120
|
super(config.plugins.common.maxConcurrentPipes, config.plugins.common.pipesBufferSize);
|
|
115
121
|
this._state = kuzzleStateEnum_1.default.STARTING;
|
|
122
|
+
/** The running shutdown, which later `shutdown()` calls wait for (#3017) */
|
|
123
|
+
this.shutdownPromise = null;
|
|
124
|
+
/** The highest exit code asked for by the `shutdown()` calls so far */
|
|
125
|
+
this.shutdownExitCode = 0;
|
|
126
|
+
/** Ends a failure's shutdown that does not finish in time */
|
|
127
|
+
this.shutdownWatchdog = null;
|
|
116
128
|
global.kuzzle = this;
|
|
117
129
|
this._state = kuzzleStateEnum_1.default.STARTING;
|
|
118
130
|
this.config = config;
|
|
@@ -204,6 +216,16 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
|
|
|
204
216
|
}
|
|
205
217
|
catch (error) {
|
|
206
218
|
this.log.error(`[X] Cannot start Kuzzle ${this.version}: ${error instanceof Error ? error.message : (0, node_util_1.inspect)(error)}`);
|
|
219
|
+
// A failed start cannot be retried (a second Kuzzle instance cannot be
|
|
220
|
+
// built), and what it did start keeps the process alive: in production,
|
|
221
|
+
// a node with no listener and an ID card still renewed (#3020). Not
|
|
222
|
+
// awaited, so the caller still gets the error before the process exits.
|
|
223
|
+
this.shutdown(1).catch((shutdownError) => {
|
|
224
|
+
this.log.error(`Unable to shut down after a failed start: ${shutdownError instanceof Error
|
|
225
|
+
? shutdownError.message
|
|
226
|
+
: (0, node_util_1.inspect)(shutdownError)}`);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
});
|
|
207
229
|
throw error;
|
|
208
230
|
}
|
|
209
231
|
}
|
|
@@ -231,6 +253,22 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
|
|
|
231
253
|
* @returns {Promise}
|
|
232
254
|
*/
|
|
233
255
|
async shutdown(exitCode = 0) {
|
|
256
|
+
// Several sources can ask for it at once (a signal, a cluster eviction,
|
|
257
|
+
// the `exit` handler that `process.exit` below fires): they share the one
|
|
258
|
+
// shutdown, and a failure reported by any of them keeps the exit non-zero
|
|
259
|
+
this.shutdownExitCode = Math.max(this.shutdownExitCode, exitCode);
|
|
260
|
+
// Not for a requested shutdown: it waits for the requests still running,
|
|
261
|
+
// however long they take
|
|
262
|
+
if (exitCode !== 0) {
|
|
263
|
+
this.shutdownWatchdog ?? (this.shutdownWatchdog = setTimeout(() => {
|
|
264
|
+
this.log.error(`[shutdown] Not finished after ${FAILURE_SHUTDOWN_TIMEOUT}ms: exiting now`);
|
|
265
|
+
process.exit(this.shutdownExitCode);
|
|
266
|
+
}, FAILURE_SHUTDOWN_TIMEOUT));
|
|
267
|
+
}
|
|
268
|
+
this.shutdownPromise ?? (this.shutdownPromise = this.runShutdown());
|
|
269
|
+
return this.shutdownPromise;
|
|
270
|
+
}
|
|
271
|
+
async runShutdown() {
|
|
234
272
|
this._state = kuzzleStateEnum_1.default.SHUTTING_DOWN;
|
|
235
273
|
this.log.info("Initiating shutdown...");
|
|
236
274
|
// Ask the network layer to stop accepting new request
|
|
@@ -257,7 +295,10 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
|
|
|
257
295
|
// `pluginsManager?.application?.log?.flush?.()` was a silent no-op and the
|
|
258
296
|
// application's buffered logs were dropped on shutdown (TD-51).
|
|
259
297
|
await this?.pluginsManager?.application?.instance?.log?.flush?.();
|
|
260
|
-
|
|
298
|
+
if (this.shutdownWatchdog !== null) {
|
|
299
|
+
clearTimeout(this.shutdownWatchdog);
|
|
300
|
+
}
|
|
301
|
+
process.exit(this.shutdownExitCode);
|
|
261
302
|
}
|
|
262
303
|
/**
|
|
263
304
|
* Execute multiple handlers only once on any given environment
|
|
@@ -608,7 +649,9 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
|
|
|
608
649
|
// nothing to do with that error
|
|
609
650
|
}
|
|
610
651
|
}
|
|
611
|
-
|
|
652
|
+
// Only abnormal terminations get here: exit non-zero, so that an
|
|
653
|
+
// `on-failure` restart policy brings the node back (#3020)
|
|
654
|
+
await this.shutdown(1);
|
|
612
655
|
}
|
|
613
656
|
}
|
|
614
657
|
exports.Kuzzle = Kuzzle;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Policy, OptimizedPolicy, OptimizedPolicyRestrictions } from "../../types/index";
|
|
2
2
|
import type { Role } from "./role";
|
|
3
|
-
import type { KuzzleRequest } from "
|
|
3
|
+
import type { KuzzleRequest } from "../../api/request";
|
|
4
4
|
/** @internal */
|
|
5
5
|
type InternalProfilePolicy = {
|
|
6
6
|
role: Role;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ControllerRight, ControllerRights } from "../../types/controllers/ControllerRights";
|
|
2
|
-
import type { KuzzleRequest } from "
|
|
2
|
+
import type { KuzzleRequest } from "../../api/request";
|
|
3
3
|
import type { OptimizedPolicyRestrictions } from "../../types/PolicyRestrictions";
|
|
4
4
|
/**
|
|
5
5
|
* @class Role
|
|
@@ -452,6 +452,18 @@ export declare class ES7 {
|
|
|
452
452
|
mappings?: TypeMapping;
|
|
453
453
|
settings?: JSONObject;
|
|
454
454
|
}): Promise<null>;
|
|
455
|
+
/**
|
|
456
|
+
* Waits until the primary shards of a collection are active, for at most
|
|
457
|
+
* `timeout`, and rejects if they are not by then.
|
|
458
|
+
*
|
|
459
|
+
* A collection another node has just created is in the cluster state, so
|
|
460
|
+
* it exists, before its primary shard is active: until then, Elasticsearch
|
|
461
|
+
* answers 503 to any document request on it. The node that creates a
|
|
462
|
+
* collection waits for its shards (`wait_for_active_shards`); a node that
|
|
463
|
+
* finds it already there calls this instead. It answers at once when the
|
|
464
|
+
* shards are already active.
|
|
465
|
+
*/
|
|
466
|
+
waitForCollectionShards(index: string, collection: string, timeout?: string): Promise<void>;
|
|
455
467
|
/**
|
|
456
468
|
* Retrieves settings definition for index/type
|
|
457
469
|
*
|
|
@@ -89,6 +89,9 @@ const HIDDEN_COLLECTION = "_kuzzle_keep";
|
|
|
89
89
|
// is refreshed while held, so a live holder is never overtaken; a crashed
|
|
90
90
|
// one leaves it for at most `ttl` (30 s).
|
|
91
91
|
const HIDDEN_COLLECTION_LOCK = { retryAttempts: Number.POSITIVE_INFINITY };
|
|
92
|
+
// How long `waitForCollectionShards` lets Elasticsearch wait for a
|
|
93
|
+
// collection's primary shards, which is also Elasticsearch's own default
|
|
94
|
+
const COLLECTION_SHARDS_TIMEOUT = "30s";
|
|
92
95
|
const ALIAS_PREFIX = "@"; // @todo next major release: Add ALIAS_PREFIX in FORBIDDEN_CHARS
|
|
93
96
|
const PRIVATE_PREFIX = "%";
|
|
94
97
|
const PUBLIC_PREFIX = "&";
|
|
@@ -1146,6 +1149,9 @@ class ES7 {
|
|
|
1146
1149
|
this._checkDynamicProperty(mappings);
|
|
1147
1150
|
const exists = await this.hasCollection(index, collection);
|
|
1148
1151
|
if (exists) {
|
|
1152
|
+
// Another node created it, maybe so recently that its primary shards
|
|
1153
|
+
// are still initializing
|
|
1154
|
+
await this.waitForCollectionShards(index, collection);
|
|
1149
1155
|
return this.updateCollection(index, collection, { mappings, settings });
|
|
1150
1156
|
}
|
|
1151
1157
|
this._checkMappings(mappings);
|
|
@@ -1167,7 +1173,9 @@ class ES7 {
|
|
|
1167
1173
|
if (lodash_1.default.get(error, "meta.body.error.type") ===
|
|
1168
1174
|
"resource_already_exists_exception") {
|
|
1169
1175
|
// race condition: the indice has been created between the "exists"
|
|
1170
|
-
// check above and this "create" attempt
|
|
1176
|
+
// check above and this "create" attempt, by a request that may not
|
|
1177
|
+
// have waited for its shards yet
|
|
1178
|
+
await this.waitForCollectionShards(index, collection);
|
|
1171
1179
|
return null;
|
|
1172
1180
|
}
|
|
1173
1181
|
throw this._esWrapper.formatESError(error);
|
|
@@ -1187,6 +1195,29 @@ class ES7 {
|
|
|
1187
1195
|
}
|
|
1188
1196
|
return null;
|
|
1189
1197
|
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Waits until the primary shards of a collection are active, for at most
|
|
1200
|
+
* `timeout`, and rejects if they are not by then.
|
|
1201
|
+
*
|
|
1202
|
+
* A collection another node has just created is in the cluster state, so
|
|
1203
|
+
* it exists, before its primary shard is active: until then, Elasticsearch
|
|
1204
|
+
* answers 503 to any document request on it. The node that creates a
|
|
1205
|
+
* collection waits for its shards (`wait_for_active_shards`); a node that
|
|
1206
|
+
* finds it already there calls this instead. It answers at once when the
|
|
1207
|
+
* shards are already active.
|
|
1208
|
+
*/
|
|
1209
|
+
async waitForCollectionShards(index, collection, timeout = COLLECTION_SHARDS_TIMEOUT) {
|
|
1210
|
+
try {
|
|
1211
|
+
await this._client.cluster.health({
|
|
1212
|
+
index: this._getAlias(index, collection),
|
|
1213
|
+
timeout,
|
|
1214
|
+
wait_for_status: "yellow",
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
catch (error) {
|
|
1218
|
+
throw this._esWrapper.formatESError(error);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1190
1221
|
/**
|
|
1191
1222
|
* Retrieves settings definition for index/type
|
|
1192
1223
|
*
|
|
@@ -35,6 +35,35 @@ const kerror = (0, kerror_1.wrap)("services", "storage");
|
|
|
35
35
|
function isResponseError(error) {
|
|
36
36
|
return Boolean(error.meta);
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* What the client puts in `message` when the response carried nothing it could
|
|
40
|
+
* read: this placeholder, or an empty string for a bodyless response.
|
|
41
|
+
*/
|
|
42
|
+
const CLIENT_PLACEHOLDER_MESSAGE = "Response Error";
|
|
43
|
+
function isUsableMessage(message) {
|
|
44
|
+
return (typeof message === "string" &&
|
|
45
|
+
message.trim() !== "" &&
|
|
46
|
+
message !== CLIENT_PLACEHOLDER_MESSAGE);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A message for an error that came with none worth keeping (typically a 503
|
|
50
|
+
* with no body while shards initialize), built from what the error still
|
|
51
|
+
* carries: the client's error name, the HTTP status and the response body.
|
|
52
|
+
*/
|
|
53
|
+
function describeUnreadableError(error) {
|
|
54
|
+
const statusCode = error.meta?.statusCode;
|
|
55
|
+
const body = error.meta?.body;
|
|
56
|
+
const hasBody = body !== undefined && body !== null && body !== "";
|
|
57
|
+
let description = statusCode === undefined
|
|
58
|
+
? "Elasticsearch request failed"
|
|
59
|
+
: `Elasticsearch responded ${statusCode}`;
|
|
60
|
+
description += hasBody
|
|
61
|
+
? `: ${(0, node_util_1.inspect)(body, { breakLength: Infinity, depth: 3 })}`
|
|
62
|
+
: " (no body)";
|
|
63
|
+
return error.name && error.name !== "Error"
|
|
64
|
+
? `${error.name}: ${description}`
|
|
65
|
+
: description;
|
|
66
|
+
}
|
|
38
67
|
const errorMessagesMapping = [
|
|
39
68
|
{
|
|
40
69
|
regex: /^\[es_rejected_execution_exception] rejected execution .*? on EsThreadPoolExecutor\[(.*?), .*$/,
|
|
@@ -258,8 +287,8 @@ class ESWrapper {
|
|
|
258
287
|
return kerror.get("unexpected_bad_request", errorMessage);
|
|
259
288
|
}
|
|
260
289
|
_handleUnknownError(error, message) {
|
|
261
|
-
debug("unhandled elasticsearch error (unhandled type: %s): %o", (0, get_1.default)(error, "
|
|
262
|
-
return kerror.get("unexpected_error", message);
|
|
290
|
+
debug("unhandled elasticsearch error (unhandled type: %s): %o", (0, get_1.default)(error, "meta.statusCode", "<no status code>"), error);
|
|
291
|
+
return kerror.get("unexpected_error", isUsableMessage(message) ? message : describeUnreadableError(error));
|
|
263
292
|
}
|
|
264
293
|
}
|
|
265
294
|
module.exports = ESWrapper;
|
|
@@ -443,6 +443,18 @@ export declare class ES8 {
|
|
|
443
443
|
mappings?: estypes.MappingTypeMapping;
|
|
444
444
|
settings?: estypes.IndicesIndexSettings;
|
|
445
445
|
}): Promise<null>;
|
|
446
|
+
/**
|
|
447
|
+
* Waits until the primary shards of a collection are active, for at most
|
|
448
|
+
* `timeout`, and rejects if they are not by then.
|
|
449
|
+
*
|
|
450
|
+
* A collection another node has just created is in the cluster state, so
|
|
451
|
+
* it exists, before its primary shard is active: until then, Elasticsearch
|
|
452
|
+
* answers 503 to any document request on it. The node that creates a
|
|
453
|
+
* collection waits for its shards (`wait_for_active_shards`); a node that
|
|
454
|
+
* finds it already there calls this instead. It answers at once when the
|
|
455
|
+
* shards are already active.
|
|
456
|
+
*/
|
|
457
|
+
waitForCollectionShards(index: string, collection: string, timeout?: string): Promise<void>;
|
|
446
458
|
/**
|
|
447
459
|
* Retrieves settings definition for index/type
|
|
448
460
|
*
|