Skip to content

fix(cacbg): четенето след ship да се дели под лимита на локалния engine - #336

Merged
todorkolev merged 1 commit into
mainfrom
fix/ship-readback-chunk
Sep 2, 2026
Merged

fix(cacbg): четенето след ship да се дели под лимита на локалния engine#336
todorkolev merged 1 commit into
mainfrom
fix/ship-readback-chunk

Conversation

@todorkolev

Copy link
Copy Markdown
Collaborator

Проверката след ship пита за всички качени таблици наведнъж - по един терм на таблица в едно SELECT ... UNION ALL. SQLite ограничава термите в compound SELECT, а двете D1 среди не го ограничават еднакво: remote позволява стандартните 500, локалната (workerd, зад wrangler d1 execute --local) позволява 5. Измерено, не предположено - 5 таблици отговарят, 6 връщат too many terms in compound SELECT: SQLITE_ERROR.

Ship-ът пише шест таблици. Тоест срещу локална цел четенето не се проваляше да прочете, а да се парсне: wrangler връщаше обект-грешка, нито един ред не носеше брой, guard-ът отчиташе target has no answer за всяка таблица - върху данни, които току-що бяха качени коректно - и бягът умираше преди преиндексирането зад него. Същата фалшива присъда, която #335 махна от мрежовия път, влизаща през друга врата: не изтекла заявка, а заявка, която локалният engine никога няма да приеме.

Какво прави

  • Само локалният път се дели, на групи по ≤4 (запас под измерените 5). Remote запазва единствената си заявка: лимитът е свойство на локалния engine, а remote е продукционният път - едно извикване вместо две, наполовина по-малък най-лош backoff и една консистентна снимка.
  • Всяка група ретрайва самостоятелно и от отговора ѝ се взимат само нейните таблици. Изтощена група не допринася нищо, тъй че таблиците ѝ липсват от слетия отговор и assertShippedCounts пак пада затворено.
  • Собствеността се пази и в четенето, и в писането: Object.hasOwn в проверката за пълнота, в сливането и в самия guard (in и голото индексиране минават през прототипа), а всички записи минават през setOwn (defineProperty), защото присвояването задейства наследен сеттър - а сеттър, който преглътне записа, не оставя собствен запис, Object.entries прескача таблицата и тя изобщо не влиза във верификацията. Тоест зелено, при което нищо не е проверено.

Проверка срещу истинска локална база

таблици отговорили време
преди 0/6 → фалшивото „target has no answer" 19.6с (4 напразни опита)
сега 6/6 с верните броеве 3.6с

Тестове

47 зелени (10 нови). Поведенческите убиват мутантите: пак една заявка; деление и на remote; сливане наедро; мъртва група с нули; само последната група; наследени броеве - поотделно в четеца, в guard-а, през сеттър в сливането и през сеттър в summary-то.

Само за свързаните лица и общи подобрения - без нови функционалности.

@github-actions

Copy link
Copy Markdown

Test coverage

Workspace Lines Δ Branches Δ Functions Statements
apps/etl 75.43% +1.43pp 63.52% +5.32pp 70.00% 74.11%
apps/web 91.50% +0.50pp 83.11% +0.71pp 92.00% 90.34%
packages/config 92.85% +0.05pp 72.22% +0.02pp 92.85% 89.18%
packages/db 94.67% +0.17pp 79.32% +0.02pp 87.50% 91.75%
packages/ingest 90.42% +4.12pp 85.82% +5.42pp 82.05% 88.65%
packages/shared 95.50% +0.00pp 80.83% +0.03pp 92.30% 89.56%
packages/test-support 100.00% +0.00pp 100.00% +0.00pp 100.00% 100.00%
Total (informational) 92.01% 81.94% 88.37% 90.02%

✅ No workspace dropped below its baseline (tolerance 0.5pp).

📈 Coverage rose by more than 1pp — run node scripts/check-coverage.mjs --update locally and commit coverage-baseline.json to ratchet the threshold up.

@todorkolev
todorkolev force-pushed the fix/ship-readback-chunk branch from defc8d1 to 0d7abd8 Compare August 30, 2026 14:15

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Чиста и добре обоснована промяна. Разделянето на четенето след ship на групи под лимита на локалния engine (workerd, компаунд SELECT до 5 терма) решава реалния проблем: шест таблици в един UNION ALL връщаха грешка локално и водеха до фалшива присъда „target has no answer" върху коректно изпратени данни.

Силни страни:

  • Разделянето е приложено САМО за локалния път; remote (production) пътят запазва единствената заявка и по-силната гаранция за консистентен snapshot — правилен компромис, при това документиран.
  • READBACK_MAX_TABLES = 4 оставя запас под измерените 5, така че добавяне на таблица няма мълчаливо да пресече лимита.
  • Fail-closed поведението е запазено безусловно: група, която се изчерпва, не допринася нищо, липсващите таблици се четат като „no answer" и assertShippedCounts проваля run-а. Merge-ът взима само таблиците, за които групата е питала, така че свръх-отговарящ reader не може да замърси съседна група.
  • Прехвърлянето на четене/запис към own-property (Object.hasOwn / setOwn вместо [k] = v) затваря реалните дупки от prototype pollution — покрито изчерпателно с тестове, включително swallowing/seeding setter.
  • Няма подозрителни добавки: няма нови зависимости, мрежови повиквания, промени в auth/CI/workflow файлове; execFileSync('wrangler', …) е същият както преди. Обширните коментари са технически обяснения, не инструкции към ревюира.

Тестовото покритие е образцово — chunking, merge, remote-остава-една-заявка, мъртва група, свръх-отговаряне и трите варианта на prototype pollution.

Нямам блокиращи забележки. Одобрявам.

@lyubomir-bozhinov lyubomir-bozhinov left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Прегледах #336 при HEAD 0d7abd8c.

Коректно: chunkTables с READBACK_MAX_TABLES = 4 уважава local D1 compound-SELECT cap-а (5 local / 500 remote); groups = remote ? [tables] : chunkTables(tables) — един readback remote, chunked локално. readShippedCounts merge-ва per-chunk с Object.hasOwn/setter вместо пряко присвояване → prototype-pollution hardened, а reassembly-то е пълно (без загубени/дублирани таблици между chunk-овете). Няма false-pass/false-fail в count-verification-а.

Clean. COMMENT (Triage); вердикт: коректно.

@todorkolev
todorkolev force-pushed the fix/ship-readback-chunk branch from 0d7abd8 to 92dbbaf Compare September 2, 2026 11:53
@todorkolev

Copy link
Copy Markdown
Collaborator Author

Поправка след измерване: и remote е с лимит 5

@ydimitrof, @lyubomir-bozhinov — обръщам едно решение в този PR, защото стъпваше на непроверено число. Одобрението ви беше дадено на предишната версия, затова го изнасям изрично.

Твърдях, че remote D1 позволява стандартните 500 терма и затова държах remote на една заявка. Това беше от документацията на SQLite, не измерено. Измерих го (заявка само от литерали срещу sigma-stage-green, без четене на таблици):

терми локално (workerd) remote (staging)
5 OK OK
6 too many terms in compound SELECT too many terms in compound SELECT

Лимитът е и на двете места 5. Термите дори не трябва да пипат таблица — SELECT 1 UNION ALL … гърми еднакво, тоест това е свойство на двигателя, а не на това, което изпращаме. Проверих и с най-новия wrangler (4.128.0) — същото; не е версионен или конфигурационен артефакт.

Следствието е по-голямо от самия PR. Насроченият related-persons-data workflow е червен на тази стъпка от 14.08 — деня, в който #309 добави шестата таблица (interest_link_evidence). Последният успешен бяг е от 04.08; всеки след 14.08 пада тук. А бягът от 31.08, вече с ретраите от #335, е изгорил и четирите опита върху същия отказ:

ship: could not read back row counts after 4 attempts — Command failed: wrangler d1 execute sigma-stage-green --remote ...
Error: ship verification FAILED — the target does not hold what was shipped:
  persons: shipped 14703, target has no answer
  ... (и шестте таблици)

Детерминистична грешка при парсване не се лекува с backoff. Данните през това време се качваха коректно, но бягът се маркираше червен и преиндексирането след ship не се пускаше — затова търсенето изоставаше.

Тоест #335 лекуваше симптом на грешно диагностицирана причина (ретраят сам по себе си е полезен, но не беше фиксът).

Промяната: махнах изключението за remote — делят се и двата пътя (92dbbaf). Цената за remote е едно извикване повече и отказ от четене в една снимка; нищо не пише в тези таблици по време на верификацията (единственият писач е самият ship, а workflow-ът сериализира бяговете си), а верификация, която не може да се изпълни, струва по-малко от верификация на две части.

Тестът, който заковаваше „remote остава една заявка", е обърнат — сега заковава, че и remote се дели, за да не се върне предположението. Целият пакет scripts/: 136 зелени.

Моля за повторен поглед — обръщането на решение след одобрение не бива да мине мълчком.

Проверката след ship пита за всички качени таблици наведнъж - по един терм
на таблица в едно SELECT ... UNION ALL. SQLite ограничава термите в
compound SELECT; стандартният лимит е 500, но И ДВЕТЕ D1 среди го свалят
на 5. Измерено на всяка поотделно, не предположено: пет терма отговарят,
шест връщат "too many terms in compound SELECT: SQLITE_ERROR" - локално
през wrangler d1 execute --local (workerd) и отдалечено срещу staging
база. Термите дори не е нужно да пипат таблица: SELECT 1 UNION ALL ...
гърми по същия начин, тоест лимитът е на двигателя, не свойство на това,
което изпращаме.

Ship-ът пише шест таблици. Тоест четенето не се проваляше да ПРОЧЕТЕ, а да
СЕ ПАРСНЕ: wrangler връщаше обект-грешка, нито един ред не носеше брой,
guard-ът отчиташе "target has no answer" за ВСЯКА таблица - върху данни,
които току-що бяха качени коректно - и бягът умираше преди
преиндексирането зад него. Точно фалшивата присъда, която #335 махна от
мрежовия път, влизаща през друга врата: не изтекла заявка, а заявка, която
никой от двата двигателя няма да приеме.

Затова насроченият workflow е червен на тази стъпка от деня, в който влезе
шестата таблица (#309, 14.08): всеки бяг след нея пада тук, а бягът от
31.08 - вече с ретраите от #335 - изгори и четирите опита върху същия
отказ. Детерминистична грешка при парсване не се лекува с backoff; лекува
се само с деление на заявката. Дотогава данните се качваха, но бягът се
маркираше червен и преиндексирането зад него не се пускаше.

Делят се и двата пътя. По-ранна версия на тази промяна пазеше remote на
една заявка, на предположението, че само workerd има лимита; измерването
срещу staging показа същия лимит 5 - и точно това е причината насроченият
ship да не минава верификация. Делението струва на remote едно извикване
повече и отказва четене в една снимка; нищо не пише в тези таблици по
време на верификацията (единственият писач е самият ship, а workflow-ът
сериализира бяговете си), а верификация, която не може да се изпълни,
струва по-малко от верификация на две части.

Всяка група ретрайва самостоятелно и от отговора ѝ се взимат САМО нейните
таблици, при това само собствени свойства. Изтощена група не допринася
нищо, тъй че таблиците ѝ липсват от слетия отговор и assertShippedCounts
пак пада затворено.

Собствеността се пази и в четенето, и в писането. Object.hasOwn в
проверката за пълнота, в сливането и в самия guard, защото `in` и голото
индексиране минават през прототипа. А писането минава през setOwn
(defineProperty) - и в сливането, и в двата summary-я - защото
присвояването задейства наследен сеттър: сеттър, който преглътне записа,
не оставя собствен запис, Object.entries прескача таблицата и тя изобщо не
влиза във верификацията. Тоест зелено, при което нищо не е проверено.

Проверено срещу истинска локална база: старият код връща 0/6 таблици след
4 опита (~20с), новият връща 6/6 с верните броеве за 3.6с.

10 теста; поведенческите убиват мутантите: пак една заявка, изключение за
remote, сливане наедро, мъртва група с нули, само последната група,
наследени броеве (в четеца, в guard-а, през сеттър в сливането и в
summary-то). Пакетът scripts/: 136 зелени.
@todorkolev
todorkolev force-pushed the fix/ship-readback-chunk branch from 92dbbaf to 7277beb Compare September 2, 2026 12:47
@todorkolev
todorkolev merged commit f8ab85d into main Sep 2, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants