From ee15c2676c64f27b2c22a67be94a1789d60ffb12 Mon Sep 17 00:00:00 2001 From: Shubham Palriwala Date: Fri, 12 Apr 2024 00:35:49 +0530 Subject: [PATCH] feat: shared cache for next caching (#2426) --- .env.example | 6 +++++ apps/web/app/api/internal/cache/client.ts | 27 -------------------- apps/web/app/api/internal/cache/route.ts | 21 --------------- apps/web/app/middleware/rateLimit.ts | 31 +++++++++++++---------- apps/web/cache-handler.mjs | 8 +++--- docker-compose.yml | 2 +- docker/docker-compose.yml | 7 +++-- kamal/deploy.yml | 20 +++++++++++++-- packages/lib/constants.ts | 3 ++- packages/lib/env.ts | 6 +++-- turbo.json | 3 ++- 11 files changed, 61 insertions(+), 73 deletions(-) delete mode 100644 apps/web/app/api/internal/cache/client.ts delete mode 100644 apps/web/app/api/internal/cache/route.ts diff --git a/.env.example b/.env.example index 30ef981d3a..6b13545f3b 100644 --- a/.env.example +++ b/.env.example @@ -172,3 +172,9 @@ ENTERPRISE_LICENSE_KEY= # OpenTelemetry URL for tracing # OPENTELEMETRY_LISTENER_URL=http://localhost:4318/v1/traces + +# The below is used for Next Caching (uses In-Memory from Next Cache if not provided) +# REDIS_URL: + +# The below is used for Rate Limiting (uses In-Memory LRU Cache if not provided) (You can use a service like Webdis for this) +# REDIS_HTTP_URL: diff --git a/apps/web/app/api/internal/cache/client.ts b/apps/web/app/api/internal/cache/client.ts deleted file mode 100644 index 7a2883621e..0000000000 --- a/apps/web/app/api/internal/cache/client.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { createClient } from "redis"; - -import { REDIS_CLIENT_URL } from "@formbricks/lib/constants"; - -const client = createClient({ - url: REDIS_CLIENT_URL!, -}); -client.on("error", (err) => console.error("Redis Client Error", err)); -client.connect(); - -type Options = { - interval: number; - allowedPerInterval: number; -}; - -export const redisRateLimiter = (options: Options) => { - return async (token: string) => { - const tokenCount = await client.INCR(token); - if (tokenCount === 1) { - await client.EXPIRE(token, options.interval / 1000); - } - if (tokenCount > options.allowedPerInterval) { - throw new Error("Rate limit exceeded for IP: " + token); - } - return; - }; -}; diff --git a/apps/web/app/api/internal/cache/route.ts b/apps/web/app/api/internal/cache/route.ts deleted file mode 100644 index 12b4459e53..0000000000 --- a/apps/web/app/api/internal/cache/route.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { redisRateLimiter } from "@/app/api/internal/cache/client"; -import { responses } from "@/app/lib/api/response"; -import { NextRequest } from "next/server"; - -export async function GET(request: NextRequest) { - const token = request.nextUrl.searchParams.get("token"); - const interval = parseInt(request.nextUrl.searchParams.get("interval") ?? "0"); - const allowedPerInterval = parseInt(request.nextUrl.searchParams.get("allowedPerInterval") ?? "0"); - if (!token) { - return responses.notAuthenticatedResponse(); - } - - try { - const rateLimiter = redisRateLimiter({ interval, allowedPerInterval }); - await rateLimiter(token); - - return responses.successResponse({ rateLimitExceeded: false }, true); - } catch (e) { - return responses.successResponse({ rateLimitExceeded: true }, true); - } -} diff --git a/apps/web/app/middleware/rateLimit.ts b/apps/web/app/middleware/rateLimit.ts index 2bc4481df4..da7d4636a1 100644 --- a/apps/web/app/middleware/rateLimit.ts +++ b/apps/web/app/middleware/rateLimit.ts @@ -1,6 +1,6 @@ import { LRUCache } from "lru-cache"; -import { REDIS_CLIENT_URL, WEBAPP_URL } from "@formbricks/lib/constants"; +import { REDIS_HTTP_URL } from "@formbricks/lib/constants"; type Options = { interval: number; @@ -22,22 +22,27 @@ const inMemoryRateLimiter = (options: Options) => { }; }; -const redisRateLimiter = (options: Options) => { - return async (token: string) => { - const tokenCountResponse = await fetch( - `${WEBAPP_URL}/api/internal/cache?token=${token}&interval=${options.interval}&allowedPerInterval=${options.allowedPerInterval}` - ); - const { - data: { rateLimitExceeded }, - } = await tokenCountResponse.json(); - if (!tokenCountResponse.ok || rateLimitExceeded) { - throw new Error("Rate limit exceeded for IP: " + token); +const redisRateLimiter = (options: Options) => async (token: string) => { + try { + const tokenCountResponse = await fetch(`${REDIS_HTTP_URL}/INCR/${token}`); + if (!tokenCountResponse.ok) { + console.error("Failed to increment token count in Redis", tokenCountResponse); + return; } - }; + + const { INCR } = await tokenCountResponse.json(); + if (INCR === 1) { + await fetch(`${REDIS_HTTP_URL}/EXPIRE/${token}/${options.interval}`); + } else if (INCR > options.allowedPerInterval) { + throw new Error(); + } + } catch (e) { + throw new Error("Rate limit exceeded for IP: " + token); + } }; export default function rateLimit(options: Options) { - if (REDIS_CLIENT_URL) { + if (REDIS_HTTP_URL) { return redisRateLimiter(options); } else { return inMemoryRateLimiter(options); diff --git a/apps/web/cache-handler.mjs b/apps/web/cache-handler.mjs index 09b843132c..887534f192 100644 --- a/apps/web/cache-handler.mjs +++ b/apps/web/cache-handler.mjs @@ -5,11 +5,13 @@ import { createClient } from "redis"; CacheHandler.onCreation(async () => { let redisHandler; - if (process.env.REDIS_CLIENT_URL) { + if (process.env.REDIS_URL) { const client = createClient({ - url: process.env.REDIS_CLIENT_URL, + url: process.env.REDIS_URL, + }); + client.on("error", (e) => { + console.error("Error in conncting to Redis client", e); }); - client.on("error", () => {}); await client.connect(); redisHandler = createRedisHandler({ diff --git a/docker-compose.yml b/docker-compose.yml index b386991d89..a01739f210 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -115,7 +115,7 @@ services: GOOGLE_CLIENT_ID: *google_client_id GOOGLE_CLIENT_SECRET: *google_client_secret CRON_SECRET: *cron_secret - REDIS_CLIENT_URL: *redis_url + REDIS_URL: *redis_url volumes: - uploads:/home/nextjs/apps/web/uploads/ diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index aa57fa28be..9e5651fd32 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -79,8 +79,11 @@ x-environment: &environment # Uncomment and set to 1 to skip onboarding for new users # ONBOARDING_DISABLED: 1 - # The below is used for Rate Limiting & Next Caching (uses In-Memory Next Cache if not provided) - # REDIS_CLIENT_URL: + # The below is used for Next Caching (uses In-Memory from Next Cache if not provided) + # REDIS_URL: + + # The below is used for Rate Limiting (uses In-Memory LRU Cache if not provided) + # REDIS_HTTP_URL: services: postgres: diff --git a/kamal/deploy.yml b/kamal/deploy.yml index d7c63868fb..077f801520 100644 --- a/kamal/deploy.yml +++ b/kamal/deploy.yml @@ -29,8 +29,9 @@ registry: env: # clear: # DB_HOST: 192.168.0.2 - # clear: - # REDIS_CLIENT_URL: redis://default:password@172.31.40.79:6379 + clear: + REDIS_URL: redis://default:password@172.31.40.79:6379 + REDIS_HTTP_URL: http://172.31.40.79:7379 secret: - IS_FORMBRICKS_CLOUD - WEBAPP_URL @@ -170,6 +171,21 @@ accessories: port: "172.31.40.79:6379:6379" directories: - data:/data + + webdis: + image: nicolas/webdis:0.1.22 + host: 18.196.187.144 + cmd: > + sh -c " + wget -O /usr/local/bin/webdis.json https://github.com/nicolasff/webdis/raw/0.1.22/webdis.json && + awk '/\"redis_host\":/ {print \"\\t\\\"redis_host\\\": \\\"172.31.40.79\\\",\"; next} /\"logfile\":/ {print \"\\t\\\"logfile\\\": \\\"/dev/stderr\\\"\"; next} {print}' /usr/local/bin/webdis.json > /usr/local/bin/webdis_modified.json && + mv /usr/local/bin/webdis_modified.json /usr/local/bin/webdis.json && + /usr/local/bin/webdis /usr/local/bin/webdis.json" + + port: "172.31.40.79:7379:7379" + directories: + - data:/data + pgbouncer: image: edoburu/pgbouncer:latest host: 18.196.187.144 diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index 92a10062bf..94d55655d5 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -176,7 +176,8 @@ export const DEBUG = env.DEBUG === "1"; // Enterprise License constant export const ENTERPRISE_LICENSE_KEY = env.ENTERPRISE_LICENSE_KEY; -export const REDIS_CLIENT_URL = env.REDIS_CLIENT_URL; +export const REDIS_URL = env.REDIS_URL; +export const REDIS_HTTP_URL = env.REDIS_HTTP_URL; export const RATE_LIMITING_DISABLED = env.RATE_LIMITING_DISABLED === "1"; export const CUSTOMER_IO_SITE_ID = env.CUSTOMER_IO_SITE_ID; diff --git a/packages/lib/env.ts b/packages/lib/env.ts index 4f323473d0..86c82a42e0 100644 --- a/packages/lib/env.ts +++ b/packages/lib/env.ts @@ -52,7 +52,8 @@ export const env = createEnv({ OIDC_SIGNING_ALGORITHM: z.string().optional(), OPENTELEMETRY_LISTENER_URL: z.string().optional(), ONBOARDING_DISABLED: z.enum(["1", "0"]).optional(), - REDIS_CLIENT_URL: z.string().optional(), + REDIS_URL: z.string().optional(), + REDIS_HTTP_URL: z.string().optional(), PASSWORD_RESET_DISABLED: z.enum(["1", "0"]).optional(), PRIVACY_URL: z .string() @@ -158,7 +159,8 @@ export const env = createEnv({ OIDC_ISSUER: process.env.OIDC_ISSUER, OIDC_SIGNING_ALGORITHM: process.env.OIDC_SIGNING_ALGORITHM, ONBOARDING_DISABLED: process.env.ONBOARDING_DISABLED, - REDIS_CLIENT_URL: process.env.REDIS_CLIENT_URL, + REDIS_URL: process.env.REDIS_URL, + REDIS_HTTP_URL: process.env.REDIS_HTTP_URL, PASSWORD_RESET_DISABLED: process.env.PASSWORD_RESET_DISABLED, PRIVACY_URL: process.env.PRIVACY_URL, RATE_LIMITING_DISABLED: process.env.RATE_LIMITING_DISABLED, diff --git a/turbo.json b/turbo.json index 0fed951728..68d4adfae4 100644 --- a/turbo.json +++ b/turbo.json @@ -118,7 +118,8 @@ "PLAYWRIGHT_CI", "PRIVACY_URL", "RATE_LIMITING_DISABLED", - "REDIS_CLIENT_URL", + "REDIS_URL", + "REDIS_HTTP_URL", "S3_ACCESS_KEY", "S3_SECRET_KEY", "S3_REGION",