Estimate repair storage needs live without relying on journal history

This commit is contained in:
admin committed 2026-10-18 17:07:27 +02:00
1 parent d22838f818
commit 72969605fd
3 files changed
+78 -13

No files matched your search

+59 -9
View File
@@ -178,6 +178,61 @@ printf '%s\\0' "$LOCKFILE" "$WORKDIR" "$STAGING_ROOT" "$ARCHIVE_DIR" "$MIN_FREE_
return fields[:6], fields[6:]
def snapshot_budget(file_stats, database_sizes):
total = 0
for size, count in file_stats:
if size < 0 or count < 0:
raise ValueError('Invalid file statistics')
total += size + count * 8192
for size in database_sizes:
if size < 0:
raise ValueError('Invalid database size')
total += size * 3 + 67108864
return total * 12 // 10 + 67108864
def estimate_budget():
# Same dry-run exclusions, file metadata, SQL expansion and growth allowance
# as installed lib/space.sh. No cleanup, write, service stop or journal needed.
script = r'''set -e
LIB_DIR=/usr/local/lib/app-backup
source "$1"
CONFIG_FILE=$2
load_config
build_plan
guard_workspaces
for i in "${!LABELS[@]}"; do
check_source_path "${SOURCES[i]}"
exclusions "${LABELS[i]}"
stats=$(rsync -aHAX --dry-run --stats --numeric-ids "${EXCLUDES[@]}" -- "${SOURCES[i]}/" "$STAGING_ROOT/.space-estimate/" 8>&- 9>&-)
bytes=$(awk '/^Total file size:/ {gsub(/,/, "", $4); print $4}' <<< "$stats")
count=$(awk '/^Number of files:/ {gsub(/,/, "", $4); print $4}' <<< "$stats")
[[ $bytes =~ ^[0-9]+$ && $count =~ ^[0-9]+$ ]] || exit 1
printf 'F %s %s\n' "$bytes" "$count"
done
for i in "${!DB_NAMES[@]}"; do
secure_file "${DB_CNFS[i]}"
size=$("$DB_CLIENT_BIN" --defaults-extra-file="${DB_CNFS[i]}" --database="${DB_NAMES[i]}" --batch --skip-column-names --execute='SELECT COALESCE(SUM(data_length+index_length),0) FROM information_schema.tables WHERE table_schema=DATABASE()')
[[ $size =~ ^[0-9]+$ ]] || exit 1
printf 'D %s\n' "$size"
done
'''
result = subprocess.run(['bash', '-c', script, 'estimate', str(COMMON), str(CONFIG)],
check=True, capture_output=True, text=True, pass_fds=(9,), timeout=900)
files, databases = [], []
for line in result.stdout.splitlines():
if re.fullmatch(r'F [0-9]+ [0-9]+', line):
_, size, count = line.split()
files.append((int(size), int(count)))
elif re.fullmatch(r'D [0-9]+', line):
databases.append(int(line.split()[1]))
else:
raise RuntimeError('Unexpected source estimation output')
if not files:
raise RuntimeError('No source file estimates')
return snapshot_budget(files, databases)
def free(path):
v = os.statvfs(path)
return v.f_bavail * v.f_frsize
@@ -233,15 +288,10 @@ def main():
private_dir(path)
if STAGING.stat().st_dev == archives.stat().st_dev or PRESERVED.stat().st_dev == archives.stat().st_dev:
raise SystemExit('Required separate filesystems are absent')
# The unchanged real estimator is run again below before activating config.
journal = subprocess.run(['journalctl', '-u', 'app-backup.service', '--since', '2026-10-18',
'--no-pager', '-o', 'cat'], check=True, capture_output=True, text=True)
budgets = re.findall(r'Estimated snapshot budget: ([0-9]+) bytes', journal.stdout)
if not budgets:
raise SystemExit('No observed snapshot budget; inspect before relocation')
budget = int(budgets[-1])
budget = estimate_budget()
print('Live snapshot budget: ' + str(budget) + ' bytes', flush=True)
if free(STAGING) < budget + reserve:
raise SystemExit('Staging filesystem lacks observed budget plus unchanged reserve')
raise SystemExit('Staging filesystem lacks live budget plus unchanged reserve')
original = CONFIG.read_bytes()
saved = STATE / 'app-backup.conf.before'
if not saved.exists():
@@ -265,7 +315,7 @@ def main():
budget + reserve + 10 * GIB, reserve + 2 * GIB)
json_atomic(STATE / 'relocation-plan.json', {
'staging': str(STAGING), 'active_archives': str(archives),
'preservation_root': str(PRESERVED), 'observed_snapshot_budget': budget,
'preservation_root': str(PRESERVED), 'live_snapshot_budget': budget,
'moves': [{'source': str(p), 'allocated_bytes': n} for p, n in moves]})
for src, amount in moves:
if free(PRESERVED) < amount + reserve + 2 * GIB: