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:
8 files changed
+39
-12
No files matched your search
+1
-1
@@ -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.
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user