removed tiles redis, now using pg

This commit is contained in:
Marco Pedone 2026-05-06 17:19:40 +02:00
parent b0f39f5f11
commit 92779d8b4e
23 changed files with 202 additions and 344 deletions

View file

@ -36,10 +36,7 @@ env:
ARUBA_PASS: "string" ARUBA_PASS: "string"
EXP_API_USER: "string" EXP_API_USER: "string"
EXP_API_PASS: "string" EXP_API_PASS: "string"
TILES_URL: "keydb://string"
KEYDB_URL: "keydb://string"
JWT_SECRET: "335922ae" JWT_SECRET: "335922ae"
SKIP_REDIS: "false"
jobs: jobs:

View file

@ -0,0 +1,17 @@
CREATE TABLE IF NOT EXISTS tiles_cache (
key TEXT NOT NULL,
value BYTEA NOT NULL,
expires_at TIMESTAMPTZ -- NULL = no expiry
);
DO $$ BEGIN IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'tiles_cache_pkey'
AND conrelid = 'public.tiles_cache'::regclass
) THEN
ALTER TABLE public.tiles_cache
ADD CONSTRAINT tiles_cache_pkey PRIMARY KEY (key);
END IF;
END $$;
CREATE INDEX IF NOT EXISTS tiles_cache_key_idx ON public.tiles_cache (key);

View file

@ -7,7 +7,7 @@ node_modules
.github .github
Dockerfile Dockerfile
docker-compose.yml docker-compose.yml
keydb-docker-compose.yml dev-docker-compose.yml
pgadmin-docker-compose.yml pgadmin-docker-compose.yml
dev_utils_compose.sh dev_utils_compose.sh
.env .env

View file

@ -13,8 +13,7 @@ COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
ENV NODE_ENV=production \ ENV NODE_ENV=production \
NEXT_TELEMETRY_DISABLED=1 \ NEXT_TELEMETRY_DISABLED=1
SKIP_REDIS="true"
ARG BASE_URL NEXT_PUBLIC_BASE_URL NEXT_PUBLIC_STRIPE_PUBLIC_KEY ARG BASE_URL NEXT_PUBLIC_BASE_URL NEXT_PUBLIC_STRIPE_PUBLIC_KEY
ENV BASE_URL=$BASE_URL \ ENV BASE_URL=$BASE_URL \
@ -29,8 +28,7 @@ FROM base AS runner
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production \ ENV NODE_ENV=production \
NEXT_TELEMETRY_DISABLED=1 \ NEXT_TELEMETRY_DISABLED=1
SKIP_REDIS="false"
ARG BASE_URL ARG BASE_URL
ENV BASE_URL=$BASE_URL ENV BASE_URL=$BASE_URL
@ -72,8 +70,6 @@ ARG EXP_API_USER
ENV EXP_API_USER=$EXP_API_USER ENV EXP_API_USER=$EXP_API_USER
ARG EXP_API_PASS ARG EXP_API_PASS
ENV EXP_API_PASS=$EXP_API_PASS ENV EXP_API_PASS=$EXP_API_PASS
ARG KEYDB_URL
ENV KEYDB_URL=$KEYDB_URL
ARG TILES_URL ARG TILES_URL
ENV TILES_URL=$TILES_URL ENV TILES_URL=$TILES_URL
ARG SKEBBY_USER ARG SKEBBY_USER

View file

@ -13,7 +13,7 @@ TODOS:
- pre attivazione se admin mostrare tastino contatti in preview - pre attivazione se admin mostrare tastino contatti in preview
- onboarding submit (toast e email che invitano a pagare) - onboarding submit (toast e email che invitano a pagare)
- migliorare onboarding e pag guida - migliorare onboarding e pag guida
- "fatto_nulla" status in servizio
AFTER MVP: AFTER MVP:
- TODO migrazione app router https://nextjs.org/docs/pages/building-your-application/upgrading/app-router-migration#migrating-from-pages-to-app - TODO migrazione app router https://nextjs.org/docs/pages/building-your-application/upgrading/app-router-migration#migrating-from-pages-to-app
- Sezione "La mia locazione" con i dati della locazione attuale dell'utente - Sezione "La mia locazione" con i dati della locazione attuale dell'utente

