Skip to content

DAOS-19616 placement: write relocated shard to both targets - #19086

Open
liuxuezhao wants to merge 1 commit into
masterfrom
lxz/relocate_rebuild_flag
Open

liuxuezhao wants to merge 1 commit into
masterfrom
lxz/relocate_rebuild_flag

Conversation

@liuxuezhao

Copy link
Copy Markdown
Contributor

A shard which an earlier rebuild put on a spare can be handed a different spare by a new, unrelated failure: obj_remap_shards() walks one shared spare sequence for the whole object and requeues a shard whose candidate is unavailable, so everybody behind it shifts. The CURRENT layout then names a target which was never written to, while the data is on the old one, which the write path stops using. The PRE_REBUILD layout, used by the migration fetch and every read only client, keeps reading the old target and goes stale, and a read-write client is offered the empty target unless the remap happened to flag it (determine_valid_spares() only ORs in the flags of the rejected candidates, so the rebuilding flag is set arbitrarily).

Write the relocated shard to both targets, the way a drain is handled, instead of hiding it from the readers:
layout_keep_relocated_source() compares the CURRENT layout with the PRE_REBUILD one, puts the old, still serving target back as the readable primary, and appends the new target as a write-only peer through pl_map_extend(). Every write then lands on both the old and the new target while the migration copies the old one to the new one, so neither reader nor migration ever sees an empty target. When a drain, reintegration or extension is in flight as well, the layout is extended from the diff against POST_REBUILD anyway, which names the target the migration really goes to, so only the primary is put back and the diff supplies the peer. A shard whose old target is down is flagged rebuilding when the remap missed it, and a spurious rebuilding flag on an unchanged spare is dropped.

The read only path computes nothing extra. The read-write path computes one extra PRE_REBUILD layout only when the placement asked about a DOWN, DRAIN or UP target (mode_dependent); otherwise the two modes provably agree. No further "on spare" gate is applied: during extension, NEW domains count towards the object class limit but are excluded from the initial placement, so a group wider than the old domain set reuses a domain and a fresh failure can change the initial target selection through reset_dom_cur_grp_v1() without any shard sitting on a historical spare. Such a gate dropped a healthy PRE_REBUILD read source from the write set (RP_5G1, layout v1).

Also included: pl_map_extend() propagates the peer allocation failure instead of overwriting it with 0; the redundant min() of two layout sizes asserted equal is removed; and the migration comment on a fetch returning iod_size 0 now describes the real causes (placement mismatch, older fetch epoch, unresolved DTX) and why -DER_DATA_LOSS is excluded from the rebuild status.

Placement UT additions: no_stale_read_source, no_hidden_peer_source, extension_preserves_read_sources (RP_5G1/RP_5G2/EC_4P1G1/EC_4P1G2, one/two groups, layout v1/v2, NEW and new-UP extension) and relocation_peer_alloc_failure (fault injection). They assert that the PRE_REBUILD read sources stay in the CURRENT write set, that every readable CURRENT entry was written to, that no target is readable twice in a group, and that every migration destination is written to.

To build and run the reproducer from the repository root:

scons --config=force --jobs=16
install/bin/jump_pl_map install/lib64/libdaos.so
install/lib64/libdaos_common.so install/lib64/libgurt.so
./utils/run_utest.py --suite_filter='^placement$' --sudo=no

With CMocka filtering enabled, a single case can be selected, e.g. install/bin/jump_pl_map -f extension_preserves_read_sources; without it the binary runs the whole placement group. With the tests but without the fix, no_stale_read_source, no_hidden_peer_source and extension_preserves_read_sources fail on the first check (a PRE_REBUILD read source the CURRENT layout does not write to); with the fix all 43 placement cases pass. No cluster or VOS I/O is needed.

Steps for the author:

  • Commit message follows the guidelines.
  • Appropriate Features or Test-tag pragmas were used.
  • Appropriate Functional Test Stages were run.
  • At least two positive code reviews including at least one code owner from each category referenced in the PR.
  • Testing is complete. If necessary, forced-landing label added and a reason added in a comment.

After all prior steps are complete:

  • Gatekeeper requested (daos-gatekeeper added as a reviewer).

