Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
204
npm/packages/replication/src/replica-set.js
Normal file
204
npm/packages/replication/src/replica-set.js
Normal file
@@ -0,0 +1,204 @@
|
||||
"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
|
||||
Reference in New Issue
Block a user