View file

@ -1,20 +1,4 @@
services: services:
tiles:
image: eqalpha/keydb:latest
volumes:
- tiles-data:/data
ports:
- "6379:6379"
command: keydb-server /etc/keydb/keydb.conf --maxmemory 100mb --maxmemory-policy volatile-lru --maxmemory-samples 5
restart: unless-stopped
keydb:
image: eqalpha/keydb:latest
volumes:
- keydb-data:/data
ports:
- "6380:6379"
restart: unless-stopped
storage: storage:
image: "ghcr.io/marcopedone/go_fs:latest" image: "ghcr.io/marcopedone/go_fs:latest"
ports: ports:
@ -34,6 +18,4 @@ services:
start_period: 10s start_period: 10s
volumes: volumes:
tiles-data:
keydb-data:
storage_data: storage_data:

View file

@ -161,6 +161,8 @@ module.exports = {
"pg_catalog.bpchar": "string", "pg_catalog.bpchar": "string",
'pg_catalog.numeric': 'number', 'pg_catalog.numeric': 'number',
//"pg_catalog.json": "string", //"pg_catalog.json": "string",
'pg_catalog.bytea': "Buffer"
}, },
preRenderHooks: [ preRenderHooks: [
@ -177,6 +179,7 @@ module.exports = {
zodTypeMap: { zodTypeMap: {
...defaultZodTypeMap, ...defaultZodTypeMap,
"pg_catalog.numeric": "z.number()", "pg_catalog.numeric": "z.number()",
"pg_catalog.bytea": "z.instanceof(Buffer)"
} }
}), }),
], ],

View file

@ -49,7 +49,6 @@
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"framer-motion": "^12.38.0", "framer-motion": "^12.38.0",
"frimousse": "^0.3.0", "frimousse": "^0.3.0",
"ioredis": "^5.9.2",
"jose": "^6.0.12", "jose": "^6.0.12",
"kysely": "^0.28.17", "kysely": "^0.28.17",
"kysely-plugin-serialize": "^0.8.2", "kysely-plugin-serialize": "^0.8.2",
@ -1818,10 +1817,6 @@
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
} }
}, },
"node_modules/@ioredis/commands": {
"version": "1.5.0",
"license": "MIT"
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13", "version": "0.3.13",
"license": "MIT", "license": "MIT",
@ -7420,13 +7415,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/cmdk": { "node_modules/cmdk": {
"version": "1.1.1", "version": "1.1.1",
"license": "MIT", "license": "MIT",
@ -7843,13 +7831,6 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/denque": {
"version": "2.1.0",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10"
}
},
"node_modules/dequal": { "node_modules/dequal": {
"version": "2.0.3", "version": "2.0.3",
"license": "MIT", "license": "MIT",
@ -9041,28 +9022,6 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/ioredis": {
"version": "5.9.2",
"license": "MIT",
"dependencies": {
"@ioredis/commands": "1.5.0",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/is-array-buffer": { "node_modules/is-array-buffer": {
"version": "3.0.5", "version": "3.0.5",
"dev": true, "dev": true,
@ -10120,14 +10079,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.defaults": {
"version": "4.2.0",
"license": "MIT"
},
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"license": "MIT"
},
"node_modules/loose-envify": { "node_modules/loose-envify": {
"version": "1.4.0", "version": "1.4.0",
"license": "MIT", "license": "MIT",
@ -12228,23 +12179,6 @@
"node": ">= 10.13.0" "node": ">= 10.13.0"
} }
}, },
"node_modules/redis-errors": {
"version": "1.2.0",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"license": "MIT",
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/reflect.getprototypeof": { "node_modules/reflect.getprototypeof": {
"version": "1.0.10", "version": "1.0.10",
"dev": true, "dev": true,
@ -12871,10 +12805,6 @@
"stacktrace-gps": "^3.0.4" "stacktrace-gps": "^3.0.4"
} }
}, },
"node_modules/standard-as-callback": {
"version": "2.1.0",
"license": "MIT"
},
"node_modules/std-env": { "node_modules/std-env": {
"version": "4.0.0", "version": "4.0.0",
"dev": true, "dev": true,

View file

@ -64,7 +64,6 @@
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"framer-motion": "^12.38.0", "framer-motion": "^12.38.0",
"frimousse": "^0.3.0", "frimousse": "^0.3.0",
"ioredis": "^5.9.2",
"jose": "^6.0.12", "jose": "^6.0.12",
"kysely": "^0.28.17", "kysely": "^0.28.17",
"kysely-plugin-serialize": "^0.8.2", "kysely-plugin-serialize": "^0.8.2",
@ -159,4 +158,4 @@
"axios": "^1.16.0" "axios": "^1.16.0"
} }
} }
} }

