UNPKG

hamok

Version:

Lightweight Distributed Object Storage on RAFT consensus algorithm

586 lines (585 loc) 33.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.HamokConnection = void 0; const events_1 = require("events"); const HamokMessage_1 = require("../messages/HamokMessage"); const GetEntries_1 = require("../messages/messagetypes/GetEntries"); const uuid_1 = require("uuid"); const logger_1 = require("../common/logger"); const ClearEntries_1 = require("../messages/messagetypes/ClearEntries"); const DeleteEntries_1 = require("../messages/messagetypes/DeleteEntries"); const GetKeys_1 = require("../messages/messagetypes/GetKeys"); const InsertEntries_1 = require("../messages/messagetypes/InsertEntries"); const RemoveEntries_1 = require("../messages/messagetypes/RemoveEntries"); const UpdateEntries_1 = require("../messages/messagetypes/UpdateEntries"); const ResponseChunker_1 = require("../messages/ResponseChunker"); const Collections = __importStar(require("../common/Collections")); const StorageAppliedCommit_1 = require("../messages/messagetypes/StorageAppliedCommit"); const StorageHelloNotification_1 = require("../messages/messagetypes/StorageHelloNotification"); const StorageStateNotification_1 = require("../messages/messagetypes/StorageStateNotification"); const logger = (0, logger_1.createLogger)('HamokConnection'); // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging class HamokConnection extends events_1.EventEmitter { config; codec; grid; waitUntilCommitHead; _responseChunker; _closed = false; _connected; _joined = false; _appliedCommitIndex = -1; _joining; _bufferedMessages = []; constructor(config, codec, grid, waitUntilCommitHead) { super(); this.config = config; this.codec = codec; this.grid = grid; this.waitUntilCommitHead = waitUntilCommitHead; this.setMaxListeners(Infinity); this._leaderChangedListener = this._leaderChangedListener.bind(this); this._responseChunker = (0, ResponseChunker_1.createResponseChunker)(config.maxOutboundKeys ?? 0, config.maxOutboundValues ?? 0); this._connected = this.grid.leaderId !== undefined; this.on('leader-changed', this._leaderChangedListener); } get closed() { return this._closed; } get localPeerId() { return this.grid.localPeerId; } get connected() { return this._connected; } get highestSeenCommitIndex() { return this._appliedCommitIndex; } close() { if (this._closed) return; this._closed = true; const rejectedRequestIds = []; for (const pendingRequest of this.grid.pendingRequests.values()) { if (pendingRequest.config.storageId !== this.config.storageId) continue; pendingRequest.reject('Connection is closed'); rejectedRequestIds.push(pendingRequest.id); } this.grid.purgeResponseForRequests(rejectedRequestIds); for (const activeOngoingRequest of this.grid.ongoingRequestsNotifier.activeOngoingRequests.values()) { if (activeOngoingRequest.storageId !== this.config.storageId) continue; this.grid.ongoingRequestsNotifier.remove(activeOngoingRequest.requestId); } this.emit('close'); this.removeAllListeners(); } async join() { if (this._joined) return; if (this._joining) return this._joining; try { this._joining = this._join(); await this._joining; this._joining = undefined; logger.debug('%s Connection for storage %s is joined', this.localPeerId, this.config.storageId); } catch (err) { logger.error('Failed to join connection, retrying', err); this._joining = undefined; if (this._closed) return; return this.join(); } } accept(message, commitIndex) { if (this._closed) { return logger.warn('Connection for storage %s is closed, cannot accept message %o', this.config.storageId, message); } if (!this._joined) { switch (message.type) { case HamokMessage_1.HamokMessage_MessageType.STORAGE_HELLO_NOTIFICATION: { const hello = this.codec.decodeStorageHelloNotification(message); if (hello.sourceEndpointId === this.grid.localPeerId) { return; } this.emit('StorageHelloNotification', hello); break; } case HamokMessage_1.HamokMessage_MessageType.STORAGE_STATE_NOTIFICATION: { const state = this.codec.decodeStorageStateNotification(message); if (state.sourceEndpointId === this.grid.localPeerId) { return; } this.emit('StorageStateNotification', state); break; } default: logger.debug('Buffering message %o until the connection is joined. commitIndex: %d', message, commitIndex); this._bufferedMessages.push([message, commitIndex]); break; } return; } if (commitIndex !== undefined) { // logger.info('%s Received message with commit index %d -> %d, %d', // this.localPeerId, // commitIndex, // message.type, // message.type === HamokMessageType.INSERT_ENTRIES_REQUEST ? this.codec.valueCodec.decode(message.values[0]) : -1 // ); if (commitIndex <= this._appliedCommitIndex) { return logger.warn('Connection for id %s Received message with commit index %d is older or equal than the last applied commit index %d', this.config.storageId, commitIndex, this._appliedCommitIndex); } // only in test purposes // if (this._appliedCommitIndex + 1 !== commitIndex) { // logger.warn('Received message with commit index %d is not the next commit index after the last applied commit index %d', commitIndex, this._appliedCommitIndex); // } this._appliedCommitIndex = commitIndex; } switch (message.type) { case HamokMessage_1.HamokMessage_MessageType.CLEAR_ENTRIES_REQUEST: this.emit('ClearEntriesRequest', this.codec.decodeClearEntriesRequest(message), commitIndex); break; case HamokMessage_1.HamokMessage_MessageType.CLEAR_ENTRIES_NOTIFICATION: this.emit('ClearEntriesNotification', this.codec.decodeClearEntriesNotification(message)); break; case HamokMessage_1.HamokMessage_MessageType.GET_ENTRIES_REQUEST: this.emit('GetEntriesRequest', this.codec.decodeGetEntriesRequest(message)); break; case HamokMessage_1.HamokMessage_MessageType.GET_SIZE_REQUEST: this.emit('GetSizeRequest', this.codec.decodeGetSizeRequest(message)); break; case HamokMessage_1.HamokMessage_MessageType.GET_KEYS_REQUEST: this.emit('GetKeysRequest', this.codec.decodeGetKeysRequest(message)); break; case HamokMessage_1.HamokMessage_MessageType.DELETE_ENTRIES_REQUEST: this.emit('DeleteEntriesRequest', this.codec.decodeDeleteEntriesRequest(message), commitIndex); break; case HamokMessage_1.HamokMessage_MessageType.DELETE_ENTRIES_NOTIFICATION: this.emit('DeleteEntriesNotification', this.codec.decodeDeleteEntriesNotification(message)); break; case HamokMessage_1.HamokMessage_MessageType.REMOVE_ENTRIES_REQUEST: this.emit('RemoveEntriesRequest', this.codec.decodeRemoveEntriesRequest(message), commitIndex); break; case HamokMessage_1.HamokMessage_MessageType.REMOVE_ENTRIES_NOTIFICATION: this.emit('RemoveEntriesNotification', this.codec.decodeRemoveEntriesNotification(message)); break; case HamokMessage_1.HamokMessage_MessageType.ENTRIES_REMOVED_NOTIFICATION: this.emit('EntriesRemovedNotification', this.codec.decodeEntriesRemovedNotification(message)); break; case HamokMessage_1.HamokMessage_MessageType.INSERT_ENTRIES_REQUEST: this.emit('InsertEntriesRequest', this.codec.decodeInsertEntriesRequest(message), commitIndex); break; case HamokMessage_1.HamokMessage_MessageType.INSERT_ENTRIES_NOTIFICATION: this.emit('InsertEntriesNotification', this.codec.decodeInsertEntriesNotification(message)); break; case HamokMessage_1.HamokMessage_MessageType.ENTRIES_INSERTED_NOTIFICATION: this.emit('EntriesInsertedNotification', this.codec.decodeEntriesInsertedNotification(message)); break; case HamokMessage_1.HamokMessage_MessageType.UPDATE_ENTRIES_REQUEST: this.emit('UpdateEntriesRequest', this.codec.decodeUpdateEntriesRequest(message), commitIndex); break; case HamokMessage_1.HamokMessage_MessageType.UPDATE_ENTRIES_NOTIFICATION: this.emit('UpdateEntriesNotification', this.codec.decodeUpdateEntriesNotification(message)); break; case HamokMessage_1.HamokMessage_MessageType.ENTRY_UPDATED_NOTIFICATION: this.emit('EntryUpdatedNotification', this.codec.decodeEntryUpdatedNotification(message)); break; case HamokMessage_1.HamokMessage_MessageType.STORAGE_APPLIED_COMMIT_NOTIFICATION: this.emit('StorageAppliedCommitNotification', this.codec.decodeStorageAppliedCommitNotification(message)); break; case HamokMessage_1.HamokMessage_MessageType.STORAGE_HELLO_NOTIFICATION: message.sourceId !== this.grid.localPeerId && this.emit('StorageHelloNotification', this.codec.decodeStorageHelloNotification(message)); break; case HamokMessage_1.HamokMessage_MessageType.STORAGE_STATE_NOTIFICATION: this.emit('StorageStateNotification', this.codec.decodeStorageStateNotification(message)); break; } } notifyStorageHello(targetPeerIds) { if (this._closed) throw new Error(`notifyStorageHello(): Cannot send message on a closed connection for storage ${this.config.storageId}`); logger.debug('%s Sending storage hello notification to %s', this.localPeerId, targetPeerIds); return this._sendMessage(this.codec.encodeStorageHelloNotification(new StorageHelloNotification_1.StorageHelloNotification(this.grid.localPeerId)), targetPeerIds); } notifyStorageState(serializedStorageSnapshot, appliedCommitIndex, targetPeerIds) { const message = new StorageStateNotification_1.StorageStateNotification(this.grid.localPeerId, appliedCommitIndex, serializedStorageSnapshot); return this._sendMessage(this.codec.encodeStorageStateNotification(message), targetPeerIds); } async requestGetEntries(keys, targetPeerIds) { if (this._closed) throw new Error(`requestGetEntries(): Cannot send message on a closed connection for storage ${this.config.storageId}`); const result = new Map(); const responseMessages = await Promise.all(Collections.splitSet(keys, this.config.maxOutboundKeys ?? 0, () => [keys]).map((batchedEntries) => this._request({ message: this.codec.encodeGetEntriesRequest(new GetEntries_1.GetEntriesRequest(batchedEntries, (0, uuid_1.v4)())), targetPeerIds, }))); responseMessages.flatMap((responses) => responses) .map((response) => this.codec.decodeGetEntriesResponse(response)) .forEach((response) => Collections.concatMaps(result, response.foundEntries)); return result; } async requestGetKeys(targetPeerIds) { if (this._closed) throw new Error(`requestGetKeys(): Cannot send message on a closed connection for storage ${this.config.storageId}`); const result = new Set(); (await this._request({ message: this.codec.encodeGetKeysRequest(new GetKeys_1.GetKeysRequest((0, uuid_1.v4)())), targetPeerIds, })) .map((response) => this.codec.decodeGetKeysResponse(response)) .forEach((response) => Collections.concatSet(result, response.keys)); return result; } async requestClearEntries(targetPeerIds) { if (this._closed) throw new Error(`requestClearEntries(): Cannot send message on a closed connection for storage ${this.config.storageId}`); return this._request({ message: this.codec.encodeClearEntriesRequest(new ClearEntries_1.ClearEntriesRequest((0, uuid_1.v4)())), targetPeerIds }).then(() => void 0); } notifyClearEntries(targetPeerIds) { if (this._closed) throw new Error(`notifyClearEntries(): Cannot send message on a closed connection for storage ${this.config.storageId}`); this._sendMessage(this.codec.encodeClearEntriesNotification(new ClearEntries_1.ClearEntriesNotification()), targetPeerIds); } async requestDeleteEntries(keys, targetPeerIds) { if (this._closed) throw new Error(`requestDeleteEntries(): Cannot send message on a closed connection for storage ${this.config.storageId}`); const result = new Set(); const responseMessages = await Promise.all(Collections.splitSet(keys, this.config.maxOutboundKeys ?? 0, () => [keys]).map((batchedEntries) => this._request({ message: this.codec.encodeDeleteEntriesRequest(new DeleteEntries_1.DeleteEntriesRequest((0, uuid_1.v4)(), batchedEntries)), targetPeerIds }))); // sort the messages by source ids to make sure the order of the responses // are consistent on all peers sortMessagesBySourceIds(responseMessages.flatMap((responses) => responses)) .map((response) => this.codec.decodeDeleteEntriesResponse(response)) .forEach((response) => Collections.concatSet(result, response.deletedKeys)); return result; } notifyDeleteEntries(keys, targetPeerIds) { if (this._closed) throw new Error(`notifyDeleteEntries(): Cannot send message on a closed connection for storage ${this.config.storageId}`); Collections.splitSet(keys, this.config.maxOutboundKeys ?? 0, () => [keys]) .map((batchedEntries) => this.codec.encodeDeleteEntriesNotification(new DeleteEntries_1.DeleteEntriesNotification(batchedEntries))) .forEach((notification) => this._sendMessage(notification, targetPeerIds)); } async requestRemoveEntries(keys, targetPeerIds, prevValue) { if (this._closed) throw new Error(`requestRemoveEntries(): Cannot send message on a closed connection for storage ${this.config.storageId}`); const result = new Map(); const responseMessages = await Promise.all(Collections.splitSet(keys, this.config.maxOutboundKeys ?? 0, () => [keys]).map((batchedEntries) => this._request({ message: this.codec.encodeRemoveEntriesRequest(new RemoveEntries_1.RemoveEntriesRequest((0, uuid_1.v4)(), batchedEntries, prevValue)), targetPeerIds }))); responseMessages.flatMap((responses) => responses) .map((response) => this.codec.decodeRemoveEntriesResponse(response)) .forEach((response) => Collections.concatMaps(result, response.removedEntries)); return result; } notifyRemoveEntries(keys, targetPeerIds) { if (this._closed) throw new Error(`notifyRemoveEntries(): Cannot send message on a closed connection for storage ${this.config.storageId}`); Collections.splitSet(keys, this.config.maxOutboundKeys ?? 0, () => [keys]) .map((batchedEntries) => this.codec.encodeRemoveEntriesNotification(new RemoveEntries_1.RemoveEntriesNotification(batchedEntries))) .forEach((notification) => this._sendMessage(notification, targetPeerIds)); } notifyEntriesRemoved(entries, targetPeerIds) { if (this._closed) throw new Error(`notifyEntriesRemoved(): Cannot send message on a closed connection for storage ${this.config.storageId}`); Collections.splitMap(entries, Math.max(this.config.maxOutboundKeys ?? 0, this.config.maxOutboundValues ?? 0), () => [entries]) .map((batchedEntries) => this.codec.encodeEntriesRemovedNotification(new RemoveEntries_1.EntriesRemovedNotification(batchedEntries))) .forEach((notification) => this._sendMessage(notification, targetPeerIds)); } async requestInsertEntries(entries, targetPeerIds) { if (this._closed) throw new Error(`requestInsertEntries(): Cannot send message on a closed connection for storage ${this.config.storageId}`); const result = new Map(); const responseMessages = await Promise.all(Collections.splitMap(entries, Math.max(this.config.maxOutboundKeys ?? 0, this.config.maxOutboundValues ?? 0), () => [entries]).map((batchedEntries) => this._request({ message: this.codec.encodeInsertEntriesRequest(new InsertEntries_1.InsertEntriesRequest((0, uuid_1.v4)(), batchedEntries)), targetPeerIds }))); responseMessages.flatMap((responses) => responses) .map((response) => this.codec.decodeInsertEntriesResponse(response)) .forEach((response) => Collections.concatMaps(result, response.existingEntries)); return result; } notifyInsertEntries(entries, targetPeerIds) { if (this._closed) throw new Error(`notifyInsertEntries(): Cannot send message on a closed connection for storage ${this.config.storageId}`); Collections.splitMap(entries, Math.max(this.config.maxOutboundKeys ?? 0, this.config.maxOutboundValues ?? 0), () => [entries]) .map((batchedEntries) => this.codec.encodeInsertEntriesNotification(new InsertEntries_1.InsertEntriesNotification(batchedEntries))) .forEach((notification) => this._sendMessage(notification, targetPeerIds)); } notifyEntriesInserted(entries, targetPeerIds) { if (this._closed) throw new Error(`notifyEntriesInserted(): Cannot send message on a closed connection for storage ${this.config.storageId}`); Collections.splitMap(entries, Math.max(this.config.maxOutboundKeys ?? 0, this.config.maxOutboundValues ?? 0), () => [entries]) .map((batchedEntries) => this.codec.encodeEntriesInsertedNotification(new InsertEntries_1.EntriesInsertedNotification(batchedEntries))) .forEach((notification) => this._sendMessage(notification, targetPeerIds)); } async requestUpdateEntries(entries, targetPeerIds, prevValue) { if (this._closed) throw new Error(`requestUpdateEntries(): Cannot send message on a closed connection for storage ${this.config.storageId}`); const result = new Map(); const responseMessages = await Promise.all(Collections.splitMap(entries, Math.max(this.config.maxOutboundKeys ?? 0, this.config.maxOutboundValues ?? 0), () => [entries]).map((batchedEntries) => this._request({ message: this.codec.encodeUpdateEntriesRequest(new UpdateEntries_1.UpdateEntriesRequest((0, uuid_1.v4)(), batchedEntries, undefined, prevValue)), targetPeerIds }))); responseMessages.flatMap((responses) => responses) .map((response) => this.codec.decodeUpdateEntriesResponse(response)) .forEach((response) => Collections.concatMaps(result, response.updatedEntries)); return result; } notifyUpdateEntries(entries, targetPeerIds) { if (this._closed) throw new Error(`notifyUpdateEntries(): Cannot send message on a closed connection for storage ${this.config.storageId}`); Collections.splitMap(entries, Math.max(this.config.maxOutboundKeys ?? 0, this.config.maxOutboundValues ?? 0), () => [entries]) .map((batchedEntries) => this.codec.encodeUpdateEntriesNotification(new UpdateEntries_1.UpdateEntriesNotification(batchedEntries))) .forEach((notification) => this._sendMessage(notification, targetPeerIds)); } notifyEntryUpdated(key, oldValue, newValue, targetPeerIds) { if (this._closed) throw new Error(`notifyEntryUpdated(): Cannot send message on a closed connection for storage ${this.config.storageId}`); const message = this.codec.encodeEntryUpdatedNotification(new UpdateEntries_1.EntryUpdatedNotification(key, newValue, oldValue)); this._sendMessage(message, targetPeerIds); } notifyStorageAppliedCommit(commitIndex, targetPeerIds) { if (this._closed) throw new Error(`notifyStorageAppliedCommit(): Cannot send message on a closed connection for storage ${this.config.storageId}`); const message = this.codec.encodeStorageAppliedCommitNotification(new StorageAppliedCommit_1.StorageAppliedCommitNotification(commitIndex)); this._sendMessage(message, targetPeerIds); } respond(type, response, targetPeerIds) { let message; switch (type) { case 'GetEntriesResponse': message = this.codec.encodeGetEntriesResponse(response); break; case 'ClearEntriesResponse': message = this.codec.encodeClearEntriesResponse(response); break; case 'DeleteEntriesResponse': message = this.codec.encodeDeleteEntriesResponse(response); break; case 'RemoveEntriesResponse': message = this.codec.encodeRemoveEntriesResponse(response); break; case 'InsertEntriesResponse': message = this.codec.encodeInsertEntriesResponse(response); break; case 'UpdateEntriesResponse': message = this.codec.encodeUpdateEntriesResponse(response); break; } if (!message) { return logger.warn('Cannot encode response for type %s', type); } for (const chunk of this._responseChunker.apply(message)) { // logger.info("Sending response message", message.type); this._sendMessage(chunk, targetPeerIds ? new Set(Array.isArray(targetPeerIds) ? targetPeerIds : [targetPeerIds]) : undefined); } } async _request(options) { options.message.storageId = this.config.storageId; options.message.protocol = HamokMessage_1.HamokMessage_MessageProtocol.STORAGE_COMMUNICATION_PROTOCOL; // if there is a join process ongoing we wait until it is finished await this._joining; return this.grid.request({ message: options.message, timeoutInMs: this.config.requestTimeoutInMs, neededResponses: this.config.neededResponse, targetPeerIds: options.targetPeerIds, submit: options.message.type ? this.config.submitting?.has(options.message.type) : false, }); } _sendMessage(message, targetPeerIds) { message.storageId = this.config.storageId; message.protocol = HamokMessage_1.HamokMessage_MessageProtocol.STORAGE_COMMUNICATION_PROTOCOL; if (this._joining) { // we only send storage hello or state notification during the join phase if (message.type !== HamokMessage_1.HamokMessage_MessageType.STORAGE_HELLO_NOTIFICATION && message.type !== HamokMessage_1.HamokMessage_MessageType.STORAGE_STATE_NOTIFICATION) { logger.debug('%s Buffering message %s until the connection is joined', this.localPeerId, message.type); this._joining.then(() => this._sendMessage(message, targetPeerIds)); return; } } this.grid.sendMessage(message, targetPeerIds); } async _join(retried = 0) { // we must buffer all messages received during join process (except state notification) this._joined = false; const stateNotification = await this._fetchStorageState(); // if we have a state notification we need to apply it if (stateNotification) { // restart if tdisconnect happens while this! try { await new Promise((resolve, reject) => { const disconnected = () => reject('disconnected'); const closed = () => reject('closed'); const done = () => { this.off('disconnected', disconnected); this.off('close', closed); resolve(); }; this.once('disconnected', disconnected); this.once('close', closed); this.emit('remote-snapshot', stateNotification.serializedStorageSnapshot, done); }); } catch (err) { logger.warn('Failed to join the storage connection %s. retried: %d', err, retried); // we restart the process until we are able to be joined or max retry count is reached return this._join(retried + 1); } // we set the applied commit index to the received one logger.info('Storage %s processed a remote snapshot and change it\'s applied commitIndex from %d to %d', this.config.storageId, this._appliedCommitIndex, stateNotification.remoteAppliedCommitIndex); this._appliedCommitIndex = stateNotification.remoteAppliedCommitIndex; } // the funny thing here is that if the remote peer committed logs meanwhile the snapshot is created and and sent it back (few heartbeats), // and those commits are related to this storage, and those are already emitted, then the commit index of the RAFT logs is higher than the commit index // the snapshot is applied on, so we need to collect those messages and replay them if (this._appliedCommitIndex < this.grid.logs.commitIndex) { const entries = this.grid.logs.collectEntries(this._appliedCommitIndex, Math.min(this.grid.logs.commitIndex + 1, // we need the commit index as well this.grid.logs.nextIndex)); logger.debug('Buffering messages %d until the connection is joined', entries.length); for (const logEntry of entries) { if (logEntry.entry.storageId !== this.config.storageId) continue; logger.debug('Processing buffered message %d', logEntry.index); // it should goes to the buffered messages this.accept(logEntry.entry, logEntry.index); } } const bufferedMessages = this._bufferedMessages; this._bufferedMessages = []; logger.trace('Buffered messages %o, appliedCommitIndex: %d, commitIndex: %d, nextIndex: %d', bufferedMessages, this._appliedCommitIndex, this.grid.logs.commitIndex, this.grid.logs.nextIndex); // now we can accept messages this._joined = true; for (const [message, commitIndex] of bufferedMessages) { if (commitIndex !== undefined && commitIndex < this._appliedCommitIndex) continue; logger.trace('%s Processing buffered message %d', this.localPeerId, commitIndex); this.accept(message, commitIndex); } } async _fetchStorageState(retried = 0) { try { if (!this.connected) { await new Promise((resolve, reject) => { const connected = () => { this.off('disconnected', disconnected); this.off('close', closed); resolve(); }; const disconnected = () => { this.off('connected', connected); this.off('close', closed); reject('disconnected'); }; const closed = () => { this.off('connected', connected); this.off('disconnected', disconnected); reject('closed'); }; this.once('connected', connected); this.once('disconnected', disconnected); this.once('close', closed); }); } } catch (err) { logger.warn('Failed to join the storage connection %s', err); // we restart the process until we are able to be joined or max retry count is reached return this._fetchStorageState(retried + 1); } const actualRemotePeerIds = new Set([...this.grid.remotePeerIds]); return new Promise((resolve) => { const timer = setTimeout(() => { this.off('StorageStateNotification', receiveStorageStateNotification); logger.debug('%s no response received for storage state notification, most likely the storage %s is alone', this.localPeerId, this.config.storageId); resolve(undefined); }, this.config.remoteStorageStateWaitingTimeoutInMs ?? 1000); const receiveStorageStateNotification = (notification) => { actualRemotePeerIds.delete(notification.sourceEndpointId); if (!notification.serializedStorageSnapshot) { // we can still receive a snapshot if (0 < actualRemotePeerIds.size) return; } clearTimeout(timer); this.off('StorageStateNotification', receiveStorageStateNotification); if (notification.serializedStorageSnapshot) { resolve({ remoteAppliedCommitIndex: notification.commitIndex, serializedStorageSnapshot: notification.serializedStorageSnapshot }); } else { resolve(undefined); } }; this.on('StorageStateNotification', receiveStorageStateNotification); this.notifyStorageHello(); }); } _leaderChangedListener(leaderId) { if (this._connected && leaderId === undefined) { this._connected = false; // this will automatically restarts the joining process if there is any ongoing this.emit('disconnected'); if (!this._joining) { // if there is no join process ongoing we must initiate one logger.trace('%s storage %s is disconnected, starting join process', this.localPeerId, this.config.storageId); this.join().catch((err) => logger.warn('Failed to join the storage connection %s', err)); } } else if (!this._connected && leaderId !== undefined) { this._connected = true; this.emit('connected'); } } } exports.HamokConnection = HamokConnection; function sortMessagesBySourceIds(messages) { const result = new Map(); for (const message of messages) { let sourceId = message.sourceId; if (!sourceId) { sourceId = '0'; } const messagesFromSource = result.get(sourceId) ?? []; messagesFromSource.push(message); result.set(sourceId, messagesFromSource); } return [...result.entries()].sort(([a], [b]) => a.localeCompare(b)).flatMap(([, msgs]) => msgs); }