Commit 3deb822
Make the stats_valid invariant hold across processes and transactions
The gate roed314#141 added was decided by a Python attribute, and only the row-level
write paths maintained the flag it read, so the invariant it advertised did not
hold: a second process kept serving what it had cached, and a replacement path
could swap new data under old caches without clearing anything.
Database-authoritative, and atomic. quick_count, quick_count_distinct and
_quick_statistic now carry
AND EXISTS (SELECT 1 FROM meta_tables WHERE name = %s AND stats_valid)
into the SELECT that reads the cached row, rather than consulting
self.table._stats_valid. That attribute is a copy taken when the table object
was built: another process's restat=False write moves the row and not the copy,
which is precisely the deployment this is for, and a rolled-back transaction
moves the copy and not the row. One statement rather than a flag query
followed by a cache query, because two statements are two snapshots and a write
can commit between them. count() on an empty query likewise answers from
meta_tables.total, a single-row metadata lookup rather than the stale copy;
with a suffix it reads the suffixed counts row instead, since the maintained
total describes the live table.
_break_stats and _restore_stats now issue their UPDATE unconditionally, through
a shared _set_stats_valid: a transition skipped because the local copy already
said so is a transition skipped on the strength of a value that may describe
neither the database nor the present. That UPDATE's row lock is also what
orders a refresh against writers, so a live refresh_stats now claims the row at
the start instead of only restoring it at the end. Overlapping work is then
serialized into one of the two acceptable outcomes -- the write is in the data
the refresh reads, or its invalidation lands after the refresh commits --
rather than a write committing mid-rebuild and having its invalidation
overwritten.
Every replacement path makes the transition, with the rename. update_from_file
(both forms), rewrite, reload, reload_all, reload_revert and the staged swaps
could all change the live data while leaving the flag true. _swap_in_tmp and
reload_final_swap now take the intended state and write it inside the
transaction that does the renames, so the flag and the relations cannot come
apart; _reload_stats_valid states the rule for reload once, since reload_all
defers its swaps to a second pass. A metafile's own stats_valid is overruled
by what the swap actually did -- the file describes the table it was exported
from, and cannot know whether these relations were rebuilt. reload_revert
invalidates, because a backup carries no validity bit of its own, and recounts
the total for the same reason.
Cache maintenance asks a different question. _record_count,
_record_count_distinct and _record_statistic chose between INSERT and
UPDATE/DELETE by calling the public lookups, which under the new gate report a
miss for a row that is physically present -- on every invalid table, which is
every table they run on. Each recorder was leaving a second row under a key
the rest of the code takes to identify at most one; the plainest case was a
write clearing the flag and then duplicating the {} total row it maintains.
They now use private physical lookups (_cached_count, _cached_count_distinct,
_cached_statistic) that ignore the flag, and _record_statistic updates in place
rather than always inserting. Exercising that branch for the first time also
turned up _record_count_distinct's update naming a column stats that has always
been called stat.
tests/test_stats_validity.py grows 30 cases: two-handle staleness for counts,
distinct counts and max/min/sum and for the total; a stale local false not
skipping an invalidation; a rolled-back restore; a threaded refresh/write race;
one per replacement path in both directions, the metafile interaction, and a
swap failing between the rename and the flag; and physical-row counts after
each recorder. Every one of them fails on 798fdd9.
Full suite: 1342 passed, 35 skipped, 1 xfailed. Ruff and the -W docs build
clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>1 parent 798fdd9 commit 3deb822
6 files changed
Lines changed: 1122 additions & 95 deletions
File tree
- psycodict
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
370 | 370 | | |
371 | 371 | | |
372 | 372 | | |
373 | | - | |
374 | | - | |
375 | | - | |
376 | | - | |
377 | | - | |
378 | | - | |
379 | | - | |
380 | | - | |
381 | | - | |
382 | | - | |
383 | | - | |
384 | | - | |
385 | | - | |
386 | | - | |
387 | | - | |
388 | | - | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
389 | 388 | | |
390 | 389 | | |
391 | 390 | | |
392 | | - | |
393 | | - | |
394 | | - | |
395 | | - | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
396 | 433 | | |
397 | 434 | | |
398 | 435 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
110 | 110 | | |
111 | 111 | | |
112 | 112 | | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
113 | 117 | | |
114 | 118 | | |
115 | 119 | | |
116 | 120 | | |
117 | 121 | | |
118 | 122 | | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
119 | 135 | | |
120 | 136 | | |
121 | 137 | | |
| |||
124 | 140 | | |
125 | 141 | | |
126 | 142 | | |
127 | | - | |
128 | | - | |
129 | | - | |
130 | | - | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
131 | 187 | | |
132 | 188 | | |
133 | 189 | | |
134 | 190 | | |
135 | 191 | | |
136 | | - | |
| 192 | + | |
137 | 193 | | |
138 | 194 | | |
139 | 195 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2268 | 2268 | | |
2269 | 2269 | | |
2270 | 2270 | | |
2271 | | - | |
| 2271 | + | |
| 2272 | + | |
| 2273 | + | |
| 2274 | + | |
| 2275 | + | |
| 2276 | + | |
| 2277 | + | |
| 2278 | + | |
| 2279 | + | |
| 2280 | + | |
| 2281 | + | |
2272 | 2282 | | |
2273 | 2283 | | |
2274 | 2284 | | |
| |||
0 commit comments