Skip to content

Commit e4623c8

Browse files
authored
test: regression test for the legacy index drop race (#1315) (#1319)
1 parent 421a660 commit e4623c8

2 files changed

Lines changed: 90 additions & 1 deletion

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ Eleven changes from [@brettwooldridge](https://github.com/brettwooldridge), most
2727
- **`clear()` and `dropIndex()` reach every layout map an index occupies** ([#1295](https://github.com/nitrite/nitrite-java/pull/1295))
2828
- `IndexManager.close()`, `clearAll()` and `dropIndexDescriptor()` acted only on the map name recorded in `IndexMeta`, which is the classic one. That already missed the composite map of a non-unique index: after `collection.clear()` its rows survived, and a query on that index returned the ids of the cleared documents alongside the new ones - two live documents, four results. It also broke `ChangeIdField`, whose `createIndex` found the previous index map still populated and rebuilt over it.
2929

30-
- **The first concurrent read of an index after a restart no longer fails dropping its legacy map** ([#1309](https://github.com/nitrite/nitrite-java/pull/1309))
30+
- **The first concurrent read of an index after a restart no longer fails dropping its legacy map** ([#1309](https://github.com/nitrite/nitrite-java/pull/1309), [#1315](https://github.com/nitrite/nitrite-java/issues/1315))
3131
- The first read of an index still in a legacy layout migrates it and drops the legacy map, once per index instance. `ComparableIndexer` created those instances with an unsynchronized check-then-act, so threads arriving together could each get an instance of their own and each run the migration. On MVStore the second drop asked `MVMap.getName()` for a map that was already gone, got `null`, and failed with `NullPointerException` in `NitriteMVStore.removeMap`; the same race also threw from `Attributes.set` through `NitriteMap.updateLastModifiedTime`.
3232
- Observed on a production system on the first multi-threaded lookup after every restart, because every close before [#1295](https://github.com/nitrite/nitrite-java/pull/1295) left an empty map under the legacy name for the next start to drop.
3333
- The indexer and the MVStore map and R-tree registries now create their entries with `computeIfAbsent`, so there is one index instance and one map wrapper per name. `NitriteMVMap` keeps the name it was opened with and acts only when its compare-and-set wins, so `drop()` and `close()` run once; `removeMap` ignores a null name and no longer creates an empty map only to remove it.
34+
- Reported against 5.3.0 in [#1315](https://github.com/nitrite/nitrite-java/issues/1315): four threads making the first find on a reopened file database failed 21 of 400 finds on 5.3.0 and none with these changes. The reporter's reproduction is now `Issue1315Test`.
3435

3536
- **Concurrent first reads of a map on an in-memory MVStore no longer fail inside H2** ([#1311](https://github.com/nitrite/nitrite-java/pull/1311))
3637
- H2's `ObjectDataType` picks the delegate that compares serialized keys on first use, through an unsynchronized field, and `SerializedObjectType.compare` checks delegates by identity. Threads making a map's first key comparison together could each install their own and fail with `UnsupportedOperationException: Can not compare`. The code is the same in h2 2.4.240 and 2.5.250.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright (c) 2017-2021 Nitrite author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package org.dizitart.no2.integration.collection;
19+
20+
import org.dizitart.no2.Nitrite;
21+
import org.dizitart.no2.collection.Document;
22+
import org.dizitart.no2.collection.NitriteCollection;
23+
import org.dizitart.no2.filters.FluentFilter;
24+
import org.dizitart.no2.index.IndexOptions;
25+
import org.dizitart.no2.index.IndexType;
26+
import org.dizitart.no2.mvstore.MVStoreModule;
27+
import org.junit.Test;
28+
29+
import java.io.File;
30+
import java.util.List;
31+
import java.util.concurrent.CopyOnWriteArrayList;
32+
import java.util.concurrent.CyclicBarrier;
33+
34+
import static org.junit.Assert.assertEquals;
35+
36+
/**
37+
* Regression test for <a href="https://github.com/nitrite/nitrite-java/issues/1315">Issue 1315</a>.
38+
* <p>
39+
* On 5.3.0 every close left an empty legacy index map behind, and the first finds on a
40+
* reopened file database raced to migrate and drop it, failing with a
41+
* {@code NullPointerException} in {@code NitriteMVStore.removeMap}. Fixed by #1295 and #1309.
42+
*/
43+
public class Issue1315Test {
44+
45+
private static Nitrite open(File file) {
46+
return Nitrite.builder()
47+
.loadModule(MVStoreModule.withConfig().filePath(file.getPath()).build())
48+
.openOrCreate();
49+
}
50+
51+
@Test
52+
public void firstUseOfANonUniqueIndexFromSeveralThreads() throws Exception {
53+
File file = File.createTempFile("nitrite-legacy-race", ".db");
54+
file.delete();
55+
try {
56+
Nitrite db = open(file);
57+
NitriteCollection items = db.getCollection("items");
58+
items.createIndex(IndexOptions.indexOptions(IndexType.NON_UNIQUE), "state");
59+
items.insert(Document.createDocument("state", "new"));
60+
db.close();
61+
62+
int threads = 4, openings = 100;
63+
List<Throwable> failures = new CopyOnWriteArrayList<>();
64+
for (int opening = 0; opening < openings; opening++) {
65+
Nitrite reopened = open(file);
66+
NitriteCollection collection = reopened.getCollection("items");
67+
CyclicBarrier start = new CyclicBarrier(threads);
68+
Thread[] workers = new Thread[threads];
69+
for (int i = 0; i < threads; i++) {
70+
workers[i] = new Thread(() -> {
71+
try {
72+
start.await();
73+
assertEquals(1, collection.find(FluentFilter.where("state").eq("new")).toList().size());
74+
} catch (Throwable t) {
75+
failures.add(t);
76+
}
77+
});
78+
workers[i].start();
79+
}
80+
for (Thread worker : workers) worker.join();
81+
reopened.close();
82+
}
83+
assertEquals(failures.toString(), 0, failures.size());
84+
} finally {
85+
file.delete();
86+
}
87+
}
88+
}

0 commit comments

Comments
 (0)