A shard which an earlier rebuild put on a spare can be handed a
different spare by a new, unrelated failure: obj_remap_shards() walks
one shared spare sequence for the whole object and requeues a shard
whose candidate is unavailable, so everybody behind it shifts. The
CURRENT layout then names a target which was never written to, while
the data is on the old one, which the write path stops using. The
PRE_REBUILD layout, used by the migration fetch and every read only
client, keeps reading the old target and goes stale, and a read-write
client is offered the empty target unless the remap happened to flag
it (determine_valid_spares() only ORs in the flags of the rejected
candidates, so the rebuilding flag is set arbitrarily).

Write the relocated shard to both targets, the way a drain is
handled, instead of hiding it from the readers:
layout_keep_relocated_source() compares the CURRENT layout with the
PRE_REBUILD one, puts the old, still serving target back as the
readable primary, and appends the new target as a write-only peer
through pl_map_extend(). Every write then lands on both the old and
the new target while the migration copies the old one to the new one,
so neither reader nor migration ever sees an empty target. When
a drain, reintegration or extension is in flight as well, the layout
is extended from the diff against POST_REBUILD anyway, which names the
target the migration really goes to, so only the primary is put back
and the diff supplies the peer. A shard whose old target is down is
flagged rebuilding when the remap missed it, and a spurious rebuilding
flag on an unchanged spare is dropped.

The read only path computes nothing extra. The read-write path
computes one extra PRE_REBUILD layout only when the placement asked
about a DOWN, DRAIN or UP target (mode_dependent); otherwise the two
modes provably agree. No further "on spare" gate is applied: during
extension, NEW domains count towards the object class limit but are
excluded from the initial placement, so a group wider than the old
domain set reuses a domain and a fresh failure can change the initial
target selection through reset_dom_cur_grp_v1() without any shard
sitting on a historical spare. Such a gate dropped a healthy
PRE_REBUILD read source from the write set (RP_5G1, layout v1).

Also included: pl_map_extend() propagates the peer allocation failure
instead of overwriting it with 0; the redundant min() of two layout
sizes asserted equal is removed; and the migration comment on a fetch
returning iod_size 0 now describes the real causes (placement
mismatch, older fetch epoch, unresolved DTX) and why -DER_DATA_LOSS is
excluded from the rebuild status.

Placement UT additions: no_stale_read_source, no_hidden_peer_source,
extension_preserves_read_sources (RP_5G1/RP_5G2/EC_4P1G1/EC_4P1G2,
one/two groups, layout v1/v2, NEW and new-UP extension) and
relocation_peer_alloc_failure (fault injection). They assert that the
PRE_REBUILD read sources stay in the CURRENT write set, that every
readable CURRENT entry was written to, that no target is readable
twice in a group, and that every migration destination is written to.

To build and run the reproducer from the repository root:

  scons --config=force --jobs=16 \
    install/bin/jump_pl_map install/lib64/libdaos.so \
    install/lib64/libdaos_common.so install/lib64/libgurt.so
  ./utils/run_utest.py --suite_filter='^placement$' --sudo=no

With CMocka filtering enabled, a single case can be selected, e.g.
install/bin/jump_pl_map -f extension_preserves_read_sources; without
it the binary runs the whole placement group. With the tests but
without the fix, no_stale_read_source, no_hidden_peer_source and
extension_preserves_read_sources fail on the first check (a
PRE_REBUILD read source the CURRENT layout does not write to); with
the fix all 43 placement cases pass. No cluster or VOS I/O is needed.

Signed-off-by: Xuezhao Liu <xuezhao.liu@hpe.com>
@liuxuezhao
liuxuezhao requested review from a team as code owners September 16, 2026 07:57
@github-actions

Copy link
Copy Markdown

Ticket title is 'FIO crc mismatch detected on 4x node IB config while running HSN cable pulls test'
Status is 'Open'
Job should run at elevated priority (1)
https://daosio.atlassian.net/browse/DAOS-19616

@github-actions github-actions Bot added the priority Ticket has high priority (automatically managed) label Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority Ticket has high priority (automatically managed)

Development

Successfully merging this pull request may close these issues.

1 participant