View file

@ -14,14 +14,12 @@ export const env = createEnv({
FIC_COMPANY_ID: z.string(), FIC_COMPANY_ID: z.string(),
INTERNAL_BASE_URL: z.string(), INTERNAL_BASE_URL: z.string(),
JWT_SECRET: z.string(), JWT_SECRET: z.string(),
KEYDB_URL: z.string(),
NODE_ENV: z.enum(["development", "test", "production"]), NODE_ENV: z.enum(["development", "test", "production"]),
PGHOST: z.string(), PGHOST: z.string(),
PGPORT: z.string(), PGPORT: z.string(),
POSTGRES_DB: z.string(), POSTGRES_DB: z.string(),
POSTGRES_PASSWORD: z.string(), POSTGRES_PASSWORD: z.string(),
POSTGRES_USER: z.string(), POSTGRES_USER: z.string(),
SKIP_REDIS: z.string(),
STRIPE_SECRET_KEY: z.string(), STRIPE_SECRET_KEY: z.string(),
STRIPE_WEBHOOK_SECRET: z.string(), STRIPE_WEBHOOK_SECRET: z.string(),
TILES_URL: z.string(), TILES_URL: z.string(),

View file

@ -1,21 +1,10 @@
import Redis from "ioredis";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { env } from "~/env"; import { appRouter } from "~/server/api/root";
import { createTRPCContext, t } from "~/server/api/trpc";
let tiles: Redis | null = null;
export function getTilesClient() {
if (env.SKIP_REDIS === "true") {
return null;
}
if (!tiles) {
tiles = new Redis(`keydb://${env.TILES_URL}`);
}
return tiles;
}
/* /*
* /api/tiles/[[...slug]] * /api/tiles/[[...slug]]
* This API route is used to fetch tiles from OpenStreetMap and cache them in KeyDb. * This API route is used to fetch tiles from OpenStreetMap and cache them.
* The tile URL is constructed using the provided slug parameters: /api/tiles/s/z/x/y * The tile URL is constructed using the provided slug parameters: /api/tiles/s/z/x/y
* The slug should be an array of 4 elements: [s, z, x, y]. * The slug should be an array of 4 elements: [s, z, x, y].
* The API caches the tile for 30 days and sets appropriate headers for caching. * The API caches the tile for 30 days and sets appropriate headers for caching.
@ -53,14 +42,13 @@ export default async function handler(
const cacheKey = `tile:${z}:${x}:${y}`; const cacheKey = `tile:${z}:${x}:${y}`;
const tiles = getTilesClient(); const ctx = await createTRPCContext({ req: req, res: res });
if (!tiles) {
return res.status(500).json({ const createCaller = t.createCallerFactory(appRouter);
error: "KeyDB client is not initialized", const caller = createCaller(ctx);
});
} const cachedTile = await caller.tiles.get(cacheKey);
const cachedTile = await tiles.getBuffer(cacheKey);
if (cachedTile) { if (cachedTile) {
res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
res.setHeader("Content-Type", "image/png"); res.setHeader("Content-Type", "image/png");
@ -80,8 +68,9 @@ export default async function handler(
} }
const tileData = await response.arrayBuffer(); const tileData = await response.arrayBuffer();
const data = Buffer.from(tileData); const data = Buffer.from(tileData);
// Cache for 30 days // Cache for 30 days
await tiles.setex(cacheKey, 2592000, data); await caller.tiles.set({ key: cacheKey, value: data, ttlSec: 2592000 });
res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
res.setHeader("Content-Type", "image/png"); res.setHeader("Content-Type", "image/png");

View file

@ -1,8 +1,5 @@
import Head from "next/head"; import Head from "next/head";
import toast from "react-hot-toast";
import { AreaRiservataLayout } from "~/components/Layout"; import { AreaRiservataLayout } from "~/components/Layout";
import { LoadingPage } from "~/components/loading";
import { Button } from "~/components/ui/button";
import type { NextPageWithLayout } from "~/pages/_app"; import type { NextPageWithLayout } from "~/pages/_app";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
@ -23,7 +20,7 @@ const Impostazioni: NextPageWithLayout = () => {
</div> </div>
</div> </div>
<StripeSection /> <StripeSection />
<RedisCacheSection /> {/* <RedisCacheSection /> */}
</div> </div>
</> </>
); );
@ -75,44 +72,44 @@ const StripeSection = () => {
); );
}; };
const RedisCacheSection = () => { // const RedisCacheSection = () => {
const { data, isLoading } = api.revalidation.getRedisStats.useQuery(); // const { data, isLoading } = api.revalidation.getRedisStats.useQuery();
const utils = api.useUtils(); // const utils = api.useUtils();
const { mutate } = api.revalidation.invalidateRedisCache.useMutation({ // const { mutate } = api.revalidation.invalidateRedisCache.useMutation({
onMutate() { // onMutate() {
toast.loading("Invalidating cache...", { // toast.loading("Invalidating cache...", {
id: "invalidate-cache", // id: "invalidate-cache",
}); // });
}, // },
onSuccess: async () => { // onSuccess: async () => {
toast.success("Cache invalidated successfully!", { // toast.success("Cache invalidated successfully!", {
id: "invalidate-cache", // id: "invalidate-cache",
}); // });
await utils.revalidation.getRedisStats.invalidate(); // await utils.revalidation.getRedisStats.invalidate();
}, // },
onError() { // onError() {
toast.error("Failed to invalidate cache.", { // toast.error("Failed to invalidate cache.", {
id: "invalidate-cache", // id: "invalidate-cache",
}); // });
}, // },
}); // });
if (isLoading) { // if (isLoading) {
return <LoadingPage />; // return <LoadingPage />;
} // }
return ( // return (
<div className="space-y-4 rounded-lg border p-4"> // <div className="space-y-4 rounded-lg border p-4">
<h4 className="font-semibold text-lg">Invalidate Redis Cache</h4> // <h4 className="font-semibold text-lg">Invalidate Redis Cache</h4>
<p>Current Cache Stats:</p> // <p>Current Cache Stats:</p>
{data?.success ? ( // {data?.success ? (
<ul className="list-inside list-disc"> // <ul className="list-inside list-disc">
<li>Pdf Schede Annuncio: {data?.schedaCount}</li> // <li>Pdf Schede Annuncio: {data?.schedaCount}</li>
<li>Pdf Condizioni: {data?.condizioniCount}</li> // <li>Pdf Condizioni: {data?.condizioniCount}</li>
</ul> // </ul>
) : ( // ) : (
<div className="text-red-500">Error fetching cache stats</div> // <div className="text-red-500">Error fetching cache stats</div>
)} // )}
<Button onClick={() => mutate()}>Invalidate Cache</Button> // <Button onClick={() => mutate()}>Invalidate Cache</Button>
</div> // </div>
); // );
}; // };

View file

@ -11,6 +11,7 @@ import type { default as UsersStorageTable } from './UsersStorage';
import type { default as ServizioAnnunciTable } from './ServizioAnnunci'; import type { default as ServizioAnnunciTable } from './ServizioAnnunci';
import type { default as UserInvitesTable } from './UserInvites'; import type { default as UserInvitesTable } from './UserInvites';
import type { default as EmailsTable } from './Emails'; import type { default as EmailsTable } from './Emails';
import type { default as TilesCacheTable } from './TilesCache';
import type { default as ComuniTable } from './Comuni'; import type { default as ComuniTable } from './Comuni';
import type { default as RinnoviTable } from './Rinnovi'; import type { default as RinnoviTable } from './Rinnovi';
import type { default as EventQueueTable } from './EventQueue'; import type { default as EventQueueTable } from './EventQueue';
@ -58,6 +59,8 @@ export default interface PublicSchema {
emails: EmailsTable; emails: EmailsTable;
tiles_cache: TilesCacheTable;
comuni: ComuniTable; comuni: ComuniTable;
rinnovi: RinnoviTable; rinnovi: RinnoviTable;

View file

@ -0,0 +1,40 @@
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
import { z } from 'zod';
/** Identifier type for public.tiles_cache */
export type TilesCacheKey = string & { __brand: 'public.tiles_cache' };
/** Represents the table public.tiles_cache */
export default interface TilesCacheTable {
key: ColumnType<TilesCacheKey, TilesCacheKey, TilesCacheKey>;
value: ColumnType<Buffer, Buffer, Buffer>;
expires_at: ColumnType<Date | null, Date | string | null, Date | string | null>;
}
export type TilesCache = Selectable<TilesCacheTable>;
export type NewTilesCache = Insertable<TilesCacheTable>;
export type TilesCacheUpdate = Updateable<TilesCacheTable>;
export const tilesCacheKey = z.string().transform(value => value as TilesCacheKey);
export const TilesCacheSchema = z.object({
key: tilesCacheKey,
value: z.instanceof(Buffer),
expires_at: z.date().nullable(),
});
export const NewTilesCacheSchema = z.object({
key: tilesCacheKey,
value: z.instanceof(Buffer),
expires_at: z.union([z.date(), z.string()]).pipe(z.coerce.date()).optional().nullable(),
});
export const TilesCacheUpdateSchema = z.object({
key: tilesCacheKey.optional(),
value: z.instanceof(Buffer).optional(),
expires_at: z.union([z.date(), z.string()]).pipe(z.coerce.date()).optional().nullable(),
});

View file

@ -15,6 +15,7 @@ import { storageRouter } from "~/server/api/routers/storage";
import { stripeRouter } from "~/server/api/routers/stripe"; import { stripeRouter } from "~/server/api/routers/stripe";
import { stripeReportsRouter } from "~/server/api/routers/stripe_reports"; import { stripeReportsRouter } from "~/server/api/routers/stripe_reports";
import { testRouter } from "~/server/api/routers/test"; import { testRouter } from "~/server/api/routers/test";
import { tilesRouter } from "~/server/api/routers/tiles";
import { usersRouter } from "~/server/api/routers/users"; import { usersRouter } from "~/server/api/routers/users";
import { createTRPCRouter } from "~/server/api/trpc"; import { createTRPCRouter } from "~/server/api/trpc";
import { appuntiRouter } from "./routers/appunti"; import { appuntiRouter } from "./routers/appunti";
@ -48,6 +49,7 @@ export const appRouter = createTRPCRouter({
test: testRouter, test: testRouter,
users: usersRouter, users: usersRouter,
//sync: syncRouter, //sync: syncRouter,
tiles: tilesRouter,
catasto: catastoRouter, catasto: catastoRouter,
banners: bannersRouter, banners: bannersRouter,
flags: flagsRouter, flags: flagsRouter,

View file

@ -25,10 +25,6 @@ import {
} from "~/server/controllers/annunci.controller"; } from "~/server/controllers/annunci.controller";
import { getIncrociAnnuncio } from "~/server/controllers/servizio.controller"; import { getIncrociAnnuncio } from "~/server/controllers/servizio.controller";
import { db } from "~/server/db"; import { db } from "~/server/db";
import {
invalidateCache,
SCHEDA_ANNUNCIO_CACHE_PREFIX,
} from "~/server/services/cache.service";
import { NewMail } from "~/server/services/mailer"; import { NewMail } from "~/server/services/mailer";
import { TypstGenerate } from "~/server/services/typst.service"; import { TypstGenerate } from "~/server/services/typst.service";
@ -36,10 +32,6 @@ export const annunciRouter = createTRPCRouter({
editAnnuncio: adminProcedure editAnnuncio: adminProcedure
.input(z.object({ annuncioId: annunciId, data: AnnunciUpdateSchema })) .input(z.object({ annuncioId: annunciId, data: AnnunciUpdateSchema }))
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
await invalidateCache(
`${SCHEDA_ANNUNCIO_CACHE_PREFIX}${input.annuncioId}`,
);
return await editAnnuncioHandler({ return await editAnnuncioHandler({
...input, ...input,
}); });

View file

@ -3,10 +3,6 @@ import { z } from "zod/v4";
import { env } from "~/env"; import { env } from "~/env";
import { adminProcedure, createTRPCRouter } from "~/server/api/trpc"; import { adminProcedure, createTRPCRouter } from "~/server/api/trpc";
import { getCodici_AnnunciHandler } from "~/server/controllers/annunci.controller"; import { getCodici_AnnunciHandler } from "~/server/controllers/annunci.controller";
import {
getCacheStats,
invalidateAllCaches,
} from "~/server/services/cache.service";
import { import {
revalidate, revalidate,
revalidateMultiple, revalidateMultiple,
@ -135,11 +131,10 @@ export const revalidationRouter = createTRPCRouter({
} }
}), }),
getRedisStats: adminProcedure.query(async () => { getRedisStats: adminProcedure.query(async () => {
return await getCacheStats(); return "await getCacheStats();";
}), }),
invalidateRedisCache: adminProcedure.mutation(async () => { invalidateRedisCache: adminProcedure.mutation(async () => {
try { try {
await invalidateAllCaches();
return { return {
status: "success", status: "success",
}; };

View file

@ -0,0 +1,20 @@
import z from "zod";
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
import { get, set } from "~/server/services/tiles.service";
export const tilesRouter = createTRPCRouter({
get: publicProcedure.input(z.string()).query(async ({ input }) => {
return await get(input);
}),
set: publicProcedure
.input(
z.object({
key: z.string(),
value: z.instanceof(Buffer),
ttlSec: z.number().optional(),
}),
)
.mutation(async ({ input }) => {
return await set(input.key, input.value, input.ttlSec);
}),
});

View file

@ -24,11 +24,6 @@ import {
convertHtmlToTypst, convertHtmlToTypst,
TypstGenerate, TypstGenerate,
} from "~/server/services/typst.service"; } from "~/server/services/typst.service";
import { getKeydbClient } from "~/utils/keydb";
import {
CONDIZIONI_CACHE_PREFIX,
SCHEDA_ANNUNCIO_CACHE_PREFIX,
} from "../services/cache.service";
type GeneratedAttachmentTypes = type GeneratedAttachmentTypes =
| { type: "scheda_annuncio"; annuncioId: AnnunciId } | { type: "scheda_annuncio"; annuncioId: AnnunciId }
@ -57,14 +52,6 @@ const generatePdfContent = async (params: GeneratedAttachmentTypes) => {
} }
case "condizioni": { case "condizioni": {
const keybd = getKeydbClient();
const cacheKey = `${CONDIZIONI_CACHE_PREFIX}${params.stringId}`;
if (keybd) {
const cached = await keybd.get(cacheKey);
if (cached) {
return cached;
}
}
const data = await db const data = await db
.selectFrom("testi_e_stringhe") .selectFrom("testi_e_stringhe")
.selectAll() .selectAll()
@ -83,30 +70,17 @@ const generatePdfContent = async (params: GeneratedAttachmentTypes) => {
data: { stringa }, data: { stringa },
}); });
const pdfBase64 = pdf.toString("base64"); const pdfBase64 = pdf.toString("base64");
if (keybd) {
await keybd.set(cacheKey, pdfBase64, "EX", 3600 * 24 * 5);
}
return pdfBase64; return pdfBase64;
} }
case "scheda_annuncio": { case "scheda_annuncio": {
const keybd = getKeydbClient();
const cacheKey = `${SCHEDA_ANNUNCIO_CACHE_PREFIX}${params.annuncioId}`;
if (keybd) {
const cached = await keybd.get(cacheKey);
if (cached) {
return cached;
}
}
const data = await getAnnunciSchedaData(params.annuncioId); const data = await getAnnunciSchedaData(params.annuncioId);
const pdf = await TypstGenerate({ const pdf = await TypstGenerate({
templateId: "annuncio.typ", templateId: "annuncio.typ",
data, data,
}); });
const pdfBase64 = pdf.toString("base64"); const pdfBase64 = pdf.toString("base64");
if (keybd) {
await keybd.set(cacheKey, pdfBase64, "EX", 3600 * 5);
}
return pdfBase64; return pdfBase64;
} }
default: default:

View file

@ -1,64 +0,0 @@
import type Redis from "ioredis";
import { getKeydbClient } from "~/utils/keydb";
export const SCHEDA_ANNUNCIO_CACHE_PREFIX = "scheda-annuncio-";
export const CONDIZIONI_CACHE_PREFIX = "condizioni-";
export const invalidateCache = async (cacheKey: string) => {
const keybd = getKeydbClient();
if (keybd) {
await keybd.del(cacheKey);
}
};
async function deleteByPrefix(redis: Redis, prefix: string) {
// Create a stream to find keys matching the pattern
const stream = redis.scanStream({
match: `${prefix}*`,
count: 100, // Process 100 keys per iteration
});
stream.on("data", async (keys: string[]) => {
if (keys.length > 0) {
// Create a pipeline to delete keys in a single network round-trip
const pipeline = redis.pipeline();
// Use UNLINK instead of DEL for better performance on large keys
keys.forEach((key) => pipeline.unlink(key));
await pipeline.exec();
console.log(`Deleted ${keys.length} keys`);
}
});
stream.on("error", (err) => {
console.error(
`Error scanning/unlinking keys with prefix ${prefix}: ${err.message}`,
);
});
}
export const invalidateAllCaches = async () => {
const keybd = getKeydbClient();
if (keybd) {
await deleteByPrefix(keybd, SCHEDA_ANNUNCIO_CACHE_PREFIX);
await deleteByPrefix(keybd, CONDIZIONI_CACHE_PREFIX);
}
};
export const getCacheStats = async () => {
const keybd = getKeydbClient();
if (keybd) {
const schedaKeys = await keybd.keys(`${SCHEDA_ANNUNCIO_CACHE_PREFIX}*`);
const condizioniKeys = await keybd.keys(`${CONDIZIONI_CACHE_PREFIX}*`);
return {
success: true,
schedaCount: schedaKeys.length,
condizioniCount: condizioniKeys.length,
};
}
return {
success: false,
schedaCount: 0,
condizioniCount: 0,
};
};

View file

@ -0,0 +1,58 @@
import { TRPCError } from "@trpc/server";
import type { TilesCacheKey } from "~/schemas/public/TilesCache";
import { db } from "~/server/db";
export async function get(key: string) {
try {
const row = await db
.selectFrom("tiles_cache")
.select(["value", "expires_at"])
.where("key", "=", key as TilesCacheKey)
// If the row is expired we treat it as missing
.where((eb) =>
eb.or([
eb("expires_at", "is", null),
eb("expires_at", ">", new Date()),
]),
)
.executeTakeFirst();
return row?.value;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Failed get kv value: ${(e as Error).message}`,
});
}
}
export async function set(
key: string,
value: Buffer,
ttlSec?: number,
): Promise<void> {
const expiresAt = ttlSec ? new Date(Date.now() + ttlSec * 1_000) : null;
try {
await db
.insertInto("tiles_cache")
.values({
key: key as TilesCacheKey,
value: value,
expires_at: expiresAt,
})
.onConflict((oc) =>
oc.column("key").doUpdateSet({
value: value,
expires_at: expiresAt,
}),
)
.execute();
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error setting KV: ${(e as Error).message}`,
});
}
}

View file

@ -1,19 +0,0 @@
import Redis from "ioredis";
import { env } from "~/env";
const keydbConfig = `keydb://${env.KEYDB_URL}`;
let keydb: Redis | null = null;
export function getKeydbClient() {
if (env.SKIP_REDIS === "true") return null;
if (!keydb) {
keydb = new Redis(keydbConfig);
}
return keydb;
}
export function getSubClient() {
if (env.SKIP_REDIS === "true") return null;
return new Redis(keydbConfig, { enableReadyCheck: false });
}

View file

@ -57,45 +57,6 @@ services:
retries: 5 retries: 5
start_period: 10s start_period: 10s
tiles:
image: eqalpha/keydb:latest
volumes:
- tiles-data:/data
networks:
- dokploy-network
expose:
- 6379
command: keydb-server /etc/keydb/keydb.conf --maxmemory 100mb --maxmemory-policy volatile-lru --maxmemory-samples 5
healthcheck:
test: ["CMD", "keydb-cli", "-h", "localhost", "ping"]
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped
deploy:
resources:
limits:
cpus: "0.5"
keydb:
image: eqalpha/keydb:latest
volumes:
- keydb-data:/data
networks:
- dokploy-network
expose:
- 6379
healthcheck:
test: ["CMD", "keydb-cli", "-h", "localhost", "ping"]
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped
deploy:
resources:
limits:
cpus: "0.5"
backend: backend:
image: "${IMAGE_REGISTRY:-ghcr.io}/${OWNER:-marcopedone}/${PREFIX:-infoalloggi}-backend:${TAG:-latest}" image: "${IMAGE_REGISTRY:-ghcr.io}/${OWNER:-marcopedone}/${PREFIX:-infoalloggi}-backend:${TAG:-latest}"
networks: networks:
@ -124,10 +85,6 @@ services:
condition: service_healthy condition: service_healthy
db: db:
condition: service_healthy condition: service_healthy
keydb:
condition: service_healthy
tiles:
condition: service_healthy
web: web:
image: "${IMAGE_REGISTRY:-ghcr.io}/${OWNER:-marcopedone}/${PREFIX:-infoalloggi}-web:${TAG:-latest}" image: "${IMAGE_REGISTRY:-ghcr.io}/${OWNER:-marcopedone}/${PREFIX:-infoalloggi}-web:${TAG:-latest}"
@ -142,8 +99,6 @@ services:
PGHOST: db PGHOST: db
PGPORT: "5432" PGPORT: "5432"
BACKENDSERVER_URL: http://backend:1323 BACKENDSERVER_URL: http://backend:1323
KEYDB_URL: keydb:6379
TILES_URL: tiles:6379
BASE_URL: ${BASE_URL} BASE_URL: ${BASE_URL}
NEXT_PUBLIC_BASE_URL: ${BASE_URL} NEXT_PUBLIC_BASE_URL: ${BASE_URL}
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY} STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
@ -196,10 +151,6 @@ services:
condition: service_healthy condition: service_healthy
db: db:
condition: service_healthy condition: service_healthy
keydb:
condition: service_healthy
tiles:
condition: service_healthy
maintenance-page: maintenance-page:
image: nginx:alpine image: nginx:alpine
restart: always restart: always
@ -224,6 +175,4 @@ networks:
volumes: volumes:
storage_data: storage_data:
tiles-data:
keydb-data:
dbdata_v18: dbdata_v18: