---
title: "Cache-uri și backfill-uri sigure: nu ștergi istoricul înainte să sosească datele noi"
description: "Un cache sigur la eșec este unul în care o reîmprospătare picată lasă datele vechi neatinse: scrii cu upsert pur, niciodată ștergere urmată de reinserare, și reîncerci exact excepția de timeout pe care o aruncă biblioteca. Backfill-ul umple goluri cu IS NULL în loc să rescrie decizii, instantaneele se verifică singure la citire, iar o valoare de azi nu răspunde niciodată la o întrebare despre trecut."
date: 2026-10-11
lang: ro
canonical: https://goingstellar.ro/ro/metode/cache-uri-si-backfill-uri-sigure-la-esec/
author: Going Stellar
publisher: GOING STELLAR S.R.L.
topic: Inginerie / Fluxuri de date
license: CC BY 4.0
alternate_en: https://goingstellar.ro/methods/fail-safe-caches-and-backfills/
---

# Cache-uri și backfill-uri sigure: nu ștergi istoricul înainte să sosească datele noi

*Un cache sigur la eșec este unul în care o reîmprospătare picată lasă datele vechi neatinse: scrii cu upsert pur, niciodată ștergere urmată de reinserare, și reîncerci exact excepția de timeout pe care o aruncă biblioteca. Backfill-ul umple goluri cu IS NULL în loc să rescrie decizii, instantaneele se verifică singure la citire, iar o valoare de azi nu răspunde niciodată la o întrebare despre trecut.*

> Inginerie · Fluxuri de date · 2026-10-11 · https://goingstellar.ro/ro/metode/cache-uri-si-backfill-uri-sigure-la-esec/

## Pe scurt

- O reîmprospătare care șterge istoricul înainte să reușească noua extragere transformă un apel API picat într-o lună de zerouri; un upsert pur lasă în schimb date vechi, dar corecte.
- Un înveliș de reîncercare nu protejează nimic dacă nu prinde excepția pe care o aruncă efectiv biblioteca client; unele biblioteci pentru API-urile Google aruncă un TimeoutError brut, nu RequestException.
- Un backfill filtrează pe IS NULL, pentru că filtrul „diferit de valoarea țintă” suprascrie pe tăcute orice decizie luată de un om după prima rulare.
- Un instantaneu ținut în cache compară la fiecare citire intrarea din care a fost construit, pentru că o invalidare care depinde de memoria fiecărui apelant va fi uitată la un moment dat.
- Un prag de timp de tipul „mai nou decât ultimul succes” sare peste orice unitate care a eșuat o dată; unitățile care pot eșua separat au nevoie de câte un rând cu starea lor.

## Ce înseamnă un cache sau un backfill sigur la eșec

Un cache sau un backfill este sigur la eșec atunci când o rulare picată lasă datele anterioare exact cum erau. Nu ștergi istoricul ca să-l reinserezi cu condiția ca noua extragere să reușească; scrii cu upsert pur (inserezi sau, dacă cheia există, actualizezi), astfel încât o extragere care eșuează sau expiră nu schimbă nimic. Un backfill umple goluri (`WHERE coloana IS NULL`), nu rescrie valori pe care un om le-a decis deja.

Ținem mai multe tabele derivate, reconstruite periodic din API-uri externe: costul zilnic de publicitate, costul pe produs, profitul lunar pe linie de business. Întrebarea utilă nu e dacă o extragere va eșua, ci cum arată tabelul după ce eșuează.

## Regula 1: upsert pur și reîncercarea timeout-ului real

Tentația, când reîmprospătezi un interval de date, este `DELETE FROM cache WHERE zi >= :start`, urmat de inserarea a ce a adus extragerea. Pare idempotent, dar nu e sigur la eșec. Dacă extragerea nu aduce nimic pentru că a expirat o cheie de acces sau API-ul n-a răspuns la timp, ștergerea a rulat deja și istoricul a dispărut.

Varianta sigură este upsert-ul: `INSERT ... ON CONFLICT (cheie) DO UPDATE`. Rândurile aduse de extragere se actualizează, iar cele neaduse își păstrează ultima valoare cunoscută. O extragere picată lasă cifrele de ieri, vechi dar corecte, pe care o verificare de prospețime le poate semnala. Cifrele șterse nu se pot semnala: arată pur și simplu ca zero.

**Reîncerci excepția pe care o aruncă biblioteca, nu pe cea la care te aștepți.** Orice apel extern stă într-o reîncercare cu pauze crescătoare, iar reîncercarea trebuie să prindă clasa corectă. O bibliotecă client pentru API-urile Google pe care o folosim aruncă un `TimeoutError` brut la un răspuns lent, nu `RequestException` din `requests`, pentru care se scrie de obicei învelișul de reîncercare. O reîncercare care prinde altă clasă pare rezolvată la revizie și nu pornește niciodată. Provoci un timeout într-un test, citești ce clasă iese și o prinzi pe nume.

## Regula 2: backfill-ul umple goluri, nu rescrie decizii

Un backfill este un script care completează o coloană nouă pe rânduri deja existente. Filtrul sigur este `WHERE coloana IS NULL`: doar rândurile care încă n-au niciun răspuns. Filtrul periculos este `WHERE coloana != tinta`: tot ce nu are deja valoarea pe care ar alege-o scriptul.

Noi am scris varianta periculoasă. Scriptul atribuia o categorie înregistrărilor existente. La prima rulare coloana era goală peste tot, așa că ambele filtre selectau aceleași rânduri și rezultatul era corect. Între timp însă formularul din interfață scrie o categorie la fiecare salvare, cu o valoare implicită. De la a doua rulare încolo, backfill-ul ar fi transformat orice alegere făcută de un om în valoarea scriptului. Am dovedit-o pe o copie a producției: am pus de mână două categorii diferite pe două înregistrări, am rulat iar backfill-ul și amândouă au ieșit cu categoria scriptului. Antetul scriptului spunea chiar că e idempotent și se poate rula de câte ori vrei, ceea ce făcea a doua rulare nu doar posibilă, ci recomandată.

**Idempotent nu înseamnă inofensiv.** O operație poate ajunge la aceeași stare la fiecare rulare și totuși să distrugă informație: decizia luată de un om între două rulări. Testul care prinde un backfill distructiv nu e „îl rulez de două ori la rând”, care trece, ci „un om a ales altceva, apoi scriptul a mai rulat o dată”.

## Regula 3: instantaneul se verifică singur la citire

Un instantaneu este orice valoare calculată o dată și păstrată pentru refolosire: o setare ținută în memorie, un context precalculat, un tabel materializat. Un instantaneu care așteaptă să fie invalidat de codul care îi schimbă sursa rămâne în urmă la prima cale de cod care uită să-l invalideze.

Am pățit-o într-o aplicație Android. Un obiect scris ca să țină toate ecranele pe aceeași temă de culori păstra un context construit o singură dată, la pornirea procesului. Comutatorul de temă din meniu schimba setarea sistemului, dar nu reîmprospăta niciodată obiectul. Din clipa în care omul alegea modul întunecat și până murea procesul, fundalurile erau închise și textul la fel: un contrast de **1,02**:1 pe aproximativ **25** de ecrane.

Reparația face instantaneul să se verifice singur la fiecare citire: compară modul pentru care a fost construit cu modul cerut acum și se reconstruiește doar când diferă. Pentru un cache de date, aceeași regulă înseamnă să păstrezi intrarea lângă valoarea calculată și s-o compari la citire, ca o invalidare uitată să producă o reconstruire, nu un răspuns greșit.

## Regula 4: starea de azi nu spune ce era atunci

O valoare curentă nu poate răspunde la o întrebare despre trecut. Am construit o regulă care oprea comenzile al căror produs nu mai avea stoc și am raportat **166** de comenzi care așteptau o marfă inexistentă. Obiecția a venit imediat: produsele erau epuizate acum, dar aveau stoc când s-au plasat comenzile. Pentru aproape fiecare produs semnalat plecaseră comenzi mai noi, într-un caz la **122** de ore după prima comandă oprită. Marfa se completează în loturi.

Stocul este un număr despre acum; comenzile sunt evenimente din trecut. Fără un istoric de stoc, întrebarea care desparte cazurile ține tot de evenimente: *a mai plecat ceva cu acest produs după ce a fost plasată comanda?* Dacă da, marfa a existat după ea și oprirea are altă cauză. Dacă nu, comanda e ultima din coadă. Aplicată în cod, lista a scăzut de la **166** la **26** de comenzi, fiecare chiar ultima pe produsul ei. Când o decizie depinde de o stare din trecut, păstrezi un instantaneu datat al acelei stări, nu o reconstruiești din ziua de azi.

## Capcanele care întorc mai puțin în loc să dea eroare

**Un plafon justificat de o măsurătoare veche.** Două limite de paginare din același sistem aveau fiecare câte un comentariu care le justifica. Un API refuză pagina **50** și peste, adică un plafon de **10.000** de rânduri, iar paginile merg de la nou la vechi, deci pentru cel mai mare magazin se citeau doar zilele cele mai recente. Acoperirea cu facturi era de **0**% între zilele **12** și **23**, în timp ce magazinele mai mici, care încăpeau sub plafon, stăteau constant pe la **90**%: grupul de control care a dovedit-o. Celălalt plafon citea **30** de pagini a câte **200**, cu comentariul „**6.000** de comenzi, peste orice fereastră de **10** zile”. Adevărat când a fost scris; mai târziu un magazin avea **9.966** de comenzi în **10** zile, iar jobul pierdea **3.966** din coadă.

Un plafon nu dă eroare. Întoarce mai puțin, iar „mai puțin” arată identic cu „atât există”. Când paginarea e ordonată, ce se pierde e sistematic coada. Acum orice constantă de tipul „N pagini, N rânduri, N zile ajung” se scrie cu volumul măsurat alături și cu o verificare la rulare care notează în log când plafonul e atins. După reparație, același job a citit **28.039** de comenzi, față de **10.000** înainte.

**Un prag de timp sare peste eșecuri pentru totdeauna.** Un importator lua linkurile de fișiere postate într-un grup de chat mai noi decât ultimul fișier importat cu succes. Un link care eșua o dată nu mai era reluat niciodată, fiindcă la rularea următoare intraseră deja linkuri mai noi, iar pragul trecuse de el. Măsurat după **16** zile de rulări programate: **8** linkuri din **59** nu ajunseseră niciodată. **Patru** nici nu fuseseră încercate; **patru** picaseră o dată, iar **trei** dintre ele mergeau la o nouă încercare. Un prag de timp notează până unde ai ajuns, nu ce ai făcut, așa că e sigur doar când prelucrarea e tot-sau-nimic.

Reparația este câte un rând pentru fiecare unitate, cu starea ei (de luat, reușit, eșuat definitiv), iar reîncercările citesc starea, nu o dată. O fereastră de valabilitate oprește reîncercările pe ce e cu adevărat mort, aici **10** zile față de cele **7** zile cât trăiesc linkurile serviciului de transfer, iar acele linkuri se raportează în continuare, nu se uită.

## Unde am greșit

Fiecare caz de mai sus a pornit de la cod care avea o justificare ce părea corectă: un comentariu care explica plafonul, o promisiune de idempotență în antet, un înveliș de reîncercare, o componentă construită exact ca să prevină defectul pe care l-a produs. Justificarea a fost tocmai lucrul care l-a oprit pe următorul cititor să verifice. Acum tratăm un comentariu care justifică o limită ca pe un semnal să măsurăm din nou, nu ca pe o dovadă. Iar când am verificat fals-pozitivele regulii de stoc, am numărat livrările recente pe produs, deși coletele acelea plecaseră când era stoc: aceeași greșeală de două ori în aceeași zi.

## Limitele metodei

Sigur la eșec înseamnă vechi dar semnalat, nu corect. Un upsert pur păstrează ultima valoare bună, deci are nevoie de o verificare de prospețime care se înroșește când valoarea nu se mai mișcă; fără ea, un cache cu upsert ascunde o pană mai mult timp decât ar ascunde-o o ștergere.

## Listă de verificare

**1. Upsert.** Niciun `DELETE` înainte de extragere; ștergerile sunt un pas separat, după ce extragerea a fost verificată.

**2. Reîncercare.** Provoci un timeout într-un test și prinzi clasa de excepție care iese de fapt.

