@@ -45,25 +45,41 @@ Recording (first N loads after startup):
4545
4646``` python
4747class CacheWarmer :
48- def __init__ (self , conn , warm_pct = 10 , decay = 0.8 ):
48+ def __init__ (self , conn , warm_pct = 10 , decay = 0.8 , flush_interval = 1000 ):
4949 self .recording = True
50+ self ._warming_done = False
5051 self ._recorded = set ()
51- self ._target_count = 0 # set from cache size * warm_pct / 100
52- self ._warm_cache = {} # zoid → (pickle_bytes, tid_bytes)
52+ self ._pending = set () # unflushed OIDs
53+ self ._target_count = 0 # set from cache size * warm_pct / 100
54+ self ._flush_interval = flush_interval
55+ self ._decayed = False # decay applied once per startup
56+ self ._warm_cache = {} # zoid → (pickle_bytes, tid_bytes)
5357 self ._decay = decay
54- self ._conn = conn # main storage PG connection
58+ self ._conn = conn # main storage PG connection
5559
5660 def record (self , zoid ):
57- """ Called from Instance.load() during recording phase."""
61+ """ Called from Instance.load() during recording phase.
62+
63+ Flushes to PG every _flush_interval OIDs to limit data loss
64+ if the pod is killed. Decay is applied only on the first flush.
65+ """
5866 if zoid in self ._recorded:
5967 return
6068 self ._recorded.add(zoid)
69+ self ._pending.add(zoid)
70+ if len (self ._pending) >= self ._flush_interval:
71+ self ._flush(decay = not self ._decayed)
72+ self ._decayed = True
6173 if len (self ._recorded) >= self ._target_count:
6274 self .recording = False
63- self ._persist()
75+ if self ._pending:
76+ self ._flush(decay = not self ._decayed)
77+
78+ def _flush (self , decay = False ):
79+ """ Write pending OIDs to cache_warm_stats. Called every N OIDs."""
6480
6581 def _persist (self ):
66- """ Decay old scores + insert new OIDs. Runs once per startup ."""
82+ """ Deprecated — replaced by incremental _flush() ."""
6783
6884 def warm (self , load_multiple_fn ):
6985 """ Load top-N ZOIDs into _warm_cache. Runs in background thread.
@@ -104,23 +120,34 @@ CREATE TABLE IF NOT EXISTS cache_warm_stats (
104120);
105121```
106122
107- ### Persist algorithm
123+ ### Flush algorithm (incremental persist)
108124
109- ``` sql
110- -- 1. Decay all existing scores
111- UPDATE cache_warm_stats SET score = score * %(decay)s;
125+ Recording flushes every ` _flush_interval ` OIDs (default 1000).
126+ At 7000 target that's ~ 7 flushes. Max data loss on pod kill: 1000 OIDs.
112127
113- -- 2. Upsert recorded ZOIDs
114- INSERT INTO cache_warm_stats (zoid, score)
115- VALUES (unnest(%(zoids)s::bigint []), 1 .0 )
116- ON CONFLICT (zoid) DO UPDATE SET score = cache_warm_stats .score + 1 .0 ;
117-
118- -- 3. Prune negligible scores
119- DELETE FROM cache_warm_stats WHERE score < 0 .01 ;
128+ ``` python
129+ def _flush (self , decay = False ):
130+ """ Write pending OIDs to PG. Decay only on first flush per startup."""
131+ zoids = list (self ._pending)
132+ self ._pending.clear()
133+ with self ._conn.cursor() as cur:
134+ if decay:
135+ cur.execute(
136+ " UPDATE cache_warm_stats SET score = score * %(d)s " ,
137+ {" d" : self ._decay},
138+ )
139+ cur.execute(
140+ " INSERT INTO cache_warm_stats (zoid, score) "
141+ " VALUES (unnest(%(z)s ::bigint[]), 1.0) "
142+ " ON CONFLICT (zoid) DO UPDATE "
143+ " SET score = cache_warm_stats.score + 1.0" ,
144+ {" z" : zoids},
145+ )
146+ if decay:
147+ cur.execute(" DELETE FROM cache_warm_stats WHERE score < 0.01" )
148+ self ._conn.commit()
120149```
121150
122- All three in one transaction — fast, ~ 7000 rows.
123-
124151### Warm algorithm
125152
126153``` sql
0 commit comments