@rivetkit/redis
Version:
_Lightweight Libraries for Backends_
354 lines (348 loc) • 11.4 kB
JavaScript
import {
KEYS,
getLogger,
logger
} from "./chunk-K6L53HR4.js";
// src/actor.ts
import invariant from "invariant";
// src/coordinate/actor-peer.ts
import {
createGenericConnDrivers,
GenericConnGlobalState
} from "@rivetkit/core";
// src/coordinate/log.ts
var logger2 = () => getLogger("redis-coordinate");
// src/coordinate/actor-peer.ts
var ActorPeer = class _ActorPeer {
#registryConfig;
#runConfig;
#driverConfig;
#coordinateDriver;
#actorDriver;
#inlineClient;
// Client type
#globalState;
#actorId;
#actorName;
#actorKey;
#isDisposed = false;
/** Connections that hold a reference to this actor. If this set is empty, the actor should be shut down. */
#referenceConnections = /* @__PURE__ */ new Set();
/** Node ID that's the leader for this actor. */
#leaderNodeId;
/** Holds the insantiated actor class if is leader. */
#loadedActor;
/** Promise that resolves when the actor has fully started (only for leaders) */
#loadedActorStartingPromise;
#heartbeatTimeout;
// TODO: Only create this when becomse leader
genericConnGlobalState = new GenericConnGlobalState();
get #isLeader() {
return this.#leaderNodeId === this.#globalState.nodeId;
}
get isLeader() {
return this.#isLeader;
}
get leaderNodeId() {
if (!this.#leaderNodeId) throw new Error("Not found leader node ID yet");
return this.#leaderNodeId;
}
get loadedActor() {
return this.#loadedActor;
}
get loadedActorStartingPromise() {
return this.#loadedActorStartingPromise;
}
constructor(registryConfig, runConfig, driverConfig, CoordinateDriver, actorDriver, inlineClient, globalState, actorId) {
this.#registryConfig = registryConfig;
this.#runConfig = runConfig;
this.#driverConfig = driverConfig;
this.#coordinateDriver = CoordinateDriver;
this.#actorDriver = actorDriver;
this.#inlineClient = inlineClient;
this.#globalState = globalState;
this.#actorId = actorId;
}
/** Acquires a `ActorPeer` for a connection and includes the connection ID in the references. */
static async acquire(registryConfig, runConfig, driverConfig, actorDriver, inlineClient, CoordinateDriver, globalState, actorId, connId) {
let peer = globalState.actorPeers.get(actorId);
if (!peer) {
peer = new _ActorPeer(
registryConfig,
runConfig,
driverConfig,
CoordinateDriver,
actorDriver,
inlineClient,
globalState,
actorId
);
globalState.actorPeers.set(actorId, peer);
await peer.#start();
}
peer.#referenceConnections.add(connId);
logger2().debug("added actor reference", {
actorId,
connId,
newReferenceCount: peer.#referenceConnections.size
});
return peer;
}
static getLeaderActorPeer(globalState, actorId) {
const peer = globalState.actorPeers.get(actorId);
if (!peer) return void 0;
if (peer.#isLeader) {
return peer;
} else {
return void 0;
}
}
static async getLeaderActor(globalState, actorId) {
const peer = _ActorPeer.getLeaderActorPeer(globalState, actorId);
if (!peer) return void 0;
if (peer.loadedActorStartingPromise) {
await peer.loadedActorStartingPromise;
}
const actor = peer.loadedActor;
if (!actor)
throw new Error("Actor is leader, but loadedActor is undefined");
return actor;
}
async #start() {
const { actor } = await this.#coordinateDriver.startActorAndAcquireLease(
this.#actorId,
this.#globalState.nodeId,
this.#driverConfig.actorPeer.leaseDuration
);
logger2().debug("starting actor peer", {
actor
});
if (!actor) {
throw new Error("Actor does not exist");
}
this.#actorName = actor.name;
this.#actorKey = actor.key;
this.#leaderNodeId = actor.leaderNodeId;
if (actor.leaderNodeId === this.#globalState.nodeId) {
logger2().debug("actor peer is leader", {
actorId: this.#actorId,
leaderNodeId: actor.leaderNodeId
});
await this.#convertToLeader();
} else {
logger2().debug("actor peer is follower", {
actorId: this.#actorId,
leaderNodeId: actor.leaderNodeId
});
this.#leaderNodeId = actor.leaderNodeId;
}
this.#scheduleHeartbeat();
}
async #heartbeat() {
if (this.#isDisposed) return;
if (this.#isLeader) {
await this.#extendLease();
} else {
await this.#attemptAcquireLease();
}
this.#scheduleHeartbeat();
}
#scheduleHeartbeat() {
let hbTimeout;
if (this.#isLeader) {
hbTimeout = this.#driverConfig.actorPeer.leaseDuration - this.#driverConfig.actorPeer.renewLeaseGrace;
} else {
hbTimeout = this.#driverConfig.actorPeer.checkLeaseInterval + Math.random() * this.#driverConfig.actorPeer.checkLeaseJitter;
}
if (hbTimeout < 0)
throw new Error("Actor peer heartbeat timeout is negative, check config");
this.#heartbeatTimeout = setTimeout(this.#heartbeat.bind(this), hbTimeout);
}
async #convertToLeader() {
if (!this.#actorName || !this.#actorKey)
throw new Error("missing name or key");
logger2().debug("peer acquired leadership", { actorId: this.#actorId });
const actorName = this.#actorName;
const definition = this.#registryConfig.use[actorName];
if (!definition)
throw new Error(`no actor definition for name ${definition}`);
const actor = definition.instantiate();
this.#loadedActor = actor;
this.#loadedActorStartingPromise = (async () => {
const connDrivers = createGenericConnDrivers(this.genericConnGlobalState);
await actor.start(
connDrivers,
this.#actorDriver,
this.#inlineClient,
this.#actorId,
this.#actorName,
this.#actorKey,
"unknown"
);
})();
await this.#loadedActorStartingPromise;
}
/**
* Extends the lease if the current leader. Called on an interval for leaders leader.
*
* If the lease has expired for any reason (e.g. connection latency or database purged), this will automatically shut down the actor.
*/
async #extendLease() {
const { leaseValid } = await this.#coordinateDriver.extendLease(
this.#actorId,
this.#globalState.nodeId,
this.#driverConfig.actorPeer.leaseDuration
);
if (leaseValid) {
logger2().trace("lease is valid", { actorId: this.#actorId });
} else {
logger2().debug("lease is invalid", { actorId: this.#actorId });
await this.#dispose(false);
}
}
/**
* Attempts to acquire a lease (aka checks if the leader's lease has expired). Called on an interval for followers.
*/
async #attemptAcquireLease() {
const { newLeaderNodeId } = await this.#coordinateDriver.attemptAcquireLease(
this.#actorId,
this.#globalState.nodeId,
this.#driverConfig.actorPeer.leaseDuration
);
const isPromoted = !this.#isLeader && newLeaderNodeId === this.#globalState.nodeId;
const leaderChanged = this.#leaderNodeId !== newLeaderNodeId && !isPromoted;
this.#leaderNodeId = newLeaderNodeId;
if (leaderChanged) {
this.#closeAllWebSockets();
}
if (isPromoted) {
if (!this.#isLeader) throw new Error("assert: should be promoted");
await this.#convertToLeader();
}
}
async removeConnectionReference(connId) {
const removed = this.#referenceConnections.delete(connId);
if (removed) {
logger2().debug("removed actor reference", {
actorId: this.#actorId,
connId,
newReferenceCount: this.#referenceConnections.size
});
} else {
logger2().warn("removed reference to actor that didn't exist", {
actorId: this.#actorId,
connId
});
}
if (this.#referenceConnections.size === 0) {
await this.#dispose(true);
}
}
#closeAllWebSockets() {
logger2().info("closing all websockets due to leader change", {
actorId: this.#actorId
});
const relayWebSockets = this.#globalState.relayWebSockets;
if (relayWebSockets) {
for (const [wsId, ws] of relayWebSockets) {
if (ws.actorId === this.#actorId) {
ws._handleClose(1001, "Actor leader changed");
relayWebSockets.delete(wsId);
}
}
}
const followerWebSockets = this.#globalState.followerWebSockets;
if (followerWebSockets) {
for (const [wsId, wsData] of followerWebSockets) {
if (wsData.actorId === this.#actorId) {
wsData.ws.close(1001, "Actor leader changed");
followerWebSockets.delete(wsId);
}
}
}
const leaderWebSockets = this.#globalState.leaderWebSockets;
if (leaderWebSockets) {
for (const [wsId, wsData] of leaderWebSockets) {
if (wsData.actorId === this.#actorId) {
if (wsData.wsContext && wsData.wsContext.close) {
wsData.wsContext.close(1001, "Actor leader changed");
}
leaderWebSockets.delete(wsId);
}
}
}
}
async #dispose(releaseLease) {
if (this.#isDisposed) return;
this.#isDisposed = true;
logger2().info("actor shutting down", { actorId: this.#actorId });
clearTimeout(this.#heartbeatTimeout);
this.#globalState.actorPeers.delete(this.#actorId);
if (this.#isLeader && this.#loadedActor) {
await this.#loadedActor.stop();
}
if (this.#isLeader && releaseLease) {
await this.#coordinateDriver.releaseLease(
this.#actorId,
this.#globalState.nodeId
);
}
logger2().info("actor shutdown success", { actorId: this.#actorId });
}
};
// src/actor.ts
var RedisActorDriver = class {
#globalState;
#redis;
#driverConfig;
constructor(globalState, redis, driverConfig) {
this.#globalState = globalState;
this.#redis = redis;
this.#driverConfig = driverConfig;
}
async loadActor(actorId) {
const actor = await ActorPeer.getLeaderActor(this.#globalState, actorId);
invariant(actor, `Actor ${actorId} is not the leader on this node`);
return actor;
}
getGenericConnGlobalState(actorId) {
const peer = ActorPeer.getLeaderActorPeer(this.#globalState, actorId);
invariant(peer, `Actor ${actorId} is not the leader on this node`);
return peer.genericConnGlobalState;
}
getContext(_actorId) {
return { redis: this.#redis, keyPrefix: this.#driverConfig.keyPrefix };
}
async readPersistedData(actorId) {
const data = await this.#redis.getBuffer(
KEYS.ACTOR.persistedData(this.#driverConfig.keyPrefix, actorId)
);
if (data !== null) return data;
return void 0;
}
async writePersistedData(actorId, data) {
await this.#redis.set(
KEYS.ACTOR.persistedData(this.#driverConfig.keyPrefix, actorId),
Buffer.from(data)
);
}
async setAlarm(actor, timestamp) {
logger().warn(
"redis driver currently does not support scheduling. alarms are currently implemented with setTimeout and will not survive sleeping.replaced with setTimeout.",
{ issue: "https://github.com/rivet-gg/rivetkit/issues/1095" }
);
const delay = Math.max(timestamp - Date.now(), 0);
setTimeout(() => {
actor.onAlarm();
}, delay);
}
getDatabase(actorId) {
return Promise.resolve(void 0);
}
};
export {
logger2 as logger,
ActorPeer,
RedisActorDriver
};
//# sourceMappingURL=chunk-W4M5YHOW.js.map