diff --git a/src/config.ts b/src/config.ts index 70e1fc3..1d5ed94 100644 --- a/src/config.ts +++ b/src/config.ts @@ -139,6 +139,15 @@ addDefaults(config, { expiryTime: 24 * 60 * 60, getTimeout: 40 }, + redisRead: { + enabled: false, + socket: { + host: "", + port: 0 + }, + disableOfflineQueue: true, + weight: 1 + }, patreon: { clientId: "", clientSecret: "", diff --git a/src/databases/Postgres.ts b/src/databases/Postgres.ts index 01f59b1..c10f094 100644 --- a/src/databases/Postgres.ts +++ b/src/databases/Postgres.ts @@ -178,11 +178,11 @@ export class Postgres implements IDatabase { private getPool(type: string, options: QueryOption): Pool { const readAvailable = this.poolRead && options.useReplica && this.isReadQuery(type); - const ignroreReadDueToFailure = this.config.postgresReadOnly.fallbackOnFail + const ignoreReadDueToFailure = this.config.postgresReadOnly.fallbackOnFail && this.lastPoolReadFail > Date.now() - 1000 * 30; const readDueToFailure = this.config.postgresReadOnly.fallbackOnFail && this.lastPoolFail > Date.now() - 1000 * 30; - if (readAvailable && !ignroreReadDueToFailure && (options.forceReplica || readDueToFailure || + if (readAvailable && !ignoreReadDueToFailure && (options.forceReplica || readDueToFailure || Math.random() > 1 / (this.config.postgresReadOnly.weight + 1))) { return this.poolRead; } else { diff --git a/src/types/config.model.ts b/src/types/config.model.ts index 406643c..e0498fb 100644 --- a/src/types/config.model.ts +++ b/src/types/config.model.ts @@ -7,6 +7,11 @@ interface RedisConfig extends redis.RedisClientOptions { getTimeout: number; } +interface RedisReadOnlyConfig extends redis.RedisClientOptions { + enabled: boolean; + weight: number; +} + export interface CustomPostgresConfig extends PoolConfig { enabled: boolean; maxTries: number; @@ -61,6 +66,7 @@ export interface SBSConfig { minimumPrefix?: string; maximumPrefix?: string; redis?: RedisConfig; + redisRead?: RedisReadOnlyConfig; maxRewardTimePerSegmentInSeconds?: number; postgres?: CustomPostgresConfig; postgresReadOnly?: CustomPostgresReadOnlyConfig; diff --git a/src/utils/redis.ts b/src/utils/redis.ts index 8e8c6d0..6c13133 100644 --- a/src/utils/redis.ts +++ b/src/utils/redis.ts @@ -25,19 +25,35 @@ let exportClient: RedisSB = { quit: () => new Promise((resolve) => resolve(null)), }; +let lastClientFail = 0; +let lastReadFail = 0; + if (config.redis?.enabled) { Logger.info("Connected to redis"); const client = createClient(config.redis); + const readClient = config.redisRead?.enabled ? createClient(config.redisRead) : null; void client.connect(); // void as we don't care about the promise + void readClient?.connect(); exportClient = client as RedisSB; + const get = client.get.bind(client); + const getRead = readClient?.get?.bind(readClient); exportClient.get = (key) => new Promise((resolve, reject) => { const timeout = config.redis.getTimeout ? setTimeout(() => reject(), config.redis.getTimeout) : null; - get(key).then((reply) => { + const chosenGet = pickChoice(get, getRead); + chosenGet(key).then((reply) => { if (timeout !== null) clearTimeout(timeout); resolve(reply); - }).catch((err) => reject(err)); + }).catch((err) => { + if (chosenGet === get) { + lastClientFail = Date.now(); + } else { + lastReadFail = Date.now(); + } + + reject(err); + }); }); exportClient.increment = (key) => new Promise((resolve, reject) => void client.multi() @@ -48,11 +64,31 @@ if (config.redis?.enabled) { .catch((err) => reject(err)) ); client.on("error", function(error) { + lastClientFail = Date.now(); Logger.error(`Redis Error: ${error}`); }); client.on("reconnect", () => { Logger.info("Redis: trying to reconnect"); }); + readClient?.on("error", function(error) { + lastReadFail = Date.now(); + Logger.error(`Redis Read-Only Error: ${error}`); + }); + readClient?.on("reconnect", () => { + Logger.info("Redis Read-Only: trying to reconnect"); + }); +} + +function pickChoice(client: T, readClient: T): T { + const readAvailable = !!readClient; + const ignoreReadDueToFailure = lastReadFail > Date.now() - 1000 * 30; + const readDueToFailure = lastClientFail > Date.now() - 1000 * 30; + if (readAvailable && !ignoreReadDueToFailure && (readDueToFailure || + Math.random() > 1 / (config.redisRead?.weight + 1))) { + return readClient; + } else { + return client; + } } export default exportClient;