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 Head from "next/head";
|
||||||
import { useState } from "react";
|
|
||||||
import toast from "react-hot-toast";
|
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 { AreaRiservataLayout } from "~/components/Layout";
|
||||||
import { LoadingPage } from "~/components/loading";
|
import { LoadingPage } from "~/components/loading";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Label } from "~/components/ui/label";
|
|
||||||
import type { NextPageWithLayout } from "~/pages/_app";
|
import type { NextPageWithLayout } from "~/pages/_app";
|
||||||
import type { AnnunciId } from "~/schemas/public/Annunci";
|
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
const Impostazioni: NextPageWithLayout = () => {
|
const Impostazioni: NextPageWithLayout = () => {
|
||||||
|
|
@ -25,7 +20,7 @@ const Impostazioni: NextPageWithLayout = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<StripeSection />
|
<StripeSection />
|
||||||
<SendEmailSection />
|
<RedisCacheSection />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
@ -77,90 +72,44 @@ const StripeSection = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SendEmailSection = () => {
|
const RedisCacheSection = () => {
|
||||||
const { data: codici, isLoading: loading } =
|
const { data, isLoading } = api.revalidation.getRedisStats.useQuery();
|
||||||
api.annunci.getOnline_AnnuncioIdCodice.useQuery();
|
const utils = api.useUtils();
|
||||||
const [selectedAnnunci, setSelectedAnnunci] = useState<
|
const { mutate } = api.revalidation.invalidateRedisCache.useMutation({
|
||||||
{ value: AnnunciId; label: string }[]
|
onMutate() {
|
||||||
>([]);
|
toast.loading("Invalidating cache...", {
|
||||||
const [email, setEmail] = useState<string | undefined>(undefined);
|
id: "invalidate-cache",
|
||||||
|
});
|
||||||
const { mutate } = api.comunicazioni.sendAnnunciEmail.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
// setSelectedAnnunci([]);
|
|
||||||
// setEmail("");
|
|
||||||
toast.success("Email inviata con successo");
|
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onSuccess: async () => {
|
||||||
console.error(error);
|
toast.success("Cache invalidated successfully!", {
|
||||||
toast.error(`Errore durante l'invio dell'email: ${error.message}`);
|
id: "invalidate-cache",
|
||||||
|
});
|
||||||
|
await utils.revalidation.getRedisStats.invalidate();
|
||||||
|
},
|
||||||
|
onError() {
|
||||||
|
toast.error("Failed to invalidate cache.", {
|
||||||
|
id: "invalidate-cache",
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (isLoading) {
|
||||||
|
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">
|
<h4 className="font-semibold text-lg">Invalidate Redis Cache</h4>
|
||||||
Invia email con annunci selezionati
|
<p>Current Cache Stats:</p>
|
||||||
</h4>
|
{data?.success ? (
|
||||||
|
<ul className="list-inside list-disc">
|
||||||
<div className="flex max-w-xl flex-col gap-2">
|
<li>Pdf Schede Annuncio: {data?.schedaCount}</li>
|
||||||
<Label className="font-medium" htmlFor="email-to">
|
<li>Pdf Condizioni: {data?.condizioniCount}</li>
|
||||||
Indirizzo email destinatario
|
</ul>
|
||||||
</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 />
|
|
||||||
) : (
|
) : (
|
||||||
<div className="flex max-w-xl flex-col">
|
<div className="text-red-500">Error fetching cache stats</div>
|
||||||
<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>
|
|
||||||
<Button
|
<Button onClick={() => mutate()}>Invalidate Cache</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>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,12 @@ import {
|
||||||
} from "~/server/controllers/annunci.controller";
|
} from "~/server/controllers/annunci.controller";
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
import { NewMail } from "~/server/services/mailer";
|
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 { zAnnuncioId, zServizioId } from "~/server/utils/zod_types";
|
||||||
import { getKeydbClient } from "~/utils/keydb";
|
|
||||||
|
|
||||||
export const annunciRouter = createTRPCRouter({
|
export const annunciRouter = createTRPCRouter({
|
||||||
editAnnuncio: adminProcedure
|
editAnnuncio: adminProcedure
|
||||||
|
|
@ -36,10 +39,10 @@ export const annunciRouter = createTRPCRouter({
|
||||||
z.object({ annuncioId: zAnnuncioId, data: z.custom<AnnunciUpdate>() }),
|
z.object({ annuncioId: zAnnuncioId, data: z.custom<AnnunciUpdate>() }),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const keybd = getKeydbClient();
|
await invalidateCache(
|
||||||
if (keybd) {
|
`${SCHEDA_ANNUNCIO_CACHE_PREFIX}${input.annuncioId}`,
|
||||||
await keybd.del(`scheda-annuncio-${input.annuncioId}`);
|
);
|
||||||
}
|
|
||||||
return await editAnnuncioHandler({
|
return await editAnnuncioHandler({
|
||||||
...input,
|
...input,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ 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/puppeteer.service";
|
||||||
import {
|
import {
|
||||||
revalidate,
|
revalidate,
|
||||||
revalidateMultiple,
|
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 { TRPCError } from "@trpc/server";
|
||||||
|
import type Redis from "ioredis";
|
||||||
import puppeteer from "puppeteer-core";
|
import puppeteer from "puppeteer-core";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import type { AnnunciId } from "~/schemas/public/Annunci";
|
import type { AnnunciId } from "~/schemas/public/Annunci";
|
||||||
import { getKeydbClient } from "~/utils/keydb";
|
import { getKeydbClient } from "~/utils/keydb";
|
||||||
import { TOKEN_CONFIG } from "../auth/configs";
|
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) => {
|
export const genPdfSchedaAnnuncioBase64 = async (annuncioId: AnnunciId) => {
|
||||||
const pdf = await genPdfSchedaAnnuncio(annuncioId);
|
const pdf = await genPdfSchedaAnnuncio(annuncioId);
|
||||||
const pdfBase64 = Buffer.from(pdf).toString("base64");
|
const pdfBase64 = Buffer.from(pdf).toString("base64");
|
||||||
|
|
@ -19,13 +81,14 @@ export const genPdfSchedaAnnuncioBase64 = async (annuncioId: AnnunciId) => {
|
||||||
|
|
||||||
export const genPdfSchedaAnnuncio = async (annuncioId: AnnunciId) => {
|
export const genPdfSchedaAnnuncio = async (annuncioId: AnnunciId) => {
|
||||||
const keybd = getKeydbClient();
|
const keybd = getKeydbClient();
|
||||||
|
const url = `/area-riservata/admin/scheda-annuncio-stampa/${annuncioId}?raw=true`;
|
||||||
if (keybd) {
|
if (keybd) {
|
||||||
const cacheKey = `scheda-annuncio-${annuncioId}`;
|
const cacheKey = `${SCHEDA_ANNUNCIO_CACHE_PREFIX}${annuncioId}`;
|
||||||
const cached = await keybd.get(cacheKey);
|
const cached = await keybd.get(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return new Uint8Array(Buffer.from(cached, "base64"));
|
return new Uint8Array(Buffer.from(cached, "base64"));
|
||||||
}
|
}
|
||||||
const scheda = await puppeteerSchedaAnnuncio(annuncioId);
|
const scheda = await puppeteerGenPdf(url);
|
||||||
await keybd.set(
|
await keybd.set(
|
||||||
cacheKey,
|
cacheKey,
|
||||||
Buffer.from(scheda).toString("base64"),
|
Buffer.from(scheda).toString("base64"),
|
||||||
|
|
@ -34,10 +97,31 @@ export const genPdfSchedaAnnuncio = async (annuncioId: AnnunciId) => {
|
||||||
);
|
);
|
||||||
return scheda;
|
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;
|
let browser = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -66,7 +150,7 @@ const puppeteerSchedaAnnuncio = async (annuncioId: AnnunciId) => {
|
||||||
? env.INTERNAL_BASE_URL
|
? env.INTERNAL_BASE_URL
|
||||||
: `http://host.docker.internal:3000`;
|
: `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" });
|
await page.goto(targetUrl, { waitUntil: "networkidle0" });
|
||||||
|
|
||||||
const pdf = await page.pdf({
|
const pdf = await page.pdf({
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue