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

@@ -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.