feat: add Redis cache invalidation and stats retrieval functionality
This commit is contained in:
parent
a36fd52af2
commit
6c9524e54b
4 changed files with 154 additions and 95 deletions
|
|
@ -1,14 +1,9 @@
|
|||
import Head from "next/head";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import Input from "~/components/custom_ui/input";
|
||||
import { MultiSelect } from "~/components/custom_ui/multiselect";
|
||||
import { AreaRiservataLayout } from "~/components/Layout";
|
||||
import { LoadingPage } from "~/components/loading";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import type { NextPageWithLayout } from "~/pages/_app";
|
||||
import type { AnnunciId } from "~/schemas/public/Annunci";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
const Impostazioni: NextPageWithLayout = () => {
|
||||
|
|
@ -25,7 +20,7 @@ const Impostazioni: NextPageWithLayout = () => {
|
|||
</div>
|
||||
</div>
|
||||
<StripeSection />
|
||||
<SendEmailSection />
|
||||
<RedisCacheSection />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
@ -77,90 +72,44 @@ const StripeSection = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const SendEmailSection = () => {
|
||||
const { data: codici, isLoading: loading } =
|
||||
api.annunci.getOnline_AnnuncioIdCodice.useQuery();
|
||||
const [selectedAnnunci, setSelectedAnnunci] = useState<
|
||||
{ value: AnnunciId; label: string }[]
|
||||
>([]);
|
||||
const [email, setEmail] = useState<string | undefined>(undefined);
|
||||
|
||||
const { mutate } = api.comunicazioni.sendAnnunciEmail.useMutation({
|
||||
onSuccess: () => {
|
||||
// setSelectedAnnunci([]);
|
||||
// setEmail("");
|
||||
toast.success("Email inviata con successo");
|
||||
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",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
toast.error(`Errore durante l'invio dell'email: ${error.message}`);
|
||||
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">
|
||||
Invia email con annunci selezionati
|
||||
</h4>
|
||||
|
||||
<div className="flex max-w-xl flex-col gap-2">
|
||||
<Label className="font-medium" htmlFor="email-to">
|
||||
Indirizzo email destinatario
|
||||
</Label>
|
||||
<Input
|
||||
id="email-to"
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Inserisci l'indirizzo email"
|
||||
type="email"
|
||||
value={email || ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex max-w-xl flex-col gap-2">
|
||||
{loading ? (
|
||||
<LoadingPage />
|
||||
<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="flex max-w-xl flex-col">
|
||||
<Label className="font-medium" htmlFor="select-annuncio">
|
||||
Seleziona gli annunci da inviare
|
||||
</Label>
|
||||
<MultiSelect
|
||||
defaultValue={undefined}
|
||||
elementId="select-annuncio"
|
||||
elementName="annuncio"
|
||||
isMulti={true}
|
||||
onValueChange={(value) => {
|
||||
setSelectedAnnunci(
|
||||
value.map((v) => ({
|
||||
label: v.label,
|
||||
value: parseInt(v.value) as AnnunciId,
|
||||
})),
|
||||
);
|
||||
}}
|
||||
options={
|
||||
codici?.map((cod) => ({
|
||||
label: cod.codice,
|
||||
value: cod.id.toString(),
|
||||
})) || []
|
||||
}
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
<div className="text-red-500">Error fetching cache stats</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
aria-label="Cancel Add Annuncio"
|
||||
disabled={!selectedAnnunci.length || !email}
|
||||
onClick={() => {
|
||||
if (!selectedAnnunci.length || !email) return;
|
||||
mutate({
|
||||
annunci: selectedAnnunci.map((annuncio) => annuncio.value),
|
||||
to: email,
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Invia
|
||||
</Button>
|
||||
|
||||
<Button onClick={() => mutate()}>Invalidate Cache</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,9 +26,12 @@ import {
|
|||
} from "~/server/controllers/annunci.controller";
|
||||
import { db } from "~/server/db";
|
||||
import { NewMail } from "~/server/services/mailer";
|
||||
import { genPdfSchedaAnnuncio } from "~/server/services/puppeteer.service";
|
||||
import {
|
||||
genPdfSchedaAnnuncio,
|
||||
invalidateCache,
|
||||
SCHEDA_ANNUNCIO_CACHE_PREFIX,
|
||||
} from "~/server/services/puppeteer.service";
|
||||
import { zAnnuncioId, zServizioId } from "~/server/utils/zod_types";
|
||||
import { getKeydbClient } from "~/utils/keydb";
|
||||
|
||||
export const annunciRouter = createTRPCRouter({
|
||||
editAnnuncio: adminProcedure
|
||||
|
|
@ -36,10 +39,10 @@ export const annunciRouter = createTRPCRouter({
|
|||
z.object({ annuncioId: zAnnuncioId, data: z.custom<AnnunciUpdate>() }),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const keybd = getKeydbClient();
|
||||
if (keybd) {
|
||||
await keybd.del(`scheda-annuncio-${input.annuncioId}`);
|
||||
}
|
||||
await invalidateCache(
|
||||
`${SCHEDA_ANNUNCIO_CACHE_PREFIX}${input.annuncioId}`,
|
||||
);
|
||||
|
||||
return await editAnnuncioHandler({
|
||||
...input,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ 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/puppeteer.service";
|
||||
import {
|
||||
revalidate,
|
||||
revalidateMultiple,
|
||||
|
|
@ -130,4 +134,23 @@ export const revalidationRouter = createTRPCRouter({
|
|||
});
|
||||
}
|
||||
}),
|
||||
getRedisStats: adminProcedure.query(async () => {
|
||||
return await getCacheStats();
|
||||
}),
|
||||
invalidateRedisCache: adminProcedure.mutation(async () => {
|
||||
try {
|
||||
await invalidateAllCaches();
|
||||
return {
|
||||
status: "success",
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("Invalidate Redis cache error:", err);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message:
|
||||
err instanceof Error ? err.message : "Invalidate Redis cache failed",
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,72 @@
|
|||
import { TRPCError } from "@trpc/server";
|
||||
import type Redis from "ioredis";
|
||||
import puppeteer from "puppeteer-core";
|
||||
import { env } from "~/env";
|
||||
import type { AnnunciId } from "~/schemas/public/Annunci";
|
||||
import { getKeydbClient } from "~/utils/keydb";
|
||||
import { TOKEN_CONFIG } from "../auth/configs";
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
export const genPdfSchedaAnnuncioBase64 = async (annuncioId: AnnunciId) => {
|
||||
const pdf = await genPdfSchedaAnnuncio(annuncioId);
|
||||
const pdfBase64 = Buffer.from(pdf).toString("base64");
|
||||
|
|
@ -19,13 +81,14 @@ export const genPdfSchedaAnnuncioBase64 = async (annuncioId: AnnunciId) => {
|
|||
|
||||
export const genPdfSchedaAnnuncio = async (annuncioId: AnnunciId) => {
|
||||
const keybd = getKeydbClient();
|
||||
const url = `/area-riservata/admin/scheda-annuncio-stampa/${annuncioId}?raw=true`;
|
||||
if (keybd) {
|
||||
const cacheKey = `scheda-annuncio-${annuncioId}`;
|
||||
const cacheKey = `${SCHEDA_ANNUNCIO_CACHE_PREFIX}${annuncioId}`;
|
||||
const cached = await keybd.get(cacheKey);
|
||||
if (cached) {
|
||||
return new Uint8Array(Buffer.from(cached, "base64"));
|
||||
}
|
||||
const scheda = await puppeteerSchedaAnnuncio(annuncioId);
|
||||
const scheda = await puppeteerGenPdf(url);
|
||||
await keybd.set(
|
||||
cacheKey,
|
||||
Buffer.from(scheda).toString("base64"),
|
||||
|
|
@ -34,10 +97,31 @@ export const genPdfSchedaAnnuncio = async (annuncioId: AnnunciId) => {
|
|||
);
|
||||
return scheda;
|
||||
}
|
||||
return await puppeteerSchedaAnnuncio(annuncioId);
|
||||
return await puppeteerGenPdf(url);
|
||||
};
|
||||
|
||||
const puppeteerSchedaAnnuncio = async (annuncioId: AnnunciId) => {
|
||||
export const genPdfCondizioni = async (condizioniId: AnnunciId) => {
|
||||
const keybd = getKeydbClient();
|
||||
const url = `/servizio/condizioni/${condizioniId}?raw=true`;
|
||||
if (keybd) {
|
||||
const cacheKey = `${CONDIZIONI_CACHE_PREFIX}${condizioniId}`;
|
||||
const cached = await keybd.get(cacheKey);
|
||||
if (cached) {
|
||||
return new Uint8Array(Buffer.from(cached, "base64"));
|
||||
}
|
||||
const scheda = await puppeteerGenPdf(url);
|
||||
await keybd.set(
|
||||
cacheKey,
|
||||
Buffer.from(scheda).toString("base64"),
|
||||
"EX",
|
||||
3600 * 24 * 5,
|
||||
);
|
||||
return scheda;
|
||||
}
|
||||
return await puppeteerGenPdf(url);
|
||||
};
|
||||
|
||||
const puppeteerGenPdf = async (url: string) => {
|
||||
let browser = null;
|
||||
|
||||
try {
|
||||
|
|
@ -66,7 +150,7 @@ const puppeteerSchedaAnnuncio = async (annuncioId: AnnunciId) => {
|
|||
? env.INTERNAL_BASE_URL
|
||||
: `http://host.docker.internal:3000`;
|
||||
|
||||
const targetUrl = `${baseUrl}/area-riservata/admin/scheda-annuncio-stampa/${annuncioId}?raw=true`;
|
||||
const targetUrl = `${baseUrl}${url}`;
|
||||
await page.goto(targetUrl, { waitUntil: "networkidle0" });
|
||||
|
||||
const pdf = await page.pdf({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue