removed tiles redis, now using pg
This commit is contained in:
parent
b0f39f5f11
commit
92779d8b4e
23 changed files with 202 additions and 344 deletions
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
|
|
@ -36,10 +36,7 @@ env:
|
|||
ARUBA_PASS: "string"
|
||||
EXP_API_USER: "string"
|
||||
EXP_API_PASS: "string"
|
||||
TILES_URL: "keydb://string"
|
||||
KEYDB_URL: "keydb://string"
|
||||
JWT_SECRET: "335922ae"
|
||||
SKIP_REDIS: "false"
|
||||
|
||||
|
||||
jobs:
|
||||
|
|
|
|||
17
apps/db/migrations/54_kvcache.up.sql
Normal file
17
apps/db/migrations/54_kvcache.up.sql
Normal 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);
|
||||
|
|
@ -7,7 +7,7 @@ node_modules
|
|||
.github
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
keydb-docker-compose.yml
|
||||
dev-docker-compose.yml
|
||||
pgadmin-docker-compose.yml
|
||||
dev_utils_compose.sh
|
||||
.env
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ COPY --from=deps /app/node_modules ./node_modules
|
|||
COPY . .
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
NEXT_TELEMETRY_DISABLED=1 \
|
||||
SKIP_REDIS="true"
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
ARG BASE_URL NEXT_PUBLIC_BASE_URL NEXT_PUBLIC_STRIPE_PUBLIC_KEY
|
||||
ENV BASE_URL=$BASE_URL \
|
||||
|
|
@ -29,8 +28,7 @@ FROM base AS runner
|
|||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
NEXT_TELEMETRY_DISABLED=1 \
|
||||
SKIP_REDIS="false"
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
ARG BASE_URL
|
||||
ENV BASE_URL=$BASE_URL
|
||||
|
|
@ -72,8 +70,6 @@ ARG EXP_API_USER
|
|||
ENV EXP_API_USER=$EXP_API_USER
|
||||
ARG EXP_API_PASS
|
||||
ENV EXP_API_PASS=$EXP_API_PASS
|
||||
ARG KEYDB_URL
|
||||
ENV KEYDB_URL=$KEYDB_URL
|
||||
ARG TILES_URL
|
||||
ENV TILES_URL=$TILES_URL
|
||||
ARG SKEBBY_USER
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ TODOS:
|
|||
- pre attivazione se admin mostrare tastino contatti in preview
|
||||
- onboarding submit (toast e email che invitano a pagare)
|
||||
- migliorare onboarding e pag guida
|
||||
|
||||
- "fatto_nulla" status in servizio
|
||||
AFTER MVP:
|
||||
- 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
|
||||
|
|
|
|||
|
|
@ -1,20 +1,4 @@
|
|||
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:
|
||||
image: "ghcr.io/marcopedone/go_fs:latest"
|
||||
ports:
|
||||
|
|
@ -34,6 +18,4 @@ services:
|
|||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
tiles-data:
|
||||
keydb-data:
|
||||
storage_data:
|
||||
|
|
|
|||
|
|
@ -161,6 +161,8 @@ module.exports = {
|
|||
"pg_catalog.bpchar": "string",
|
||||
'pg_catalog.numeric': 'number',
|
||||
//"pg_catalog.json": "string",
|
||||
'pg_catalog.bytea': "Buffer"
|
||||
|
||||
|
||||
},
|
||||
preRenderHooks: [
|
||||
|
|
@ -177,6 +179,7 @@ module.exports = {
|
|||
zodTypeMap: {
|
||||
...defaultZodTypeMap,
|
||||
"pg_catalog.numeric": "z.number()",
|
||||
"pg_catalog.bytea": "z.instanceof(Buffer)"
|
||||
}
|
||||
}),
|
||||
],
|
||||
|
|
|
|||
70
apps/infoalloggi/package-lock.json
generated
70
apps/infoalloggi/package-lock.json
generated
|
|
@ -49,7 +49,6 @@
|
|||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "^12.38.0",
|
||||
"frimousse": "^0.3.0",
|
||||
"ioredis": "^5.9.2",
|
||||
"jose": "^6.0.12",
|
||||
"kysely": "^0.28.17",
|
||||
"kysely-plugin-serialize": "^0.8.2",
|
||||
|
|
@ -1818,10 +1817,6 @@
|
|||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@ioredis/commands": {
|
||||
"version": "1.5.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"license": "MIT",
|
||||
|
|
@ -7420,13 +7415,6 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cluster-key-slot": {
|
||||
"version": "1.1.2",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
|
|
@ -7843,13 +7831,6 @@
|
|||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"license": "MIT",
|
||||
|
|
@ -9041,28 +9022,6 @@
|
|||
"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": {
|
||||
"version": "3.0.5",
|
||||
"dev": true,
|
||||
|
|
@ -10120,14 +10079,6 @@
|
|||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.4.0",
|
||||
"license": "MIT",
|
||||
|
|
@ -12228,23 +12179,6 @@
|
|||
"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": {
|
||||
"version": "1.0.10",
|
||||
"dev": true,
|
||||
|
|
@ -12871,10 +12805,6 @@
|
|||
"stacktrace-gps": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/standard-as-callback": {
|
||||
"version": "2.1.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "4.0.0",
|
||||
"dev": true,
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@
|
|||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "^12.38.0",
|
||||
"frimousse": "^0.3.0",
|
||||
"ioredis": "^5.9.2",
|
||||
"jose": "^6.0.12",
|
||||
"kysely": "^0.28.17",
|
||||
"kysely-plugin-serialize": "^0.8.2",
|
||||
|
|
@ -159,4 +158,4 @@
|
|||
"axios": "^1.16.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,14 +14,12 @@ export const env = createEnv({
|
|||
FIC_COMPANY_ID: z.string(),
|
||||
INTERNAL_BASE_URL: z.string(),
|
||||
JWT_SECRET: z.string(),
|
||||
KEYDB_URL: z.string(),
|
||||
NODE_ENV: z.enum(["development", "test", "production"]),
|
||||
PGHOST: z.string(),
|
||||
PGPORT: z.string(),
|
||||
POSTGRES_DB: z.string(),
|
||||
POSTGRES_PASSWORD: z.string(),
|
||||
POSTGRES_USER: z.string(),
|
||||
SKIP_REDIS: z.string(),
|
||||
STRIPE_SECRET_KEY: z.string(),
|
||||
STRIPE_WEBHOOK_SECRET: z.string(),
|
||||
TILES_URL: z.string(),
|
||||
|
|
|
|||
|
|
@ -1,21 +1,10 @@
|
|||
import Redis from "ioredis";
|
||||
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]]
|
||||
* 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 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.
|
||||
|
|
@ -53,14 +42,13 @@ export default async function handler(
|
|||
|
||||
const cacheKey = `tile:${z}:${x}:${y}`;
|
||||
|
||||
const tiles = getTilesClient();
|
||||
if (!tiles) {
|
||||
return res.status(500).json({
|
||||
error: "KeyDB client is not initialized",
|
||||
});
|
||||
}
|
||||
const ctx = await createTRPCContext({ req: req, res: res });
|
||||
|
||||
const createCaller = t.createCallerFactory(appRouter);
|
||||
const caller = createCaller(ctx);
|
||||
|
||||
const cachedTile = await caller.tiles.get(cacheKey);
|
||||
|
||||
const cachedTile = await tiles.getBuffer(cacheKey);
|
||||
if (cachedTile) {
|
||||
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
||||
res.setHeader("Content-Type", "image/png");
|
||||
|
|
@ -80,8 +68,9 @@ export default async function handler(
|
|||
}
|
||||
const tileData = await response.arrayBuffer();
|
||||
const data = Buffer.from(tileData);
|
||||
|
||||
// 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("Content-Type", "image/png");
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import Head from "next/head";
|
||||
import toast from "react-hot-toast";
|
||||
import { AreaRiservataLayout } from "~/components/Layout";
|
||||
import { LoadingPage } from "~/components/loading";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import type { NextPageWithLayout } from "~/pages/_app";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
|
|
@ -23,7 +20,7 @@ const Impostazioni: NextPageWithLayout = () => {
|
|||
</div>
|
||||
</div>
|
||||
<StripeSection />
|
||||
<RedisCacheSection />
|
||||
{/* <RedisCacheSection /> */}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
@ -75,44 +72,44 @@ const StripeSection = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const RedisCacheSection = () => {
|
||||
const { data, isLoading } = api.revalidation.getRedisStats.useQuery();
|
||||
const utils = api.useUtils();
|
||||
const { mutate } = api.revalidation.invalidateRedisCache.useMutation({
|
||||
onMutate() {
|
||||
toast.loading("Invalidating cache...", {
|
||||
id: "invalidate-cache",
|
||||
});
|
||||
},
|
||||
onSuccess: async () => {
|
||||
toast.success("Cache invalidated successfully!", {
|
||||
id: "invalidate-cache",
|
||||
});
|
||||
await utils.revalidation.getRedisStats.invalidate();
|
||||
},
|
||||
onError() {
|
||||
toast.error("Failed to invalidate cache.", {
|
||||
id: "invalidate-cache",
|
||||
});
|
||||
},
|
||||
});
|
||||
if (isLoading) {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
return (
|
||||
<div className="space-y-4 rounded-lg border p-4">
|
||||
<h4 className="font-semibold text-lg">Invalidate Redis Cache</h4>
|
||||
<p>Current Cache Stats:</p>
|
||||
{data?.success ? (
|
||||
<ul className="list-inside list-disc">
|
||||
<li>Pdf Schede Annuncio: {data?.schedaCount}</li>
|
||||
<li>Pdf Condizioni: {data?.condizioniCount}</li>
|
||||
</ul>
|
||||
) : (
|
||||
<div className="text-red-500">Error fetching cache stats</div>
|
||||
)}
|
||||
// const RedisCacheSection = () => {
|
||||
// const { data, isLoading } = api.revalidation.getRedisStats.useQuery();
|
||||
// const utils = api.useUtils();
|
||||
// const { mutate } = api.revalidation.invalidateRedisCache.useMutation({
|
||||
// onMutate() {
|
||||
// toast.loading("Invalidating cache...", {
|
||||
// id: "invalidate-cache",
|
||||
// });
|
||||
// },
|
||||
// onSuccess: async () => {
|
||||
// toast.success("Cache invalidated successfully!", {
|
||||
// id: "invalidate-cache",
|
||||
// });
|
||||
// await utils.revalidation.getRedisStats.invalidate();
|
||||
// },
|
||||
// onError() {
|
||||
// toast.error("Failed to invalidate cache.", {
|
||||
// id: "invalidate-cache",
|
||||
// });
|
||||
// },
|
||||
// });
|
||||
// if (isLoading) {
|
||||
// return <LoadingPage />;
|
||||
// }
|
||||
// return (
|
||||
// <div className="space-y-4 rounded-lg border p-4">
|
||||
// <h4 className="font-semibold text-lg">Invalidate Redis Cache</h4>
|
||||
// <p>Current Cache Stats:</p>
|
||||
// {data?.success ? (
|
||||
// <ul className="list-inside list-disc">
|
||||
// <li>Pdf Schede Annuncio: {data?.schedaCount}</li>
|
||||
// <li>Pdf Condizioni: {data?.condizioniCount}</li>
|
||||
// </ul>
|
||||
// ) : (
|
||||
// <div className="text-red-500">Error fetching cache stats</div>
|
||||
// )}
|
||||
|
||||
<Button onClick={() => mutate()}>Invalidate Cache</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// <Button onClick={() => mutate()}>Invalidate Cache</Button>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import type { default as UsersStorageTable } from './UsersStorage';
|
|||
import type { default as ServizioAnnunciTable } from './ServizioAnnunci';
|
||||
import type { default as UserInvitesTable } from './UserInvites';
|
||||
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 RinnoviTable } from './Rinnovi';
|
||||
import type { default as EventQueueTable } from './EventQueue';
|
||||
|
|
@ -58,6 +59,8 @@ export default interface PublicSchema {
|
|||
|
||||
emails: EmailsTable;
|
||||
|
||||
tiles_cache: TilesCacheTable;
|
||||
|
||||
comuni: ComuniTable;
|
||||
|
||||
rinnovi: RinnoviTable;
|
||||
|
|
|
|||
40
apps/infoalloggi/src/schemas/public/TilesCache.ts
Normal file
40
apps/infoalloggi/src/schemas/public/TilesCache.ts
Normal 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(),
|
||||
});
|
||||
|
|
@ -15,6 +15,7 @@ import { storageRouter } from "~/server/api/routers/storage";
|
|||
import { stripeRouter } from "~/server/api/routers/stripe";
|
||||
import { stripeReportsRouter } from "~/server/api/routers/stripe_reports";
|
||||
import { testRouter } from "~/server/api/routers/test";
|
||||
import { tilesRouter } from "~/server/api/routers/tiles";
|
||||
import { usersRouter } from "~/server/api/routers/users";
|
||||
import { createTRPCRouter } from "~/server/api/trpc";
|
||||
import { appuntiRouter } from "./routers/appunti";
|
||||
|
|
@ -48,6 +49,7 @@ export const appRouter = createTRPCRouter({
|
|||
test: testRouter,
|
||||
users: usersRouter,
|
||||
//sync: syncRouter,
|
||||
tiles: tilesRouter,
|
||||
catasto: catastoRouter,
|
||||
banners: bannersRouter,
|
||||
flags: flagsRouter,
|
||||
|
|
|
|||
|
|
@ -25,10 +25,6 @@ import {
|
|||
} from "~/server/controllers/annunci.controller";
|
||||
import { getIncrociAnnuncio } from "~/server/controllers/servizio.controller";
|
||||
import { db } from "~/server/db";
|
||||
import {
|
||||
invalidateCache,
|
||||
SCHEDA_ANNUNCIO_CACHE_PREFIX,
|
||||
} from "~/server/services/cache.service";
|
||||
import { NewMail } from "~/server/services/mailer";
|
||||
import { TypstGenerate } from "~/server/services/typst.service";
|
||||
|
||||
|
|
@ -36,10 +32,6 @@ export const annunciRouter = createTRPCRouter({
|
|||
editAnnuncio: adminProcedure
|
||||
.input(z.object({ annuncioId: annunciId, data: AnnunciUpdateSchema }))
|
||||
.mutation(async ({ input }) => {
|
||||
await invalidateCache(
|
||||
`${SCHEDA_ANNUNCIO_CACHE_PREFIX}${input.annuncioId}`,
|
||||
);
|
||||
|
||||
return await editAnnuncioHandler({
|
||||
...input,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,10 +3,6 @@ import { z } from "zod/v4";
|
|||
import { env } from "~/env";
|
||||
import { adminProcedure, createTRPCRouter } from "~/server/api/trpc";
|
||||
import { getCodici_AnnunciHandler } from "~/server/controllers/annunci.controller";
|
||||
import {
|
||||
getCacheStats,
|
||||
invalidateAllCaches,
|
||||
} from "~/server/services/cache.service";
|
||||
import {
|
||||
revalidate,
|
||||
revalidateMultiple,
|
||||
|
|
@ -135,11 +131,10 @@ export const revalidationRouter = createTRPCRouter({
|
|||
}
|
||||
}),
|
||||
getRedisStats: adminProcedure.query(async () => {
|
||||
return await getCacheStats();
|
||||
return "await getCacheStats();";
|
||||
}),
|
||||
invalidateRedisCache: adminProcedure.mutation(async () => {
|
||||
try {
|
||||
await invalidateAllCaches();
|
||||
return {
|
||||
status: "success",
|
||||
};
|
||||
|
|
|
|||
20
apps/infoalloggi/src/server/api/routers/tiles.ts
Normal file
20
apps/infoalloggi/src/server/api/routers/tiles.ts
Normal 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);
|
||||
}),
|
||||
});
|
||||
|
|
@ -24,11 +24,6 @@ import {
|
|||
convertHtmlToTypst,
|
||||
TypstGenerate,
|
||||
} 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: "scheda_annuncio"; annuncioId: AnnunciId }
|
||||
|
|
@ -57,14 +52,6 @@ const generatePdfContent = async (params: GeneratedAttachmentTypes) => {
|
|||
}
|
||||
|
||||
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
|
||||
.selectFrom("testi_e_stringhe")
|
||||
.selectAll()
|
||||
|
|
@ -83,30 +70,17 @@ const generatePdfContent = async (params: GeneratedAttachmentTypes) => {
|
|||
data: { stringa },
|
||||
});
|
||||
const pdfBase64 = pdf.toString("base64");
|
||||
if (keybd) {
|
||||
await keybd.set(cacheKey, pdfBase64, "EX", 3600 * 24 * 5);
|
||||
}
|
||||
|
||||
return pdfBase64;
|
||||
}
|
||||
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 pdf = await TypstGenerate({
|
||||
templateId: "annuncio.typ",
|
||||
data,
|
||||
});
|
||||
const pdfBase64 = pdf.toString("base64");
|
||||
if (keybd) {
|
||||
await keybd.set(cacheKey, pdfBase64, "EX", 3600 * 5);
|
||||
}
|
||||
|
||||
return pdfBase64;
|
||||
}
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
58
apps/infoalloggi/src/server/services/tiles.service.ts
Normal file
58
apps/infoalloggi/src/server/services/tiles.service.ts
Normal 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}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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 });
|
||||
}
|
||||
|
|
@ -57,45 +57,6 @@ services:
|
|||
retries: 5
|
||||
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:
|
||||
image: "${IMAGE_REGISTRY:-ghcr.io}/${OWNER:-marcopedone}/${PREFIX:-infoalloggi}-backend:${TAG:-latest}"
|
||||
networks:
|
||||
|
|
@ -124,10 +85,6 @@ services:
|
|||
condition: service_healthy
|
||||
db:
|
||||
condition: service_healthy
|
||||
keydb:
|
||||
condition: service_healthy
|
||||
tiles:
|
||||
condition: service_healthy
|
||||
|
||||
web:
|
||||
image: "${IMAGE_REGISTRY:-ghcr.io}/${OWNER:-marcopedone}/${PREFIX:-infoalloggi}-web:${TAG:-latest}"
|
||||
|
|
@ -142,8 +99,6 @@ services:
|
|||
PGHOST: db
|
||||
PGPORT: "5432"
|
||||
BACKENDSERVER_URL: http://backend:1323
|
||||
KEYDB_URL: keydb:6379
|
||||
TILES_URL: tiles:6379
|
||||
BASE_URL: ${BASE_URL}
|
||||
NEXT_PUBLIC_BASE_URL: ${BASE_URL}
|
||||
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
|
||||
|
|
@ -196,10 +151,6 @@ services:
|
|||
condition: service_healthy
|
||||
db:
|
||||
condition: service_healthy
|
||||
keydb:
|
||||
condition: service_healthy
|
||||
tiles:
|
||||
condition: service_healthy
|
||||
maintenance-page:
|
||||
image: nginx:alpine
|
||||
restart: always
|
||||
|
|
@ -224,6 +175,4 @@ networks:
|
|||
|
||||
volumes:
|
||||
storage_data:
|
||||
tiles-data:
|
||||
keydb-data:
|
||||
dbdata_v18:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue