From 4fa96f01f84cdf36c35066bb69242335d529d0cd Mon Sep 17 00:00:00 2001 From: adminer Date: Mon, 27 Jul 2026 08:28:43 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B8=D1=82=D1=8C=20my?= =?UTF-8?q?/scripts/PATCH=5FCLODE.txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- my/scripts/PATCH_CLODE.txt | 177 ------------------------------------- 1 file changed, 177 deletions(-) delete mode 100644 my/scripts/PATCH_CLODE.txt diff --git a/my/scripts/PATCH_CLODE.txt b/my/scripts/PATCH_CLODE.txt deleted file mode 100644 index 40eb35b..0000000 --- a/my/scripts/PATCH_CLODE.txt +++ /dev/null @@ -1,177 +0,0 @@ -https://claude.ai/chat/f688c411-b11e-4455-918b-fde5e6577430 - -Diagnosed replication snapshot duplication caused by version listing failures -Diagnosed replication snapshot duplication caused by version listing failures - -Отличная зацепка — снапшот chunk_9XG1J8T0/nomad/ha-zah@zrepl_20260724_054520_000 реально существует (0B allocated, 195M refer — то есть данные там есть), но планировщик каждый цикл продолжает считать, что на приёмнике версий нет вообще. Значит проблема не в наличии данных, а в том, что ListFilesystemVersions (то, чем сверяется планировщик) их почему-то не видит. Погружусь в код листинга снапшотов на ресивере. - -Ran 3 commands, viewed 3 files -Ran 3 commands, viewed 3 files - -Кажется, нашёл настоящий баг — и это очень похоже на правду. Проверю до конца, как формируется список receiverFS/IsPlaceholder, которым пользуется планировщик (не тот же код, что читает placeholder при самом receive). - -Viewed 5 files, ran 4 commands -Viewed 5 files, ran 4 commands - -Нашёл настоящую причину, и она серьёзнее, чем казалось — это баг именно от моего патча, а не от zrepl самого по себе. Дело в сопоставлении списков sender/receiver в планировщике. - -Проверяю, есть ли envconst.String для строкового значения -Проверяю, есть ли envconst.String для строкового значения -Вот и реальный баг — и он от патча - -Планировщик (replication_logic.go:302-306) сопоставляет sender и receiver filesystem'ы строковым сравнением путей: - -go -for _, rfs := range rfss { - if rfs.Path == fs.Path { // fs.Path — путь СЕНДЕРА, напр. "hdd_9XG1J9J1/nomad/ha-zah" - receiverFS = rfs - } -} - -А rfs.Path формируется в Receiver.ListFilesystems (endpoint.go:695) как a.TrimPrefix(root) — локальный путь chunk_9XG1J8T0/nomad/ha-zah минус root_fs = nomad/ha-zah. - -До патча это совпадало с sender-путём один-в-один (root_fs + sender_path → обрезаем root_fs → получаем обратно sender_path). А теперь, когда MapToLocal откусывает ещё и hdd_9XG1J9J1, обратная обрезка даёts nomad/ha-zah — а сендер сообщает hdd_9XG1J9J1/nomad/ha-zah. Строки не совпадают → receiverFS всегда nil → планировщик каждый раз считает, что на приёмнике версий нет → пытается full receive → ловит "destination exists". Именно поэтому цикл повторяется бесконечно. - -Чиню — добавляю обратное восстановление отрезанного префикса именно в отчёт ListFilesystems, чтобы сопоставление путей снова работало: - -Architected shared helper function to consolidate prefix-handling logic -Architected shared helper function to consolidate prefix-handling logic - -Есть всё нужное. Переписываю патч целиком на литеральный строковый префикс — это устраняет и проблему с расчётом пути, и делает обратное сопоставление в ListFilesystems возможным: - -Reconciled edge case behavior and prepared filesystem prefix restoration -Reconciled edge case behavior and prepared filesystem prefix restoration -Смотрю текущее состояние MapToLocal перед переработкой -Переключаю strip с "N компонентов" на литеральный строковый префикс, чтобы его можно было симметрично восстановить в ListFilesystems для сопоставления путей с sender'ом -endpoint.go -+35 --18 - -Reconciled edge case behavior and prepared filesystem prefix restoration. - -Done - -Теперь патчу ListFilesystems, чтобы при формировании отчётного пути (используемого для сверки с sender) обрезанный префикс восстанавливался обратно: - -Восстанавливаю отрезанный sender-префикс в пути, который ListFilesystems возвращает планировщику — иначе receiver Path не совпадает с sender Path и планировщик считает receiver пустым на каждом цикле -Восстанавливаю отрезанный sender-префикс в пути, который ListFilesystems возвращает планировщику — иначе receiver Path не совпадает с sender Path и планировщик считает receiver пустым на каждом цикле - -Проверяю синтаксис и то, что старая переменная ZREPL_STRIP_SENDER_PREFIX_COMPS больше нигде не используется: - -Проверяю синтаксис и итоговое использование переменных окружения -Проверяю синтаксис и итоговое использование переменных окружения - -Синтаксис чистый, старой переменной нигде не осталось. Собираю (насколько позволяет toolchain в песочнице): - -Ran 2 commands -Ran 2 commands - -Отлично, всё корректно, включая граничный случай равных путей. Патч готов. Итог: - -Что было не так - -Планировщик сопоставляет filesystem'ы sender'а и receiver'а строковым равенством путей (replication_logic.go:303: rfs.Path == fs.Path). До патча receiver.Path (локальный путь минус root_fs) всегда совпадал с sender.Path один в один. Мой первый патч ломал этот инвариант: обрезав ещё и hdd_9XG1J9J1 при записи, но не восстанавливая его при отчёте — receiver стал сообщать "nomad/ha-zah", а sender — "hdd_9XG1J9J1/nomad/ha-zah". Строки никогда не совпадали → receiverFS всегда nil → планировщик каждый цикл считал, что на приёмнике ничего нет → пытался полный recv в уже занятый датасет → ошибка. Бесконечный цикл был не багом zrepl, а прямым следствием несимметричности моего же патча. - -Что изменилось -Заменил обрезку "N компонентов" на обрезку литерального строкового префикса — это делает операцию обратимой. -В MapToLocal префикс отрезается при вычислении локального пути хранения. -В ListFilesystems тот же префикс приклеивается обратно к пути, который сообщается планировщику — так путь снова совпадает с тем, что говорит sender, и сопоставление работает. -Как применить - -Пересоберите бинарник, затем поменяйте имя переменной в systemd-юните (старое ZREPL_STRIP_SENDER_PREFIX_COMPS=1 → новое, с явным значением префикса): - -ini -Environment=ZREPL_STRIP_SENDER_PREFIX=hdd_9XG1J9J1 -bash -systemctl daemon-reload -systemctl restart zrepl_rcv - -Датасеты, скорее всего, снова придётся снести (zfs destroy -r chunk_9XG1J8T0/nomad) и дать реплицироваться с нуля, раз в прошлый раз всё было в неконсистентном состоянии из-за бага. После рестарта проверьте: - -bash -journalctl -u zrepl_rcv -n 100 --no-pager | grep -iE "conflict|receive failed|error" - -Ключевой признак, что теперь всё работает правильно: строка "conflict automatically resolved" с пустым sorted receiver versions должна появиться только один раз (при первой инициализации датасета), а не повторяться на каждом цикле после успешного приёма. - -а где патч? - -Показываю итоговый diff патча -Показываю итоговый diff патча - -дай исправленные функции - -Показываю итоговый код исправленных функций целиком -Показываю итоговый код исправленных функций целиком - -Вот три исправленные функции целиком (файл internal/endpoint/endpoint.go): - -1. Новая вспомогательная функция (строки 638–654): - -go -// senderPrefixDatasetPath returns the configured literal sender-side prefix -// (e.g. "hdd_9XG1J9J1") to strip from incoming filesystem paths, or nil if -// ZREPL_STRIP_SENDER_PREFIX is unset/empty. Using a literal string (rather -// than a component count) lets us losslessly reconstruct the sender's -// original path later in ListFilesystems, which is required for the -// sender/receiver filesystem-matching in the replication planner to work. -func senderPrefixDatasetPath() *zfs.DatasetPath { - s := envconst.String("ZREPL_STRIP_SENDER_PREFIX", "") - if s == "" { - return nil - } - dp, err := zfs.NewDatasetPath(s) - if err != nil || dp.Length() == 0 { - return nil - } - return dp -} - -2. MapToLocal (строки 656–672): - -go -func (f subroot) MapToLocal(fs string) (*zfs.DatasetPath, error) { - p, err := zfs.NewDatasetPath(fs) - if err != nil { - return nil, err - } - if p.Length() == 0 { - return nil, errors.Errorf("cannot map empty filesystem") - } - // PATCH: strip a configured literal prefix (e.g. the source zpool name) - // from the *sender's* path before appending it under root_fs. - if prefix := senderPrefixDatasetPath(); prefix != nil && p.HasPrefix(prefix) { - p.TrimPrefix(prefix) - } - c := f.localRoot.Copy() - c.Extend(p) - return c, nil -} - -3. Кусок ListFilesystems, где формируется Path (строки 712–734, остальная часть функции без изменений): - -go - a.TrimPrefix(root) - - // PATCH: re-prepend the literal sender prefix that MapToLocal strips, - // so that the Path we report here matches what the sender reports - // for the same filesystem (see replication_logic.go's Path-based - // sender/receiver matching). Without this, the planner can never - // find a matching receiverFS and will treat every cycle as if the - // filesystem doesn't exist yet on the receiver. - reportPath := a.ToString() - if prefix := senderPrefixDatasetPath(); prefix != nil { - if reportPath == "" { - reportPath = prefix.ToString() - } else { - reportPath = prefix.ToString() + "/" + reportPath - } - } - - fs := &pdu.Filesystem{ - Path: reportPath, - IsPlaceholder: ph.IsPlaceholder, - ResumeToken: token, - } - fss = append(fss, fs) - -Больше в файле ничего не менялось — MapToLocal вызывается ещё и из Receive/ListFilesystemVersions/DestroySnapshots, но там аргументом всегда приходит исходный путь сендера (req.Filesystem), так что дополнительных правок там не требуется. \ No newline at end of file