Skip to content

A measured zero incremental is indistinguishable from never-measured — omitempty erases it on the API status fields #10297

Description

@kaovilai

> [!Note]
> Responses generated with Claude

Not Ceph-specific, and wider than the block data mover — the same erasure affects PodVolumeBackup and the filesystem/kopia path.

Summary

A measured incremental size of zero is indistinguishable from never measured. omitempty on the API status fields drops it, so a perfect incremental — nothing changed, nothing transferred — reports the same as a backup from a version or data mover that never recorded the figure.

velero backup describe --details for a zero-delta run:

Data Movement:
  Data Mover: velero-block
  Uploader Type: kopia
  Moved data Size (bytes): 3221225472
  Result: succeeded

Moved data Size is the device size, not the transferred volume. With no incremental line, the best possible CBT outcome displays as if the whole device moved.

To be precise about what is not ambiguous: a genuine full transfer and a whole-device CBT fallback both set a non-zero IncrementalSize and do print Incremental data Size (bytes): N (guard at pkg/cmd/util/output/backup_describer.go:742). Those are distinguishable. The collision is specifically measured-zero vs never-measured — the latter covering backups predating the field, plugin data movers, and any uploader that does not report.

This diverges from agreed intent rather than raising a new opinion: on PR #9357 the reviewer states the expected output for an unchanged backup is INCREMENTAL BYTES: 0.

velero backup describe -o json has the mirror-image defect: backup_structured_describer.go:470-472 gates on Size > 0 || IncrementalSize > 0, so it emits "incrementalSize": 0 — always present, ambiguous in the opposite direction.

The DataUpload printcolumn (pkg/apis/velero/v2alpha1/data_upload_types.go:202) has the same problem: a completed zero-delta DataUpload renders identically to one still running.

Where the zero is lost

pkg/apis/velero/v2alpha1/data_upload_types.go:168-170 and pkg/apis/velero/v1/pod_volume_backup_types.go:121-123 are int64 with omitempty, so a measured 0 is dropped from the serialized status.

pkg/datapath/types.go:37 (the wire struct crossing the data-mover-pod → controller boundary) also matters here, but not as originally described: a plain non-pointer int64 with omitempty is lossless for zero only within a single mover version — omitted on marshal, reconstituted as 0 on unmarshal. It is not lossless across mover versions: a data mover from release-1.17 or earlier predates this field entirely and never writes the key, and a non-pointer field can't distinguish "key absent because this mover doesn't know about it" from "key absent because it happened to omit a zero value." Both collapse to the same reconstituted 0, reintroducing the exact measured-zero-vs-never-measured ambiguity this issue is about, just one hop earlier and for a different reason (mover version, not display formatting). This was caught in review on the fix PR: #10309 (comment), resolved at #10309 (comment).

pkg/uploader/block/snapshot.go:86 produces the right value, pkg/controller/data_upload_controller.go:515 and pkg/controller/pod_volume_backup_controller.go:553 persist it, and pkg/backup/backup.go:1336 copies it into volumeInfo.json.

That the sibling Size field at internal/volume/volumes_information.go:170 carries no omitempty suggests the tag was incidental rather than deliberate.

Why it matters

The design calls for this figure explicitly — "By the end of the backup, incremental size is also returned by the uploader… The size indicates how much data are unique so processed by the uploader". Erasing a measured zero defeats that for exactly the case where CBT performed best.

Suggested fix

  1. pkg/apis/velero/v2alpha1/data_upload_types.go:168-170 and pkg/apis/velero/v1/pod_volume_backup_types.go:121-123 — *int64. A plain omitempty drop is not enough: the field shipped in v1.18.0–v1.18.2, so backups exist whose stored volume info has no incrementalSize, and those would unmarshal to 0 and falsely render a perfect incremental. Both fields already carry +optional, so the generated CRD schema is unchanged and no regeneration is required.
  2. pkg/datapath/types.go:37 — also *int64 with omitempty restored (not a plain non-pointer int64), for the mover-version reason above: an old mover's omitted key unmarshals to nil ("not measured"), a current mover's genuine zero still serializes the key and unmarshals to a non-nil pointer to 0 ("measured zero"), nonzero values are unaffected, and an old controller can still unmarshal a numeric value from a new mover. pkg/controller/data_upload_controller.go:515 and pod_volume_backup_controller.go:553 then assign this field directly to their already-*int64,omitempty CRD status field instead of wrapping it — both sides are the same pointer type.
  3. internal/volume/volumes_information.go:178 and :228 — propagate as *int64.
  4. pkg/cmd/util/output/backup_describer.go:742 and :928 — relax the guards from > 0 to != nil.

Verified with that change: the same zero-delta scenario reports 0 rather than absent and renders Incremental data Size (bytes): 0, while an older backup described with the new client still correctly prints no incremental line.

Related

The display-guard half overlaps #9833 (Backup/restore describe for block data mover) and the two are best implemented together. #9833 also carries the design's other two unimplemented reporting items — the actual backup type in volumeInfo.json (L465-466) and the actual data mover in the header (L468).

Reproduce

Run any successful CBT incremental, then immediately run a second incremental with no intervening writes. .status.incrementalBytes will be absent and velero backup describe --details will show the full device size with no incremental line.

Environment

velero main @ 293f6f6a6; still reproduces on upstream/main @ ae6f40c28. ODF 4.21.10, ceph-csi RBD.

Found during #9714 (Ceph CBT) validation; driver-independent and not limited to the block data mover.

Activity

  1. changed the title [-]A perfect (zero-delta) incremental is unrepresentable — status field erased by omitempty renders identically to a full transfer[/-] [+]A measured zero incremental is indistinguishable from never-measured — omitempty erases it on two hops[/+] on Aug 16, 2026
  2. self-assigned this
    on Aug 16, 2026
  3. kaovilai commented on Aug 16, 2026

    @kaovilai
    MemberAuthor

    Note

    Responses generated with Claude

    Cross-reference: the two design reporting items originally noted in this issue's "Related, tracked separately" section are already covered by #9833 (Backup/restore describe for block data mover) — design L465-466 (actual backup type in volumeInfo.json, so a fallback is visible) and L468 (the DataMover header reflecting the actual mover). I've added them there with the observed Data Mover: velero-fs header contradiction rather than opening a separate issue.

    This issue remains distinct from #9833: the erasure happens before describe ever sees the value — omitempty on pkg/datapath/types.go:37 destroys it crossing the data-mover-pod → controller JSON boundary, and again on the API status fields. It also affects PodVolumeBackup and the filesystem/kopia path, not just the block data mover.

    The display-gate half of the fix (pkg/cmd/util/output/backup_describer.go:742 and :928) does overlap #9833's territory, so the two are probably best implemented together.

  4. changed the title [-]A measured zero incremental is indistinguishable from never-measured — omitempty erases it on two hops[/-] [+]A measured zero incremental is indistinguishable from never-measured — omitempty erases it on the API status fields[/+] on Aug 16, 2026
  5. Lyndon-Li commented on Aug 17, 2026

    @Lyndon-Li
    Contributor

    I also noticed this, I think it is a valid enhancement request.

  6. added this to the v1.19 milestone on Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions