fix(cacbg): четенето след ship да се дели под лимита на локалния engine - #336
Conversation
Test coverage
✅ No workspace dropped below its baseline (tolerance 0.5pp). 📈 Coverage rose by more than 1pp — run |
defc8d1 to
0d7abd8
Compare
ydimitrof
left a comment
There was a problem hiding this comment.
Чиста и добре обоснована промяна. Разделянето на четенето след 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
left a comment
There was a problem hiding this comment.
Прегледах #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); вердикт: коректно.
0d7abd8 to
92dbbaf
Compare
Поправка след измерване: и remote е с лимит 5@ydimitrof, @lyubomir-bozhinov — обръщам едно решение в този PR, защото стъпваше на непроверено число. Одобрението ви беше дадено на предишната версия, затова го изнасям изрично. Твърдях, че remote D1 позволява стандартните 500 терма и затова държах remote на една заявка. Това беше от документацията на SQLite, не измерено. Измерих го (заявка само от литерали срещу
Лимитът е и на двете места 5. Термите дори не трябва да пипат таблица — Следствието е по-голямо от самия PR. Насроченият Детерминистична грешка при парсване не се лекува с backoff. Данните през това време се качваха коректно, но бягът се маркираше червен и преиндексирането след ship не се пускаше — затова търсенето изоставаше. Тоест #335 лекуваше симптом на грешно диагностицирана причина (ретраят сам по себе си е полезен, но не беше фиксът). Промяната: махнах изключението за remote — делят се и двата пътя ( Тестът, който заковаваше „remote остава една заявка", е обърнат — сега заковава, че и remote се дели, за да не се върне предположението. Целият пакет Моля за повторен поглед — обръщането на решение след одобрение не бива да мине мълчком. |
Проверката след 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 зелени.
92dbbaf to
7277beb
Compare
Проверката след 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 никога няма да приеме.Какво прави
assertShippedCountsпак пада затворено.Object.hasOwnв проверката за пълнота, в сливането и в самия guard (inи голото индексиране минават през прототипа), а всички записи минават презsetOwn(defineProperty), защото присвояването задейства наследен сеттър - а сеттър, който преглътне записа, не оставя собствен запис,Object.entriesпрескача таблицата и тя изобщо не влиза във верификацията. Тоест зелено, при което нищо не е проверено.Проверка срещу истинска локална база
Тестове
47 зелени (10 нови). Поведенческите убиват мутантите: пак една заявка; деление и на remote; сливане наедро; мъртва група с нули; само последната група; наследени броеве - поотделно в четеца, в guard-а, през сеттър в сливането и през сеттър в summary-то.
Само за свързаните лица и общи подобрения - без нови функционалности.