Is your feature request related to a problem? Please describe
The offline _split API (POST /<index>/_split/<target-index>, ResizeType.SPLIT) performs no disk-space validation at request time. A split can be accepted when the node hosting the parent primary does not have enough free space to complete it; the failure only surfaces later as target shards stuck UNASSIGNED, discoverable solely via _cluster/allocation/explain.
How much space a split actually needs
Let S = the parent shard's primary store size. Splitting that shard requires at least S of additional free space on the node hosting the parent primary — i.e. room for roughly one more full copy of the shard. This floor holds regardless of how many child shards the parent is split into:
StoreRecovery.addIndices (server/src/main/java/org/opensearch/index/shard/StoreRecovery.java:220-274) wraps the target directory in Lucene's HardlinkCopyDirectoryWrapper (:237), so writer.addIndexes(sources) hardlinks the parent's segment files into each child. In the common case this adds ~0 physical bytes, since hardlinks share inodes.
writer.deleteDocuments(new ShardSplittingQuery(...)) (:257-259) is a hard delete that only writes per-segment .liv live-docs bitsets (~maxDoc/8 bytes). It frees nothing.
- The subsequent merge —
indexShard.getIndexer().forceMerge(false, -1, ...) (:206), which resolves to IndexWriter.maybeMerge() (InternalEngine.java:1701-1703) — rewrites each child's live documents into new, unshared segment files and only then drops that child's hardlinks. Summed across the children this is ~S of newly allocated physical bytes, on top of the parent, which still occupies its own S until the source index is deleted.
Two consequences worth calling out:
- Because hardlinks are refcounted at the inode level, deleting the source index frees nothing while any child still links its segment files.
- The footprint therefore grows after the split reports complete, during step 3 — past the point where any allocation-time disk check is still looking.
Why the existing disk accounting doesn't cover this
DiskThresholdDecider.getExpectedShardSize (.../decider/DiskThresholdDecider.java:645-681) does correctly estimate each split child at the full parent size S (via IndexMetadata.selectRecoverFromShards, which returns a singleton for split), so the per-child canAllocate check is reasonable in isolation.
- However
DiskThresholdDecider.sizeOfRelocatingShards (:130-137) explicitly skips these shards from its accounting. Split children are INITIALIZING with relocatingNodeId == null (ShardRouting.initialize, ShardRouting.java:544-564), so they hit continue under the comment: "the only initializing-but-not-relocating shards with a nonzero expected shard size will be ones created by a resize (shrink/split/clone) operation which we expect to happen using hard links, so they shouldn't be taking any additional space and can be ignored here."
- The
ClusterInfo.ReservedSpace backstop does not fire either. Hardlinked files are recorded as reused — StoreRecovery.java:345: index.addFileDetail(dest, l, true); // hardlinked - we treat it as reused — and reused files are excluded from bytesStillToRecover(). Reserved space therefore reads as ~0 (or UNKNOWN, since setFileDetailsComplete() is only called after addIndexes returns, :255-256).
- There is no request-time check at all.
MetadataCreateIndexService.validateSplitIndex (.../metadata/MetadataCreateIndexService.java:1890-1893) validates only routing-shard arithmetic and the write block. TransportResizeAction (.../shrink/TransportResizeAction.java:145-231) does fetch store stats, but consumes them only for shrink's max_shard_size calculation (:286-288); for split they are unused.
Net effect: the accounting assumes hardlinks make a split free, so the ~S of additional space the post-split merge genuinely requires is never validated before the split is accepted.
Describe the solution you'd like
- Add a reusable helper in
DiskThresholdDecider that answers whether a given node can accommodate N additional bytes without breaching the configured high watermark (reusing DiskThresholdSettings, ClusterInfo disk usages, and ReservedSpace).
- Call it from the split validation path (alongside
validateSplitIndex) with S = the parent shard's primary store size, so that a _split request is rejected synchronously with a clear error when the node hosting the parent primary lacks ~S of free space — instead of being accepted and silently producing unassigned shards.
- Keep the check to the
1 × S floor. That is the requirement that always holds, and it is simple to compute from data already available at request time.
DiskThresholdDecider is the natural home for (1) because the same per-node "can this node take another N bytes" question is needed by the in-place/online shard split effort (MetadataInPlaceSplitShardService, SplitShardsMetadata, RecoverySource.Type.IN_PLACE_SPLIT_SHARD) once its allocation/routing layer lands. That work has "Disk, max shards per node/index, awareness validation changes" as an open, unfiled item in the tracking meta-issue #13254, so a shared helper avoids building this twice.
Related component
Indexing
Describe alternatives you've considered
No response
Additional context
Is your feature request related to a problem? Please describe
The offline
_splitAPI (POST /<index>/_split/<target-index>,ResizeType.SPLIT) performs no disk-space validation at request time. A split can be accepted when the node hosting the parent primary does not have enough free space to complete it; the failure only surfaces later as target shards stuckUNASSIGNED, discoverable solely via_cluster/allocation/explain.How much space a split actually needs
Let
S= the parent shard's primary store size. Splitting that shard requires at leastSof additional free space on the node hosting the parent primary — i.e. room for roughly one more full copy of the shard. This floor holds regardless of how many child shards the parent is split into:StoreRecovery.addIndices(server/src/main/java/org/opensearch/index/shard/StoreRecovery.java:220-274) wraps the target directory in Lucene'sHardlinkCopyDirectoryWrapper(:237), sowriter.addIndexes(sources)hardlinks the parent's segment files into each child. In the common case this adds ~0 physical bytes, since hardlinks share inodes.writer.deleteDocuments(new ShardSplittingQuery(...))(:257-259) is a hard delete that only writes per-segment.livlive-docs bitsets (~maxDoc/8 bytes). It frees nothing.indexShard.getIndexer().forceMerge(false, -1, ...)(:206), which resolves toIndexWriter.maybeMerge()(InternalEngine.java:1701-1703) — rewrites each child's live documents into new, unshared segment files and only then drops that child's hardlinks. Summed across the children this is ~Sof newly allocated physical bytes, on top of the parent, which still occupies its ownSuntil the source index is deleted.Two consequences worth calling out:
Why the existing disk accounting doesn't cover this
DiskThresholdDecider.getExpectedShardSize(.../decider/DiskThresholdDecider.java:645-681) does correctly estimate each split child at the full parent sizeS(viaIndexMetadata.selectRecoverFromShards, which returns a singleton for split), so the per-childcanAllocatecheck is reasonable in isolation.DiskThresholdDecider.sizeOfRelocatingShards(:130-137) explicitly skips these shards from its accounting. Split children areINITIALIZINGwithrelocatingNodeId == null(ShardRouting.initialize,ShardRouting.java:544-564), so they hitcontinueunder the comment: "the only initializing-but-not-relocating shards with a nonzero expected shard size will be ones created by a resize (shrink/split/clone) operation which we expect to happen using hard links, so they shouldn't be taking any additional space and can be ignored here."ClusterInfo.ReservedSpacebackstop does not fire either. Hardlinked files are recorded as reused —StoreRecovery.java:345:index.addFileDetail(dest, l, true); // hardlinked - we treat it as reused— and reused files are excluded frombytesStillToRecover(). Reserved space therefore reads as ~0 (orUNKNOWN, sincesetFileDetailsComplete()is only called afteraddIndexesreturns,:255-256).MetadataCreateIndexService.validateSplitIndex(.../metadata/MetadataCreateIndexService.java:1890-1893) validates only routing-shard arithmetic and the write block.TransportResizeAction(.../shrink/TransportResizeAction.java:145-231) does fetch store stats, but consumes them only for shrink'smax_shard_sizecalculation (:286-288); for split they are unused.Net effect: the accounting assumes hardlinks make a split free, so the
~Sof additional space the post-split merge genuinely requires is never validated before the split is accepted.Describe the solution you'd like
DiskThresholdDeciderthat answers whether a given node can accommodateNadditional bytes without breaching the configured high watermark (reusingDiskThresholdSettings,ClusterInfodisk usages, andReservedSpace).validateSplitIndex) withS= the parent shard's primary store size, so that a_splitrequest is rejected synchronously with a clear error when the node hosting the parent primary lacks ~Sof free space — instead of being accepted and silently producing unassigned shards.1 × Sfloor. That is the requirement that always holds, and it is simple to compute from data already available at request time.DiskThresholdDecideris the natural home for (1) because the same per-node "can this node take anotherNbytes" question is needed by the in-place/online shard split effort (MetadataInPlaceSplitShardService,SplitShardsMetadata,RecoverySource.Type.IN_PLACE_SPLIT_SHARD) once its allocation/routing layer lands. That work has "Disk, max shards per node/index, awareness validation changes" as an open, unfiled item in the tracking meta-issue #13254, so a shared helper avoids building this twice.Related component
Indexing
Describe alternatives you've considered
No response
Additional context