**3. Backfill.** Filtrezi pe `IS NULL` și testezi „un om a ales altceva, apoi scriptul a mai rulat”.

**4. Instantaneu.** Păstrezi intrarea lângă valoarea derivată și le compari la fiecare citire.

**5. Întrebări despre trecut.** Le răspunzi cu evenimente sau instantanee datate, niciodată cu valoarea de azi.

**6. Plafoane.** Scrii volumul măsurat lângă fiecare limită și notezi în log când e atinsă.

**7. Unități.** Orice element care poate eșua separat primește un rând cu starea lui.

## Întrebări și răspunsuri

### De ce e periculos să ștergi și apoi să inserezi într-un tabel cache?

Ștergerea urmată de inserare scoate rândurile vechi înainte să fi sosit datele noi. Dacă API-ul extern nu răspunde la timp sau o cheie de acces a expirat, extragerea nu aduce nimic, iar istoricul a dispărut deja, deci rapoartele arată zerouri în loc de o eroare. Un upsert pur (INSERT ... ON CONFLICT DO UPDATE) actualizează doar rândurile aduse și le lasă pe celelalte la ultima valoare cunoscută, pe care o verificare de prospețime o poate semnala.

### Care este filtrul sigur pentru un script de backfill?

Un backfill selectează doar rândurile care încă n-au valoare, cu WHERE coloana IS NULL. Filtrul pe rândurile diferite de valoarea țintă pare echivalent la prima rulare, când coloana e goală, dar la fiecare rulare ulterioară suprascrie alegerile făcute de oameni în interfață. Testezi un backfill cu scenariul „un om a pus altă valoare, apoi scriptul a mai rulat”, nu doar rulându-l de două ori la rând.

### Un script idempotent poate fi rulat oricând din nou fără grijă?

Nu. Idempotent înseamnă că scriptul ajunge la aceeași stare la fiecare rulare, nu că păstrează informația. Un backfill care readuce fiecare rând la valoarea calculată de el este idempotent și totuși șterge deciziile luate de oameni între rulări. Siguranța cere ca scriptul să atingă doar rândurile fără răspuns și să raporteze, nu să șteargă, rândurile pe care acum le consideră greșite.

### Cum verific dacă o limită de paginare pierde date pe tăcute?

Scrii volumul măsurat lângă limită și notezi în log, la rulare, de fiecare dată când limita e atinsă, pentru că un plafon întoarce mai puține rânduri fără să dea eroare. Compari ce citește instrumentul cu ce ai în propria bază pentru aceeași fereastră sau păstrezi un grup de control destul de mic încât să încapă sub plafon. Cu ordonare de la nou la vechi, rândurile pierdute sunt mereu cele mai vechi din fereastră.

### De ce are nevoie fiecare element dintr-un import de un rând propriu de stare?

Un import care își amintește doar ora ultimului succes sare peste orice element care a eșuat o dată, pentru că pragul trece de el imediat ce reușesc elemente mai noi. Un rând pe element, cu o stare precum de luat, reușit sau eșuat definitiv, lasă reîncercările să citească starea, nu o dată. O fereastră de valabilitate oprește apoi reîncercările pe elementele cu adevărat moarte, care se raportează în continuare.

## Cum se citează

**Citare:** Going Stellar (2026). *Cache-uri și backfill-uri sigure: nu ștergi istoricul înainte să sosească datele noi*. GOING STELLAR S.R.L.. https://goingstellar.ro/ro/metode/cache-uri-si-backfill-uri-sigure-la-esec/

```bibtex
@misc{goingstellar2026fail,
  title  = {Cache-uri și backfill-uri sigure: nu ștergi istoricul înainte să sosească datele noi},
  author = {{Going Stellar}},
  year   = {2026},
  url    = {https://goingstellar.ro/ro/metode/cache-uri-si-backfill-uri-sigure-la-esec/},
  note   = {2026-10-11}
}
```

Licență. Se poate cita liber, cu atribuire.

## Mai există și

- Pagina în HTML: https://goingstellar.ro/ro/metode/cache-uri-si-backfill-uri-sigure-la-esec/
- Același articol în engleză: https://goingstellar.ro/methods/fail-safe-caches-and-backfills/
