@clickup/ent-framework
Version:
A PostgreSQL graph-database-alike library with microsharding and row-level security
205 lines • 10.3 kB
TypeScript
import type { DesperateAny, MaybeCallable, PickPartial, MaybeAsyncCallable } from "../internal/misc";
import type { Client } from "./Client";
import { Island } from "./Island";
import type { LocalCache } from "./LocalCache";
import type { Loggers } from "./Loggers";
import { Shard } from "./Shard";
import type { ShardNamer } from "./ShardNamer";
/**
* Options for Cluster constructor.
*/
export interface ClusterOptions<TClient extends Client, TNode> {
/** Islands configuration of the Cluster. */
islands: MaybeAsyncCallable<ClusterIslands<TNode>>;
/** Given a node of some Island, instantiates a Client for this node. Called
* when a new node appears in the Cluster statically or dynamically. */
createClient: (node: TNode) => TClient;
/** Loggers to be injected into all Clients returned by createClient(). */
loggers: Loggers;
/** An instance of LocalCache which may be used for auxiliary purposes when
* discovering Shards/Clients. */
localCache?: LocalCache | null;
/** How often to recheck for changes in `options.islands`. If it is SYNC, then
* by default - often, like every 500 ms (since it's assumed that
* `options.islands` calculation is cheap). If it is ASYNC, then by default -
* not so often, every `shardsDiscoverIntervalMs` (we assume that getting the
* list of Island nodes may be expensive, e.g. fetching from AWS API or so).
* If the Islands list here changes, then we trigger Shards rediscovery and
* Clients recreation ASAP. */
reloadIslandsIntervalMs?: MaybeCallable<number>;
/** Info on how to build/parse Shard names. */
shardNamer?: ShardNamer | null;
/** How often to run Shards rediscovery in normal circumstances. */
shardsDiscoverIntervalMs?: MaybeCallable<number>;
/** Jitter for shardsDiscoverIntervalMs and reloadIslandsIntervalMs. */
shardsDiscoverIntervalJitter?: MaybeCallable<number>;
/** Used in the following situations:
* 1. If we think that we know the Island of a particular Shard, but an
* attempt to access it fails. This means that maybe the Shard is migrating
* to another Island. So, we wait a bit and retry that many times. We
* should not do it too many times though, because all DB requests will be
* blocked waiting for the resolution.
* 2. If we sent a WRITE request to a Client, but it appeared that this Client
* is a replica, and the master moved to some other Client. In this case,
* we wait a bit and ping all Clients of the Island to refresh, who is
* master and who is replica. */
runOnShardErrorRetryCount?: MaybeCallable<number>;
/** How much time to wait before we retry rediscovering the entire Cluster
* after a Shard-to-Island resolution error. The time here should be just
* enough to wait for switching the Shard from one Island to another
* (typically quick). */
runOnShardErrorRediscoverClusterDelayMs?: MaybeCallable<number>;
/** How much time to wait before sending discover requests to all Clients of
* the Island trying to find the new master (or to reconnect). The time here
* may reach several seconds, since some DBs shut down the old master and
* promote some replica to it not simultaneously. */
runOnShardErrorRediscoverIslandDelayMs?: MaybeCallable<number>;
}
/**
* A type of `ClusterOptions#islands` property. Represents the full list of
* Islands and their corresponding Nodes (masters and replicas).
*/
export type ClusterIslands<TNode> = ReadonlyArray<{
no: number;
nodes: readonly TNode[];
}>;
/**
* Cluster is a collection of Islands and an orchestration of shardNo -> Island
* resolution.
*
* It's unknown beforehand, which Island some particular Shard belongs to; the
* resolution is done asynchronously and lazily.
*
* Shard 0 is a special "global" Shard.
*/
export declare class Cluster<TClient extends Client, TNode = DesperateAny> {
/** Default values for the constructor options. */
static readonly DEFAULT_OPTIONS: Required<PickPartial<ClusterOptions<Client, never>>>;
/** The complete registry of all initialized Clients. Cluster nodes may change
* at runtime, so once a new node appears, its Client is added to the
* registry. Also, the Clients of disappeared nodes are eventually removed
* from the registry on the next Shards discovery. */
private clientRegistry;
/** The complete registry of all Islands ever created. If some Island changes
* configuration, its old version is eventually removed from the registry
* during the next Shards discovery. */
private islandRegistry;
/** Represents the result of the recent successful call to
* `options.islands()`. */
private islandsCache;
/** Represents the result of the recent successful Shards discovery. */
private shardsDiscoverCache;
/** Once set to true, Clients for newly appearing nodes will be pre-warmed. */
private prewarmEnabled;
/** Cluster configuration options. */
readonly options: Required<ClusterOptions<TClient, TNode>>;
/**
* Initializes the Cluster, but doesn't send any queries yet, even discovery
* queries (also, no implicit prewarming).
*/
constructor(options: ClusterOptions<TClient, TNode>);
/**
* Signals the Cluster to keep the Clients pre-warmed, e.g. open. (It's up to
* the particular Client's implementation, what does a "pre-warmed Client"
* mean; typically, it's keeping some minimal number of pooled connections.)
*
* Except when `randomizedDelayMs` is passed as 0, the actual prewarm (and
* Islands discovery) queries will run with a randomized delay between N/2 and
* N ms. It is better to operate in such mode: if multiple Node processes
* start simultaneously in the cluster, then the randomization helps to avoid
* new connections burst (new connections establishment is expensive for e.g.
* pgbouncer or when DB is accessed over SSL).
*/
prewarm(randomizedDelayMs?: number, onInitialPrewarm?: (delayMs: number) => void): void;
/**
* Returns a global Shard of the Cluster. This method is made synchronous
* intentionally, to defer the I/O and possible errors to the moment of the
* actual query.
*/
globalShard(): Shard<TClient>;
/**
* Returns all currently known (discovered) non-global Shards in the Cluster.
*/
nonGlobalShards(): Promise<ReadonlyArray<Shard<TClient>>>;
/**
* Returns Shard of a particular id. This method is made synchronous
* intentionally, to defer the I/O and possible errors to the moment of the
* actual query.
*
* Why is it important? Because Shards may go up and down temporarily at
* random moments of time. Imagine we made this method async and asserted that
* the Shard is actually available at the moment when the method is called.
* What would happen if the Shard object was stored somewhere as "successful"
* by the caller, then the Island went down, and then a query is sent to the
* Shard in, say, 20 seconds? We'd get an absolutely different exception, at
* the moment of the query. We don't want this to happen: we want all of the
* exceptions to be thrown with a consistent call stack (e.g. at the moment of
* the query), no matter whether it was an immediate call or a deferred one.
*/
shard(id: string): Shard<TClient>;
/**
* Returns a Shard if we know its number. The idea: for each Shard number
* (even for non-discovered yet Shards), we keep the corresponding Shard
* object in a Memoize cache, so Shards with the same number always resolve
* into the same Shard object. Then, an actual Island locating process happens
* when the caller wants to get a Client of that Shard (and it throws if such
* Shard hasn't been discovered actually).
*/
shardByNo(shardNo: number): Shard<TClient>;
/**
* Returns a random Shard among the ones which are currently known
* (discovered) in the Cluster.
*/
randomShard(seed?: object): Promise<Shard<TClient>>;
/**
* Returns an Island by its number.
*/
island(islandNo: number): Promise<Island<TClient>>;
/**
* Returns all Islands in the Cluster.
*/
islands(): Promise<Array<Island<TClient>>>;
/**
* Triggers shards rediscovery and finishes as soon as it's done. To be used
* in unit tests mostly, because in real life, it's enough to just modify the
* cluster configuration.
*/
rediscover(what?: "islands" | "shards"): Promise<void>;
/**
* Runs the body function with retries. The Island injected into the body
* function is located automatically by the Shard number. In case of an error
* after any run attempt, calls onAttemptError().
*/
private runOnShard;
/**
* Runs the whole-cluster rediscover after a delay, hoping that we'll load the
* new Shards-to-Island mapping.
*
* Multiple concurrent calls to this method will be coalesced into one
* (including the delay period):
* 1. This protects against the burst of rediscover requests caused by
* multiple failing concurrent queries.
* 2. It also allows to keep the queries batched when they are retried (i.e.
* the whole batch will be retried, not individual queries).
*/
private rediscoverCluster;
/**
* Runs Island#rediscover() after a delay.
*
* Multiple concurrent calls to this method will be coalesced into one
* (including the delay period):
* 1. This protects against the burst of rediscover requests caused by
* multiple failing concurrent queries.
* 2. It also allows to keep the queries batched when they are retried (i.e.
* the whole batch will be retried, not individual queries).
*/
private rediscoverIsland;
/**
* Runs the actual Shards discovery queries over all Islands and updates the
* mapping from each Shard number to an Island where it lives. These queries
* may be expensive, so it's expected that the returned Promise is heavily
* cached by the caller code.
*/
private shardsDiscoverExpensive;
}
//# sourceMappingURL=Cluster.d.ts.map