Skip to content

SEP-1648: Ship per-upload-selection xtrabackup payload variants - #1302

Open
marcuscruz-percona wants to merge 7 commits into
mainfrom
SEP-1648
Open

SEP-1648: Ship per-upload-selection xtrabackup payload variants#1302
marcuscruz-percona wants to merge 7 commits into
mainfrom
SEP-1648

Conversation

@marcuscruz-percona

@marcuscruz-percona marcuscruz-percona commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • The xtrabackup payload carries all three upload-provider classes but a task only ever exercises the providers on its form, and the file sits two bytes under the 16 KiB Nomad dispatch limit. Provider code is now delimited with # GEN:UPLOAD-BEGIN/END markers and scripts/gen_xtrabackup_payload_variants.py emits one payload per upload selection with the unreachable providers stripped. The canonical payload is the all-providers variant, so it stays hand-edited and is never rewritten; the other seven are generated beside it. Markers cost 0 bytes at dispatch (16,382 before and after) because the executor minifies first.
  • build_backup_spec selects the variant by the set of providers on the form, so form ordering cannot change the dispatched payload, and requests boto3 only when the shipped variant actually imports it.
  • Variant filenames are owned by app/sep/apps/mysql_backups/payload_variants.py, read by both the dispatcher and the generator, so the two cannot disagree on a name. It is stdlib-only and the generator loads it by path — importing the package would stand up the FastAPI app, which a pre-commit hook must not need.
  • Drift is guarded by --check, wired as a pre-commit hook and as make regen-xtrabackup-variants[-check]. Tests cover the generator, its malformed-region handling, drift detection and repair, per-variant size, and dispatch selection.

Reclaimed margin against the 16 KiB gate (minify + gzip, the pipeline the gate applies):

Selection Size Margin
none 14,672 +1,712
rsync 15,024 +1,360
gsutil 15,218 +1,166
rsync + gsutil 15,483 +901
s3 15,654 +730
rsync + s3 15,963 +421
s3 + gsutil 16,118 +266
all three (canonical) 16,382 +2

Two acceptance criteria need amending

  • "single-provider variants measure at least 900 bytes of margin" is not achievable. s3 alone lands at 730 — S3 is the largest provider block, so an s3-only variant can only shed the other two. The other single-provider variants clear 900 comfortably.
  • The all-providers variant gains nothing, by construction. It omits no provider, so there is nothing to strip. This matters for the blocked ticket: three-way merging SEP-1672's fix onto this branch and measuring, 7 of 8 variants pass and the canonical fails at −159. Closing that last case needs a different lever, most plausibly the attribute-name mangling this ticket's Risks section already identifies (~980 bytes, and it would benefit all 19 payloads).

Three implementation notes worth a reviewer's attention

  • Variant filenames end in _payload (xtrabackup_s3_payload, not xtrabackup_payload_s3) because the size gate's pattern is (?:payload|[^/]*_payload)$. The other naming would have been silently skipped by the very gate this ticket exists to satisfy.
  • The generator refuses to write a variant that parses but still references a name nothing in it binds, or a name belonging exclusively to an omitted provider. The payloads are never executed by CI, so a region drawn too narrowly would otherwise surface mid-backup on a customer host. The unbound-name sweep needs no bookkeeping, so it also covers symbols added to a region after this PR.
  • Restore is untouched: restore/spec.py still dispatches the canonical payload for every backup type, because a restore may have to reach any provider. Only the backup path is variant-selected.

Existing stored tasks keep their recorded payload reference and the full-provider payload they point at still ships, so this reaches tasks created or updated after the upgrade only.

Tested

  • Run an XtraBackup task with Rsync upload selected; confirm the backup uploads and the dispatched payload is xtrabackup_rsync_payload
  • Run an XtraBackup task with S3 upload selected; confirm upload succeeds and boto3 is installed by the task
  • Run an XtraBackup task with Google Cloud Storage upload selected; confirm upload succeeds
  • Run an XtraBackup task with no upload configured; confirm it completes and boto3 is absent from requirements
  • Run an XtraBackup task with all three providers selected; confirm it dispatches the unchanged canonical payload

