Skip to content

fix: Fix clone historicals and inter-historical move handling of partial load profiles - #19843

Open
capistrant wants to merge 8 commits into
apache:masterfrom
capistrant:clone-historical-partial-fix
Open

fix: Fix clone historicals and inter-historical move handling of partial load profiles#19843
capistrant wants to merge 8 commits into
apache:masterfrom
capistrant:clone-historical-partial-fix

Conversation

@capistrant

@capistrant capistrant commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Description

Segment moves and Historical cloning did not have proper handling for partial load profiles. When a segment should have been being loaded partially, it was not.

This PR now makes a change to SegmentLocalCacheManager#load to identify a move from a partial load to a full load and properly respond to that by releasing all pinned holds and trying to re-write the info file. This is in place of a work-a-round I was doing in the clone code to properly handle a clone processing a partial to full by doing a drop then try again on next coordinator cycle.

Release note

Fix a partial load rule application bug for inter-historical segment balancing and historical cloning


Key changed/added classes in this PR
  • CloneHistoriclas
  • SegmentLoadQueueManager

This PR has:

  • been self-reviewed.
  • added documentation for new or modified features or behaviors.
  • a release note entry in the PR description.
  • added Javadocs for most classes and all non-trivial methods. Linked related entities via Javadoc links.
  • added or updated version, license, or notice information in licenses.yaml
  • added comments explaining the "why" and the intent of the code wherever would not be obvious for an unfamiliar reader.
  • added unit tests or modified existing tests to cover new code paths, ensuring the threshold for code coverage is met.
  • added integration tests.
  • been tested in a test Druid cluster.

@capistrant capistrant added the Bug label Jul 31, 2026
Comment on lines +216 to +220
final long fullSize = Long.parseLong(
cluster.callApi().runSql(
"SELECT \"size\" FROM sys.segments WHERE datasource = '" + dataSource + "'"
).trim()
);
final String result = cluster.callApi().runSql(
"SELECT curr_size FROM sys.servers WHERE server_type = 'historical' AND server = '" + host + "'"
).trim();
return result.isEmpty() ? 0L : Long.parseLong(result);

@FrankChen021 FrankChen021 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity Findings
P0 0
P1 1
P2 1
P3 0
Total 2

Reviewed 8 of 8 changed files.


This is an automated review by Codex GPT-5.6-Sol

loadableSegment,
targetServer,
SegmentAction.LOAD,
sourceProfile == null ? null : sourceProfile.asRequest()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Clear partial state before cloning a full load

When the source profile is null and the target already has a partial profile, this queues a plain LOAD. On a virtual-storage Historical, however, the plain SegmentLocalCacheManager.load path does not clear PartialSegmentMetadataCacheEntry's rule or rewrite its partial info file; SegmentManager also leaves the existing segment in place. The announcer then reports a plain profile, so the coordinator believes the clone is full while the old rule holds remain, and a restart re-applies and re-announces the stale partial profile. Explicitly clear the persisted partial rule for this conversion or perform a drop/reload. The added unit test only verifies the queued request, not this Historical-side transition.

