Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package dev.morphia.mapping.codec;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;

import com.mongodb.lang.Nullable;

Expand Down Expand Up @@ -35,7 +35,8 @@
*/
@MorphiaInternal
public class MorphiaCodecProvider implements CodecProvider {
private final Map<Class<?>, Codec<?>> codecs = new HashMap<>();
// Looked up concurrently. Not computeIfAbsent: building a codec looks up other codecs, which may land back here.
private final Map<Class<?>, Codec<?>> codecs = new ConcurrentHashMap<>();
private final Mapper mapper;
private final Conversions conversions;
private final List<PropertyCodecProvider> propertyCodecProviders = new ArrayList<>();
Expand Down Expand Up @@ -74,7 +75,11 @@ public <T> Codec<T> get(Class<T> type, CodecRegistry registry) {
if (model.hasLifecycle(PreLoad.class) || model.hasLifecycle(PostLoad.class) || mapper.hasListeners()) {
codec.setDecoder(new LifecycleDecoder(codec));
}
codecs.put(type, codec);
// another thread may have built one meanwhile; keep the first so every caller shares it
MorphiaCodec<T> existing = (MorphiaCodec<T>) codecs.putIfAbsent(type, codec);
if (existing != null) {
codec = existing;
}
}

return codec;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

Expand All @@ -25,6 +29,7 @@
import dev.morphia.config.MorphiaConfig;
import dev.morphia.mapping.DiscriminatorFunction;
import dev.morphia.mapping.MapperType;
import dev.morphia.mapping.codec.MorphiaCodecProvider;
import dev.morphia.mapping.codec.pojo.EntityModel;
import dev.morphia.mapping.codec.pojo.PropertyModel;
import dev.morphia.mapping.codec.pojo.critter.CritterEntityModel;
Expand Down Expand Up @@ -72,6 +77,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

Expand Down Expand Up @@ -226,6 +232,36 @@ void modelHashesAreStableAcrossEncoding() {
}
}

@Test
void concurrentCodecLookupsShareOneCodec() throws Exception {
int threads = 8;
ExecutorService executor = Executors.newFixedThreadPool(threads);
try {
for (MapperType mapper : MapperType.values()) {
MorphiaDatastore datastore = datastore(Variant.DEFAULTS, mapper);
for (int round = 0; round < 20; round++) {
MorphiaCodecProvider provider = new MorphiaCodecProvider(datastore);
CountDownLatch start = new CountDownLatch(1);
List<Future<Codec<HarnessScalars>>> results = new ArrayList<>();
for (int i = 0; i < threads; i++) {
results.add(executor.submit(() -> {
start.await();
return provider.get(HarnessScalars.class, datastore.getCodecRegistry());
}));
}
start.countDown();

Codec<HarnessScalars> first = results.get(0).get();
for (Future<Codec<HarnessScalars>> result : results) {
assertSame(first, result.get(), mapper + ": every lookup should get the one cached codec");
}
}
}
} finally {
executor.shutdownNow();
}
}

@Test
void readsLoadOnlyProperties() {
// @LoadOnly values are never written, so the fixtures can't show that they are read
Expand Down
2 changes: 1 addition & 1 deletion design/mapping-game-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ structural item covers are moved to that item (see Phases 4 and 5).
This also removes the deep `equals` cost on `CopyOnWriteArraySet.add` and in `EntityEncoder`.
- Audit tests for any that rely on structural equality.

### 2.8 `MorphiaCodecProvider.codecs` is a plain `HashMap` (D8)
### ~~2.8 `MorphiaCodecProvider.codecs` is a plain `HashMap` (D8)~~
- Switch to `ConcurrentHashMap`, using `get` + `putIfAbsent`. Not `computeIfAbsent`, because codec construction
recurses into other codec lookups.

Expand Down
Loading