UNPKG

@rivetkit/redis

Version:

_Lightweight Libraries for Backends_

354 lines (348 loc) 11.4 kB
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