Skip to content

Commit

Permalink
support advanced redis cluster config / urls (growthbook#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
bryce-fitzsimons authored May 8, 2023
1 parent 4cd5b58 commit 8c6bfad
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 10 deletions.
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

0 comments on commit 8c6bfad

Please sign in to comment.