Skip to content

Commit 15fcc08

Browse files
darjisagar7Sagar Darji
andauthored
Add interface for the Multi format merge flow (#20908)
Signed-off-by: Sagar Darji <darsaga@amazon.com> Co-authored-by: Sagar Darji <darsaga@amazon.com>
1 parent 53b08e1 commit 15fcc08

11 files changed

Lines changed: 856 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
2626
- Add ref_path support for package-based hunspell dictionary loading ([#20840](https://github.com/opensearch-project/OpenSearch/pull/20840))
2727
- Add support for enabling pluggable data formats, starting with phase-1 of decoupling shard from engine, and introducing basic abstractions ([#20675](https://github.com/opensearch-project/OpenSearch/pull/20675))
2828
- Add concurrent queue in libs and composite engine sandbox plugin ([#20909](https://github.com/opensearch-project/OpenSearch/pull/20909))
29+
- Add interface for the Multi format merge flow ([#20908](https://github.com/opensearch-project/OpenSearch/pull/20908))
2930

3031
- Add warmup phase to wait for lag to catch up in pull-based ingestion before serving ([#20526](https://github.com/opensearch-project/OpenSearch/pull/20526))
3132
- Add a new static method to IndicesOptions API to expose `STRICT_EXPAND_OPEN_HIDDEN_FORBID_CLOSED` index option ([#20980](https://github.com/opensearch-project/OpenSearch/pull/20980))

sandbox/plugins/composite-engine/src/main/java/org/opensearch/composite/CompositeIndexingExecutionEngine.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public class CompositeIndexingExecutionEngine implements IndexingExecutionEngine
6161
private final Set<IndexingExecutionEngine<?, ?>> secondaryEngines;
6262
private final CompositeDataFormat compositeDataFormat;
6363
private final LockablePool<CompositeWriter> writerPool;
64+
private final AtomicLong writerGenerationCounter;
6465

6566
/**
6667
* Constructs a CompositeIndexingExecutionEngine by reading index settings to
@@ -113,7 +114,7 @@ public CompositeIndexingExecutionEngine(
113114
this.compositeDataFormat = new CompositeDataFormat(allFormats);
114115

115116
// Create the writer pool internally, matching the reference code pattern
116-
AtomicLong writerGenerationCounter = new AtomicLong(0);
117+
writerGenerationCounter = new AtomicLong(0);
117118
this.writerPool = new LockablePool<>(
118119
() -> new CompositeWriter(this, writerGenerationCounter.getAndIncrement()),
119120
LinkedList::new,
@@ -232,6 +233,11 @@ public RefreshResult refresh(RefreshInput refreshInput) throws IOException {
232233
return new RefreshResult(refreshedSegments);
233234
}
234235

236+
@Override
237+
public long getNextWriterGeneration() {
238+
return writerGenerationCounter.getAndIncrement();
239+
}
240+
235241
@Override
236242
public CompositeDataFormat getDataFormat() {
237243
return compositeDataFormat;

sandbox/plugins/composite-engine/src/test/java/org/opensearch/composite/CompositeTestHelper.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.util.HashMap;
3232
import java.util.Map;
3333
import java.util.Set;
34+
import java.util.concurrent.atomic.AtomicLong;
3435

3536
/**
3637
* Shared test utilities for composite engine tests.
@@ -134,6 +135,7 @@ public String toString() {
134135
static class StubIndexingExecutionEngine implements IndexingExecutionEngine<DataFormat, DocumentInput<?>> {
135136

136137
private final DataFormat dataFormat;
138+
private final AtomicLong writerGeneration = new AtomicLong(0);
137139

138140
StubIndexingExecutionEngine(DataFormat dataFormat) {
139141
this.dataFormat = dataFormat;
@@ -162,6 +164,11 @@ public DataFormat getDataFormat() {
162164
@Override
163165
public void deleteFiles(Map<String, Collection<String>> filesToDelete) {}
164166

167+
@Override
168+
public long getNextWriterGeneration() {
169+
return writerGeneration.getAndIncrement();
170+
}
171+
165172
@Override
166173
public DocumentInput<?> newDocumentInput() {
167174
return new StubDocumentInput();

server/src/main/java/org/opensearch/index/engine/dataformat/IndexingExecutionEngine.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ public interface IndexingExecutionEngine<T extends DataFormat, P extends Documen
4949
*/
5050
RefreshResult refresh(RefreshInput refreshInput) throws IOException;
5151

52+
/**
53+
* Returns the next writer generation number to be used when creating a new writer.
54+
* Each writer is associated with a monotonically increasing generation number
55+
* that uniquely identifies it within this engine's lifecycle.
56+
*
57+
* @return the next writer generation number
58+
*/
59+
long getNextWriterGeneration();
60+
5261
/**
5362
* Returns the data format handled by this engine.
5463
*
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.index.engine.dataformat.merge;
10+
11+
import org.apache.logging.log4j.Logger;
12+
import org.apache.logging.log4j.message.ParameterizedMessage;
13+
import org.opensearch.common.annotation.ExperimentalApi;
14+
import org.opensearch.common.concurrent.GatedCloseable;
15+
import org.opensearch.common.logging.Loggers;
16+
import org.opensearch.core.index.shard.ShardId;
17+
import org.opensearch.index.engine.dataformat.MergeResult;
18+
import org.opensearch.index.engine.exec.CatalogSnapshot;
19+
import org.opensearch.index.engine.exec.Indexer;
20+
import org.opensearch.index.engine.exec.Segment;
21+
22+
import java.util.ArrayDeque;
23+
import java.util.Collection;
24+
import java.util.Deque;
25+
import java.util.HashSet;
26+
import java.util.List;
27+
import java.util.Set;
28+
29+
/**
30+
* Abstract handler responsible for managing segment merge operations.
31+
* <p>
32+
* Subclasses define the merge policy by implementing {@link #findMerges()} and
33+
* {@link #findForceMerges(int)}, while this base class manages the pending merge
34+
* queue and lifecycle callbacks.
35+
*
36+
* @opensearch.experimental
37+
*/
38+
@ExperimentalApi
39+
public abstract class MergeHandler {
40+
41+
private final Deque<OneMerge> mergingSegments = new ArrayDeque<>();
42+
private final Set<Segment> currentlyMergingSegments = new HashSet<>();
43+
private final Indexer indexer;
44+
private final Logger logger;
45+
46+
public MergeHandler(Indexer indexer, ShardId shardId) {
47+
this.logger = Loggers.getLogger(getClass(), shardId);
48+
this.indexer = indexer;
49+
}
50+
51+
/**
52+
* Finds merges that should be executed based on the current segment state.
53+
*
54+
* @return a collection of merges to execute, or an empty collection if none are needed
55+
*/
56+
public abstract Collection<OneMerge> findMerges();
57+
58+
/**
59+
* Finds merges required to reduce the number of segments to at most {@code maxSegmentCount}.
60+
*
61+
* @param maxSegmentCount the maximum number of segments allowed after merging
62+
* @return a collection of merges to execute
63+
*/
64+
public abstract Collection<OneMerge> findForceMerges(int maxSegmentCount);
65+
66+
/**
67+
* Updates the set of pending merges. Called to refresh the merge queue
68+
* when the segment state changes.
69+
*/
70+
public synchronized void updatePendingMerges() {
71+
Collection<OneMerge> oneMerges = findMerges();
72+
for (OneMerge oneMerge : oneMerges) {
73+
boolean isValidMerge = true;
74+
for (Segment segment : oneMerge.getSegmentsToMerge()) {
75+
if (currentlyMergingSegments.contains(segment)) {
76+
isValidMerge = false;
77+
break;
78+
}
79+
}
80+
if (isValidMerge) {
81+
registerMerge(oneMerge);
82+
}
83+
}
84+
}
85+
86+
/**
87+
* Registers a merge to be executed.
88+
*
89+
* @param merge the merge to register
90+
*/
91+
public synchronized void registerMerge(OneMerge merge) {
92+
try (GatedCloseable<CatalogSnapshot> catalogSnapshotReleasableRef = indexer.acquireSnapshot()) {
93+
// Validate segments exist in catalog
94+
List<Segment> catalogSegments = catalogSnapshotReleasableRef.get().getSegments();
95+
for (Segment mergeSegment : merge.getSegmentsToMerge()) {
96+
if (!catalogSegments.contains(mergeSegment)) {
97+
return;
98+
}
99+
}
100+
} catch (Exception e) {
101+
logger.warn("Failed to acquire snapshots", e);
102+
throw new RuntimeException(e);
103+
}
104+
mergingSegments.add(merge);
105+
currentlyMergingSegments.addAll(merge.getSegmentsToMerge());
106+
logger.debug(() -> new ParameterizedMessage("Registered merge [{}], mergingSegments: [{}]", merge, mergingSegments));
107+
}
108+
109+
/**
110+
* Returns whether there are any pending merges in the queue.
111+
*
112+
* @return {@code true} if there are pending merges
113+
*/
114+
public synchronized boolean hasPendingMerges() {
115+
return !mergingSegments.isEmpty();
116+
}
117+
118+
/**
119+
* Retrieves and removes the next pending merge from the queue.
120+
*
121+
* @return the next merge to execute, or {@code null} if the queue is empty
122+
*/
123+
public synchronized OneMerge getNextMerge() {
124+
if (mergingSegments.isEmpty()) {
125+
return null;
126+
}
127+
return mergingSegments.removeFirst();
128+
}
129+
130+
/**
131+
* Callback invoked when a merge completes successfully.
132+
*
133+
* @param oneMerge the merge that finished
134+
*/
135+
public synchronized void onMergeFinished(OneMerge oneMerge) {
136+
removeMergingSegments(oneMerge);
137+
updatePendingMerges();
138+
}
139+
140+
/**
141+
* Callback invoked when a merge fails.
142+
*
143+
* @param oneMerge the merge that failed
144+
*/
145+
public synchronized void onMergeFailure(OneMerge oneMerge) {
146+
removeMergingSegments(oneMerge);
147+
logger.warn(() -> new ParameterizedMessage("Merge failed for OneMerge [{}]", oneMerge));
148+
}
149+
150+
/**
151+
* Executes the given merge operation.
152+
*
153+
* @param oneMerge the merge to execute
154+
* @return the result of the merge
155+
*/
156+
public abstract MergeResult doMerge(OneMerge oneMerge);
157+
158+
private synchronized void removeMergingSegments(OneMerge oneMerge) {
159+
mergingSegments.remove(oneMerge);
160+
oneMerge.getSegmentsToMerge().forEach(currentlyMergingSegments::remove);
161+
}
162+
163+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.index.engine.dataformat.merge;
10+
11+
import org.apache.logging.log4j.Logger;
12+
import org.apache.logging.log4j.message.ParameterizedMessage;
13+
import org.opensearch.common.annotation.ExperimentalApi;
14+
import org.opensearch.common.logging.Loggers;
15+
import org.opensearch.core.index.shard.ShardId;
16+
import org.opensearch.index.IndexSettings;
17+
import org.opensearch.index.MergeSchedulerConfig;
18+
import org.opensearch.index.merge.MergeStats;
19+
20+
/**
21+
* Schedules and coordinates segment merge operations for a shard.
22+
* <p>
23+
* This scheduler delegates merge selection to a {@link MergeHandler} and controls
24+
* concurrency via configurable thread and merge count limits sourced from
25+
* {@link MergeSchedulerConfig}.
26+
*
27+
* @opensearch.experimental
28+
*/
29+
@ExperimentalApi
30+
public class MergeScheduler {
31+
32+
private final Logger logger;
33+
private volatile int maxConcurrentMerges;
34+
private volatile int maxMergeCount;
35+
private final MergeSchedulerConfig mergeSchedulerConfig;
36+
37+
/** true if we should rate-limit writes for each merge */
38+
private boolean doAutoIOThrottle = false;
39+
40+
/** Initial value for IO write rate limit when doAutoIOThrottle is true */
41+
private static final double START_MB_PER_SEC = 20.0;
42+
43+
/** Current IO writes throttle rate */
44+
protected double targetMBPerSec = START_MB_PER_SEC;
45+
46+
/**
47+
* Creates a new merge scheduler.
48+
*
49+
* @param mergeHandler the handler that selects and executes merges
50+
* @param shardId the shard this scheduler is associated with
51+
* @param indexSettings the index settings providing merge scheduler configuration
52+
*/
53+
public MergeScheduler(MergeHandler mergeHandler, ShardId shardId, IndexSettings indexSettings) {
54+
logger = Loggers.getLogger(getClass(), shardId);
55+
this.mergeSchedulerConfig = indexSettings.getMergeSchedulerConfig();
56+
refreshConfig();
57+
}
58+
59+
/**
60+
* Refreshes the max concurrent merge thread count and max merge count from
61+
* the current {@link MergeSchedulerConfig}. No-op if the values have not changed.
62+
*/
63+
public synchronized void refreshConfig() {
64+
int newMaxThreadCount = mergeSchedulerConfig.getMaxThreadCount();
65+
int newMaxMergeCount = mergeSchedulerConfig.getMaxMergeCount();
66+
67+
if (newMaxThreadCount == this.maxConcurrentMerges && newMaxMergeCount == this.maxMergeCount) {
68+
return;
69+
}
70+
71+
logger.info(
72+
() -> new ParameterizedMessage(
73+
"Updating from merge scheduler config: maxThreadCount {} -> {}, " + "maxMergeCount {} -> {}",
74+
this.maxConcurrentMerges,
75+
newMaxThreadCount,
76+
this.maxMergeCount,
77+
newMaxMergeCount
78+
)
79+
);
80+
81+
this.maxConcurrentMerges = newMaxThreadCount;
82+
this.maxMergeCount = newMaxMergeCount;
83+
}
84+
85+
/**
86+
* Triggers pending merge operations. Merges are selected by the
87+
* underlying {@link MergeHandler} and executed up to the configured
88+
* concurrency limits.
89+
*/
90+
public void triggerMerges() {
91+
92+
}
93+
94+
/**
95+
* Forces a merge down to at most {@code maxNumSegment} segments.
96+
*
97+
* @param maxNumSegment the maximum number of segments after the force merge
98+
*/
99+
public void forceMerge(int maxNumSegment) {
100+
101+
}
102+
103+
/**
104+
* Turn on dynamic IO throttling, to adaptively rate limit writes bytes/sec to the minimal rate
105+
* necessary so merges do not fall behind. By default, this is disabled and writes are not
106+
* rate-limited.
107+
*/
108+
public synchronized void enableAutoIOThrottle() {
109+
doAutoIOThrottle = true;
110+
targetMBPerSec = START_MB_PER_SEC;
111+
}
112+
113+
/**
114+
* Returns the currently set per-merge IO writes rate limit, if {@link #enableAutoIOThrottle} was
115+
* called, else {@code Double.POSITIVE_INFINITY}.
116+
*/
117+
public synchronized double getIORateLimitMBPerSec() {
118+
if (doAutoIOThrottle) {
119+
return targetMBPerSec;
120+
}
121+
122+
return Double.POSITIVE_INFINITY;
123+
}
124+
125+
/**
126+
* Returns the current merge statistics for this scheduler.
127+
*
128+
* @return the merge stats
129+
*/
130+
public MergeStats stats() {
131+
return new MergeStats();
132+
}
133+
}

0 commit comments

Comments
 (0)