git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
204 lines
7.3 KiB
JavaScript
204 lines
7.3 KiB
JavaScript
"use strict";
|
|
/**
|
|
* Replica Set Management
|
|
* Manages a set of replicas for distributed data storage
|
|
*/
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.ReplicaSet = void 0;
|
|
const eventemitter3_1 = __importDefault(require("eventemitter3"));
|
|
const types_js_1 = require("./types.js");
|
|
/** Default configuration */
|
|
const DEFAULT_CONFIG = {
|
|
name: 'default',
|
|
minQuorum: 2,
|
|
heartbeatInterval: 1000,
|
|
healthCheckTimeout: 5000,
|
|
failoverPolicy: types_js_1.FailoverPolicy.Automatic,
|
|
};
|
|
/** Manages a set of replicas */
|
|
class ReplicaSet extends eventemitter3_1.default {
|
|
constructor(name, config) {
|
|
super();
|
|
this.replicas = new Map();
|
|
this.heartbeatTimer = null;
|
|
this.config = { ...DEFAULT_CONFIG, name, ...config };
|
|
}
|
|
/** Get replica set name */
|
|
get name() {
|
|
return this.config.name;
|
|
}
|
|
/** Get the primary replica */
|
|
get primary() {
|
|
for (const replica of this.replicas.values()) {
|
|
if (replica.role === types_js_1.ReplicaRole.Primary && replica.status === types_js_1.ReplicaStatus.Active) {
|
|
return replica;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
/** Get all secondary replicas */
|
|
get secondaries() {
|
|
return Array.from(this.replicas.values()).filter((r) => r.role === types_js_1.ReplicaRole.Secondary && r.status === types_js_1.ReplicaStatus.Active);
|
|
}
|
|
/** Get all active replicas */
|
|
get activeReplicas() {
|
|
return Array.from(this.replicas.values()).filter((r) => r.status === types_js_1.ReplicaStatus.Active);
|
|
}
|
|
/** Get replica count */
|
|
get size() {
|
|
return this.replicas.size;
|
|
}
|
|
/** Check if quorum is met */
|
|
get hasQuorum() {
|
|
const activeCount = this.activeReplicas.length;
|
|
return activeCount >= this.config.minQuorum;
|
|
}
|
|
/** Add a replica to the set */
|
|
addReplica(id, address, role) {
|
|
if (this.replicas.has(id)) {
|
|
throw new Error(`Replica ${id} already exists`);
|
|
}
|
|
// Check if adding a primary when one exists
|
|
if (role === types_js_1.ReplicaRole.Primary && this.primary) {
|
|
throw new Error('Primary already exists in replica set');
|
|
}
|
|
const replica = {
|
|
id,
|
|
address,
|
|
role,
|
|
status: types_js_1.ReplicaStatus.Active,
|
|
lastSeen: Date.now(),
|
|
lag: 0,
|
|
};
|
|
this.replicas.set(id, replica);
|
|
this.emit(types_js_1.ReplicationEvent.ReplicaAdded, replica);
|
|
return replica;
|
|
}
|
|
/** Remove a replica from the set */
|
|
removeReplica(id) {
|
|
const replica = this.replicas.get(id);
|
|
if (!replica)
|
|
return false;
|
|
this.replicas.delete(id);
|
|
this.emit(types_js_1.ReplicationEvent.ReplicaRemoved, replica);
|
|
// If primary was removed, trigger failover
|
|
if (replica.role === types_js_1.ReplicaRole.Primary && this.config.failoverPolicy === types_js_1.FailoverPolicy.Automatic) {
|
|
this.triggerFailover();
|
|
}
|
|
return true;
|
|
}
|
|
/** Get a replica by ID */
|
|
getReplica(id) {
|
|
return this.replicas.get(id);
|
|
}
|
|
/** Update replica status */
|
|
updateStatus(id, status) {
|
|
const replica = this.replicas.get(id);
|
|
if (!replica) {
|
|
throw types_js_1.ReplicationError.replicaNotFound(id);
|
|
}
|
|
const previousStatus = replica.status;
|
|
replica.status = status;
|
|
replica.lastSeen = Date.now();
|
|
if (previousStatus !== status) {
|
|
this.emit(types_js_1.ReplicationEvent.ReplicaStatusChanged, {
|
|
replica,
|
|
previousStatus,
|
|
newStatus: status,
|
|
});
|
|
// Check for failover conditions
|
|
if (replica.role === types_js_1.ReplicaRole.Primary &&
|
|
status === types_js_1.ReplicaStatus.Failed &&
|
|
this.config.failoverPolicy === types_js_1.FailoverPolicy.Automatic) {
|
|
this.triggerFailover();
|
|
}
|
|
}
|
|
}
|
|
/** Update replica lag */
|
|
updateLag(id, lag) {
|
|
const replica = this.replicas.get(id);
|
|
if (replica) {
|
|
replica.lag = lag;
|
|
replica.lastSeen = Date.now();
|
|
}
|
|
}
|
|
/** Promote a secondary to primary */
|
|
promote(id) {
|
|
const replica = this.replicas.get(id);
|
|
if (!replica) {
|
|
throw types_js_1.ReplicationError.replicaNotFound(id);
|
|
}
|
|
if (replica.role === types_js_1.ReplicaRole.Primary) {
|
|
return; // Already primary
|
|
}
|
|
// Demote current primary
|
|
const currentPrimary = this.primary;
|
|
if (currentPrimary) {
|
|
currentPrimary.role = types_js_1.ReplicaRole.Secondary;
|
|
}
|
|
// Promote new primary
|
|
replica.role = types_js_1.ReplicaRole.Primary;
|
|
this.emit(types_js_1.ReplicationEvent.PrimaryChanged, {
|
|
previousPrimary: currentPrimary?.id,
|
|
newPrimary: id,
|
|
});
|
|
}
|
|
/** Trigger automatic failover */
|
|
triggerFailover() {
|
|
this.emit(types_js_1.ReplicationEvent.FailoverStarted, {});
|
|
// Find the best candidate (lowest lag, active secondary)
|
|
const candidates = this.secondaries
|
|
.filter((r) => r.status === types_js_1.ReplicaStatus.Active)
|
|
.sort((a, b) => a.lag - b.lag);
|
|
if (candidates.length === 0) {
|
|
this.emit(types_js_1.ReplicationEvent.Error, types_js_1.ReplicationError.noPrimary());
|
|
return;
|
|
}
|
|
const newPrimary = candidates[0];
|
|
this.promote(newPrimary.id);
|
|
this.emit(types_js_1.ReplicationEvent.FailoverCompleted, { newPrimary: newPrimary.id });
|
|
}
|
|
/** Start heartbeat monitoring */
|
|
startHeartbeat() {
|
|
if (this.heartbeatTimer)
|
|
return;
|
|
this.heartbeatTimer = setInterval(() => {
|
|
const now = Date.now();
|
|
for (const replica of this.replicas.values()) {
|
|
if (now - replica.lastSeen > this.config.healthCheckTimeout) {
|
|
if (replica.status === types_js_1.ReplicaStatus.Active) {
|
|
this.updateStatus(replica.id, types_js_1.ReplicaStatus.Offline);
|
|
}
|
|
}
|
|
}
|
|
}, this.config.heartbeatInterval);
|
|
}
|
|
/** Stop heartbeat monitoring */
|
|
stopHeartbeat() {
|
|
if (this.heartbeatTimer) {
|
|
clearInterval(this.heartbeatTimer);
|
|
this.heartbeatTimer = null;
|
|
}
|
|
}
|
|
/** Get all replicas */
|
|
getAllReplicas() {
|
|
return Array.from(this.replicas.values());
|
|
}
|
|
/** Get replica set stats */
|
|
getStats() {
|
|
const replicas = Array.from(this.replicas.values());
|
|
return {
|
|
total: replicas.length,
|
|
active: replicas.filter((r) => r.status === types_js_1.ReplicaStatus.Active).length,
|
|
syncing: replicas.filter((r) => r.status === types_js_1.ReplicaStatus.Syncing).length,
|
|
offline: replicas.filter((r) => r.status === types_js_1.ReplicaStatus.Offline).length,
|
|
failed: replicas.filter((r) => r.status === types_js_1.ReplicaStatus.Failed).length,
|
|
hasQuorum: this.hasQuorum,
|
|
};
|
|
}
|
|
}
|
|
exports.ReplicaSet = ReplicaSet;
|
|
//# sourceMappingURL=replica-set.js.map
|