Automated: full suite green; make lint clean; all pre-commit hooks pass; the size gate passes for all eight variants; --check is idempotent on the checked-in tree; the suite no longer writes into the payload tree while it runs.

Checklist

  • New/modified functions have type hints and rST docstrings
  • New tests added for new features or bug fixes
  • All tests pass locally (make test)
  • Pre-commit hooks pass (make run-pre-commit)
  • Database migrations generated if models changed (make makemigrations) — N/A, no model changes
  • User-facing changes documented (README, inline help, UI text)
  • Configuration changes documented with examples — N/A, no configuration changes
  • Changelog fragment added under changelog.d/ if the change is user-facing (make changelog-add), or confirmed N/A

The xtrabackup payload ships all three upload-provider classes but a task
only ever exercises the providers named in its form, and the payload sits
two bytes under the 16 KiB Nomad dispatch limit. Delimit each provider's
imports, constants, class and dispatch entry with GEN:UPLOAD marker
comments and add a generator that emits one variant per selection with the
unreachable providers stripped.

The canonical payload is the all-providers variant, so it stays hand-edited
and is never rewritten; the other seven are generated beside it. Markers
cost nothing at dispatch because the executor minifies before sending.

Variant filenames end in _payload so the check-nomad-payload-size hook's
file pattern matches every one of them. The generator refuses to write a
variant that parses but still references an omitted provider's exclusive
names -- the payloads are never executed by CI, so that orphan would
otherwise only surface mid-backup on a customer host.

A --check mode guards against drift, wired as a pre-commit hook and as
make regen-xtrabackup-variants[-check].
build_backup_spec now picks the payload by the set of upload providers on
the form, so a task carries only the provider code it can reach: from 1,712
bytes of reclaimed dispatch budget for a backup with no upload configured
down to zero for one selecting all three.

Selection is keyed on the set, not the list, so form ordering cannot change
the dispatched payload. boto3 is requested only when the shipped variant
actually imports it -- asking for it otherwise makes the task install a
dependency it never loads. Mydumper and binlog requirements are untouched.
Copilot AI review requested due to automatic review settings August 6, 2026 22:08
@marcuscruz-percona marcuscruz-percona self-assigned this Aug 6, 2026
@marcuscruz-percona marcuscruz-percona added python qa in progress Someone is currently testing this PR - do not merge it labels Aug 6, 2026

Copilot AI 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.

Pull request overview

This PR introduces size-optimized dispatch payloads for the MySQL backups XtraBackup task by generating per-upload-selection payload variants, keeping the canonical all-providers payload as the single hand-edited source while stripping unreachable provider code from the other variants.

Changes:

  • Added a generator (scripts/gen_xtrabackup_payload_variants.py) that renders and validates seven generated xtrabackup_*_payload variants from the canonical xtrabackup_payload.
  • Updated XtraBackup spec building to select the dispatched payload variant based on the set of selected upload providers, and to include boto3 only when an S3-capable variant is dispatched.
  • Added tests, snapshots, Make targets, pre-commit drift checking, and a changelog fragment to guard against variant drift and ensure correct selection.

Reviewed changes

Copilot reviewed 10 out of 17 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
scripts/gen_xtrabackup_payload_variants.py New generator that strips marked provider regions, adds a banner, validates omissions, and supports --check drift detection.
app/sep/apps/mysql_backups/xtrabackup_payload Adds # GEN:UPLOAD-BEGIN/END markers around provider-specific imports, constants, classes, and dispatch entries.
app/sep/apps/mysql_backups/xtrabackup_noupload_payload New generated “no upload” payload variant emitted from the canonical source.
app/sep/apps/mysql_backups/xtrabackup_rsync_payload Generated variant for rsync-only selection.
app/sep/apps/mysql_backups/xtrabackup_s3_payload Generated variant for s3-only selection.
app/sep/apps/mysql_backups/xtrabackup_gsutil_payload Generated variant for gsutil-only selection.
app/sep/apps/mysql_backups/xtrabackup_rsync_s3_payload Generated variant for rsync+s3 selection.
app/sep/apps/mysql_backups/xtrabackup_rsync_gsutil_payload Generated variant for rsync+gsutil selection.
app/sep/apps/mysql_backups/xtrabackup_s3_gsutil_payload Generated variant for s3+gsutil selection.
app/sep/apps/mysql_backups/spec.py Selects the correct payload variant for XtraBackup and conditionally appends boto3 only for S3 selections.
tests/scripts/test_gen_xtrabackup_payload_variants.py New unit tests for generator correctness, drift guarding, validation, and size gating.
tests/app/sep/apps/mysql_backups/test_payload_variant_selection.py New tests that pin selection → payload mapping and boto3 requirements behavior.
tests/app/sep/apps/mysql_backups/test_deps.py Updates expected payload name for XtraBackup dependency test case to match variant selection.
tests/app/sep/snapshots/payload/mysql_backups__spec_path.json Snapshot update to reflect variant payload selection in the generated spec.
.pre-commit-config.yaml Adds a pre-commit hook to enforce that generated variants are in sync (--check).
Makefile Adds regen-xtrabackup-variants and regen-xtrabackup-variants-check targets.
changelog.d/SEP-1648.changed.md Documents the user-visible change in payload dispatch behavior and conditional boto3 inclusion.
Suppressed comments (6)

tests/scripts/test_gen_xtrabackup_payload_variants.py:102

  • Test-function docstring should start with an imperative verb (e.g., “Assert …”); starting with “The …” doesn’t meet the docstring mood rule applied to function docstrings.
        """The canonical payload is the all-providers variant and stays hand-edited."""

tests/scripts/test_gen_xtrabackup_payload_variants.py:170

  • Test-function docstring should start with an imperative verb; starting with “An …” doesn’t follow the docstring mood rule for functions.
        """An opened region with no END is a hard error, not a silent truncation."""

tests/scripts/test_gen_xtrabackup_payload_variants.py:175

  • Test-function docstring should start with an imperative verb; starting with “Two …” doesn’t follow the docstring mood rule for functions.
        """Two overlapping regions are rejected -- the omission would be ambiguous."""

tests/scripts/test_gen_xtrabackup_payload_variants.py:181

  • Test-function docstring should start with an imperative verb; starting with “A …” doesn’t follow the docstring mood rule for functions.
        """A stray END marker is rejected rather than ignored."""

tests/scripts/test_gen_xtrabackup_payload_variants.py:186

  • Test-function docstring should start with an imperative verb; starting with “A …” doesn’t follow the docstring mood rule for functions.
        """A marker naming a provider the generator does not know is a hard error."""

