Merge lp:~pbeaman/akiban-persistit/fix-1058254-join-corrupts-keys into lp:akiban-persistit

Proposed by Peter Beaman
Status: Merged
Approved by: Peter Beaman
Approved revision: 375
Merged at revision: 373
Proposed branch: lp:~pbeaman/akiban-persistit/fix-1058254-join-corrupts-keys
Merge into: lp:akiban-persistit
Diff against target: 527 lines (+353/-21)
9 files modified
src/main/java/com/persistit/Buffer.java (+19/-1)
src/main/java/com/persistit/Exchange.java (+2/-0)
src/main/java/com/persistit/MVV.java (+2/-2)
src/main/java/com/persistit/VolumeStructure.java (+1/-2)
src/main/java/com/persistit/util/SequencerConstants.java (+10/-0)
src/main/java/com/persistit/util/ThreadSequencer.java (+17/-0)
src/test/java/com/persistit/Bug1022567Test.java (+235/-0)
src/test/java/com/persistit/Bug1041003Test.java (+8/-16)
src/test/java/com/persistit/Bug1058254Test.java (+59/-0)
To merge this branch: bzr merge lp:~pbeaman/akiban-persistit/fix-1058254-join-corrupts-keys
Reviewer Review Type Date Requested Status
Akiban Build User Needs Fixing
Nathan Williams Approve
Review via email: mp+127075@code.launchpad.net

Description of the change

Bug fixes for 1058254 Join on range delete can change successor keys and 1022567 Stress test failure in deallocateGarbageChain.

The primary change is in the calculation of newEbc in Buffer#join. The original code ignored the possibility if page A and C are being joined where A->B and B->C, then the ebc for some key on page B could be less than the value computed by joinMeasure. That case is now handled and a small unit test is provided to exercise that code.

Since 1058254 was found while hunting 1022567, and because the fix for 1022567 involves only the removal of a condition in the assert statement that fired, that change is also included in this branch. However, note that the unit test for this is a bit tricky and requires an enhancement of the ThreadSequencer code to permit a condition to be tested.

To post a comment you must log in.
Revision history for this message
Nathan Williams (nwilliams) wrote :

I know I asked you last week but have forgotten. Why is it legitimate for the right pointer to already be the garbage root?

This looks good otherwise I think.

review: Needs Information
Revision history for this message
Peter Beaman (pbeaman) wrote :

I added an explanation to the original bug at https://bugs.launchpad.net/akiban-persistit/+bug/1022567. The unit test Bug1022567Test included in this proposal was used to demonstrate the feasibility of this mechanism.

Revision history for this message
Nathan Williams (nwilliams) wrote :

Thanks for the explanation. Makes sense to me.

review: Approve
Revision history for this message
Akiban Build User (build-akiban) wrote :

There were 2 failures during build/test:

* job system-tests-mtr failed at build number 2585: http://172.16.20.104:8080/job/system-tests-mtr/2585/

* view must-pass failed: system-tests-mtr is red

review: Needs Fixing
Revision history for this message
Peter Beaman (pbeaman) wrote :

Dopey problem. Re-approving.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/main/java/com/persistit/Buffer.java'
2--- src/main/java/com/persistit/Buffer.java 2012-09-27 18:33:06 +0000
3+++ src/main/java/com/persistit/Buffer.java 2012-09-28 22:19:23 +0000
4@@ -2448,7 +2448,25 @@
5 final long measureRight = buffer.joinMeasure(KEY_BLOCK_START, foundAt2);
6 kbData = buffer.getInt(foundAt2);
7 final int oldEbc = decodeKeyBlockEbc(kbData);
8- final int newEbc = Math.min(oldEbc, Math.min((int) (measureLeft >>> 32), (int) (measureRight >>> 32)));
9+
10+ final int newEbc;
11+ if (_rightSibling == buffer._page) {
12+ /*
13+ * Pages are are contiguous so first key of right sibling is
14+ * identical to max key of the current page.
15+ */
16+ newEbc = Math.min(oldEbc, Math.min((int) (measureLeft >>> 32), (int) (measureRight >>> 32)));
17+ } else {
18+ /*
19+ * Since pages are not contiguous, we need to extract the max key
20+ * from the current page and compute the ebc from it.
21+ */
22+ keyAt(foundAt1, indexKey);
23+ final int firstUnique = indexKey.firstUniqueByteIndex(spareKey);
24+ newEbc = Math.min(Math.min(oldEbc, firstUnique),
25+ Math.min((int) (measureLeft >>> 32), (int) (measureRight >>> 32)));
26+ }
27+
28 tail = decodeKeyBlockTail(kbData);
29 tbData = buffer.getInt(tail);
30
31
32=== modified file 'src/main/java/com/persistit/Exchange.java'
33--- src/main/java/com/persistit/Exchange.java 2012-09-27 18:23:17 +0000
34+++ src/main/java/com/persistit/Exchange.java 2012-09-28 22:19:23 +0000
35@@ -33,6 +33,7 @@
36 import static com.persistit.Key.LTEQ;
37 import static com.persistit.Key.RIGHT_GUARD_KEY;
38 import static com.persistit.Key.maxStorableKeySize;
39+import static com.persistit.util.SequencerConstants.DEALLOCATE_CHAIN_A;
40 import static com.persistit.util.SequencerConstants.WRITE_WRITE_STORE_A;
41 import static com.persistit.util.ThreadSequencer.sequence;
42
43@@ -3523,6 +3524,7 @@
44 left = lc._deallocLeftPage;
45 right = lc._deallocRightPage;
46 if (left != 0) {
47+ sequence(DEALLOCATE_CHAIN_A);
48 _volume.getStructure().deallocateGarbageChain(left, right);
49 lc._deallocLeftPage = 0;
50 lc._deallocRightPage = 0;
51
52=== modified file 'src/main/java/com/persistit/MVV.java'
53--- src/main/java/com/persistit/MVV.java 2012-09-27 22:17:34 +0000
54+++ src/main/java/com/persistit/MVV.java 2012-09-28 22:19:23 +0000
55@@ -316,7 +316,7 @@
56 */
57 to++;
58 int next = to;
59-
60+
61 while (next < end) {
62 final long curVersion = getVersion(target, to);
63 final int vlength = getLength(target, to);
64@@ -342,7 +342,7 @@
65 next = to;
66 }
67 } else if (curVersion > versionHandle) {
68- if (vh2ts(versionHandle) != vh2ts(curVersion)) {
69+ if (vh2ts(versionHandle) != vh2ts(curVersion)) {
70 throw new IllegalStateException("Versions out of order");
71 }
72 remainder = end - to;
73
74=== modified file 'src/main/java/com/persistit/VolumeStructure.java'
75--- src/main/java/com/persistit/VolumeStructure.java 2012-09-27 18:22:25 +0000
76+++ src/main/java/com/persistit/VolumeStructure.java 2012-09-28 22:19:23 +0000
77@@ -541,8 +541,7 @@
78 try {
79 final long garbagePage = getGarbageRoot();
80 if (garbagePage != 0) {
81- if (left == garbagePage || right == garbagePage) {
82- Debug.$assert0.t(false);
83+ if (left == garbagePage) {
84 throw new IllegalStateException("De-allocating page that is already garbage: " + "root="
85 + garbagePage + " left=" + left + " right=" + right);
86 }
87
88=== modified file 'src/main/java/com/persistit/util/SequencerConstants.java'
89--- src/main/java/com/persistit/util/SequencerConstants.java 2012-08-24 13:57:19 +0000
90+++ src/main/java/com/persistit/util/SequencerConstants.java 2012-09-28 22:19:23 +0000
91@@ -93,4 +93,14 @@
92 int[][] LONG_RECORD_ALLOCATE_SCHEDULED = new int[][] { array(LONG_RECORD_ALLOCATE_B),
93 array(LONG_RECORD_ALLOCATE_A, LONG_RECORD_ALLOCATE_B) };
94
95+ /*
96+ * Used in testing delete/deallocate sequence in Bug1022567Test
97+ */
98+ int DEALLOCATE_CHAIN_A = allocate("DEALLOCATE_CHAIN_A");
99+ int DEALLOCATE_CHAIN_B = allocate("DEALLOCATE_CHAIN_B");
100+ int DEALLOCATE_CHAIN_C = allocate("DEALLOCATE_CHAIN_C");
101+ int[][] DEALLOCATE_CHAIN_SCHEDULED = new int[][] { array(DEALLOCATE_CHAIN_A, DEALLOCATE_CHAIN_B),
102+ array(DEALLOCATE_CHAIN_B), array(DEALLOCATE_CHAIN_A, DEALLOCATE_CHAIN_C),
103+ array(DEALLOCATE_CHAIN_A, DEALLOCATE_CHAIN_C) };
104+
105 }
106
107=== modified file 'src/main/java/com/persistit/util/ThreadSequencer.java'
108--- src/main/java/com/persistit/util/ThreadSequencer.java 2012-08-24 13:57:19 +0000
109+++ src/main/java/com/persistit/util/ThreadSequencer.java 2012-09-28 22:19:23 +0000
110@@ -110,8 +110,16 @@
111
112 private final static List<String> LOCATIONS = new ArrayList<String>();
113
114+ private final static List<Condition> CONDITIONS = new ArrayList<Condition>();
115+
116 private final static int MAX_LOCATIONS = 64;
117
118+ public static class Condition {
119+ public boolean enabled() {
120+ return true;
121+ }
122+ }
123+
124 public synchronized static int allocate(final String locationName) {
125 for (final String alreadyRegistered : LOCATIONS) {
126 assert !alreadyRegistered.equals(locationName) : "Location name " + locationName + " is already in use";
127@@ -119,6 +127,7 @@
128 final int value = LOCATIONS.size();
129 assert value < MAX_LOCATIONS : "Too many ThreadSequence locations";
130 LOCATIONS.add(locationName);
131+ CONDITIONS.add(new Condition());
132 return value;
133 }
134
135@@ -167,6 +176,10 @@
136 }
137 }
138
139+ public static void setCondition(final int location, final Condition condition) {
140+ CONDITIONS.set(location, condition);
141+ }
142+
143 public static String sequencerHistory() {
144 return ENABLED_SEQUENCER.history();
145 }
146@@ -369,6 +382,10 @@
147 assert location >= 0 && location < MAX_LOCATIONS : "Location must be between 0 and 63, inclusive";
148 Semaphore semaphore = null;
149
150+ if (!CONDITIONS.get(location).enabled()) {
151+ return;
152+ }
153+
154 synchronized (this) {
155 if ((_enabled & (1L << location)) == 0) {
156 return;
157
158=== added file 'src/test/java/com/persistit/Bug1022567Test.java'
159--- src/test/java/com/persistit/Bug1022567Test.java 1970-01-01 00:00:00 +0000
160+++ src/test/java/com/persistit/Bug1022567Test.java 2012-09-28 22:19:23 +0000
161@@ -0,0 +1,235 @@
162+/**
163+ * Copyright © 2012 Akiban Technologies, Inc. All rights reserved.
164+ *
165+ * This program and the accompanying materials are made available
166+ * under the terms of the Eclipse Public License v1.0 which
167+ * accompanies this distribution, and is available at
168+ * http://www.eclipse.org/legal/epl-v10.html
169+ *
170+ * This program may also be available under different license terms.
171+ * For more information, see www.akiban.com or contact licensing@akiban.com.
172+ *
173+ * Contributors:
174+ * Akiban Technologies, Inc.
175+ */
176+
177+package com.persistit;
178+
179+import static com.persistit.unit.UnitTestProperties.VOLUME_NAME;
180+import static com.persistit.util.SequencerConstants.DEALLOCATE_CHAIN_A;
181+import static com.persistit.util.SequencerConstants.DEALLOCATE_CHAIN_B;
182+import static com.persistit.util.SequencerConstants.DEALLOCATE_CHAIN_C;
183+import static com.persistit.util.SequencerConstants.DEALLOCATE_CHAIN_SCHEDULED;
184+import static com.persistit.util.ThreadSequencer.addSchedules;
185+import static com.persistit.util.ThreadSequencer.enableSequencer;
186+import static com.persistit.util.ThreadSequencer.sequence;
187+import static com.persistit.util.ThreadSequencer.setCondition;
188+
189+import java.util.ArrayList;
190+import java.util.List;
191+import java.util.Properties;
192+
193+import org.junit.Test;
194+
195+import com.persistit.exception.PersistitException;
196+import com.persistit.unit.UnitTestProperties;
197+import com.persistit.util.ThreadSequencer;
198+import com.persistit.util.ThreadSequencer.Condition;
199+
200+/**
201+ * MixtureTxn1 suite failed with this Exception:
202+ *
203+ * Stress6 [Thread-195] FAILED [Thread-195]: java.lang.IllegalStateException:
204+ * De-allocating page that is already garbage: root=36689 left=72838 right=36689
205+ *
206+ * at com.persistit.VolumeStructure.deallocateGarbageChain(VolumeStructure.java:
207+ * 510)
208+ *
209+ * As it turns out, the condition being asserted is a rare but legitimate state
210+ * and the bug fix it to remove a conditional clause from the assert statement
211+ * in VolumeStructure#deallocateGarbageChain. The recreate1022567 method below
212+ * reproduces the legitimate case, and without the change in VolumeStructure
213+ * cuases a the IllegalStateException.
214+ */
215+
216+public class Bug1022567Test extends PersistitUnitTestCase {
217+
218+ private final static String TREE_NAME = "Bug1022567";
219+
220+ private Exchange getExchange() throws PersistitException {
221+ return _persistit.getExchange(VOLUME_NAME, TREE_NAME, true);
222+ }
223+
224+ @Override
225+ public Properties getProperties(final boolean cleanup) {
226+ return UnitTestProperties.getBiggerProperties(cleanup);
227+ }
228+
229+ /**
230+ * This method carefully constructs a scenario in which the right sibling of
231+ * a chain being deallocated is the garbage root page. The reported bug is
232+ * that this condition causes an assert, when in fact the condition is rare
233+ * but legitimate. Prior to fixing the bug this method throws an
234+ * IllegalStateException.
235+ *
236+ * @throws Exception
237+ */
238+ @Test
239+ public void recreate1022567() throws Exception {
240+ try {
241+ enableSequencer(true);
242+ addSchedules(DEALLOCATE_CHAIN_SCHEDULED);
243+
244+ final long mainThreadId = Thread.currentThread().getId();
245+ setCondition(DEALLOCATE_CHAIN_A, new Condition() {
246+ @Override
247+ public boolean enabled() {
248+ return Thread.currentThread().getId() == mainThreadId;
249+ }
250+ });
251+ for (int loop = 0; loop < 1000; loop++) {
252+ _persistit.getVolume(VOLUME_NAME).truncate();
253+ final Exchange ex1 = getExchange();
254+ final List<Integer> keys = new ArrayList<Integer>();
255+ /*
256+ * Lay down 4 pages
257+ */
258+ long nextAvailable = 0;
259+ ex1.getValue().put(createString(2000));
260+ for (int i = 0;; i++) {
261+ ex1.clear().append(i).store();
262+ final long newNextAvailable = nextAvailablePage();
263+ if (newNextAvailable != nextAvailable) {
264+ keys.add(i - 1);
265+ nextAvailable = newNextAvailable;
266+ if (keys.size() > 3) {
267+ break;
268+ }
269+ }
270+ }
271+ final Key key1 = new Key(_persistit);
272+ final Key key2 = new Key(_persistit);
273+ key1.to(keys.get(1));
274+ key2.to(keys.get(3));
275+
276+ // Need to create a race: this thread needs to remove the inside
277+ // two
278+ // pages and then let the other thread proceed before
279+ // deallocating
280+ // them.
281+
282+ final Thread t = new Thread(new Runnable() {
283+ @Override
284+ public void run() {
285+ try {
286+ sequence(DEALLOCATE_CHAIN_B);
287+ final Exchange ex2 = getExchange();
288+ ex2.to(Key.AFTER);
289+ while (ex2.previous()) {
290+ ex2.remove();
291+ }
292+ sequence(DEALLOCATE_CHAIN_C);
293+ } catch (final PersistitException e) {
294+ e.printStackTrace();
295+ }
296+ }
297+ });
298+ t.start();
299+ key1.nudgeLeft();
300+ key2.nudgeLeft();
301+ ex1.removeKeyRange(key1, key2);
302+ t.join();
303+ }
304+ } finally {
305+ ThreadSequencer.disableSequencer();
306+ }
307+ }
308+
309+ @Test
310+ public void deleteEnlargingRanges() throws Exception {
311+ final Exchange ex = getExchange();
312+ final VolumeStructure vs = ex.getVolume().getStructure();
313+ final List<KeyState> keys = new ArrayList<KeyState>();
314+ /*
315+ * Lay down about 1000 pages
316+ */
317+ long nextAvailable = 0;
318+ ex.getValue().put(RED_FOX);
319+ for (int i = 0;; i++) {
320+ ex.clear().append(i).store();
321+ final long newNextAvailable = nextAvailablePage();
322+ if (newNextAvailable != nextAvailable) {
323+ keys.add(new KeyState(ex.getKey()));
324+ nextAvailable = newNextAvailable;
325+ if (nextAvailable > 1000) {
326+ break;
327+ }
328+ }
329+ }
330+ final List<Thread> threads = new ArrayList<Thread>();
331+ for (int i = 2; i < 480; i++) {
332+ final int offset = i;
333+ final Thread t = new Thread(new Runnable() {
334+ @Override
335+ public void run() {
336+ try {
337+ final Key key1 = new Key(_persistit);
338+ final Key key2 = new Key(_persistit);
339+ keys.get(500 - offset).copyTo(key1);
340+ keys.get(500 + offset).copyTo(key2);
341+ final Exchange exchange = _persistit.getExchange(VOLUME_NAME, TREE_NAME, false);
342+ exchange.removeKeyRange(key1, key2);
343+ } catch (final PersistitException e) {
344+ e.printStackTrace();
345+ }
346+ }
347+ });
348+ t.start();
349+ threads.add(t);
350+ }
351+ for (final Thread t : threads) {
352+ t.join();
353+ }
354+ }
355+
356+ @Test
357+ public void deleteEnlargingRangesSingleThread() throws Exception {
358+ final Exchange ex = getExchange();
359+ final VolumeStructure vs = ex.getVolume().getStructure();
360+ final List<KeyState> keys = new ArrayList<KeyState>();
361+ /*
362+ * Lay down about 1000 pages
363+ */
364+ long nextAvailable = 0;
365+ ex.getValue().put(createString(2000));
366+ for (int i = 0;; i++) {
367+ ex.clear().append(i).store();
368+ final long newNextAvailable = nextAvailablePage();
369+ if (newNextAvailable != nextAvailable) {
370+ keys.add(new KeyState(ex.getKey()));
371+ nextAvailable = newNextAvailable;
372+ if (nextAvailable > 1000) {
373+ break;
374+ }
375+ }
376+ }
377+ for (int i = 2; i < 80; i++) {
378+ final int offset = i;
379+ try {
380+ final Key key1 = new Key(_persistit);
381+ final Key key2 = new Key(_persistit);
382+ keys.get(500 - offset).copyTo(key1);
383+ keys.get(500 + offset).copyTo(key2);
384+ final Exchange exchange = _persistit.getExchange(VOLUME_NAME, TREE_NAME, false);
385+ exchange.removeKeyRange(key1, key2);
386+ } catch (final PersistitException e) {
387+ e.printStackTrace();
388+ }
389+ }
390+ }
391+
392+ private long nextAvailablePage() throws PersistitException {
393+ return _persistit.getVolume(VOLUME_NAME).getStorage().getNextAvailablePage();
394+ }
395+
396+}
397
398=== modified file 'src/test/java/com/persistit/Bug1041003Test.java'
399--- src/test/java/com/persistit/Bug1041003Test.java 2012-09-26 21:30:03 +0000
400+++ src/test/java/com/persistit/Bug1041003Test.java 2012-09-28 22:19:23 +0000
401@@ -15,26 +15,17 @@
402
403 package com.persistit;
404
405-import static org.junit.Assert.assertEquals;
406-import static org.junit.Assert.assertTrue;
407 import static org.junit.Assert.fail;
408
409-import java.io.File;
410 import java.io.IOException;
411-import java.io.RandomAccessFile;
412 import java.nio.ByteBuffer;
413 import java.nio.channels.FileChannel;
414 import java.util.Properties;
415-import java.util.Timer;
416-import java.util.TimerTask;
417
418 import org.junit.Test;
419
420 import com.persistit.JournalRecord.JE;
421 import com.persistit.Transaction.CommitPolicy;
422-import com.persistit.exception.CorruptJournalException;
423-import com.persistit.exception.CorruptVolumeException;
424-import com.persistit.exception.PersistitException;
425 import com.persistit.exception.PersistitIOException;
426 import com.persistit.unit.UnitTestProperties;
427
428@@ -74,15 +65,15 @@
429 @Test
430 public void leaveTransactionBufferFlipped() throws Exception {
431 /*
432- * Need first journal file almost full so that an attempt to write a transaction
433- * will force a rollover.
434+ * Need first journal file almost full so that an attempt to write a
435+ * transaction will force a rollover.
436 */
437 final Exchange exchange = _persistit.getExchange(_volumeName, "Bug1041003Test", true);
438 _persistit.flush();
439 final JournalManager jman = _persistit.getJournalManager();
440- ByteBuffer bb = ByteBuffer.allocate(BLOCKSIZE);
441- long size = BLOCKSIZE - JE.OVERHEAD - jman.getCurrentAddress() - 1;
442- bb.position((int)(size - JournalRecord.TX.OVERHEAD));
443+ final ByteBuffer bb = ByteBuffer.allocate(BLOCKSIZE);
444+ final long size = BLOCKSIZE - JE.OVERHEAD - jman.getCurrentAddress() - 1;
445+ bb.position((int) (size - JournalRecord.TX.OVERHEAD));
446 jman.writeTransactionToJournal(bb, 1, 2, 0);
447 final Transaction txn = _persistit.getTransaction();
448 final ErrorInjectingFileChannel eifc = errorInjectingChannel(_persistit.getJournalManager().getFileChannel(
449@@ -115,11 +106,12 @@
450 try {
451 exchange.getValue().put(RED_FOX + RED_FOX);
452 /*
453- * Bug 1041003 causes the following line to throw an IllegalStateException.
454+ * Bug 1041003 causes the following line to throw an
455+ * IllegalStateException.
456 */
457 exchange.to(1).store();
458 txn.commit(CommitPolicy.HARD);
459- } catch (IllegalStateException e) {
460+ } catch (final IllegalStateException e) {
461 fail("Bug 1041003 strikes: " + e);
462 } finally {
463 txn.end();
464
465=== added file 'src/test/java/com/persistit/Bug1058254Test.java'
466--- src/test/java/com/persistit/Bug1058254Test.java 1970-01-01 00:00:00 +0000
467+++ src/test/java/com/persistit/Bug1058254Test.java 2012-09-28 22:19:23 +0000
468@@ -0,0 +1,59 @@
469+/**
470+ * Copyright © 2012 Akiban Technologies, Inc. All rights reserved.
471+ *
472+ * This program and the accompanying materials are made available
473+ * under the terms of the Eclipse Public License v1.0 which
474+ * accompanies this distribution, and is available at
475+ * http://www.eclipse.org/legal/epl-v10.html
476+ *
477+ * This program may also be available under different license terms.
478+ * For more information, see www.akiban.com or contact licensing@akiban.com.
479+ *
480+ * Contributors:
481+ * Akiban Technologies, Inc.
482+ */
483+
484+package com.persistit;
485+
486+import static com.persistit.unit.UnitTestProperties.VOLUME_NAME;
487+import static org.junit.Assert.assertEquals;
488+import static org.junit.Assert.assertTrue;
489+
490+import org.junit.Test;
491+
492+import com.persistit.exception.PersistitException;
493+
494+/**
495+ * A range delete that crosses at least two page boundaries can write an
496+ * incorrect key when merging records; this causes it and subsequent keys within
497+ * the page to be wrong and also leads to a discontinuity between the max key in
498+ * the page and the first key of its right sibling.
499+ */
500+
501+public class Bug1058254Test extends PersistitUnitTestCase {
502+
503+ private final static String TREE_NAME = "Bug1022567";
504+
505+ private Exchange getExchange() throws PersistitException {
506+ return _persistit.getExchange(VOLUME_NAME, TREE_NAME, true);
507+ }
508+
509+ @Test
510+ public void joinDiscontinuity() throws Exception {
511+ final Exchange ex = getExchange();
512+ ex.getValue().put(createString(2000));
513+ for (int i = 3444; i < 3600; i++) {
514+ ex.to(i).store();
515+ }
516+ final Key key1 = new Key(_persistit);
517+ final Key key2 = new Key(_persistit);
518+ key1.to(3445);
519+ key2.to(3557);
520+ ex.removeKeyRange(key1, key2);
521+ ex.to(Key.BEFORE);
522+ assertTrue(ex.next());
523+ assertEquals(3444, ex.getKey().decodeInt());
524+ assertTrue(ex.next());
525+ assertEquals(3557, ex.getKey().decodeInt());
526+ }
527+}

Subscribers

People subscribed via source and target branches