Optimisation du refresh cache avec batch processing

Problème : Le refresh cache de tous les projets timeout côté serveur à cause
du trop grand nombre de projets à traiter en une seule requête.

Solution : Batch processing avec indicateur de progression
- Backend : traite 10 projets par batch avec offset/limit
- Frontend : fait plusieurs requêtes successives et affiche la progression
- Timeout réduit à 60s par batch au lieu de illimité
- Bouton désactivé pendant le traitement
- Ajout invalidateNotificationsCache() pour vider aussi ce cache

Affichage : "15/50 (30%)" pendant le traitement, puis "Terminé (50)"

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-01-15 12:08:13 +01:00
parent 86db1f5a0c
commit a57b0c203a
2 changed files with 119 additions and 20 deletions

View file

@ -7,6 +7,7 @@
:icon="icon"
:title="title"
@click="refreshCache()"
:disabled="isProcessing"
>{{ text }}</k-button
>
</div>
@ -24,6 +25,8 @@ const { pageUri, pageStatus, lastCacheUpdate } = defineProps({
const text = ref("Rafraîchir");
const icon = ref("refresh");
const theme = ref("aqua-icon");
const isProcessing = ref(false);
const title = computed(() => {
return lastCacheUpdate?.length > 0
? "Dernière mise à jour : " + lastCacheUpdate
@ -31,25 +34,89 @@ const title = computed(() => {
});
async function refreshCache() {
text.value = "En cours…";
isProcessing.value = true;
icon.value = "loader";
theme.value = "orange-icon";
// Pour les projets multiples (batch processing)
if (pageUri === 'projects') {
await refreshAllProjects();
} else {
await refreshSingleProject();
}
}
async function refreshAllProjects() {
let offset = 0;
const limit = 10; // 10 projets par batch
let hasMore = true;
let total = 0;
try {
while (hasMore) {
const init = {
method: "POST",
"Content-Type": "application/json",
body: JSON.stringify({
pageUri: 'projects',
offset,
limit
}),
};
const res = await fetch("/refresh-cache.json", init);
const json = await res.json();
if (json.status === "error") {
throw new Error(json.message);
}
total = json.total;
hasMore = json.hasMore;
offset = json.nextOffset;
// Mise à jour de la progression
const progress = Math.round((json.processed / json.total) * 100);
text.value = `${json.processed}/${json.total} (${progress}%)`;
console.log(`Batch terminé : ${json.processed}/${json.total} projets`);
}
// Succès
text.value = `Terminé (${total})`;
icon.value = "check";
theme.value = "green-icon";
setTimeout(() => {
location.href = location.href;
}, 1500);
} catch (error) {
console.error(error);
text.value = "Erreur";
icon.value = "alert";
theme.value = "red-icon";
isProcessing.value = false;
}
}
async function refreshSingleProject() {
text.value = "En cours…";
const init = {
method: "POST",
"Content-Type": "application/json",
body: JSON.stringify({ pageUri }),
};
const res = await fetch("/refresh-cache.json", init);
const json = await res.json();
try {
const res = await fetch("/refresh-cache.json", init);
const json = await res.json();
if (json.status === "error") {
throw new Error(json.message);
}
if (json.status === "error") {
console.error(json);
text.value = "Erreur";
icon.value = "alert";
theme.value = "red-icon";
} else {
console.log(json);
text.value = "Terminé";
icon.value = "check";
@ -58,6 +125,13 @@ async function refreshCache() {
setTimeout(() => {
location.href = location.href;
}, 1500);
} catch (error) {
console.error(error);
text.value = "Erreur";
icon.value = "alert";
theme.value = "red-icon";
isProcessing.value = false;
}
}
</script>

View file

@ -1,5 +1,5 @@
<?php
set_time_limit(0);
set_time_limit(60);
return [
'pattern' => '/refresh-cache.json',
@ -10,17 +10,42 @@ return [
if ($data->pageUri === 'projects') {
$projects = page('projects')->children();
foreach ($projects as $project) {
$project->rebuildStepsCache();
$formatter = new IntlDateFormatter('fr_FR', IntlDateFormatter::SHORT, IntlDateFormatter::SHORT, 'Europe/Paris');
$project->update([
'lastCacheUpdate' => $formatter->format(time())
]);
// Support du batch processing
$offset = isset($data->offset) ? intval($data->offset) : 0;
$limit = isset($data->limit) ? intval($data->limit) : 10; // 10 projets par batch par défaut
$total = $projects->count();
// Slice pour ne traiter qu'un batch
$batch = $projects->slice($offset, $limit);
$processed = 0;
foreach ($batch as $project) {
try {
$project->rebuildStepsCache();
$project->invalidateNotificationsCache();
$formatter = new IntlDateFormatter('fr_FR', IntlDateFormatter::SHORT, IntlDateFormatter::SHORT, 'Europe/Paris');
$project->update([
'lastCacheUpdate' => $formatter->format(time())
]);
$processed++;
} catch (\Throwable $e) {
error_log("Error refreshing cache for project {$project->slug()}: " . $e->getMessage());
}
}
$remaining = max(0, $total - ($offset + $processed));
$hasMore = $remaining > 0;
return [
'satus' => 'success',
'message' => 'Données des pages projets rafraîchies avec succès.'
'status' => 'success',
'message' => "Batch terminé : $processed projets traités.",
'processed' => $offset + $processed,
'total' => $total,
'remaining' => $remaining,
'hasMore' => $hasMore,
'nextOffset' => $hasMore ? $offset + $limit : null
];
} else {
try {
@ -41,7 +66,7 @@ return [
if (!$project) {
return [
'satus' => 'error',
'status' => 'error',
'message' => 'Impossible de rafraîchir les données de la page ' . $data->pageUri . '. Aucun projet correspondant.'
];
}
@ -55,7 +80,7 @@ return [
return [
'satus' => 'success',
'status' => 'success',
'message' => 'Données de la page ' . $data->pageUri . ' rafraîchie avec succès.'
];
}