Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 16728a7e83 build(deps): bump google.golang.org/grpc from 1.78.0 to 1.79.3
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.78.0 to 1.79.3.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.78.0...v1.79.3)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.79.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-19 02:14:29 +00:00
5 changed files with 15 additions and 355 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ require (
golang.org/x/sync v0.19.0
golang.org/x/sys v0.41.0
golang.org/x/tools v0.41.0
google.golang.org/grpc v1.78.0
google.golang.org/grpc v1.79.3
google.golang.org/protobuf v1.36.11
)
+12 -12
View File
@@ -166,16 +166,16 @@ github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd h1:SSo67WLS+99QE
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd/go.mod h1:JmNwisZzOvW4GfpfLvhZ+gtyKLsIiA+WC+wNKJGJaFg=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
@@ -235,8 +235,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+2 -43
View File
@@ -635,25 +635,6 @@ func (f subroot) UserSpecifiedDatasets() zfs.UserSpecifiedDatasetsSet {
}
}
// 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
}
func (f subroot) MapToLocal(fs string) (*zfs.DatasetPath, error) {
p, err := zfs.NewDatasetPath(fs)
if err != nil {
@@ -662,17 +643,11 @@ func (f subroot) MapToLocal(fs string) (*zfs.DatasetPath, error) {
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
}
func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
@@ -713,28 +688,12 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
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,
Path: a.ToString(),
IsPlaceholder: ph.IsPlaceholder,
ResumeToken: token,
}
fss = append(fss, fs)
}
if len(fss) == 0 {
getLogger(ctx).Debug("no filesystems found")
@@ -1144,4 +1103,4 @@ func doDestroySnapshots(ctx context.Context, lp *zfs.DatasetPath, snaps []*pdu.F
return &pdu.DestroySnapshotsRes{
Results: ress,
}, nil
}
}
-177
View File
@@ -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 (строки 656672):
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), так что дополнительных правок там не требуется.
-122
View File
@@ -1,122 +0,0 @@
#!/usr/bin/env bash
#
# zrepl_last_snapshot_age.sh
#
# Показывает, сколько минут назад был создан последний снапшот
# (по умолчанию с префиксом "zrepl_") на резервной (приёмной) стороне.
#
# Использование:
# ./zrepl_last_snapshot_age.sh <датасет> [<датасет2> ...]
# ./zrepl_last_snapshot_age.sh -r <датасет> # рекурсивно по всем дочерним
# ./zrepl_last_snapshot_age.sh -p zrepl_ -r chunk_9XG1J8T0/nomad
#
# Опции:
# -r рекурсивно проверять все дочерние датасеты
# -p PREFIX фильтровать снапшоты по префиксу имени (по умолчанию "zrepl_")
# -w MINUTES задать порог в минутах: если снапшот старше — exit code 1
# (удобно для мониторинга / cron)
# -q тихий режим: не печатать построчный вывод, только итог
#
# Коды возврата:
# 0 — всё ок (снапшот найден, и если задан -w, он не старше порога)
# 1 — снапшот найден, но старше порога -w
# 2 — снапшот не найден вообще (для датасета/выборки)
# 3 — ошибка использования / zfs недоступен
set -euo pipefail
PREFIX="zrepl_"
RECURSIVE=0
WARN_MINUTES=""
QUIET=0
usage() {
grep '^#' "$0" | sed -e '1d' -e 's/^# \{0,1\}//'
exit 3
}
while getopts ":rp:w:qh" opt; do
case "$opt" in
r) RECURSIVE=1 ;;
p) PREFIX="$OPTARG" ;;
w) WARN_MINUTES="$OPTARG" ;;
q) QUIET=1 ;;
h) usage ;;
*) usage ;;
esac
done
shift $((OPTIND - 1))
if [[ $# -lt 1 ]]; then
echo "Ошибка: укажите хотя бы один датасет." >&2
usage
fi
if ! command -v zfs >/dev/null 2>&1; then
echo "Ошибка: команда zfs не найдена." >&2
exit 3
fi
now_epoch=$(date +%s)
overall_newest_epoch=0
overall_newest_name=""
any_found=0
for dataset in "$@"; do
zfs_opts=(-H -p -o name,creation -t snapshot -s creation)
if [[ $RECURSIVE -eq 1 ]]; then
zfs_opts+=(-r)
else
zfs_opts+=(-d 1)
fi
# -H без заголовков, -p значения в "машинном" виде (creation -> epoch seconds)
mapfile -t lines < <(zfs list "${zfs_opts[@]}" "$dataset" 2>/dev/null | grep "@${PREFIX}" || true)
if [[ ${#lines[@]} -eq 0 ]]; then
if [[ $QUIET -eq 0 ]]; then
echo "[$dataset] снапшотов с префиксом '${PREFIX}' не найдено"
fi
continue
fi
# последняя строка — самый свежий снапшот (сортировка -s creation)
last_line="${lines[-1]}"
snap_name="${last_line%%$'\t'*}"
snap_epoch="${last_line##*$'\t'}"
age_seconds=$(( now_epoch - snap_epoch ))
age_minutes=$(( age_seconds / 60 ))
any_found=1
if (( snap_epoch > overall_newest_epoch )); then
overall_newest_epoch=$snap_epoch
overall_newest_name=$snap_name
fi
if [[ $QUIET -eq 0 ]]; then
printf "[%s] последний снапшот: %s (%d мин. назад)\n" \
"$dataset" "$snap_name" "$age_minutes"
fi
done
if [[ $any_found -eq 0 ]]; then
echo "Ни одного снапшота с префиксом '${PREFIX}' не найдено ни в одном из указанных датасетов." >&2
exit 2
fi
overall_age_minutes=$(( (now_epoch - overall_newest_epoch) / 60 ))
echo "---"
printf "Самый свежий снапшот в выборке: %s (%d мин. назад)\n" \
"$overall_newest_name" "$overall_age_minutes"
if [[ -n "$WARN_MINUTES" ]]; then
if (( overall_age_minutes > WARN_MINUTES )); then
echo "ВНИМАНИЕ: последний снапшот старше порога ${WARN_MINUTES} мин." >&2
exit 1
fi
fi
exit 0