Skip to content

Commit 59efe22

Browse files
authored
LUCENE-8962: Allow waiting for all merges in a merge spec (#1585)
This change adds infrastructure to allow straight forward waiting on one or more merges or an entire merge specification. This is a basis for LUCENE-8962.
1 parent 207efbc commit 59efe22

5 files changed

Lines changed: 233 additions & 24 deletions

File tree

lucene/core/src/java/org/apache/lucene/index/IndexWriter.java

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2129,12 +2129,12 @@ public final void maybeMerge() throws IOException {
21292129

21302130
private final void maybeMerge(MergePolicy mergePolicy, MergeTrigger trigger, int maxNumSegments) throws IOException {
21312131
ensureOpen(false);
2132-
if (updatePendingMerges(mergePolicy, trigger, maxNumSegments)) {
2132+
if (updatePendingMerges(mergePolicy, trigger, maxNumSegments) != null) {
21332133
mergeScheduler.merge(mergeSource, trigger);
21342134
}
21352135
}
21362136

2137-
private synchronized boolean updatePendingMerges(MergePolicy mergePolicy, MergeTrigger trigger, int maxNumSegments)
2137+
private synchronized MergePolicy.MergeSpecification updatePendingMerges(MergePolicy mergePolicy, MergeTrigger trigger, int maxNumSegments)
21382138
throws IOException {
21392139

21402140
// In case infoStream was disabled on init, but then enabled at some
@@ -2144,22 +2144,21 @@ private synchronized boolean updatePendingMerges(MergePolicy mergePolicy, MergeT
21442144
assert maxNumSegments == UNBOUNDED_MAX_MERGE_SEGMENTS || maxNumSegments > 0;
21452145
assert trigger != null;
21462146
if (stopMerges) {
2147-
return false;
2147+
return null;
21482148
}
21492149

21502150
// Do not start new merges if disaster struck
21512151
if (tragedy.get() != null) {
2152-
return false;
2152+
return null;
21532153
}
2154-
boolean newMergesFound = false;
2154+
21552155
final MergePolicy.MergeSpecification spec;
21562156
if (maxNumSegments != UNBOUNDED_MAX_MERGE_SEGMENTS) {
21572157
assert trigger == MergeTrigger.EXPLICIT || trigger == MergeTrigger.MERGE_FINISHED :
21582158
"Expected EXPLICT or MERGE_FINISHED as trigger even with maxNumSegments set but was: " + trigger.name();
21592159

21602160
spec = mergePolicy.findForcedMerges(segmentInfos, maxNumSegments, Collections.unmodifiableMap(segmentsToMerge), this);
2161-
newMergesFound = spec != null;
2162-
if (newMergesFound) {
2161+
if (spec != null) {
21632162
final int numMerges = spec.merges.size();
21642163
for(int i=0;i<numMerges;i++) {
21652164
final MergePolicy.OneMerge merge = spec.merges.get(i);
@@ -2169,14 +2168,13 @@ private synchronized boolean updatePendingMerges(MergePolicy mergePolicy, MergeT
21692168
} else {
21702169
spec = mergePolicy.findMerges(trigger, segmentInfos, this);
21712170
}
2172-
newMergesFound = spec != null;
2173-
if (newMergesFound) {
2171+
if (spec != null) {
21742172
final int numMerges = spec.merges.size();
21752173
for(int i=0;i<numMerges;i++) {
21762174
registerMerge(spec.merges.get(i));
21772175
}
21782176
}
2179-
return newMergesFound;
2177+
return spec;
21802178
}
21812179

21822180
/** Expert: to be used by a {@link MergePolicy} to avoid
@@ -4289,7 +4287,7 @@ private synchronized void mergeFinish(MergePolicy.OneMerge merge) {
42894287
@SuppressWarnings("try")
42904288
private synchronized void closeMergeReaders(MergePolicy.OneMerge merge, boolean suppressExceptions) throws IOException {
42914289
final boolean drop = suppressExceptions == false;
4292-
try (Closeable finalizer = merge::mergeFinished) {
4290+
try (Closeable finalizer = () -> merge.mergeFinished(suppressExceptions == false)) {
42934291
IOUtils.applyToAll(merge.readers, sr -> {
42944292
final ReadersAndUpdates rld = getPooledInstance(sr.getOriginalSegmentInfo(), false);
42954293
// We still hold a ref so it should not have been removed:

lucene/core/src/java/org/apache/lucene/index/MergePolicy.java

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@
2323
import java.util.List;
2424
import java.util.Map;
2525
import java.util.Map.Entry;
26+
import java.util.Optional;
2627
import java.util.Set;
28+
import java.util.concurrent.CompletableFuture;
29+
import java.util.concurrent.ExecutionException;
30+
import java.util.concurrent.TimeUnit;
31+
import java.util.concurrent.TimeoutException;
2732
import java.util.concurrent.atomic.AtomicLong;
2833
import java.util.concurrent.locks.Condition;
2934
import java.util.concurrent.locks.ReentrantLock;
@@ -37,6 +42,7 @@
3742
import org.apache.lucene.util.Bits;
3843
import org.apache.lucene.util.IOSupplier;
3944
import org.apache.lucene.util.InfoStream;
45+
import org.apache.lucene.util.ThreadInterruptedException;
4046

4147
/**
4248
* <p>Expert: a MergePolicy determines the sequence of
@@ -76,7 +82,7 @@ public abstract class MergePolicy {
7682
* @lucene.experimental */
7783
public static class OneMergeProgress {
7884
/** Reason for pausing the merge thread. */
79-
public static enum PauseReason {
85+
public enum PauseReason {
8086
/** Stopped (because of throughput rate set to 0, typically). */
8187
STOPPED,
8288
/** Temporarily paused because of exceeded throughput rate. */
@@ -196,6 +202,7 @@ final void setMergeThread(Thread owner) {
196202
*
197203
* @lucene.experimental */
198204
public static class OneMerge {
205+
private final CompletableFuture<Boolean> mergeCompleted = new CompletableFuture<>();
199206
SegmentCommitInfo info; // used by IndexWriter
200207
boolean registerDone; // used by IndexWriter
201208
long mergeGen; // used by IndexWriter
@@ -222,7 +229,7 @@ public static class OneMerge {
222229
volatile long mergeStartNS = -1;
223230

224231
/** Total number of documents in segments to be merged, not accounting for deletions. */
225-
public final int totalMaxDoc;
232+
final int totalMaxDoc;
226233
Throwable error;
227234

228235
/** Sole constructor.
@@ -233,13 +240,8 @@ public OneMerge(List<SegmentCommitInfo> segments) {
233240
throw new RuntimeException("segments must include at least one segment");
234241
}
235242
// clone the list, as the in list may be based off original SegmentInfos and may be modified
236-
this.segments = new ArrayList<>(segments);
237-
int count = 0;
238-
for(SegmentCommitInfo info : segments) {
239-
count += info.info.maxDoc();
240-
}
241-
totalMaxDoc = count;
242-
243+
this.segments = List.copyOf(segments);
244+
totalMaxDoc = segments.stream().mapToInt(i -> i.info.maxDoc()).sum();
243245
mergeProgress = new OneMergeProgress();
244246
}
245247

@@ -251,8 +253,12 @@ public void mergeInit() throws IOException {
251253
mergeProgress.setMergeThread(Thread.currentThread());
252254
}
253255

254-
/** Called by {@link IndexWriter} after the merge is done and all readers have been closed. */
255-
public void mergeFinished() throws IOException {
256+
/** Called by {@link IndexWriter} after the merge is done and all readers have been closed.
257+
* @param success true iff the merge finished successfully ie. was committed */
258+
public void mergeFinished(boolean success) throws IOException {
259+
if (mergeCompleted.complete(success) == false) {
260+
throw new IllegalStateException("merge has already finished");
261+
}
256262
}
257263

258264
/** Wrap the reader in order to add/remove information to the merged segment. */
@@ -362,6 +368,37 @@ public void checkAborted() throws MergeAbortedException {
362368
public OneMergeProgress getMergeProgress() {
363369
return mergeProgress;
364370
}
371+
372+
/**
373+
* Waits for this merge to be completed
374+
* @return true if the merge finished within the specified timeout
375+
*/
376+
boolean await(long timeout, TimeUnit timeUnit) {
377+
try {
378+
mergeCompleted.get(timeout, timeUnit);
379+
return true;
380+
} catch (InterruptedException e) {
381+
throw new ThreadInterruptedException(e);
382+
} catch (ExecutionException | TimeoutException e) {
383+
return false;
384+
}
385+
}
386+
387+
/**
388+
* Returns true if the merge has finished or false if it's still running or
389+
* has not been started. This method will not block.
390+
*/
391+
boolean isDone() {
392+
return mergeCompleted.isDone();
393+
}
394+
395+
/**
396+
* Returns true iff the merge completed successfully or false if the merge succeeded with a failure.
397+
* This method will not block and return an empty Optional if the merge has not finished yet
398+
*/
399+
Optional<Boolean> hasCompletedSuccessfully() {
400+
return Optional.ofNullable(mergeCompleted.getNow(null));
401+
}
365402
}
366403

367404
/**
@@ -399,6 +436,22 @@ public String segString(Directory dir) {
399436
}
400437
return b.toString();
401438
}
439+
440+
/**
441+
* Waits if necessary for at most the given time for all merges.
442+
*/
443+
boolean await(long timeout, TimeUnit unit) {
444+
try {
445+
CompletableFuture<Void> future = CompletableFuture.allOf(merges.stream()
446+
.map(m -> m.mergeCompleted).collect(Collectors.toList()).toArray(CompletableFuture<?>[]::new));
447+
future.get(timeout, unit);
448+
return true;
449+
} catch (InterruptedException e) {
450+
throw new ThreadInterruptedException(e);
451+
} catch (ExecutionException | TimeoutException e) {
452+
return false;
453+
}
454+
}
402455
}
403456

404457
/** Exception thrown if there are any problems while executing a merge. */

lucene/core/src/test/org/apache/lucene/index/TestDemoParallelLeafReader.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ public CodecReader wrapForMerge(CodecReader reader) throws IOException {
538538
}
539539

540540
@Override
541-
public void mergeFinished() throws IOException {
541+
public void mergeFinished(boolean success) throws IOException {
542542
Throwable th = null;
543543
for (ParallelLeafReader r : parallelReaders) {
544544
try {

lucene/core/src/test/org/apache/lucene/index/TestIndexWriter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4181,7 +4181,7 @@ public boolean keepFullyDeletedSegment(IOSupplier<CodecReader> readerIOSupplier)
41814181
SetOnce<Boolean> onlyFinishOnce = new SetOnce<>();
41824182
return new MergePolicy.OneMerge(merge.segments) {
41834183
@Override
4184-
public void mergeFinished() {
4184+
public void mergeFinished(boolean success) {
41854185
onlyFinishOnce.set(true);
41864186
}
41874187
};
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.lucene.index;
19+
20+
import java.io.IOException;
21+
import java.nio.charset.StandardCharsets;
22+
import java.util.Collections;
23+
import java.util.LinkedList;
24+
import java.util.List;
25+
import java.util.concurrent.TimeUnit;
26+
import java.util.concurrent.atomic.AtomicBoolean;
27+
import java.util.concurrent.atomic.AtomicInteger;
28+
29+
import org.apache.lucene.store.Directory;
30+
import org.apache.lucene.util.LuceneTestCase;
31+
import org.apache.lucene.util.StringHelper;
32+
import org.apache.lucene.util.TestUtil;
33+
import org.apache.lucene.util.Version;
34+
35+
public class TestMergePolicy extends LuceneTestCase {
36+
37+
public void testWaitForOneMerge() throws IOException, InterruptedException {
38+
try (Directory dir = newDirectory()) {
39+
MergePolicy.MergeSpecification ms = createRandomMergeSpecification(dir, 1 + random().nextInt(10));
40+
for (MergePolicy.OneMerge m : ms.merges) {
41+
assertFalse(m.hasCompletedSuccessfully().isPresent());
42+
}
43+
Thread t = new Thread(() -> {
44+
try {
45+
for (MergePolicy.OneMerge m : ms.merges) {
46+
m.mergeFinished(true);
47+
}
48+
} catch (IOException e) {
49+
throw new AssertionError(e);
50+
}
51+
});
52+
t.start();
53+
assertTrue(ms.await(100, TimeUnit.HOURS));
54+
for (MergePolicy.OneMerge m : ms.merges) {
55+
assertTrue(m.hasCompletedSuccessfully().get());
56+
}
57+
t.join();
58+
}
59+
}
60+
61+
public void testTimeout() throws IOException, InterruptedException {
62+
try (Directory dir = newDirectory()) {
63+
MergePolicy.MergeSpecification ms = createRandomMergeSpecification(dir, 3);
64+
for (MergePolicy.OneMerge m : ms.merges) {
65+
assertFalse(m.hasCompletedSuccessfully().isPresent());
66+
}
67+
Thread t = new Thread(() -> {
68+
try {
69+
ms.merges.get(0).mergeFinished(true);
70+
} catch (IOException e) {
71+
throw new AssertionError(e);
72+
}
73+
});
74+
t.start();
75+
assertFalse(ms.await(10, TimeUnit.MILLISECONDS));
76+
assertFalse(ms.merges.get(1).hasCompletedSuccessfully().isPresent());
77+
t.join();
78+
}
79+
}
80+
81+
public void testTimeoutLargeNumberOfMerges() throws IOException, InterruptedException {
82+
try (Directory dir = newDirectory()) {
83+
MergePolicy.MergeSpecification ms = createRandomMergeSpecification(dir, 10000);
84+
for (MergePolicy.OneMerge m : ms.merges) {
85+
assertFalse(m.hasCompletedSuccessfully().isPresent());
86+
}
87+
AtomicInteger i = new AtomicInteger(0);
88+
AtomicBoolean stop = new AtomicBoolean(false);
89+
Thread t = new Thread(() -> {
90+
while (stop.get() == false) {
91+
try {
92+
ms.merges.get(i.getAndIncrement()).mergeFinished(true);
93+
Thread.sleep(1);
94+
} catch (IOException | InterruptedException e) {
95+
throw new AssertionError(e);
96+
}
97+
}
98+
});
99+
t.start();
100+
assertFalse(ms.await(10, TimeUnit.MILLISECONDS));
101+
stop.set(true);
102+
t.join();
103+
for (int j = 0; j < ms.merges.size(); j++) {
104+
if (j < i.get()) {
105+
assertTrue(ms.merges.get(j).hasCompletedSuccessfully().get());
106+
} else {
107+
assertFalse(ms.merges.get(j).hasCompletedSuccessfully().isPresent());
108+
}
109+
}
110+
}
111+
}
112+
113+
public void testFinishTwice() throws IOException {
114+
try (Directory dir = newDirectory()) {
115+
MergePolicy.MergeSpecification spec = createRandomMergeSpecification(dir, 1);
116+
MergePolicy.OneMerge oneMerge = spec.merges.get(0);
117+
oneMerge.mergeFinished(true);
118+
expectThrows(IllegalStateException.class, () -> oneMerge.mergeFinished(false));
119+
}
120+
}
121+
122+
public void testTotalMaxDoc() throws IOException {
123+
try (Directory dir = newDirectory()) {
124+
MergePolicy.MergeSpecification spec = createRandomMergeSpecification(dir, 1);
125+
int docs = 0;
126+
MergePolicy.OneMerge oneMerge = spec.merges.get(0);
127+
for (SegmentCommitInfo info : oneMerge.segments) {
128+
docs += info.info.maxDoc();
129+
}
130+
assertEquals(docs, oneMerge.totalMaxDoc);
131+
}
132+
}
133+
134+
private static MergePolicy.MergeSpecification createRandomMergeSpecification(Directory dir, int numMerges) {
135+
MergePolicy.MergeSpecification ms = new MergePolicy.MergeSpecification();
136+
for (int ii = 0; ii < numMerges; ++ii) {
137+
final SegmentInfo si = new SegmentInfo(
138+
dir, // dir
139+
Version.LATEST, // version
140+
Version.LATEST, // min version
141+
TestUtil.randomSimpleString(random()), // name
142+
random().nextInt(1000), // maxDoc
143+
random().nextBoolean(), // isCompoundFile
144+
null, // codec
145+
Collections.emptyMap(), // diagnostics
146+
TestUtil.randomSimpleString(// id
147+
random(),
148+
StringHelper.ID_LENGTH,
149+
StringHelper.ID_LENGTH).getBytes(StandardCharsets.US_ASCII),
150+
Collections.emptyMap(), // attributes
151+
null /* indexSort */);
152+
final List<SegmentCommitInfo> segments = new LinkedList<SegmentCommitInfo>();
153+
segments.add(new SegmentCommitInfo(si, 0, 0, 0, 0, 0, StringHelper.randomId()));
154+
ms.add(new MergePolicy.OneMerge(segments));
155+
}
156+
return ms;
157+
}
158+
}

0 commit comments

Comments
 (0)