kuzzle 2.60.0-beta.3 → 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/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/node.d.ts +13 -2
- package/dist/lib/cluster/node.js +20 -4
- package/dist/lib/cluster/subscriber.d.ts +2 -0
- package/dist/lib/cluster/subscriber.js +6 -0
- 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/kuzzle/internalIndexHandler.js +24 -6
- 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
|
@@ -77,7 +77,7 @@ declare class DocumentController extends NativeController {
|
|
|
77
77
|
errors: any;
|
|
78
78
|
successes: any;
|
|
79
79
|
}>;
|
|
80
|
-
export(request: KuzzleRequest): Promise<import("
|
|
80
|
+
export(request: KuzzleRequest): Promise<import("../../types").HttpStream>;
|
|
81
81
|
/**
|
|
82
82
|
* @param {Request} request
|
|
83
83
|
* @returns {Promise<Object>}
|
|
@@ -21,6 +21,8 @@ type CommandingNode = {
|
|
|
21
21
|
};
|
|
22
22
|
ip: string;
|
|
23
23
|
nodeId: string;
|
|
24
|
+
/** Whether `nodeId` can be read: false until the handshake sets it. */
|
|
25
|
+
hasNodeId: boolean;
|
|
24
26
|
activity: Activity[];
|
|
25
27
|
/** Only `serialize()`: the command layer never mutates the state. */
|
|
26
28
|
fullState: {
|
|
@@ -102,6 +104,11 @@ declare class ClusterCommand {
|
|
|
102
104
|
* This method can (and should) never return a rejected promise.
|
|
103
105
|
*/
|
|
104
106
|
listen(): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Answers one request. Every request gets exactly one reply: REP/REQ
|
|
109
|
+
* requires it, and a request left unanswered wedges the peer's socket.
|
|
110
|
+
*/
|
|
111
|
+
private answer;
|
|
105
112
|
/**
|
|
106
113
|
* Closes opened sockets and marks this server as closed.
|
|
107
114
|
*/
|
|
@@ -159,32 +159,9 @@ class ClusterCommand {
|
|
|
159
159
|
while (this.state === stateEnum.RUNNING) {
|
|
160
160
|
try {
|
|
161
161
|
const [type, data] = await this.server.receive();
|
|
162
|
-
//
|
|
163
|
-
// request
|
|
164
|
-
|
|
165
|
-
switch (type?.toString()) {
|
|
166
|
-
case commandMessages_1.commandTopic.FULLSTATE:
|
|
167
|
-
await this.sendFullState();
|
|
168
|
-
break;
|
|
169
|
-
case commandMessages_1.commandTopic.HANDSHAKE:
|
|
170
|
-
if (data === undefined) {
|
|
171
|
-
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
await this.handleHandshake(data);
|
|
175
|
-
break;
|
|
176
|
-
case commandMessages_1.commandTopic.RETRANSMIT:
|
|
177
|
-
if (data === undefined) {
|
|
178
|
-
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
179
|
-
break;
|
|
180
|
-
}
|
|
181
|
-
await this.handleRetransmit(data);
|
|
182
|
-
break;
|
|
183
|
-
default:
|
|
184
|
-
// REP/REQ sockets expect a reply to each request made, so we have
|
|
185
|
-
// to send some kind of response on an invalid request received
|
|
186
|
-
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
187
|
-
}
|
|
162
|
+
// NOSONAR (S9382): a REP socket strictly alternates receive and send,
|
|
163
|
+
// so a request must be answered before the next one can be read.
|
|
164
|
+
await this.answer(type, data); // NOSONAR
|
|
188
165
|
}
|
|
189
166
|
catch (e) {
|
|
190
167
|
if (this.state !== stateEnum.CLOSED) {
|
|
@@ -193,6 +170,47 @@ class ClusterCommand {
|
|
|
193
170
|
}
|
|
194
171
|
}
|
|
195
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Answers one request. Every request gets exactly one reply: REP/REQ
|
|
175
|
+
* requires it, and a request left unanswered wedges the peer's socket.
|
|
176
|
+
*/
|
|
177
|
+
async answer(type, data) {
|
|
178
|
+
// The socket is bound before the handshake sets this node's id, and a
|
|
179
|
+
// peer still talking to a dead node on this IP can reach it (#3031). A
|
|
180
|
+
// node with no id yet has no state to serve and cannot take a peer in:
|
|
181
|
+
// every request is answered DISCARDED, which the requester treats as no
|
|
182
|
+
// answer.
|
|
183
|
+
if (!this.node.hasNodeId) {
|
|
184
|
+
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// A REQ frame carries a topic and, for a handshake, a payload. A request
|
|
188
|
+
// missing either is answered with DISCARDED, which is what this socket
|
|
189
|
+
// owes every REQ — the two frames were read unchecked.
|
|
190
|
+
switch (type?.toString()) {
|
|
191
|
+
case commandMessages_1.commandTopic.FULLSTATE:
|
|
192
|
+
await this.sendFullState();
|
|
193
|
+
break;
|
|
194
|
+
case commandMessages_1.commandTopic.HANDSHAKE:
|
|
195
|
+
if (data === undefined) {
|
|
196
|
+
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
await this.handleHandshake(data);
|
|
200
|
+
break;
|
|
201
|
+
case commandMessages_1.commandTopic.RETRANSMIT:
|
|
202
|
+
if (data === undefined) {
|
|
203
|
+
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
await this.handleRetransmit(data);
|
|
207
|
+
break;
|
|
208
|
+
default:
|
|
209
|
+
// REP/REQ sockets expect a reply to each request made, so we have
|
|
210
|
+
// to send some kind of response on an invalid request received
|
|
211
|
+
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
196
214
|
/**
|
|
197
215
|
* Closes opened sockets and marks this server as closed.
|
|
198
216
|
*/
|
|
@@ -339,7 +357,19 @@ class ClusterCommand {
|
|
|
339
357
|
// No payload message for a full state request
|
|
340
358
|
await req.send([commandMessages_1.commandTopic.FULLSTATE, null]);
|
|
341
359
|
try {
|
|
342
|
-
|
|
360
|
+
// NOSONAR (S9382): one node at a time by design, the loop stops at
|
|
361
|
+
// the first one that serves a full state.
|
|
362
|
+
const [topic, payload] = await req.receive(); // NOSONAR
|
|
363
|
+
// DISCARDED carries an empty frame, which would decode as an empty
|
|
364
|
+
// full state: a node that cannot serve one (#3031) is skipped like
|
|
365
|
+
// one that does not answer.
|
|
366
|
+
if (topic?.toString() === commandMessages_1.commandTopic.FULLSTATE && payload) {
|
|
367
|
+
fullState = payload;
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
this.logger.warn(`Unable to fetch a full state from node ${id} (request discarded)`);
|
|
371
|
+
idx = (idx + 1) % nodes.length;
|
|
372
|
+
}
|
|
343
373
|
}
|
|
344
374
|
catch {
|
|
345
375
|
// no response from the remote node in a timely fashion... retrying
|
|
@@ -403,7 +433,16 @@ class ClusterCommand {
|
|
|
403
433
|
await req.send([commandMessages_1.commandTopic.HANDSHAKE, payload]);
|
|
404
434
|
let response = null;
|
|
405
435
|
try {
|
|
406
|
-
[,
|
|
436
|
+
const [topic, answer] = await req.receive();
|
|
437
|
+
// DISCARDED carries an empty frame, which would decode as a handshake
|
|
438
|
+
// answer with nothing in it: a node that refused the handshake (#3031)
|
|
439
|
+
// is one that did not answer.
|
|
440
|
+
if (topic?.toString() === commandMessages_1.commandTopic.HANDSHAKE && answer) {
|
|
441
|
+
response = answer;
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
this.logger.warn(`Couldn't complete handshake with node ${id}: request discarded`);
|
|
445
|
+
}
|
|
407
446
|
}
|
|
408
447
|
catch {
|
|
409
448
|
this.logger.warn(`Couldn't complete handshake with node ${id}: no response received`);
|
|
@@ -41,10 +41,21 @@ declare class ClusterNode {
|
|
|
41
41
|
* The id of the ID card this node reserved during `handshake()`.
|
|
42
42
|
*
|
|
43
43
|
* Null until the handshake gets that far, and read as a `string` at
|
|
44
|
-
* seventeen sites here plus `subscriber.ts` and `command.ts
|
|
45
|
-
*
|
|
44
|
+
* seventeen sites here plus `subscriber.ts` and `command.ts`. The accessor
|
|
45
|
+
* says once where that can fail: an internal read before the handshake is a
|
|
46
|
+
* bug, and it throws.
|
|
47
|
+
*
|
|
48
|
+
* Network input is not internal: the command socket is bound before the
|
|
49
|
+
* handshake, and a node restarted on the IP of a dead one can be reached by
|
|
50
|
+
* peers still talking to that dead node (#3031). The paths network input
|
|
51
|
+
* reaches check `hasNodeId` first.
|
|
46
52
|
*/
|
|
47
53
|
get nodeId(): string;
|
|
54
|
+
/**
|
|
55
|
+
* Whether the handshake has set this node's id yet: the non-throwing check
|
|
56
|
+
* for the paths network input reaches before it has (#3031).
|
|
57
|
+
*/
|
|
58
|
+
get hasNodeId(): boolean;
|
|
48
59
|
set nodeId(id: string);
|
|
49
60
|
private heartbeatTimer;
|
|
50
61
|
readonly idCardHandler: ClusterIdCardHandler;
|
package/dist/lib/cluster/node.js
CHANGED
|
@@ -200,8 +200,14 @@ class ClusterNode {
|
|
|
200
200
|
* The id of the ID card this node reserved during `handshake()`.
|
|
201
201
|
*
|
|
202
202
|
* Null until the handshake gets that far, and read as a `string` at
|
|
203
|
-
* seventeen sites here plus `subscriber.ts` and `command.ts
|
|
204
|
-
*
|
|
203
|
+
* seventeen sites here plus `subscriber.ts` and `command.ts`. The accessor
|
|
204
|
+
* says once where that can fail: an internal read before the handshake is a
|
|
205
|
+
* bug, and it throws.
|
|
206
|
+
*
|
|
207
|
+
* Network input is not internal: the command socket is bound before the
|
|
208
|
+
* handshake, and a node restarted on the IP of a dead one can be reached by
|
|
209
|
+
* peers still talking to that dead node (#3031). The paths network input
|
|
210
|
+
* reaches check `hasNodeId` first.
|
|
205
211
|
*/
|
|
206
212
|
get nodeId() {
|
|
207
213
|
if (this._nodeId === null) {
|
|
@@ -209,6 +215,13 @@ class ClusterNode {
|
|
|
209
215
|
}
|
|
210
216
|
return this._nodeId;
|
|
211
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Whether the handshake has set this node's id yet: the non-throwing check
|
|
220
|
+
* for the paths network input reaches before it has (#3031).
|
|
221
|
+
*/
|
|
222
|
+
get hasNodeId() {
|
|
223
|
+
return this._nodeId !== null;
|
|
224
|
+
}
|
|
212
225
|
set nodeId(id) {
|
|
213
226
|
this._nodeId = id;
|
|
214
227
|
}
|
|
@@ -389,8 +402,11 @@ class ClusterNode {
|
|
|
389
402
|
this.remoteNodes.delete(nodeId);
|
|
390
403
|
this.fullState.removeNode(nodeId);
|
|
391
404
|
subscriber.dispose();
|
|
392
|
-
|
|
393
|
-
|
|
405
|
+
// A subscriber can evict its remote node (heartbeat lost, gap, invalid
|
|
406
|
+
// message) before this node's handshake: there is no evictor id to
|
|
407
|
+
// broadcast then, and the eviction stays local (#3031).
|
|
408
|
+
if (broadcast && this._nodeId !== null) {
|
|
409
|
+
this.publisher.sendNodeEvicted(this._nodeId, nodeId, reason);
|
|
394
410
|
}
|
|
395
411
|
if (this.countActiveNodes() < this.config.minimumNodes) {
|
|
396
412
|
global.kuzzle.state = kuzzleStateEnum_1.default.NOT_ENOUGH_NODES;
|
|
@@ -15,6 +15,8 @@ type SubscribingNode = {
|
|
|
15
15
|
config: IKuzzleConfiguration["cluster"];
|
|
16
16
|
heartbeatDelay: number;
|
|
17
17
|
nodeId: string;
|
|
18
|
+
/** Whether `nodeId` can be read: false until the handshake sets it. */
|
|
19
|
+
hasNodeId: boolean;
|
|
18
20
|
fullState: State;
|
|
19
21
|
eventEmitter: {
|
|
20
22
|
emit(event: string, payload: unknown): void;
|
|
@@ -383,6 +383,12 @@ class ClusterSubscriber {
|
|
|
383
383
|
* @param message - decoded NodeEvicted protobuf message
|
|
384
384
|
*/
|
|
385
385
|
async handleNodeEviction(message) {
|
|
386
|
+
// Before the handshake this node has no id to compare, and is not part of
|
|
387
|
+
// the cluster the eviction is about: the message is ignored (#3031).
|
|
388
|
+
if (!this.localNode.hasNodeId) {
|
|
389
|
+
debug("[CLUSTER] Eviction of node %s by %s ignored: the handshake has not run", message.nodeId, message.evictor);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
386
392
|
if (message.nodeId === this.localNode.nodeId) {
|
|
387
393
|
this.logger.error(`[CLUSTER] Node evicted by ${message.evictor}. Reason: ${message.reason}`);
|
|
388
394
|
// A failure, like `evictSelf`'s: exit non-zero (#2785).
|
|
@@ -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
|
|
@@ -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
|
}
|
|
@@ -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
|
*
|
|
@@ -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 = "&";
|
|
@@ -1168,6 +1171,9 @@ class ES8 {
|
|
|
1168
1171
|
this._checkDynamicProperty(mappings);
|
|
1169
1172
|
const exists = await this.hasCollection(index, collection);
|
|
1170
1173
|
if (exists) {
|
|
1174
|
+
// Another node created it, maybe so recently that its primary shards
|
|
1175
|
+
// are still initializing
|
|
1176
|
+
await this.waitForCollectionShards(index, collection);
|
|
1171
1177
|
return this.updateCollection(index, collection, { mappings, settings });
|
|
1172
1178
|
}
|
|
1173
1179
|
this._checkMappings(mappings);
|
|
@@ -1189,7 +1195,9 @@ class ES8 {
|
|
|
1189
1195
|
if (lodash_1.default.get(error, "meta.body.error.type") ===
|
|
1190
1196
|
"resource_already_exists_exception") {
|
|
1191
1197
|
// race condition: the indice has been created between the "exists"
|
|
1192
|
-
// check above and this "create" attempt
|
|
1198
|
+
// check above and this "create" attempt, by a request that may not
|
|
1199
|
+
// have waited for its shards yet
|
|
1200
|
+
await this.waitForCollectionShards(index, collection);
|
|
1193
1201
|
return null;
|
|
1194
1202
|
}
|
|
1195
1203
|
throw this._esWrapper.formatESError(error);
|
|
@@ -1209,6 +1217,29 @@ class ES8 {
|
|
|
1209
1217
|
}
|
|
1210
1218
|
return null;
|
|
1211
1219
|
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Waits until the primary shards of a collection are active, for at most
|
|
1222
|
+
* `timeout`, and rejects if they are not by then.
|
|
1223
|
+
*
|
|
1224
|
+
* A collection another node has just created is in the cluster state, so
|
|
1225
|
+
* it exists, before its primary shard is active: until then, Elasticsearch
|
|
1226
|
+
* answers 503 to any document request on it. The node that creates a
|
|
1227
|
+
* collection waits for its shards (`wait_for_active_shards`); a node that
|
|
1228
|
+
* finds it already there calls this instead. It answers at once when the
|
|
1229
|
+
* shards are already active.
|
|
1230
|
+
*/
|
|
1231
|
+
async waitForCollectionShards(index, collection, timeout = COLLECTION_SHARDS_TIMEOUT) {
|
|
1232
|
+
try {
|
|
1233
|
+
await this._client.cluster.health({
|
|
1234
|
+
index: this._getAlias(index, collection),
|
|
1235
|
+
timeout,
|
|
1236
|
+
wait_for_status: "yellow",
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
catch (error) {
|
|
1240
|
+
throw this._esWrapper.formatESError(error);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1212
1243
|
/**
|
|
1213
1244
|
* Retrieves settings definition for index/type
|
|
1214
1245
|
*
|
|
@@ -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;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { KDocument, KDocumentContent } from "kuzzle-types";
|
|
2
2
|
import type { JSONObject } from "../JSONObject";
|
|
3
|
-
import type { KuzzleRequest
|
|
3
|
+
import type { KuzzleRequest } from "../../api/request";
|
|
4
|
+
import type { PipeEventHandler } from "../EventHandler";
|
|
4
5
|
/**
|
|
5
6
|
* Events with documents only having the `_id`
|
|
6
7
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JSONObject } from "../JSONObject";
|
|
2
|
-
import type { PipeEventHandler } from "
|
|
2
|
+
import type { PipeEventHandler } from "../EventHandler";
|
|
3
3
|
import type { ClientConnection } from "../ClientConnection";
|
|
4
4
|
import type { HttpMessage } from "../HttpMessage";
|
|
5
5
|
export type EventHTTPBeforeParsingPayload = {
|
package/dist/lib/util/mutex.d.ts
CHANGED
|
@@ -101,7 +101,8 @@ export declare class Mutex {
|
|
|
101
101
|
*
|
|
102
102
|
* From then on, `lock()` never resolves: the process is about to exit, and
|
|
103
103
|
* answering `false` would send its caller down a path meant for a lock
|
|
104
|
-
* held by another node.
|
|
104
|
+
* held by another node. And `unlock()` on a lock it already freed resolves
|
|
105
|
+
* without doing anything, since its holder may still try to give it back.
|
|
105
106
|
*/
|
|
106
107
|
static releaseAllBeforeExit(): Promise<void>;
|
|
107
108
|
get locked(): boolean;
|
package/dist/lib/util/mutex.js
CHANGED
|
@@ -165,6 +165,11 @@ class Mutex {
|
|
|
165
165
|
* @return {Promise}
|
|
166
166
|
*/
|
|
167
167
|
async unlock() {
|
|
168
|
+
// `releaseAllBeforeExit` already gave this lock back: its holder is only
|
|
169
|
+
// catching up, and throwing would turn a requested shutdown into a crash.
|
|
170
|
+
if (!this._locked && exiting) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
168
173
|
if (!this._locked) {
|
|
169
174
|
throw fatal.get("assertion_failed", `tried to unlock the resource "${this.resource}", which is not locked (mutex id: ${this.mutexId})`);
|
|
170
175
|
}
|
|
@@ -214,7 +219,8 @@ class Mutex {
|
|
|
214
219
|
*
|
|
215
220
|
* From then on, `lock()` never resolves: the process is about to exit, and
|
|
216
221
|
* answering `false` would send its caller down a path meant for a lock
|
|
217
|
-
* held by another node.
|
|
222
|
+
* held by another node. And `unlock()` on a lock it already freed resolves
|
|
223
|
+
* without doing anything, since its holder may still try to give it back.
|
|
218
224
|
*/
|
|
219
225
|
static async releaseAllBeforeExit() {
|
|
220
226
|
exiting = true;
|
package/dist/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kuzzle",
|
|
3
3
|
"author": "The Kuzzle Team <support@kuzzle.io>",
|
|
4
|
-
"version": "2.60.0-beta.
|
|
4
|
+
"version": "2.60.0-beta.4",
|
|
5
5
|
"description": "Kuzzle is an open-source solution that handles all the data management through a secured API, with a large choice of protocols.",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "rm -Rf ./dist && tsc && node -r ts-node/register/transpile-only ./bin/copy-binaries.ts",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kuzzle",
|
|
3
3
|
"author": "The Kuzzle Team <support@kuzzle.io>",
|
|
4
|
-
"version": "2.60.0-beta.
|
|
4
|
+
"version": "2.60.0-beta.4",
|
|
5
5
|
"description": "Kuzzle is an open-source solution that handles all the data management through a secured API, with a large choice of protocols.",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "rm -Rf ./dist && tsc && node -r ts-node/register/transpile-only ./bin/copy-binaries.ts",
|