Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Redis Cluster - advanced config #22

Merged
merged 1 commit into from
May 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,28 @@ By default, features are cached in memory in GrowthBook Proxy; you may provide y
- `CACHE_STALE_TTL` - Number of seconds until a cache entry is considered stale
- `CACHE_EXPIRES_TTL` - Number of seconds until a cache entry is expired

For Redis, you can also run in cluster mode:
#### Redis Cluster
Redis-specific options for cluster mode:<br />
_(Note that CACHE_CONNECTION_URL is ignored when using cluster mode)_
- `USE_CLUSTER` - "true" or "1" to enable
- `CLUSTER_ROOT_NODES` - comma-separated URLs to your cluster seed nodes
- Note: CACHE_CONNECTION_URL is ignored when using cluster mode
- `CLUSTER_ROOT_NODES` - simple: comma-separated URLs to your cluster seed nodes
- `CLUSTER_ROOT_NODES_JSON` - advanced: JSON array of ClusterNode objects (ioredis)
- `CLUSTER_OPTIONS_JSON` - advanced: JSON object of ClusterOptions (ioredis)

#### MongoDB
Mongo-specific options:

For MongoDB, you can optionally set the following options:
- `CACHE_DATABASE_NAME` - Mongo database name (default is `proxy`)
- `CACHE_COLLECTION_NAME` - Mongo collection name (default is `cache`)


### Horizontally scaling

For horizontally scaled GrowthBook Proxy clusters, we provide a basic mechanism for keeping your proxy instances in sync, which uses Redis Pub/Sub. To use this feature, you must use Redis as your cache engine and set the following option:

- `PUBLISH_PAYLOAD_TO_CHANNEL` - "true" or "1" to enable


### SSL termination & HTTP2

Although we recommend terminating SSL using your load balancer, you can also configure the GrowthBook Proxy to handle SSL termination directly. It supports HTTP/2 by default, which is required for high performance streaming.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"node": ">=16"
},
"description": "GrowthBook proxy server for caching, realtime updates, telemetry, etc",
"version": "1.0.18",
"version": "1.0.19",
"main": "dist/app.js",
"license": "MIT",
"repository": {
Expand Down
6 changes: 6 additions & 0 deletions src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ export default async () => {
clusterRootNodes: process.env.CLUSTER_ROOT_NODES
? process.env.CLUSTER_ROOT_NODES.replace(" ", "").split(",")
: undefined,
clusterRootNodesJSON: process.env.CLUSTER_ROOT_NODES_JSON
? JSON.parse(process.env.CLUSTER_ROOT_NODES_JSON)
: undefined,
clusterOptionsJSON: process.env.CLUSTER_OPTIONS_JSON
? JSON.parse(process.env.CLUSTER_OPTIONS_JSON)
: undefined,
},
};

Expand Down
35 changes: 30 additions & 5 deletions src/services/cache/RedisCache.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Redis, { Cluster, ClusterNode } from "ioredis";
import Redis, { Cluster, ClusterNode, ClusterOptions } from "ioredis";

import { v4 as uuidv4 } from "uuid";
import logger from "../logger";
Expand All @@ -21,7 +21,8 @@ export class RedisCache {
public readonly allowStale: boolean;

private readonly useCluster: boolean;
private readonly clusterRootNodes: ClusterNode[];
private readonly clusterRootNodes?: ClusterNode[];
private readonly clusterOptions?: ClusterOptions;

private readonly appContext?: Context;

Expand All @@ -34,7 +35,9 @@ export class RedisCache {
useAdditionalMemoryCache,
publishPayloadToChannel = false,
useCluster = false,
clusterRootNodes = [],
clusterRootNodes,
clusterRootNodesJSON,
clusterOptionsJSON,
}: CacheSettings = {},
appContext?: Context
) {
Expand All @@ -44,7 +47,9 @@ export class RedisCache {
this.allowStale = allowStale;
this.publishPayloadToChannel = publishPayloadToChannel;
this.useCluster = useCluster;
this.clusterRootNodes = clusterRootNodes;
this.clusterRootNodes =
clusterRootNodesJSON ?? this.transformRootNodes(clusterRootNodes);
this.clusterOptions = clusterOptionsJSON;

this.appContext = appContext;

Expand All @@ -64,7 +69,10 @@ export class RedisCache {
: new Redis();
} else {
if (this.clusterRootNodes) {
this.client = new Redis.Cluster(this.clusterRootNodes);
this.client = new Redis.Cluster(
this.clusterRootNodes,
this.clusterOptions
);
} else {
throw new Error("No cluster root nodes");
}
Expand Down Expand Up @@ -244,4 +252,21 @@ export class RedisCache {
public getsubscriberClient() {
return this.subscriberClient;
}

private transformRootNodes(rootNodes?: string[]): ClusterNode[] | undefined {
if (!rootNodes) return undefined;
return rootNodes
.map((node) => {
try {
const url = new URL(node);
const host = url.protocol + "//" + url.hostname + url.pathname;
const port = parseInt(url.port);
return { host, port };
} catch (e) {
logger.error(e, "Error parsing Redis cluster node");
return undefined;
}
})
.filter(Boolean) as ClusterNode[];
}
}
4 changes: 4 additions & 0 deletions src/services/cache/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ClusterNode, ClusterOptions } from "ioredis";

import { Context } from "../../types";
import logger from "../logger";
import { MemoryCache } from "./MemoryCache";
Expand All @@ -21,6 +23,8 @@ export interface CacheSettings {
publishPayloadToChannel?: boolean; // for RedisCache pub/sub
useCluster?: boolean; // for RedisCache
clusterRootNodes?: string[]; // for RedisCache
clusterRootNodesJSON?: ClusterNode[]; // for RedisCache
clusterOptionsJSON?: ClusterOptions; // for RedisCache
}

export type FeaturesCache = MemoryCache | RedisCache | MongoCache | null;
Expand Down