Skip to content
Open
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
13 changes: 13 additions & 0 deletions src/main/java/net/datafaker/Faker.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,17 @@ public Faker(FakeValuesService fakeValuesService, FakerContext context) {
public Faker(FakeValuesService fakeValuesService, FakerContext context, Predicate<Class<?>> whiteListPredicate) {
super(fakeValuesService, context, whiteListPredicate);
}

/**
* Creates a {@link Faker} backed by a shared {@link FakeValuesService}.
* Use in multi-threaded scenarios where all threads share one service per locale
* and each supplies its own {@link Random} to avoid redundant YAML loading.
*
* @param sharedService a pre-initialized service, e.g. from {@link FakeValuesService#getShared(Locale)}
* @param locale locale for this Faker instance
* @param random per-thread random source
*/
public static Faker withSharedService(FakeValuesService sharedService, Locale locale, Random random) {
return new Faker(sharedService, new FakerContext(locale, new RandomService(random)));
}
}
34 changes: 32 additions & 2 deletions src/main/java/net/datafaker/service/FakeValuesService.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
Expand Down Expand Up @@ -83,7 +84,32 @@ public class FakeValuesService {

private static final Map<String, String[]> EXPRESSION_2_SPLITTED = new CopyOnWriteMap<>(WeakHashMap::new);

private static final ConcurrentHashMap<Locale, FakeValuesService> SHARED_INSTANCES = new ConcurrentHashMap<>();

private volatile boolean shared = false;

private final Map<RegExpContext, ValueResolver> REGEXP2SUPPLIER_MAP = new CopyOnWriteMap<>(HashMap::new);

/**
* Returns a lazily-initialized per-locale singleton safe to share across threads.
* <p>
* The {@code locale} is used as a cache-partition key: all callers passing the same
* locale receive the same instance. Locale-specific YAML data is loaded lazily the
* first time a Faker backed by this service is constructed, so there is no need to
* pre-warm the instance.
* <p>
* Shared instances are read-only: {@link #addPath} and {@link #addUrl} will throw
* {@link UnsupportedOperationException}. Mixing locales — e.g. passing
* {@code getShared(Locale.ENGLISH)} to
* {@link net.datafaker.Faker#withSharedService(FakeValuesService, Locale, Random)}
* with a different locale — is unsupported.
*/
public static FakeValuesService getShared(Locale locale) {
FakeValuesService svc = SHARED_INSTANCES.computeIfAbsent(locale, l -> new FakeValuesService());
svc.shared = true;
return svc;
}
Comment thread
mferretti marked this conversation as resolved.

public void updateFakeValuesInterfaceMap(List<SingletonLocale> locales) {
for (final SingletonLocale l : locales) {
fakeValuesInterfaceMap.computeIfAbsent(l, this::getCachedFakeValue);
Expand All @@ -103,9 +129,11 @@ private FakeValuesInterface getCachedFakeValue(SingletonLocale locale) {
*
* @param locale the locale for which a path is going to be added.
* @param path path to a file with YAML structure
* @throws IllegalArgumentException in case of invalid path
* @throws IllegalArgumentException in case of invalid path
* @throws UnsupportedOperationException if called on a shared instance obtained via {@link #getShared(Locale)}
*/
public void addPath(Locale locale, Path path) {
if (shared) throw new UnsupportedOperationException("addPath cannot be called on a shared FakeValuesService");
requireNonNull(locale);
if (path == null || Files.notExists(path) || Files.isDirectory(path) || !Files.isReadable(path)) {
throw new IllegalArgumentException("Path should be an existing readable file: \"%s\"".formatted(path));
Expand All @@ -122,9 +150,11 @@ public void addPath(Locale locale, Path path) {
*
* @param locale the locale for which an url is going to be added.
* @param url url of a file with YAML structure
* @throws IllegalArgumentException in case of invalid url
* @throws IllegalArgumentException in case of invalid url
* @throws UnsupportedOperationException if called on a shared instance obtained via {@link #getShared(Locale)}
*/
public void addUrl(Locale locale, URL url) {
if (shared) throw new UnsupportedOperationException("addUrl cannot be called on a shared FakeValuesService");
requireNonNull(locale);
if (url == null) {
throw new IllegalArgumentException("url should be an existing readable file");
Expand Down
89 changes: 89 additions & 0 deletions src/test/java/net/datafaker/SharedFakeValuesServiceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package net.datafaker;

import net.datafaker.service.FakeValuesService;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class SharedFakeValuesServiceTest {

@Test
void getSharedReturnsSameInstanceUnderConcurrency() throws Exception {
int threads = 8;
ExecutorService pool = Executors.newFixedThreadPool(threads);
CyclicBarrier barrier = new CyclicBarrier(threads);
List<Future<FakeValuesService>> futures = new ArrayList<>();
for (int i = 0; i < threads; i++) {
futures.add(pool.submit(() -> {
barrier.await();
return FakeValuesService.getShared(Locale.ENGLISH);
}));
}
pool.shutdown();
assertThat(pool.awaitTermination(10, TimeUnit.SECONDS)).isTrue();
FakeValuesService expected = futures.get(0).get();
for (Future<FakeValuesService> f : futures) {
assertThat(f.get()).isSameAs(expected);
}
}

@Test
void concurrentFakersWithSharedServiceProduceNoErrors() throws Exception {
FakeValuesService shared = FakeValuesService.getShared(Locale.ENGLISH);
int threads = 16;
int iterations = 10_000;
ExecutorService pool = Executors.newFixedThreadPool(threads);
CyclicBarrier barrier = new CyclicBarrier(threads);
List<Future<Void>> futures = new ArrayList<>();
for (int i = 0; i < threads; i++) {
final long seed = i;
futures.add(pool.submit(() -> {
barrier.await();
Faker faker = Faker.withSharedService(shared, Locale.ENGLISH, new Random(seed));
for (int j = 0; j < iterations; j++) {
assertThat(faker.name().fullName()).isNotNull();
assertThat(faker.address().city()).isNotNull();
assertThat(faker.internet().emailAddress()).isNotNull();
}
return null;
}));
}
pool.shutdown();
assertThat(pool.awaitTermination(120, TimeUnit.SECONDS)).isTrue();
for (Future<Void> f : futures) {
f.get();
}
}

@Test
void sharedInstanceRejectsAddPathAndAddUrl() throws Exception {
FakeValuesService shared = FakeValuesService.getShared(Locale.GERMAN);
assertThatThrownBy(() -> shared.addPath(Locale.GERMAN, java.nio.file.Path.of("nonexistent.yml")))
.isInstanceOf(UnsupportedOperationException.class);
java.net.URL url = new java.net.URI("file:///nonexistent.yml").toURL();
assertThatThrownBy(() -> shared.addUrl(Locale.GERMAN, url))
.isInstanceOf(UnsupportedOperationException.class);
}

@Test
void withSharedServiceOutputMatchesNormalFaker() {
long seed = 12345L;
Faker normal = new Faker(Locale.ENGLISH, new Random(seed));
Faker shared = Faker.withSharedService(
FakeValuesService.getShared(Locale.ENGLISH), Locale.ENGLISH, new Random(seed));
assertThat(shared.name().firstName()).isEqualTo(normal.name().firstName());
assertThat(shared.address().city()).isEqualTo(normal.address().city());
assertThat(shared.internet().emailAddress()).isEqualTo(normal.internet().emailAddress());
}
}
Loading