tests/scripts/test_gen_xtrabackup_payload_variants.py:191

  • Docstring first line should start with an imperative verb; starting with “A region …” doesn’t follow the docstring mood rule for function docstrings.
        """A region drawn too narrowly leaves an orphan, which must fail at build time.

Comment thread tests/scripts/test_gen_xtrabackup_payload_variants.py Outdated
Single-source the variant naming rule, widen the orphan guard, and stop the
test suite writing into the payload tree.

The naming rule had two independent implementations -- one in the generator,
one in build_backup_spec -- that had to agree or the dispatcher would name a
payload that does not exist. Both now read payload_variants.py. It is
stdlib-only and the generator loads it by path: importing the package would
stand up the FastAPI app, which a pre-commit hook must not need. The size
hook's file pattern now also fires on the rule file and the generator.

The orphan check only intersected against a hand-maintained exclusive-name
list, so a symbol added to a region later would ship stranded. validate() now
also sweeps for names nothing in the variant binds, which needs no bookkeeping.

test_canonical_is_never_rewritten called the generator against the working
tree, so a suite run rewrote the seven variants and silently repaired any
drift the --check guard exists to surface. It now runs against a tmp_path
copy, and two new tests cover drift detection and repair.

No variant shifts a byte.
@marcuscruz-percona

Copy link
Copy Markdown
Contributor Author

Pushed 2542953b8, addressing a two-axis review of this branch (repo standards + the ticket's acceptance criteria). Summary of what changed and what did not.

Correctness

The orphan guard was narrower than the hazard it names. validate() only intersected against the hand-maintained EXCLUSIVE_NAMES dict, so a symbol added to a GEN:UPLOAD region after this PR — say a GS_RETRIES constant referenced outside its region — would have been written out silently and only surfaced mid-backup on a customer host. validate() now also sweeps for names nothing in the variant binds, which needs no bookkeeping to stay correct. Both checks run; the exclusive-name list still catches stranded names something else happens to bind. All eight shipped variants pass the sweep with zero false positives, and there is now a test asserting that.

The test suite was writing into the payload tree. test_canonical_is_never_rewritten called main([]) against the working tree, so every make test run rewrote the seven variants — meaning a drifted variant would have been silently repaired by running the tests, which is precisely the failure the --check guard exists to surface. It now runs against a tmp_path copy, and two new tests cover the negative case: --check fails on a drifted variant and writes nothing, and a regeneration run repairs it.

Structure

The variant naming rule had two independent implementations — variant_name() in the generator and _xtrabackup_payload_name() in spec.py. A divergence between them dispatches a payload that does not exist. Both now read app/sep/apps/mysql_backups/payload_variants.py. It is stdlib-only and the generator loads it by path rather than importing it: mysql_backups/__init__ builds the FastAPI app, which a pre-commit hook must not have to stand up. The size hook's files: pattern now fires on the rule file and the generator too, not just on the payloads.

_XTRABACKUP_UPLOAD_PROVIDERS restated UploadProvider — it is a StrEnum with auto(), so the members already are the slugs, in declaration order. Dropped, with a contract test pinning PROVIDERS == tuple(p.value for p in UploadProvider) so a renamed or reordered enum member fails CI instead of dispatching a missing payload.

Also: dead set() on a three-item list, a redundant re-sort in selections(), sys.exitraise SystemExit to match gen_pbm_payloads.py, opaque GEN/SIZE test aliases, and missing return annotations and :return: lines.

Measurements

The margin table in the description was stale — it has been corrected against a fresh measurement through the gate's own minify+gzip pipeline. The Jira comment's numbers were the accurate ones. The floor is s3 at +730, and the canonical stays at +2.

No variant shifts a single byte through any of this: --check reports all seven unchanged.

Not changed, deliberately

restore/spec.py still dispatches the canonical payload for every backup type. A restore may have to reach any provider, so it has no upload selection to key on. Called out in the description so it does not read as an oversight.

Verification

9456 passed, 421 skipped, 0 failed (up 6 from the previous run), make lint clean, all pre-commit hooks pass, git status clean after a full suite run.

Two acceptance criteria still need a decision, both flagged in the description and in a comment on SEP-1648: AC7's "at least 900 bytes" is unachievable for s3 (measured floor 730), and the all-providers selection cannot gain anything by construction. AC8, the manual per-provider run, needs a live backup host and is outstanding.

Eleven test docstrings opened with a noun phrase or a literal, which
breaks the repo's imperative-mood docstring rule.
@marcuscruz-percona marcuscruz-percona added qa passed Tests for this PR are completed and successful. and removed qa in progress Someone is currently testing this PR - do not merge it labels Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  app/sep
  inventory.py
  app/sep/apps/mysql_backups
  payload_variants.py
  spec.py
  xtrabackup_payload 22-44, 169-176, 1697, 1757-1764, 1902-1908, 1988-1993, 2252-2264
  app/sep/apps/mysql_backups/restore
  deps.py
  app/sep/sync/syncers
  pmm.py
Project Total  

This report was generated by python-coverage-comment-action

@marcuscruz-percona

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Co-authored-by: marcuscruz-percona <272857389+marcuscruz-percona@users.noreply.github.com>

Copilot AI commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved. I merged origin/main into this branch and fixed the conflict in Makefile (keeping both the xtrabackup variant targets and lint-pipelines). Addressed in commit b84cd57.

@marcuscruz-percona

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Co-authored-by: marcuscruz-percona <272857389+marcuscruz-percona@users.noreply.github.com>

Copilot AI commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Done. I merged origin/main and resolved the conflicts in Makefile and tests/app/sep/apps/mysql_backups/test_deps.py. Addressed in commit 6010c52.

@github-actions github-actions Bot added the app:mysql_backups PR touches the mysql_backups app slice label Aug 10, 2026
@github-actions github-actions Bot added the large-diff Over 1500 changed lines, generated files discounted label Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:mysql_backups PR touches the mysql_backups app slice large-diff Over 1500 changed lines, generated files discounted python qa passed Tests for this PR are completed and successful.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants