mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #4496 from Infisical/feat/redis-read-replica
feat: added support for redis read replica and updated doc
This commit is contained in:
@@ -3,6 +3,7 @@ import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext";
|
||||
import { applyJitter } from "@app/lib/dates";
|
||||
import { delay as delayMs } from "@app/lib/delay";
|
||||
import { ExecutionResult, Redlock, Settings } from "@app/lib/red-lock";
|
||||
import { Redis, Cluster } from "ioredis";
|
||||
|
||||
export const PgSqlLock = {
|
||||
BootUpMigration: 2023,
|
||||
@@ -102,30 +103,57 @@ export type TKeyStoreFactory = {
|
||||
getKeysByPattern: (pattern: string, limit?: number) => Promise<string[]>;
|
||||
};
|
||||
|
||||
export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys): TKeyStoreFactory => {
|
||||
const redis = buildRedisFromConfig(redisConfigKeys);
|
||||
const redisLock = new Redlock([redis], { retryCount: 2, retryDelay: 200 });
|
||||
const pickPrimaryOrSecondaryRedis = (primary: Redis | Cluster, secondaries?: Array<Redis | Cluster>) => {
|
||||
if (!secondaries || !secondaries.length) return primary;
|
||||
const selectedReplica = secondaries[Math.floor(Math.random() * secondaries.length)];
|
||||
return selectedReplica;
|
||||
};
|
||||
|
||||
interface TKeyStoreFactoryDTO extends TRedisConfigKeys {
|
||||
REDIS_READ_REPLICAS?: { host: string; port: number }[];
|
||||
}
|
||||
|
||||
export const keyStoreFactory = (redisConfigKeys: TKeyStoreFactoryDTO): TKeyStoreFactory => {
|
||||
const primaryRedis = buildRedisFromConfig(redisConfigKeys);
|
||||
const redisReadReplicas = redisConfigKeys.REDIS_READ_REPLICAS?.map((el) => {
|
||||
if (redisConfigKeys.REDIS_URL) {
|
||||
const primaryNode = new URL(redisConfigKeys?.REDIS_URL);
|
||||
primaryNode.hostname = el.host;
|
||||
primaryNode.port = String(el.port);
|
||||
return buildRedisFromConfig({ ...redisConfigKeys, REDIS_URL: primaryNode.toString() });
|
||||
}
|
||||
|
||||
if (redisConfigKeys.REDIS_SENTINEL_HOSTS) {
|
||||
return buildRedisFromConfig({ ...redisConfigKeys, REDIS_SENTINEL_HOSTS: [el] });
|
||||
}
|
||||
|
||||
return buildRedisFromConfig({ ...redisConfigKeys, REDIS_CLUSTER_HOSTS: [el] });
|
||||
});
|
||||
const redisLock = new Redlock([primaryRedis], { retryCount: 2, retryDelay: 200 });
|
||||
|
||||
const setItem = async (key: string, value: string | number | Buffer, prefix?: string) =>
|
||||
redis.set(prefix ? `${prefix}:${key}` : key, value);
|
||||
primaryRedis.set(prefix ? `${prefix}:${key}` : key, value);
|
||||
|
||||
const getItem = async (key: string, prefix?: string) => redis.get(prefix ? `${prefix}:${key}` : key);
|
||||
const getItem = async (key: string, prefix?: string) =>
|
||||
pickPrimaryOrSecondaryRedis(primaryRedis, redisReadReplicas).get(prefix ? `${prefix}:${key}` : key);
|
||||
|
||||
const getItems = async (keys: string[], prefix?: string) =>
|
||||
redis.mget(keys.map((key) => (prefix ? `${prefix}:${key}` : key)));
|
||||
pickPrimaryOrSecondaryRedis(primaryRedis, redisReadReplicas).mget(
|
||||
keys.map((key) => (prefix ? `${prefix}:${key}` : key))
|
||||
);
|
||||
|
||||
const setItemWithExpiry = async (
|
||||
key: string,
|
||||
expiryInSeconds: number | string,
|
||||
value: string | number | Buffer,
|
||||
prefix?: string
|
||||
) => redis.set(prefix ? `${prefix}:${key}` : key, value, "EX", expiryInSeconds);
|
||||
) => primaryRedis.set(prefix ? `${prefix}:${key}` : key, value, "EX", expiryInSeconds);
|
||||
|
||||
const deleteItem = async (key: string) => redis.del(key);
|
||||
const deleteItem = async (key: string) => primaryRedis.del(key);
|
||||
|
||||
const deleteItemsByKeyIn = async (keys: string[]) => {
|
||||
if (keys.length === 0) return 0;
|
||||
return redis.del(keys);
|
||||
return primaryRedis.del(keys);
|
||||
};
|
||||
|
||||
const deleteItems = async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }: TDeleteItems) => {
|
||||
@@ -135,12 +163,12 @@ export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys): TKeyStoreFac
|
||||
do {
|
||||
// Await in loop is needed so that Redis is not overwhelmed
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const [nextCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 1000); // Count should be 1000 - 5000 for prod loads
|
||||
const [nextCursor, keys] = await primaryRedis.scan(cursor, "MATCH", pattern, "COUNT", 1000); // Count should be 1000 - 5000 for prod loads
|
||||
cursor = nextCursor;
|
||||
|
||||
for (let i = 0; i < keys.length; i += batchSize) {
|
||||
const batch = keys.slice(i, i + batchSize);
|
||||
const pipeline = redis.pipeline();
|
||||
const pipeline = primaryRedis.pipeline();
|
||||
for (const key of batch) {
|
||||
pipeline.unlink(key);
|
||||
}
|
||||
@@ -156,9 +184,9 @@ export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys): TKeyStoreFac
|
||||
return totalDeleted;
|
||||
};
|
||||
|
||||
const incrementBy = async (key: string, value: number) => redis.incrby(key, value);
|
||||
const incrementBy = async (key: string, value: number) => primaryRedis.incrby(key, value);
|
||||
|
||||
const setExpiry = async (key: string, expiryInSeconds: number) => redis.expire(key, expiryInSeconds);
|
||||
const setExpiry = async (key: string, expiryInSeconds: number) => primaryRedis.expire(key, expiryInSeconds);
|
||||
|
||||
const waitTillReady = async ({
|
||||
key,
|
||||
@@ -189,7 +217,13 @@ export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys): TKeyStoreFac
|
||||
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const [nextCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 1000);
|
||||
const [nextCursor, keys] = await pickPrimaryOrSecondaryRedis(primaryRedis, redisReadReplicas).scan(
|
||||
cursor,
|
||||
"MATCH",
|
||||
pattern,
|
||||
"COUNT",
|
||||
1000
|
||||
);
|
||||
cursor = nextCursor;
|
||||
allKeys.push(...keys);
|
||||
|
||||
|
||||
@@ -57,6 +57,22 @@ const envSchema = z
|
||||
.optional()
|
||||
.describe("Comma-separated list of Redis Cluster host:port pairs. Eg: 192.168.65.254:6379,192.168.65.254:6380")
|
||||
),
|
||||
REDIS_READ_REPLICAS: zpStr(
|
||||
z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Comma-separated list of Redis read replicas host:port pairs. Eg: 192.168.65.254:6379,192.168.65.254:6380"
|
||||
)
|
||||
),
|
||||
REDIS_CLUSTER_ENABLE_TLS: z
|
||||
.enum(["true", "false"])
|
||||
.default("false")
|
||||
.transform((el) => el === "true"),
|
||||
REDIS_CLUSTER_AWS_ELASTICACHE_DNS_LOOKUP_MODE: z
|
||||
.enum(["true", "false"])
|
||||
.default("false")
|
||||
.transform((el) => el === "true"),
|
||||
HOST: zpStr(z.string().default("localhost")),
|
||||
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default(
|
||||
`postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`
|
||||
@@ -377,6 +393,12 @@ const envSchema = z
|
||||
const [host, port] = el.trim().split(":");
|
||||
return { host: host.trim(), port: Number(port.trim()) };
|
||||
}),
|
||||
REDIS_READ_REPLICAS: data.REDIS_READ_REPLICAS?.trim()
|
||||
?.split(",")
|
||||
.map((el) => {
|
||||
const [host, port] = el.trim().split(":");
|
||||
return { host: host.trim(), port: Number(port.trim()) };
|
||||
}),
|
||||
isSecretScanningConfigured:
|
||||
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
|
||||
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
|
||||
|
||||
@@ -6,6 +6,9 @@ export type TRedisConfigKeys = Partial<{
|
||||
REDIS_PASSWORD: string;
|
||||
|
||||
REDIS_CLUSTER_HOSTS: { host: string; port: number }[];
|
||||
REDIS_CLUSTER_ENABLE_TLS: boolean;
|
||||
// ref: https://github.com/redis/ioredis?tab=readme-ov-file#special-note-aws-elasticache-clusters-with-tls
|
||||
REDIS_CLUSTER_AWS_ELASTICACHE_DNS_LOOKUP_MODE: boolean;
|
||||
|
||||
REDIS_SENTINEL_HOSTS: { host: string; port: number }[];
|
||||
REDIS_SENTINEL_MASTER_NAME: string;
|
||||
@@ -19,9 +22,13 @@ export const buildRedisFromConfig = (cfg: TRedisConfigKeys) => {
|
||||
|
||||
if (cfg.REDIS_CLUSTER_HOSTS) {
|
||||
return new Redis.Cluster(cfg.REDIS_CLUSTER_HOSTS, {
|
||||
dnsLookup: cfg.REDIS_CLUSTER_AWS_ELASTICACHE_DNS_LOOKUP_MODE
|
||||
? (address, callback) => callback(null, address)
|
||||
: undefined,
|
||||
redisOptions: {
|
||||
username: cfg.REDIS_USERNAME,
|
||||
password: cfg.REDIS_PASSWORD
|
||||
password: cfg.REDIS_PASSWORD,
|
||||
tls: cfg?.REDIS_CLUSTER_ENABLE_TLS ? {} : undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -190,6 +190,12 @@ Redis is used for caching and background tasks. You can use either a standalone
|
||||
Comma-separated list of Redis Cluster host:port pairs. ```
|
||||
192.168.65.254:26379,192.168.65.254:26380 ```
|
||||
</ParamField>
|
||||
<ParamField query="REDIS_CLUSTER_ENABLE_TLS" type="boolean" default="false">
|
||||
Enable Redis TLS encryption on connection.
|
||||
</ParamField>
|
||||
<ParamField query="REDIS_CLUSTER_AWS_ELASTICACHE_DNS_LOOKUP_MODE" type="boolean" default="false">
|
||||
Enable this if you are using AWS encrypt on transit for Elasticache cluster. For more information refer .
|
||||
</ParamField>
|
||||
<ParamField query="REDIS_USERNAME" type="string" default="none">
|
||||
Authentication username for Redis Node
|
||||
</ParamField>
|
||||
@@ -197,6 +203,20 @@ Redis is used for caching and background tasks. You can use either a standalone
|
||||
Authentication password for Redis Node
|
||||
</ParamField>
|
||||
</Tab>
|
||||
<Tab title="Redis Read Replica">
|
||||
<ParamField
|
||||
query="REDIS_READ_REPLICAS"
|
||||
type="string"
|
||||
default="none"
|
||||
optional
|
||||
>
|
||||
Comma-separated list of Redis read replicas host:port pairs. ```
|
||||
192.168.65.254:26379,192.168.65.254:26380 ```
|
||||
</ParamField>
|
||||
|
||||
The paramters like username, password, tls, redis type of the primary instance will be inherited.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Email Service
|
||||
|
||||
Reference in New Issue
Block a user