5 Commity e4b76d1de7 ... c2ed0ad826

Autor SHA1 Wiadomość Data
  gpt-engineer-app[bot] c2ed0ad826 Met updated changelog 1 miesiąc temu
  gpt-engineer-app[bot] f9a75eee97 Changes 1 miesiąc temu
  gpt-engineer-app[bot] 3fe9e8215a Optimize RSS fetch insert logic 1 miesiąc temu
  gpt-engineer-app[bot] a3af5f9db7 Changes 1 miesiąc temu
  gpt-engineer-app[bot] ded8dd0369 Save plan in Lovable 1 miesiąc temu
3 zmienionych plików z 83 dodań i 68 usunięć
  1. 19 42
      .lovable/plan.md
  2. 14 0
      src/data/changelog.ts
  3. 50 26
      supabase/functions/fetch-rss/index.ts

+ 19 - 42
.lovable/plan.md

@@ -1,51 +1,28 @@
 
+## Diagnostique
 
-## Proteger les articles non lus des abonnements actifs
+### Sources du problème Disk IO
 
-### Nouvelle regle de purge
+1. **Cron trop fréquent** : `*/10 * * * *` = toutes les 10 min × 49 flux actifs = **~14 000 appels/jour** avec des upserts massifs sur la table `articles`
+2. **Index manquant sur `last_seen_at`** : la purge et les requêtes de filtrage font un full scan sur la table articles (34 MB, 2002 lignes)
+3. **Upsert massif** : chaque fetch-rss fait un upsert de tous les articles du flux, même si rien n'a changé — cela génère des écritures inutiles
 
-| Statut | Abonnement actif | Action |
-|--------|-------------------|--------|
-| Epingle | Peu importe | Protege |
-| Non lu + Non epingle | Oui (au moins 1 abonne) | Protege |
-| Non lu + Non epingle | Non (aucun abonne) | Supprime |
-| Lu + Non epingle | Peu importe | Supprime |
+### Plan d'action
 
-### Impact estime (donnees actuelles)
+**1. Réduire la fréquence du cron** (impact immédiat, fort)
+- Passer de `*/10 * * * *` à `*/30 * * * *` (toutes les 30 minutes)
+- Les flux RSS ne se mettent généralement pas à jour plus souvent que ça
+- Réduit les I/O de **3x**
 
-| Metrique | Valeur |
-|----------|--------|
-| Articles eligibles (> 48h) | 103 |
-| Epingles (proteges) | 9 |
-| Non lus avec abonnement (proteges) | 4 |
-| Non lus sans abonnement (supprimes) | 0 |
-| Lus non epingles (supprimes) | 90 |
-| **Total supprime** | **~90** |
+**2. Ajouter un index sur `last_seen_at`** (impact sur la purge)
+- Migration SQL : `CREATE INDEX idx_articles_last_seen_at ON public.articles USING btree (last_seen_at);`
+- Rend la purge beaucoup plus rapide (index scan au lieu de seq scan)
 
-### Details techniques
+**3. Optimiser l'upsert dans fetch-rss** (impact moyen)
+- Mettre à jour `last_seen_at` uniquement si l'article existe déjà
+- Ne pas mettre à jour le contenu si le guid existe déjà (éviter les writes inutiles)
 
-**Migration SQL** - Mise a jour de `purge_old_articles` et `test_purge_articles` :
-
-La condition de protection passe de :
-```text
-NOT EXISTS (is_pinned = true)
-```
-A :
-```text
-NOT EXISTS (is_pinned = true)
-AND NOT (article non lu ET feed a au moins un abonne)
-```
-
-Concretement, un article est supprime si :
-1. `last_seen_at` depasse 48h
-2. Il n'est epingle par personne
-3. ET il est soit lu par au moins un utilisateur, soit son flux n'a aucun abonne actif
-
-La fonction `test_purge_articles` sera aussi mise a jour pour refleter cette logique.
-
-### Fichiers modifies
-
-- **Migration SQL** : nouvelle fonction `purge_old_articles` et `test_purge_articles`
-- **src/data/changelog.ts** : ajout d'une entree pour documenter le changement
-- **.lovable/plan.md** : mise a jour de la documentation
+### Fichiers modifiés
 
+- **Migration SQL** : ajout de l'index `last_seen_at` + mise à jour du cron à 30 min
+- **`supabase/functions/fetch-rss/index.ts`** : optimiser l'upsert pour ne pas réécrire les articles existants inutilement

+ 14 - 0
src/data/changelog.ts

