UNPKG

@clickup/ent-framework

Version:

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

106 lines 4.33 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Shard = exports.STALE_REPLICA = exports.MASTER = void 0; const misc_1 = require("../internal/misc"); /** * Master freshness: reads always go to master. */ exports.MASTER = Symbol("MASTER"); /** * Stale replica freshness: reads always go to a replica, even if it's stale. */ exports.STALE_REPLICA = Symbol("STALE_REPLICA"); /** * Shard lives within an Island with one master and N replicas. */ class Shard { constructor( /** Shard number. */ no, /** A middleware to wrap queries with. It's responsible for locating the * right Island and retrying the call to body() (i.e. failed queries) in * case e.g. a shard is moved to another Island. */ runOnShard) { this.no = no; this.runOnShard = runOnShard; this.shardClients = new WeakMap(); /** The last known Island number where this Shard was discovered. It may be * out of date after the Shard is moved, and also it may be null in case there * was no discovery happened yet. */ this.lastKnownIslandNo = null; } /** * Chooses the right Client to be used for this Shard. We don't memoize, * because the Shard may relocate to another Island during re-discovery. */ async client(timeline) { const [client] = await this.runOnShard(this.no, async (island) => this.clientImpl(island, timeline, undefined)); return client; } /** * Runs a query after choosing the right Client (destination connection, * Shard, annotation etc.) */ async run(query, annotation, timeline, freshness, onAttemptError) { return this.runOnShard(this.no, async (island, attempt) => { const [client, whyClient] = await this.clientImpl(island, freshness ?? timeline, query.IS_WRITE ? true : undefined); // Throws if e.g. the Shard was there by the moment we got its client // above, but it probably disappeared (during migration) and appeared on // some other Island. const res = await query.run(client, { ...annotation, whyClient, attempt: annotation.attempt + attempt, }); if (query.IS_WRITE && freshness !== exports.STALE_REPLICA) { timeline.setPos(await client.timelineManager.currentPos(), (0, misc_1.maybeCall)(client.timelineManager.maxLagMs)); } return res; }, onAttemptError); } /** * Throws if this Shard does not exist, or its Island is down, or something * else is wrong with it. */ async assertDiscoverable() { await this.client(exports.MASTER); } /** * An extended Client selection logic. There are multiple reasons (8+ in total * so far) why a master or a replica may be chosen to send the query to. We * don't @Memoize, because the Shard may relocate to another Island during * re-discovery, so we have to run this logic every time. */ async clientImpl(island, timeline, isWrite) { if (isWrite) { return [this.withShard(island.master()), "master-bc-is-write"]; } if (timeline === exports.MASTER) { return [this.withShard(island.master()), "master-bc-master-freshness"]; } const replica = island.replica(); if (replica.role() !== "replica") { return [this.withShard(replica), "master-bc-no-replicas"]; } if (timeline === exports.STALE_REPLICA) { return [this.withShard(replica), "replica-bc-stale-replica-freshness"]; } const isCaughtUp = timeline.isCaughtUp(await replica.timelineManager.currentPos()); return isCaughtUp ? [this.withShard(replica), isCaughtUp] : [this.withShard(island.master()), "master-bc-replica-not-caught-up"]; } /** * Returns a Shard-aware Client from an Island Client. */ withShard(client) { let shardClient = this.shardClients.get(client); if (!shardClient) { shardClient = client.withShard(this.no); this.shardClients.set(client, shardClient); } return shardClient; } } exports.Shard = Shard; //# sourceMappingURL=Shard.js.map