Skip to content

Commit 51f128e

Browse files
fix: one index instance and one map wrapper per name, and a drop that cannot run twice (#1309)
The first read of a non-unique index after a store reopens runs the lazy layout migration in SingleFieldIndex, which drops the legacy map. That migration is guarded per SingleFieldIndex instance, but ComparableIndexer.findNitriteIndex created instances with an unsynchronized check-then-act, so several threads arriving at once each got their own instance and each ran the migration. NitriteMVMap.drop() likewise tested its dropped flag and then ignored the result of its compare-and-set, and NitriteMVStore.openMap handed out one wrapper per caller. Once the first thread had removed the map, MVMap.getName() answered null for the rest, and NitriteMVStore.removeMap(null) died in ConcurrentHashMap.remove: NullPointerException: Cannot invoke "Object.hashCode()" because "key" is null at NitriteMVStore.removeMap at NitriteMVMap.drop at SingleFieldIndex.migrateLegacyIndex A second shape of the same race is a NullPointerException from Attributes.set via NitriteMap.updateLastModifiedTime, which read getName() several times. Seen on a production system on the first multi-threaded lookup after every restart, since every close left an empty map under the legacy name (fixed by #1295) and every start had to drop it again. - ComparableIndexer registers the index with computeIfAbsent, so the per-instance guard in SingleFieldIndex is the guard. - NitriteMVStore.openMap and openRTree register the wrapper with computeIfAbsent, so every holder shares one dropped flag; removeMap ignores a null name and does not open (and thereby create) a map that is no longer in the store. - NitriteMVMap captures its name at open and acts only when its compare-and-set succeeds, so drop() and close() run once and never ask MVMap for a name it no longer has. - NitriteMap.updateLastModifiedTime reads the name once. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent b3ca3d6 commit 51f128e

4 files changed

Lines changed: 44 additions & 44 deletions

File tree

nitrite-mvstore-adapter/src/main/java/org/dizitart/no2/mvstore/NitriteMVMap.java

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
class NitriteMVMap<Key, Value> implements NitriteMap<Key, Value> {
4747

4848
private final MVMap<Key, Value> mvMap;
49+
// captured at open: MVMap.getName() answers null once the map is removed from the store,
50+
// which is exactly when the registry and the catalog still have to be told the name
51+
private final String name;
4952
private final NitriteStore<?> nitriteStore;
5053
private final MVStore mvStore;
5154
private final AtomicBoolean droppedFlag;
@@ -54,6 +57,7 @@ class NitriteMVMap<Key, Value> implements NitriteMap<Key, Value> {
5457

5558
NitriteMVMap(final MVMap<Key, Value> mvMap, final NitriteStore<?> nitriteStore) {
5659
this.mvMap = mvMap;
60+
this.name = mvMap.getName();
5761
this.nitriteStore = nitriteStore;
5862
this.mvStore = mvMap.getStore();
5963
this.closedFlag = new AtomicBoolean(false);
@@ -89,7 +93,7 @@ public void clear() {
8993

9094
@Override
9195
public String getName() {
92-
return mvMap.getName();
96+
return name;
9397
}
9498

9599
@Override
@@ -254,15 +258,16 @@ public boolean isEmpty() {
254258

255259
@Override
256260
public void drop() {
257-
if (!droppedFlag.get()) {
258-
droppedFlag.compareAndSet(false, true);
259-
closedFlag.compareAndSet(false, true);
261+
// the compare-and-set is the guard: two threads that both saw the flag clear must not
262+
// both remove the map
263+
if (droppedFlag.compareAndSet(false, true)) {
264+
closedFlag.set(true);
260265
releaseVersionUsages();
261266

262267
final MVStore.TxCounter txCounter = mvStore.registerVersionUsage();
263268
try {
264-
nitriteStore.closeMap(mvMap.getName());
265-
nitriteStore.removeMap(mvMap.getName());
269+
nitriteStore.closeMap(name);
270+
nitriteStore.removeMap(name);
266271
} finally {
267272
mvStore.deregisterVersionUsage(txCounter);
268273
}
@@ -276,10 +281,9 @@ public boolean isDropped() {
276281

277282
@Override
278283
public void close() {
279-
if (!closedFlag.get() && !droppedFlag.get()) {
280-
closedFlag.compareAndSet(false, true);
284+
if (!droppedFlag.get() && closedFlag.compareAndSet(false, true)) {
281285
releaseVersionUsages();
282-
nitriteStore.closeMap(mvMap.getName());
286+
nitriteStore.closeMap(name);
283287
}
284288
}
285289

nitrite-mvstore-adapter/src/main/java/org/dizitart/no2/mvstore/NitriteMVStore.java

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -147,14 +147,12 @@ public boolean hasMap(String mapName) {
147147
@Override
148148
@SuppressWarnings("unchecked")
149149
public <Key, Value> NitriteMap<Key, Value> openMap(String mapName, Class<?> keyType, Class<?> valueType) {
150-
if (nitriteMapRegistry.containsKey(mapName)) {
151-
return (NitriteMVMap<Key, Value>) nitriteMapRegistry.get(mapName);
152-
}
153-
154-
MVMap<Key, Value> mvMap = openMVMap(mapName, null);
155-
NitriteMVMap<Key, Value> nitriteMVMap = new NitriteMVMap<>(mvMap, this);
156-
nitriteMapRegistry.put(mapName, nitriteMVMap);
157-
return nitriteMVMap;
150+
// one wrapper per map, however many threads open it at once, so a drop() or close()
151+
// through any holder is the drop or close every holder sees
152+
return (NitriteMVMap<Key, Value>) nitriteMapRegistry.computeIfAbsent(mapName, name -> {
153+
MVMap<Key, Value> mvMap = openMVMap(name, null);
154+
return new NitriteMVMap<>(mvMap, this);
155+
});
158156
}
159157

160158
@Override
@@ -173,8 +171,15 @@ public void closeRTree(String rTreeName) {
173171

174172
@Override
175173
public void removeMap(String name) {
176-
MVMap<?, ?> mvMap = openMVMap(name, null);
177-
mvStore.removeMap(mvMap);
174+
if (StringUtils.isNullOrEmpty(name)) {
175+
return;
176+
}
177+
// a map another thread has already removed is simply gone; openMVMap would create an
178+
// empty map of that name only to remove it again
179+
if (mvStore.hasMap(name)) {
180+
MVMap<?, ?> mvMap = openMVMap(name, null);
181+
mvStore.removeMap(mvMap);
182+
}
178183
getCatalog().remove(name);
179184
nitriteMapRegistry.remove(name);
180185
}
@@ -191,14 +196,10 @@ public void removeRTree(String rTreeName) {
191196
@Override
192197
@SuppressWarnings({"unchecked", "rawtypes"})
193198
public <Key extends BoundingBox, Value> NitriteRTree<Key, Value> openRTree(String mapName, Class<?> keyType, Class<?> valueType) {
194-
if (nitriteRTreeMapRegistry.containsKey(mapName)) {
195-
return (NitriteMVRTreeMap) nitriteRTreeMapRegistry.get(mapName);
196-
}
197-
198-
MVRTreeMap<Value> map = (MVRTreeMap<Value>) openMVMap(mapName, new MVRTreeMap.Builder<>());
199-
NitriteMVRTreeMap<Key, Value> nitriteMVRTreeMap = new NitriteMVRTreeMap(map, this);
200-
nitriteRTreeMapRegistry.put(mapName, nitriteMVRTreeMap);
201-
return nitriteMVRTreeMap;
199+
return (NitriteMVRTreeMap) nitriteRTreeMapRegistry.computeIfAbsent(mapName, name -> {
200+
MVRTreeMap<Value> map = (MVRTreeMap<Value>) openMVMap(name, new MVRTreeMap.Builder<>());
201+
return new NitriteMVRTreeMap(map, this);
202+
});
202203
}
203204

204205
@Override

nitrite/src/main/java/org/dizitart/no2/index/ComparableIndexer.java

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,11 @@ private NitriteIndex findNitriteIndex(IndexDescriptor indexDescriptor, NitriteCo
105105
throw new IndexingException("Index descriptor cannot be null");
106106
}
107107

108-
if (indexRegistry.containsKey(indexDescriptor)) {
109-
return indexRegistry.get(indexDescriptor);
110-
}
111-
112-
NitriteIndex nitriteIndex;
113-
if (indexDescriptor.isCompoundIndex()) {
114-
nitriteIndex = new CompoundIndex(indexDescriptor, nitriteConfig.getNitriteStore());
115-
} else {
116-
nitriteIndex = new SingleFieldIndex(indexDescriptor, nitriteConfig.getNitriteStore());
117-
}
118-
indexRegistry.put(indexDescriptor, nitriteIndex);
119-
return nitriteIndex;
108+
// One instance per descriptor, however many threads ask for it at once. The lazy layout
109+
// migration in SingleFieldIndex is guarded per instance, so two instances for the same
110+
// index would each migrate and drop the legacy map, and the second drop fails.
111+
return indexRegistry.computeIfAbsent(indexDescriptor, descriptor -> descriptor.isCompoundIndex()
112+
? new CompoundIndex(descriptor, nitriteConfig.getNitriteStore())
113+
: new SingleFieldIndex(descriptor, nitriteConfig.getNitriteStore()));
120114
}
121115
}

nitrite/src/main/java/org/dizitart/no2/store/NitriteMap.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -243,15 +243,16 @@ default void setAttributes(Attributes attributes) {
243243
*/
244244
default void updateLastModifiedTime() {
245245
if (!isDropped()) {
246-
if (isNullOrEmpty(getName())
247-
|| META_MAP_NAME.equals(getName())) return;
246+
// read once: an adapter may answer null as soon as the map is removed from the store
247+
String name = getName();
248+
if (isNullOrEmpty(name) || META_MAP_NAME.equals(name)) return;
248249

249250
NitriteMap<String, Attributes> metaMap = getStore().openMap(META_MAP_NAME, String.class, Attributes.class);
250251
if (metaMap != null) {
251-
Attributes attributes = metaMap.get(getName());
252+
Attributes attributes = metaMap.get(name);
252253
if (attributes == null) {
253-
attributes = new Attributes(getName());
254-
metaMap.put(getName(), attributes);
254+
attributes = new Attributes(name);
255+
metaMap.put(name, attributes);
255256
}
256257
attributes.set(Attributes.LAST_MODIFIED_TIME, Long.toString(System.currentTimeMillis()));
257258
}

0 commit comments

Comments
 (0)