@clickup/ent-framework
Version:
A PostgreSQL graph-database-alike library with microsharding and row-level security
106 lines • 4.33 kB
JavaScript
"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