UNPKG

@clickup/ent-framework

Version:

A PostgreSQL graph-database-alike library with microsharding and row-level security

79 lines 3.23 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Timeline = void 0; const SEP = ":"; /** * Even when pos is expired, we still continue to serialize it for some time for * better debugging. Having this gap allows the system trigger * "replica-bc-pos-expired" reason of a replica choice longer. */ const SERIALIZE_EXPIRATION_GAP_MS = 600 * 1000; /** * Tracks replication lag timeline position at master per "user" and Ent. * - serialization format: "pos:expiresAt" * - wipes expired records (expiration is calculated at assignment moment) * * How replication lag (timeline) tracking works: for each * microshard+Ent+"user", we know the “last write-ahead log write position” * which that user (typically, VC#principal) made recently. This info can be * propagated through e.g. user's session and push notifications/subscriptions * channels automatically (“serialized timeline” and “timelines merging”). So * the next time the same user tries to read the data from the same Ent on the * same microshard, Ent Framework makes a choice, whether the replica is “good * enough” for this already; if not, it falls back to master read. I.e. the data * is not granular to individual Ent ID, it’s granular to the * user+Ent+microshard, and thus it is decoupled from IDs. */ class Timeline { constructor(state = "unknown") { this.state = state; } static deserialize(data, prevTimeline) { const parts = data ? data.split(SEP) : []; const pos = BigInt(parts[0] || "0"); if (prevTimeline && prevTimeline.state !== "unknown" && prevTimeline.state.pos >= pos) { // The previous timeline holds a more recent WAL position than the one // we're deserializing, so we should respect it better. return prevTimeline; } return new this(pos ? { pos, expiresAt: parseInt(parts[1] || "0") } : "unknown"); } static cloneMap(timelines) { const copy = new Map(); for (const [key, timeline] of timelines.entries()) { if (timeline.state !== "unknown") { copy.set(key, new Timeline(timeline.state)); } } return copy; } serialize() { return this.state === "unknown" || Date.now() >= this.state.expiresAt + SERIALIZE_EXPIRATION_GAP_MS ? undefined : this.state.pos.toString() + SEP + this.state.expiresAt; } setPos(pos, maxLagMs) { if (this.state !== "unknown" && this.state.pos >= pos) { // We already hold a "more recent" pos, so don't need to update it. return; } this.state = { pos, expiresAt: Date.now() + maxLagMs }; } isCaughtUp(replicaPos) { return this.state === "unknown" ? "replica-bc-master-state-unknown" : replicaPos >= this.state.pos ? "replica-bc-caught-up" : Date.now() >= this.state.expiresAt ? "replica-bc-pos-expired" : false; } reset() { this.state = "unknown"; } } exports.Timeline = Timeline; //# sourceMappingURL=Timeline.js.map