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
This commit is contained in:
Thomas Camlong committed 2026-08-08 09:17:39 +02:00
1 parent 51467ded12
commit 3db87d781f
8 files changed
+39 -12

No files matched your search

+1 -1
View File
@@ -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
@@ -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 <container-id> /bin/bash homarr <your-command-goes-here>``. \
2. Run ``docker exec -it <container-id> homarr <your-command-goes-here>``. \
Replace ``<container-id>`` with your container ID from step 1. \
Replace ``<your-command-goes-here>`` with the command you want to execute (see sub-pages).
@@ -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 <your-container-id>``
1. Obtain the container ID of your Homarr container using ``docker ps -a | grep homarr``.
2. Delete the container using ``docker rm -f <your-container-id>``
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.
+8
View File
@@ -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
@@ -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),
});
@@ -19,7 +19,11 @@ type SubscriptionCallback = (message: string) => MaybePromise<void>;
*/
export class ChannelSubscriptionTracker {
private static subscriptions = new Map<string, Map<string, SubscriptionCallback>>();
private static redis = getSubscriberClient();
private static redis: ReturnType<typeof getSubscriberClient> | undefined;
private static getRedis() {
this.redis ??= getSubscriberClient();
return this.redis;
}
private static memoryUnsubscribers = new Map<string, () => 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);
+14 -2
View File
@@ -159,17 +159,29 @@ export const createGetSetChannel = <TData>(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<string, { token: string; expiresAt: number }>();
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",
+1 -1
View File
@@ -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.