@@ -8,6 +8,20 @@ export interface ChangelogEntry {
 }
 
 export const changelogData: ChangelogEntry[] = [
+  {
+    version: "1.14.0",
+    date: "2026-03-02",
+    category: "improvement",
+    title: "Optimisation du budget Disk IO",
+    description: "Réduction significative des opérations disque pour éviter l'épuisement du budget Disk IO Supabase.",
+    details: [
+      "Cron de récupération RSS réduit de 10 à 30 minutes (3x moins d'appels/jour)",
+      "Ajout d'un index sur articles(last_seen_at) pour accélérer la purge",
+      "fetch-rss optimisé : UPDATE last_seen_at uniquement pour les articles existants",
+      "INSERT uniquement pour les nouveaux articles (élimination des upserts massifs)",
+      "Réduction estimée de ~80% des écritures disque par cycle de fetch"
+    ]
+  },
   {
     version: "1.13.1",
     date: "2026-02-09",

+ 50 - 26
supabase/functions/fetch-rss/index.ts

@@ -226,21 +226,42 @@ serve(async (req) => {
 
     // Save articles to database
     const now = new Date().toISOString();
-    const articlesToInsert = items.map(item => {
-      // Calculate read time (rough estimate: 200 words per minute)
+
+    // First, get existing article GUIDs for this feed to avoid full rewrites
+    const { data: existingArticles } = await supabaseClient
+      .from('articles')
+      .select('guid')
+      .eq('feed_id', feedId)
+
+    const existingGuids = new Set((existingArticles || []).map((a: { guid: string | null }) => a.guid))
+
+    const newItems = items.filter(item => !existingGuids.has(item.guid))
+    const existingItems = items.filter(item => existingGuids.has(item.guid))
+
+    console.log(`New articles: ${newItems.length}, existing: ${existingItems.length}`)
+
+    // Only update last_seen_at for existing articles (no content rewrite = less I/O)
+    if (existingItems.length > 0) {
+      const { error: updateError } = await supabaseClient
+        .from('articles')
+        .update({ last_seen_at: now })
+        .eq('feed_id', feedId)
+        .in('guid', existingItems.map(i => i.guid).filter(Boolean))
+
+      if (updateError) {
+        console.error('Error updating last_seen_at:', updateError)
+      }
+    }
+
+    // Insert only new articles
+    const articlesToInsert = newItems.map(item => {
       const wordCount = (item.description || '').split(' ').length
       const readTime = Math.max(1, Math.ceil(wordCount / 200))
 
-      // Parse and validate date
       let publishedAt: string;
       try {
-        if (item.pubDate) {
-          publishedAt = new Date(item.pubDate).toISOString();
-        } else {
-          publishedAt = new Date().toISOString();
-        }
-      } catch (error) {
-        console.log(`Invalid date format: ${item.pubDate}, using current date`);
+        publishedAt = item.pubDate ? new Date(item.pubDate).toISOString() : new Date().toISOString();
+      } catch {
         publishedAt = new Date().toISOString();
       }
 
@@ -258,18 +279,18 @@ serve(async (req) => {
       }
     })
 
-    console.log(`Preparing to insert ${articlesToInsert.length} articles`)
+    console.log(`Preparing to insert ${articlesToInsert.length} new articles`)
 
-    // Insert articles - on conflict update last_seen_at
-    const { error: insertError } = await supabaseClient
-      .from('articles')
-      .upsert(articlesToInsert, { 
-        onConflict: 'feed_id,guid'
-      })
+    // Insert only truly new articles
+    if (articlesToInsert.length > 0) {
+      const { error: insertError } = await supabaseClient
+        .from('articles')
+        .insert(articlesToInsert)
 
-    if (insertError) {
-      console.error('Error inserting articles:', insertError)
-      throw insertError
+      if (insertError) {
+        console.error('Error inserting articles:', insertError)
+        throw insertError
+      }
     }
 
     // Get current article count for this feed
@@ -283,7 +304,7 @@ serve(async (req) => {
     }
 
     // Update feed's last_fetched_at and article_count
-    const { error: updateError } = await supabaseClient
+    const { error: feedUpdateError } = await supabaseClient
       .from('feeds')
       .update({ 
         last_fetched_at: new Date().toISOString(),
@@ -292,17 +313,20 @@ serve(async (req) => {
       })
       .eq('id', feedId)
 
-    if (updateError) {
-      console.error('Error updating feed:', updateError)
-      throw updateError
+    if (feedUpdateError) {
+      console.error('Error updating feed:', feedUpdateError)
+      throw feedUpdateError
     }
 
-    console.log(`Successfully processed ${articlesToInsert.length} articles for feed ${feedId}`)
+    const totalProcessed = newItems.length + existingItems.length
+    console.log(`Feed ${feedId}: ${newItems.length} new articles inserted, ${existingItems.length} existing updated`)
 
     return new Response(
       JSON.stringify({ 
         success: true, 
-        articlesProcessed: articlesToInsert.length 
+        articlesProcessed: totalProcessed,
+        newArticles: newItems.length,
+        updatedArticles: existingItems.length
       }),
       { 
         headers: { ...corsHeaders, 'Content-Type': 'application/json' }