mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 06:28:11 +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 { applyJitter } from "@app/lib/dates";
|
||||||
import { delay as delayMs } from "@app/lib/delay";
|
import { delay as delayMs } from "@app/lib/delay";
|
||||||
import { ExecutionResult, Redlock, Settings } from "@app/lib/red-lock";
|
import { ExecutionResult, Redlock, Settings } from "@app/lib/red-lock";
|
||||||
|
import { Redis, Cluster } from "ioredis";
|
||||||
|
|
||||||
export const PgSqlLock = {
|
export const PgSqlLock = {
|
||||||
BootUpMigration: 2023,
|
BootUpMigration: 2023,
|
||||||
@@ -102,30 +103,57 @@ export type TKeyStoreFactory = {
|
|||||||
getKeysByPattern: (pattern: string, limit?: number) => Promise<string[]>;
|
getKeysByPattern: (pattern: string, limit?: number) => Promise<string[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys): TKeyStoreFactory => {
|
const pickPrimaryOrSecondaryRedis = (primary: Redis | Cluster, secondaries?: Array<Redis | Cluster>) => {
|
||||||
const redis = buildRedisFromConfig(redisConfigKeys);
|
if (!secondaries || !secondaries.length) return primary;
|
||||||
const redisLock = new Redlock([redis], { retryCount: 2, retryDelay: 200 });
|
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) =>
|
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) =>
|
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 (
|
const setItemWithExpiry = async (
|
||||||
key: string,
|
key: string,
|
||||||
expiryInSeconds: number | string,
|
expiryInSeconds: number | string,
|
||||||
value: string | number | Buffer,
|
value: string | number | Buffer,
|
||||||
prefix?: string
|
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[]) => {
|
const deleteItemsByKeyIn = async (keys: string[]) => {
|
||||||
if (keys.length === 0) return 0;
|
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) => {
|
const deleteItems = async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }: TDeleteItems) => {
|
||||||
@@ -135,12 +163,12 @@ export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys): TKeyStoreFac
|
|||||||
do {
|
do {
|
||||||
// Await in loop is needed so that Redis is not overwhelmed
|
// Await in loop is needed so that Redis is not overwhelmed
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// 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;
|
cursor = nextCursor;
|
||||||
|
|
||||||
for (let i = 0; i < keys.length; i += batchSize) {
|
for (let i = 0; i < keys.length; i += batchSize) {
|
||||||
const batch = keys.slice(i, i + batchSize);
|
const batch = keys.slice(i, i + batchSize);
|
||||||
const pipeline = redis.pipeline();
|
const pipeline = primaryRedis.pipeline();
|
||||||
for (const key of batch) {
|
for (const key of batch) {
|
||||||
pipeline.unlink(key);
|
pipeline.unlink(key);
|
||||||
}
|
}
|
||||||
@@ -156,9 +184,9 @@ export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys): TKeyStoreFac
|
|||||||
return totalDeleted;
|
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 ({
|
const waitTillReady = async ({
|
||||||
key,
|
key,
|
||||||
@@ -189,7 +217,13 @@ export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys): TKeyStoreFac
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// 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;
|
cursor = nextCursor;
|
||||||
allKeys.push(...keys);
|
allKeys.push(...keys);
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,22 @@ const envSchema = z
|
|||||||
.optional()
|
.optional()
|
||||||
.describe("Comma-separated list of Redis Cluster host:port pairs. Eg: 192.168.65.254:6379,192.168.65.254:6380")
|
.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")),
|
HOST: zpStr(z.string().default("localhost")),
|
||||||
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default(
|
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}`
|
`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(":");
|
const [host, port] = el.trim().split(":");
|
||||||
return { host: host.trim(), port: Number(port.trim()) };
|
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:
|
isSecretScanningConfigured:
|
||||||
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
|
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
|
||||||
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
|
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ export type TRedisConfigKeys = Partial<{
|
|||||||
REDIS_PASSWORD: string;
|
REDIS_PASSWORD: string;
|
||||||
|
|
||||||
REDIS_CLUSTER_HOSTS: { host: string; port: number }[];
|
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_HOSTS: { host: string; port: number }[];
|
||||||
REDIS_SENTINEL_MASTER_NAME: string;
|
REDIS_SENTINEL_MASTER_NAME: string;
|
||||||
@@ -19,9 +22,13 @@ export const buildRedisFromConfig = (cfg: TRedisConfigKeys) => {
|
|||||||
|
|
||||||
if (cfg.REDIS_CLUSTER_HOSTS) {
|
if (cfg.REDIS_CLUSTER_HOSTS) {
|
||||||
return new Redis.Cluster(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: {
|
redisOptions: {
|
||||||
username: cfg.REDIS_USERNAME,
|
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. ```
|
Comma-separated list of Redis Cluster host:port pairs. ```
|
||||||
192.168.65.254:26379,192.168.65.254:26380 ```
|
192.168.65.254:26379,192.168.65.254:26380 ```
|
||||||
</ParamField>
|
</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">
|
<ParamField query="REDIS_USERNAME" type="string" default="none">
|
||||||
Authentication username for Redis Node
|
Authentication username for Redis Node
|
||||||
</ParamField>
|
</ParamField>
|
||||||
@@ -197,6 +203,20 @@ Redis is used for caching and background tasks. You can use either a standalone
|
|||||||
Authentication password for Redis Node
|
Authentication password for Redis Node
|
||||||
</ParamField>
|
</ParamField>
|
||||||
</Tab>
|
</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>
|
</Tabs>
|
||||||
|
|
||||||
## Email Service
|
## Email Service
|
||||||
|
|||||||
Reference in New Issue
Block a user