From 3db87d781fe4636e1b232bde6506a0341c17ce41 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sat, 8 Aug 2026 09:17:39 +0200 Subject: [PATCH] fix: address CodeRabbit review issues - redis-handler: add TLS_CA and USERNAME env support matching core client - connection.ts: fix Boolean() truthy bug (CI="false" was disabling Redis) - channel.ts: add in-memory lock for single-instance fallback mode - channel-subscription-tracker: lazy-init subscriber client - board.ts: invalidate user cache on board delete for home-board users - .env.example: document REDIS_CACHE_DB vs DATABASE_INDEX collision risk - CLI docs: fix docker exec command (remove /bin/bash prefix) - Docker docs: fix docker rm -f and docker ps -a for stopped containers --- .env.example | 2 +- apps/docs/docs/advanced/command-line/index.mdx | 2 +- .../docs/getting-started/installation/docker.mdx | 6 +++--- packages/api/src/router/board.ts | 8 ++++++++ packages/next-cache-handler/src/redis-handler.ts | 3 +++ .../src/lib/channel-subscription-tracker.ts | 12 ++++++++---- packages/redis/src/lib/channel.ts | 16 ++++++++++++++-- packages/redis/src/lib/connection.ts | 2 +- 8 files changed, 39 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index 87e1b5a5a..5cb724033 100644 --- a/.env.example +++ b/.env.example @@ -56,7 +56,7 @@ TURBO_TELEMETRY_DISABLED=1 # DOCKER_SOCKET_PATHS= # Redis DB index for the Next.js cache handler (default: 1). -# Separate from the main pub/sub DB (0) to allow independent FLUSHDB. +# Must differ from REDIS_DATABASE_INDEX (default: 0) to avoid cache ops affecting app data. # REDIS_CACHE_DB=1 # Enable kubernetes tool diff --git a/apps/docs/docs/advanced/command-line/index.mdx b/apps/docs/docs/advanced/command-line/index.mdx index 0e3e802f7..835199501 100644 --- a/apps/docs/docs/advanced/command-line/index.mdx +++ b/apps/docs/docs/advanced/command-line/index.mdx @@ -13,7 +13,7 @@ You should also create a backup of the database, in case you break something. ## Execute CLI with Docker 1. Execute ``docker ps | grep homarr`` on the root system with Docker to find the container ID of your Homarr container. -2. Run ``docker exec -it /bin/bash homarr ``. \ +2. Run ``docker exec -it homarr ``. \ Replace ```` with your container ID from step 1. \ Replace ```` with the command you want to execute (see sub-pages). diff --git a/apps/docs/docs/getting-started/installation/docker.mdx b/apps/docs/docs/getting-started/installation/docker.mdx index 956a6c2d6..de0332c73 100644 --- a/apps/docs/docs/getting-started/installation/docker.mdx +++ b/apps/docs/docs/getting-started/installation/docker.mdx @@ -84,7 +84,7 @@ Docker will mount the configuration files and icons to your host machine. Please ### Updating To update Homarr, you must remove your container first. Make sure that you've mounted your data and that you have access to it, so your configuration doesn't get lost. -1. Run ``docker rm homarr`` to remove the container. +1. Run ``docker rm -f homarr`` to stop and remove the container. 2. Pull the latest Homarr image ``docker pull ghcr.io/homarr-labs/homarr:latest``. 3. Re-run the command you used to install Homarr. @@ -98,8 +98,8 @@ Want to update all your containers automatically? Checkout [Watchtower](https:// ### Uninstalling -1. Obtain the container ID of your Homarr container using ``docker ps | grep homarr``. -2. Delete the container using ``docker rm `` +1. Obtain the container ID of your Homarr container using ``docker ps -a | grep homarr``. +2. Delete the container using ``docker rm -f `` 3. Prune unused Docker images using ``docker image prune`` (*Warning: this also removes you other unused images - not just Homarr*) 4. (Optional): Prune any network or volumes if you use any. 5. (Optional): Remove Homarr from your reverse proxy, VPNs or tunnels if you use any. diff --git a/packages/api/src/router/board.ts b/packages/api/src/router/board.ts index cec88bb74..281d5f24a 100644 --- a/packages/api/src/router/board.ts +++ b/packages/api/src/router/board.ts @@ -648,8 +648,16 @@ export const boardRouter = createTRPCRouter({ columns: { name: true }, }); + const affectedUsers = await ctx.db.query.users.findMany({ + columns: { id: true }, + where: eq(users.homeBoardId, input.id), + }); + await ctx.db.delete(boards).where(eq(boards.id, input.id)); + for (const user of affectedUsers) { + invalidateUserCache(user.id); + } invalidateBoardCache(input.id, existingBoard?.name); }), setHomeBoard: protectedProcedure diff --git a/packages/next-cache-handler/src/redis-handler.ts b/packages/next-cache-handler/src/redis-handler.ts index ce26feac2..c0bbf4c87 100644 --- a/packages/next-cache-handler/src/redis-handler.ts +++ b/packages/next-cache-handler/src/redis-handler.ts @@ -55,11 +55,14 @@ export class RedisCacheHandler implements CacheHandler { private loggedError = false; constructor() { + const tlsCa = process.env.REDIS_TLS_CA; this.redis = new Redis({ host: process.env.REDIS_HOST ?? "localhost", port: Number(process.env.REDIS_PORT ?? 6379), password: process.env.REDIS_PASSWORD || undefined, + username: process.env.REDIS_USERNAME || undefined, db: Number(process.env.REDIS_CACHE_DB ?? 1), + tls: tlsCa ? { ca: tlsCa } : undefined, lazyConnect: true, retryStrategy: (times) => Math.min(times * 100, 3000), }); diff --git a/packages/redis/src/lib/channel-subscription-tracker.ts b/packages/redis/src/lib/channel-subscription-tracker.ts index 42f28af18..1866655cb 100644 --- a/packages/redis/src/lib/channel-subscription-tracker.ts +++ b/packages/redis/src/lib/channel-subscription-tracker.ts @@ -19,7 +19,11 @@ type SubscriptionCallback = (message: string) => MaybePromise; */ export class ChannelSubscriptionTracker { private static subscriptions = new Map>(); - private static redis = getSubscriberClient(); + private static redis: ReturnType | undefined; + private static getRedis() { + this.redis ??= getSubscriberClient(); + return this.redis; + } private static memoryUnsubscribers = new Map void>(); private static listenerActive = false; @@ -48,7 +52,7 @@ export class ChannelSubscriptionTracker { this.activateListener(); this.listenerActive = true; } - const redis = this.redis; + const redis = this.getRedis(); if (redis) { void redis.subscribe(channelName); } @@ -79,7 +83,7 @@ export class ChannelSubscriptionTracker { this.memoryUnsubscribers.get(channelName)?.(); this.memoryUnsubscribers.delete(channelName); } else { - const redis = this.redis; + const redis = this.getRedis(); if (redis) { void redis.unsubscribe(channelName); } @@ -109,7 +113,7 @@ export class ChannelSubscriptionTracker { */ private static activateListener() { logger.debug("Activating listener"); - const redis = this.redis; + const redis = this.getRedis(); if (!redis) return; redis.on("message", (channel, message) => { this.dispatchMessage(channel, message); diff --git a/packages/redis/src/lib/channel.ts b/packages/redis/src/lib/channel.ts index 5df779525..4f9ead18d 100644 --- a/packages/redis/src/lib/channel.ts +++ b/packages/redis/src/lib/channel.ts @@ -159,17 +159,29 @@ export const createGetSetChannel = (name: string) => { * The token prevents a process from releasing a lock that expired and was * acquired by another process in the meantime. */ +// ponytail: in-memory lock map for single-instance fallback; upgrade to redis-based lock when Redis is available +const memoryLocks = new Map(); + export const createLockChannel = (name: string) => { return { acquireAsync: async (ttlSeconds: number) => { const token = createId(); - if (!dataClient) return token; + if (!dataClient) { + const existing = memoryLocks.get(name); + if (existing && existing.expiresAt > Date.now()) return null; + memoryLocks.set(name, { token, expiresAt: Date.now() + ttlSeconds * 1000 }); + return token; + } const result = await dataClient.set(name, token, "EX", ttlSeconds, "NX"); return result === "OK" ? token : null; }, releaseAsync: async (token: string) => { - if (!dataClient) return; + if (!dataClient) { + const existing = memoryLocks.get(name); + if (existing?.token === token) memoryLocks.delete(name); + return; + } await dataClient.eval( "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", diff --git a/packages/redis/src/lib/connection.ts b/packages/redis/src/lib/connection.ts index b7b06296f..74bc3bd73 100644 --- a/packages/redis/src/lib/connection.ts +++ b/packages/redis/src/lib/connection.ts @@ -4,7 +4,7 @@ import { createRedisClient } from "@homarr/core/infrastructure/redis"; let dataClient: RedisClient | undefined; let subscriberClient: RedisClient | undefined; -const isRedisExplicitlyDisabled = () => Boolean(process.env.CI) || Boolean(process.env.DISABLE_REDIS_LOGS); +const isRedisExplicitlyDisabled = () => process.env.CI === "true" || process.env.CI === "1" || process.env.DISABLE_REDIS_LOGS === "true" || process.env.DISABLE_REDIS_LOGS === "1"; /** * In-process pub/sub fallback for single-instance deployments when Redis is unavailable.