*/
public PartialLoadProfile asRequest()
{
return loadedBytes == null ? this : forRequest(wrappedLoadSpec, fingerprint);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Rebase forwarded profiles onto the current load spec

wrappedLoadSpec contains the source announcement's original deep-storage delegate, and asRequest carries it forward verbatim. Both new callers start with a DataSegment fetched from the current metadata snapshot, but SegmentHolder subsequently replaces that segment's loadSpec with this stale wrapper. If a segment payload was corrected or migrated since the source loaded, clone and move requests therefore use the old location; after that object is removed, cloning fails and balancing or decommissioning moves cannot complete. Preserve the selection and fingerprint while rebuilding the wrapper with the latest metadata loadSpec as its delegate.

* Returns the clone target to a full load of {@code segment}, for when the source has stopped holding it partially.
* <p>
* A plain load request on top of the existing replica does not achieve this. A historical that receives an unwrapped
* load request for a segment it already holds under a partial-load rule keeps that rule applied: its holds go on

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.

A historical that receives an unwrapped load request for a segment it already holds under a partial-load rule keeps that rule applied

Is this a good thing, or are you working around a Historical bug here? To me it seems to make sense to have the Historical able to handle a new load request in this case, rather than requiring a drop + load.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, this was me trying to keep the change clone related only. latest branch attempts to make load() properly handle this transition in load profile, which simplifies the clone handling

{
if (targetServer.isLoadingSegment(segment)) {
if (targetServer.cancelOperation(SegmentAction.LOAD, segment)) {
loadSegmentOnTargetServer(segment, null, targetServer, params);

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.

Isn't it possible that a LOAD was in-flight to a target server that already had the segment loaded, so the cancelOperation succeeding doesn't necessarily mean the target doesn't have the segment? I think this could happen from the call to loadSegmentOnTargetServer on line 131, if the projected profile fingerprint had changed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the new load() handling is acceptable, we should no longer need to consider this. but yes you are right and I have a commit with this handled explicitly in case there are issues with the proposed changes that make this special handling not needed anymore.

@FrankChen021 FrankChen021 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity Findings
P0 0
P1 1
P2 0
P3 0
Total 1

Reviewed 11 of 11 changed files.


This is an automated review by Codex GPT-5.6-Sol

{
if (targetServer.isLoadingSegment(segment)) {
if (targetServer.cancelOperation(SegmentAction.LOAD, segment)) {
loadSegmentOnTargetServer(segment, null, targetServer, params);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Drop a served partial replica after cancelling its reload

A target can simultaneously serve an existing partial replica and have another partial reload queued—for example, profile A is loaded while profile B is waiting when the source transitions to full. Cancellation succeeds here, but the subsequent plain load runs over the existing A replica. As documented above, the ordinary virtual-storage load path does not clear the applied rule or partial info file, while its plain announcement makes the coordinator record a null profile; later clone cycles therefore consider the target full and never repair it. After cancellation, check whether the target already serves a partial replica and drop it first; only queue the immediate full load when no replica is currently served.

* the segment using the prior wrapper and re-announce the old rule until the coordinator resyncs.
*/
private void writePartialInfoFile(DataSegment segment) throws IOException
private void rewriteInfoFile(DataSegment segment) throws IOException

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not totally sure this rename was necessary. But since it is now called during a partial --> full migration I took out the partial bit. I'm ok to add back if we want

rewriteInfoFile(dataSegment);
}
catch (IOException e) {
log.warn(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as of now I've accepted the consequence that on restart the info file is not desirable as it will partial load to the prior state. the coordinator should quickly try to reconcile though

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could flip the order i think if you wanted to only release the rule holds if you updated the info file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok ya I flipped and now throw a segment load exception on fail. I convinced myself it was better to get a correct in memory state eagerly and live with sub-optimal restart behavior if the info file update failed. but that seems like it is probably short sighted

@kfaraz kfaraz 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.

minor nitpicks

Comment on lines +120 to +124
if (!targetProjectedSegments.contains(segment)
|| !Objects.equals(
fingerprintOf(sourceProfile),
fingerprintOf(targetServer.getProjectedProfile(segment))
)) {

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.

Maybe put this check in a new method shouldLoadSegmentOnTargetServer()

* <li>{@code loadedBytes} is dropped. A profile read back off a server carries the footprint that server
* realized, which belongs to that server's announcement and not to a request.</li>
* <li>The wrapper's {@link PartialLoadSpec#DELEGATE_FIELD} is replaced with {@code segment}'s load spec. The
* wrapper was built when the source server was asked to load, so it carries whatever deep-storage location

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.

Is this needed? I think in most places, we already assume that the load spec (and the payload in general) of a DataSegment is immutable.
Is that not applicable for the partial-load segments?

@capistrant capistrant Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are right. got talked into needing to be defensive by an agent review who convinced me I could be handing back a stale request. But I cannot find evidence that it could possibly be stale so it looks to me like useless behavior. will revert

* carrying no load spec, or one whose load spec is already a partial-load wrapper (an outbound request segment
* rather than the metadata view, which would otherwise nest one wrapper inside another).
*/
public PartialLoadProfile asRequestFor(DataSegment segment)

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.

Nit: rename the method to indicate that this method modifies the profile for loading on a different server. (I feel the "clone" prefix serves the "move" case too, since there too, we first clone the segment on target server and then delete it from the source server)

Suggested change
public PartialLoadProfile asRequestFor(DataSegment segment)
public PartialLoadProfile asCloneRequestFor(DataSegment segment)

Comment on lines +190 to +191
// `segment` is the metadata-resolved segment (see TierSegmentBalancer.getLoadableSegment), which is what
// asRequestFor needs to rebase the request onto the segment's current location.

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.

This comment should probably move into the javadoc of getProjectedProfile() method.

rewriteInfoFile(dataSegment);
}
catch (IOException e) {
log.warn(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could flip the order i think if you wanted to only release the rule holds if you updated the info file

continue;
}
cacheEntry.setOnUnmount(null);
if (isFullLoadRequest

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you need to check this here? if this is true, we already should have called loadPartial instead

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, after analysis, I think it is at best a waste of time. dropping

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants