diff --git a/src/main/java/org/dataloader/DataLoader.java b/src/main/java/org/dataloader/DataLoader.java index d80823f..31e6a2c 100644 --- a/src/main/java/org/dataloader/DataLoader.java +++ b/src/main/java/org/dataloader/DataLoader.java @@ -229,6 +229,12 @@ public Optional> getIfCompleted(K key) { * @return the future of the value */ public CompletableFuture load(@NonNull K key, @Nullable Object keyContext) { + CompletableFuture result = loadImpl(key, keyContext); + options.getDispatchStrategy().loadCalled(this); + return result; + } + + private CompletableFuture loadImpl(@NonNull K key, @Nullable Object keyContext) { return helper.load(nonNull(key), keyContext); } @@ -275,8 +281,9 @@ public CompletableFuture> loadMany(List keys, List keyContext if (i < keyContexts.size()) { keyContext = keyContexts.get(i); } - collect.add(load(key, keyContext)); + collect.add(loadImpl(key, keyContext)); } + options.getDispatchStrategy().loadCalled(this); return CompletableFutureKit.allOf(collect); } @@ -302,8 +309,9 @@ public CompletableFuture> loadMany(Map keysAndContexts) { for (Map.Entry entry : keysAndContexts.entrySet()) { K key = entry.getKey(); Object keyContext = entry.getValue(); - collect.put(key, load(key, keyContext)); + collect.put(key, loadImpl(key, keyContext)); } + options.getDispatchStrategy().loadCalled(this); return CompletableFutureKit.allOf(collect); } diff --git a/src/main/java/org/dataloader/DataLoaderOptions.java b/src/main/java/org/dataloader/DataLoaderOptions.java index f7c006f..e8628ab 100644 --- a/src/main/java/org/dataloader/DataLoaderOptions.java +++ b/src/main/java/org/dataloader/DataLoaderOptions.java @@ -55,6 +55,7 @@ public class DataLoaderOptions { private final ValueCacheOptions valueCacheOptions; private final BatchLoaderScheduler batchLoaderScheduler; private final DataLoaderInstrumentation instrumentation; + private final DispatchStrategy dispatchStrategy; /** * Creates a new data loader options with default settings. @@ -72,6 +73,7 @@ public DataLoaderOptions() { valueCacheOptions = DEFAULT_VALUE_CACHE_OPTIONS; batchLoaderScheduler = null; instrumentation = DataLoaderInstrumentationHelper.NOOP_INSTRUMENTATION; + dispatchStrategy = DispatchStrategy.NO_OP; } private DataLoaderOptions(Builder builder) { @@ -87,6 +89,7 @@ private DataLoaderOptions(Builder builder) { this.valueCacheOptions = builder.valueCacheOptions; this.batchLoaderScheduler = builder.batchLoaderScheduler; this.instrumentation = builder.instrumentation; + this.dispatchStrategy = builder.dispatchStrategy; } /** @@ -116,6 +119,7 @@ public static DataLoaderOptions.Builder newOptions(DataLoaderOptions otherOption * Will transform the current options in to a builder ands allow you to build a new set of options * * @param builderConsumer the consumer of a builder that has this objects starting values + * * @return a new {@link DataLoaderOptions} object */ public DataLoaderOptions transform(Consumer builderConsumer) { @@ -126,19 +130,21 @@ public DataLoaderOptions transform(Consumer builderConsumer) { @Override public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; + if (o == null || getClass() != o.getClass()) { + return false; + } DataLoaderOptions that = (DataLoaderOptions) o; return batchingEnabled == that.batchingEnabled - && cachingEnabled == that.cachingEnabled - && cachingExceptionsEnabled == that.cachingExceptionsEnabled - && maxBatchSize == that.maxBatchSize - && Objects.equals(cacheKeyFunction, that.cacheKeyFunction) && - Objects.equals(cacheMap, that.cacheMap) && - Objects.equals(valueCache, that.valueCache) && - Objects.equals(statisticsCollector, that.statisticsCollector) && - Objects.equals(environmentProvider, that.environmentProvider) && - Objects.equals(valueCacheOptions, that.valueCacheOptions) && - Objects.equals(batchLoaderScheduler, that.batchLoaderScheduler); + && cachingEnabled == that.cachingEnabled + && cachingExceptionsEnabled == that.cachingExceptionsEnabled + && maxBatchSize == that.maxBatchSize + && Objects.equals(cacheKeyFunction, that.cacheKeyFunction) && + Objects.equals(cacheMap, that.cacheMap) && + Objects.equals(valueCache, that.valueCache) && + Objects.equals(statisticsCollector, that.statisticsCollector) && + Objects.equals(environmentProvider, that.environmentProvider) && + Objects.equals(valueCacheOptions, that.valueCacheOptions) && + Objects.equals(batchLoaderScheduler, that.batchLoaderScheduler); } @@ -254,7 +260,12 @@ public DataLoaderInstrumentation getInstrumentation() { return instrumentation; } + public DispatchStrategy getDispatchStrategy() { + return dispatchStrategy; + } + public static class Builder { + private DispatchStrategy dispatchStrategy = DispatchStrategy.NO_OP; private boolean batchingEnabled; private boolean cachingEnabled; private boolean cachingExceptionsEnabled; @@ -285,12 +296,14 @@ public Builder() { this.valueCacheOptions = other.valueCacheOptions; this.batchLoaderScheduler = other.batchLoaderScheduler; this.instrumentation = other.instrumentation; + this.dispatchStrategy = other.dispatchStrategy; } /** * Sets the option that determines whether batch loading is enabled. * * @param batchingEnabled {@code true} to enable batch loading, {@code false} otherwise + * * @return this builder for fluent coding */ public Builder setBatchingEnabled(boolean batchingEnabled) { @@ -302,6 +315,7 @@ public Builder setBatchingEnabled(boolean batchingEnabled) { * Sets the option that determines whether caching is enabled. * * @param cachingEnabled {@code true} to enable caching, {@code false} otherwise + * * @return this builder for fluent coding */ public Builder setCachingEnabled(boolean cachingEnabled) { @@ -313,6 +327,7 @@ public Builder setCachingEnabled(boolean cachingEnabled) { * Sets the option that determines whether exceptional values are cache enabled. * * @param cachingExceptionsEnabled {@code true} to enable caching exceptional values, {@code false} otherwise + * * @return this builder for fluent coding */ public Builder setCachingExceptionsEnabled(boolean cachingExceptionsEnabled) { @@ -324,6 +339,7 @@ public Builder setCachingExceptionsEnabled(boolean cachingExceptionsEnabled) { * Sets the function to use for creating the cache key, if caching is enabled. * * @param cacheKeyFunction the cache key function to use + * * @return this builder for fluent coding */ public Builder setCacheKeyFunction(CacheKey cacheKeyFunction) { @@ -335,6 +351,7 @@ public Builder setCacheKeyFunction(CacheKey cacheKeyFunction) { * Sets the cache map implementation to use for caching, if caching is enabled. * * @param cacheMap the cache map instance + * * @return this builder for fluent coding */ public Builder setCacheMap(CacheMap cacheMap) { @@ -346,6 +363,7 @@ public Builder setCacheMap(CacheMap cacheMap) { * Sets the value cache implementation to use for caching values, if caching is enabled. * * @param valueCache the value cache instance + * * @return this builder for fluent coding */ public Builder setValueCache(ValueCache valueCache) { @@ -358,6 +376,7 @@ public Builder setValueCache(ValueCache valueCache) { * before they are split into multiple class * * @param maxBatchSize the maximum batch size + * * @return this builder for fluent coding */ public Builder setMaxBatchSize(int maxBatchSize) { @@ -371,6 +390,7 @@ public Builder setMaxBatchSize(int maxBatchSize) { * a common value * * @param statisticsCollector the statistics collector to use + * * @return this builder for fluent coding */ public Builder setStatisticsCollector(Supplier statisticsCollector) { @@ -382,6 +402,7 @@ public Builder setStatisticsCollector(Supplier statisticsCo * Sets the batch loader environment provider that will be used to give context to batch load functions * * @param environmentProvider the batch loader context provider + * * @return this builder for fluent coding */ public Builder setBatchLoaderContextProvider(BatchLoaderContextProvider environmentProvider) { @@ -393,6 +414,7 @@ public Builder setBatchLoaderContextProvider(BatchLoaderContextProvider environm * Sets the {@link ValueCacheOptions} that control how the {@link ValueCache} will be used * * @param valueCacheOptions the value cache options + * * @return this builder for fluent coding */ public Builder setValueCacheOptions(ValueCacheOptions valueCacheOptions) { @@ -405,6 +427,7 @@ public Builder setValueCacheOptions(ValueCacheOptions valueCacheOptions) { * to some future time. * * @param batchLoaderScheduler the scheduler + * * @return this builder for fluent coding */ public Builder setBatchLoaderScheduler(BatchLoaderScheduler batchLoaderScheduler) { @@ -416,6 +439,7 @@ public Builder setBatchLoaderScheduler(BatchLoaderScheduler batchLoaderScheduler * Sets in a new {@link DataLoaderInstrumentation} * * @param instrumentation the new {@link DataLoaderInstrumentation} + * * @return this builder for fluent coding */ public Builder setInstrumentation(DataLoaderInstrumentation instrumentation) { @@ -423,6 +447,11 @@ public Builder setInstrumentation(DataLoaderInstrumentation instrumentation) { return this; } + public Builder setDispatchStrategy(DispatchStrategy dispatchStrategy) { + this.dispatchStrategy = dispatchStrategy; + return this; + } + public DataLoaderOptions build() { return new DataLoaderOptions(this); } diff --git a/src/main/java/org/dataloader/DataLoaderRegistry.java b/src/main/java/org/dataloader/DataLoaderRegistry.java index 6bc79f6..028966e 100644 --- a/src/main/java/org/dataloader/DataLoaderRegistry.java +++ b/src/main/java/org/dataloader/DataLoaderRegistry.java @@ -46,24 +46,27 @@ public class DataLoaderRegistry { protected final Map> dataLoaders; protected final @Nullable DataLoaderInstrumentation instrumentation; + private final DispatchStrategy dispatchStrategy; + public DataLoaderRegistry() { - this(new ConcurrentHashMap<>(), null); + this(new ConcurrentHashMap<>(), null, DispatchStrategy.NO_OP); } private DataLoaderRegistry(Builder builder) { - this(builder.dataLoaders, builder.instrumentation); + this(builder.dataLoaders, builder.instrumentation, builder.dispatchStrategy); } - protected DataLoaderRegistry(Map> dataLoaders, @Nullable DataLoaderInstrumentation instrumentation) { + protected DataLoaderRegistry(Map> dataLoaders, @Nullable DataLoaderInstrumentation instrumentation, DispatchStrategy dispatchStrategy) { this.dataLoaders = instrumentDLs(dataLoaders, instrumentation); this.instrumentation = instrumentation; + this.dispatchStrategy = dispatchStrategy; } private Map> instrumentDLs(Map> incomingDataLoaders, @Nullable DataLoaderInstrumentation registryInstrumentation) { Map> dataLoaders = new ConcurrentHashMap<>(incomingDataLoaders); if (registryInstrumentation != null) { - dataLoaders.replaceAll((k, existingDL) -> nameAndInstrumentDL(k, registryInstrumentation, existingDL)); + dataLoaders.replaceAll((k, existingDL) -> nameAndInstrumentDL(k, registryInstrumentation, existingDL, dispatchStrategy)); } return dataLoaders; } @@ -74,9 +77,10 @@ protected DataLoaderRegistry(Map> dataLoaders, @Nullabl * @param key the key used to register the data loader * @param registryInstrumentation the common registry {@link DataLoaderInstrumentation} * @param existingDL the existing data loader + * * @return a new {@link DataLoader} or the same one if there is nothing to change */ - private static DataLoader nameAndInstrumentDL(String key, @Nullable DataLoaderInstrumentation registryInstrumentation, DataLoader existingDL) { + private static DataLoader nameAndInstrumentDL(String key, @Nullable DataLoaderInstrumentation registryInstrumentation, DataLoader existingDL, DispatchStrategy dispatchStrategy) { existingDL = checkAndSetName(key, existingDL); if (registryInstrumentation == null) { @@ -92,18 +96,18 @@ protected DataLoaderRegistry(Map> dataLoaders, @Nullabl } if (existingInstrumentation == DataLoaderInstrumentationHelper.NOOP_INSTRUMENTATION) { // replace it with the registry one - return mkInstrumentedDataLoader(existingDL, options, registryInstrumentation); + return mkInstrumentedDataLoader(existingDL, options, registryInstrumentation, dispatchStrategy); } if (existingInstrumentation instanceof ChainedDataLoaderInstrumentation) { // avoids calling a chained inside a chained DataLoaderInstrumentation newInstrumentation = ((ChainedDataLoaderInstrumentation) existingInstrumentation).prepend(registryInstrumentation); - return mkInstrumentedDataLoader(existingDL, options, newInstrumentation); + return mkInstrumentedDataLoader(existingDL, options, newInstrumentation, dispatchStrategy); } else { DataLoaderInstrumentation newInstrumentation = new ChainedDataLoaderInstrumentation().add(registryInstrumentation).add(existingInstrumentation); - return mkInstrumentedDataLoader(existingDL, options, newInstrumentation); + return mkInstrumentedDataLoader(existingDL, options, newInstrumentation, dispatchStrategy); } } else { - return mkInstrumentedDataLoader(existingDL, options, registryInstrumentation); + return mkInstrumentedDataLoader(existingDL, options, registryInstrumentation, dispatchStrategy); } } @@ -116,12 +120,12 @@ protected DataLoaderRegistry(Map> dataLoaders, @Nullabl return dataLoader; } - private static DataLoader mkInstrumentedDataLoader(DataLoader existingDL, DataLoaderOptions options, DataLoaderInstrumentation newInstrumentation) { - return existingDL.transform(builder -> builder.options(setInInstrumentation(options, newInstrumentation))); + private static DataLoader mkInstrumentedDataLoader(DataLoader existingDL, DataLoaderOptions options, DataLoaderInstrumentation newInstrumentation, DispatchStrategy dispatchStrategy) { + return existingDL.transform(builder -> builder.options(setInInstrumentation(options, newInstrumentation, dispatchStrategy))); } - private static DataLoaderOptions setInInstrumentation(DataLoaderOptions options, DataLoaderInstrumentation newInstrumentation) { - return options.transform(optionsBuilder -> optionsBuilder.setInstrumentation(newInstrumentation)); + private static DataLoaderOptions setInInstrumentation(DataLoaderOptions options, DataLoaderInstrumentation newInstrumentation, DispatchStrategy dispatchStrategy) { + return options.transform(optionsBuilder -> optionsBuilder.setInstrumentation(newInstrumentation).setDispatchStrategy(dispatchStrategy)); } /** @@ -140,11 +144,12 @@ private static DataLoaderOptions setInInstrumentation(DataLoaderOptions options, * object that was registered. * * @param dataLoader the named data loader to register + * * @return this registry */ public DataLoaderRegistry register(DataLoader dataLoader) { String name = Assertions.nonNull(dataLoader.getName(), () -> "The DataLoader must have a non null name"); - dataLoaders.put(name, nameAndInstrumentDL(name, instrumentation, dataLoader)); + dataLoaders.put(name, nameAndInstrumentDL(name, instrumentation, dataLoader, dispatchStrategy)); return this; } @@ -157,10 +162,11 @@ public DataLoaderRegistry register(DataLoader dataLoader) { * * @param key the key to put the data loader under * @param dataLoader the data loader to register + * * @return this registry */ public DataLoaderRegistry register(String key, DataLoader dataLoader) { - dataLoaders.put(key, nameAndInstrumentDL(key, instrumentation, dataLoader)); + dataLoaders.put(key, nameAndInstrumentDL(key, instrumentation, dataLoader, dispatchStrategy)); return this; } @@ -173,10 +179,11 @@ public DataLoaderRegistry register(String key, DataLoader dataLoader) { * * @param key the key to put the data loader under * @param dataLoader the data loader to register + * * @return the data loader instance that was registered */ public DataLoader registerAndGet(String key, DataLoader dataLoader) { - dataLoaders.put(key, nameAndInstrumentDL(key, instrumentation, dataLoader)); + dataLoaders.put(key, nameAndInstrumentDL(key, instrumentation, dataLoader, dispatchStrategy)); return Objects.requireNonNull(getDataLoader(key)); } @@ -195,6 +202,7 @@ public DataLoader registerAndGet(String key, DataLoader dataL * @param mappingFunction the function to compute a data loader * @param the type of keys * @param the type of values + * * @return a data loader */ @SuppressWarnings("unchecked") @@ -202,7 +210,7 @@ public DataLoader computeIfAbsent(final String key, final Function> mappingFunction) { return (DataLoader) dataLoaders.computeIfAbsent(key, (k) -> { DataLoader dl = mappingFunction.apply(k); - return nameAndInstrumentDL(key, instrumentation, dl); + return nameAndInstrumentDL(key, instrumentation, dl, dispatchStrategy); }); } @@ -211,6 +219,7 @@ public DataLoader computeIfAbsent(final String key, * and return a new combined registry * * @param registry the registry to combine into this registry + * * @return a new combined registry */ public DataLoaderRegistry combine(DataLoaderRegistry registry) { @@ -239,6 +248,7 @@ public DataLoaderRegistry combine(DataLoaderRegistry registry) { * This will unregister a new dataloader * * @param key the key of the data loader to unregister + * * @return this registry */ public DataLoaderRegistry unregister(String key) { @@ -252,6 +262,7 @@ public DataLoaderRegistry unregister(String key) { * @param key the key of the data loader * @param the type of keys * @param the type of values + * * @return a data loader or null if it's not present */ @SuppressWarnings("unchecked") @@ -322,6 +333,7 @@ public static Builder newRegistry() { public static class Builder { private final Map> dataLoaders = new HashMap<>(); + public DispatchStrategy dispatchStrategy = DispatchStrategy.NO_OP; private @Nullable DataLoaderInstrumentation instrumentation; /** @@ -329,6 +341,7 @@ public static class Builder { * * @param key the key to put the data loader under * @param dataLoader the data loader to register + * * @return this builder for a fluent pattern */ public Builder register(String key, DataLoader dataLoader) { @@ -341,6 +354,7 @@ public Builder register(String key, DataLoader dataLoader) { * from a previous {@link DataLoaderRegistry} * * @param otherRegistry the previous {@link DataLoaderRegistry} + * * @return this builder for a fluent pattern */ public Builder registerAll(DataLoaderRegistry otherRegistry) { @@ -353,6 +367,11 @@ public Builder instrumentation(DataLoaderInstrumentation instrumentation) { return this; } + public Builder dispatchStrategy(DispatchStrategy dispatchStrategy) { + this.dispatchStrategy = dispatchStrategy; + return this; + } + /** * @return the newly built {@link DataLoaderRegistry} */ diff --git a/src/main/java/org/dataloader/DispatchStrategy.java b/src/main/java/org/dataloader/DispatchStrategy.java new file mode 100644 index 0000000..8efed66 --- /dev/null +++ b/src/main/java/org/dataloader/DispatchStrategy.java @@ -0,0 +1,16 @@ +package org.dataloader; + +import org.dataloader.annotations.PublicApi; +import org.jspecify.annotations.NullMarked; + +@NullMarked +@PublicApi +public interface DispatchStrategy { + + DispatchStrategy NO_OP = new DispatchStrategy() { + }; + + default void loadCalled(DataLoader dataLoader) { + + } +} diff --git a/src/main/java/org/dataloader/NotBusyDispatchStrategy.java b/src/main/java/org/dataloader/NotBusyDispatchStrategy.java new file mode 100644 index 0000000..f43ec90 --- /dev/null +++ b/src/main/java/org/dataloader/NotBusyDispatchStrategy.java @@ -0,0 +1,185 @@ +package org.dataloader; + +import org.dataloader.annotations.PublicApi; +import org.jspecify.annotations.NullMarked; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A dispatch strategy that dispatches immediately if it is not busy and not currently dispatching. + *

+ * Busy is determined by a busy counter, which is > 0 if busy, or zero when not busy. + * The two methods to increase and decrease the busy counter are {@link #incrementBusyCount()} and {@link #decrementBusyCount()} + *

+ * This Strategy must be configured as part of {@link DispatchStrategy} + */ +@PublicApi +@NullMarked +public class NotBusyDispatchStrategy implements DispatchStrategy { + + + // 30 bits for busy counting + // 1 bit for dataLoaderToDispatch + // 1 bit for currentlyDispatching + + // Bit positions (from right to left) + static final int currentlyDispatchingShift = 0; + static final int dataLoaderToDispatchShift = 1; + static final int busyCountShift = 2; + + // mask + static final int booleanMask = 1; + static final int busyCountMask = (1 << 30) - 1; + + public static int getBusyCount(int state) { + return (state >> busyCountShift) & busyCountMask; + } + + public static int setBusyCount(int state, int busyCount) { + return (state & ~(busyCountMask << busyCountShift)) | + (busyCount << busyCountShift); + } + + public static int setDataLoaderToDispatch(int state, boolean dataLoaderToDispatch) { + return (state & ~(booleanMask << dataLoaderToDispatchShift)) | + ((dataLoaderToDispatch ? 1 : 0) << dataLoaderToDispatchShift); + } + + public static int setCurrentlyDispatching(int state, boolean currentlyDispatching) { + return (state & ~(booleanMask << currentlyDispatchingShift)) | + ((currentlyDispatching ? 1 : 0) << currentlyDispatchingShift); + } + + + public static boolean getDataLoaderToDispatch(int state) { + return ((state >> dataLoaderToDispatchShift) & booleanMask) != 0; + } + + public static boolean getCurrentlyDispatching(int state) { + return ((state >> currentlyDispatchingShift) & booleanMask) != 0; + } + + + private final AtomicInteger state = new AtomicInteger(); + private final DataLoaderRegistry dataLoaderRegistry; + + public NotBusyDispatchStrategy(DataLoaderRegistry dataLoaderRegistry) { + this.dataLoaderRegistry = dataLoaderRegistry; + } + + + private int incrementBusyCountImpl() { + while (true) { + int oldState = getState(); + int busyCount = getBusyCount(oldState); + int newState = setBusyCount(oldState, busyCount + 1); + if (tryUpdateState(oldState, newState)) { + return newState; + } + } + } + + private int decrementBusyCountImpl() { + while (true) { + int oldState = getState(); + int busyCount = getBusyCount(oldState); + int newState = setBusyCount(oldState, busyCount - 1); + if (tryUpdateState(oldState, newState)) { + return newState; + } + } + } + + private int getState() { + return state.get(); + } + + + private boolean tryUpdateState(int oldState, int newState) { + return state.compareAndSet(oldState, newState); + } + + + public void decrementBusyCount() { + int newState = decrementBusyCountImpl(); + if (getBusyCount(newState) == 0 && getDataLoaderToDispatch(newState) && !getCurrentlyDispatching(newState)) { + dispatchImpl(); + } + } + + public void incrementBusyCount() { + incrementBusyCountImpl(); + } + + + private void newDataLoaderInvocationMaybeDispatch() { + int currentState; + while (true) { + int oldState = getState(); + if (getDataLoaderToDispatch(oldState)) { + return; + } + int newState = setDataLoaderToDispatch(oldState, true); + if (tryUpdateState(oldState, newState)) { + currentState = newState; + break; + } + } + + if (getBusyCount(currentState) == 0 && !getCurrentlyDispatching(currentState)) { + dispatchImpl(); + } + } + + + private void dispatchImpl() { + while (true) { + int oldState = getState(); + if (!getDataLoaderToDispatch(oldState)) { + int newState = setCurrentlyDispatching(oldState, false); + if (tryUpdateState(oldState, newState)) { + return; + } + } + int newState = setCurrentlyDispatching(oldState, true); + newState = setDataLoaderToDispatch(newState, false); + if (tryUpdateState(oldState, newState)) { + break; + } + } + + List> dataLoaders = dataLoaderRegistry.getDataLoaders(); + List>> allDispatchedCFs = new ArrayList<>(); + for (DataLoader dataLoader : dataLoaders) { + CompletableFuture> dispatch = dataLoader.dispatch(); + allDispatchedCFs.add(dispatch); + } + CompletableFuture.allOf(allDispatchedCFs.toArray(new CompletableFuture[0])) + .whenComplete((unused, throwable) -> { + dispatchImpl(); + }); + + } + + @Override + public void loadCalled(DataLoader dataLoader) { + newDataLoaderInvocationMaybeDispatch(); + } + + private static String printState(int state) { + return "busyCount= " + getBusyCount(state) + + ",dataLoaderToDispatch= " + getDataLoaderToDispatch(state) + + ",currentlyDispatching= " + getCurrentlyDispatching(state); + } + + @Override + public String toString() { + return "NotBusyDispatchStrategy{" + + "state=" + printState(getState()) + + ", dataLoaderRegistry=" + dataLoaderRegistry + + '}'; + } +}