hamok
Version:
Lightweight Distributed Object Storage on RAFT consensus algorithm
586 lines (585 loc) • 33.1 kB
JavaScript
"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);
}