[jOOQ/jOOQ#19003] Make DefaultExecuteContext available to internals via

ThreadLocal
This commit is contained in:
Lukas Eder 2025-09-05 11:06:49 +02:00
parent 08f853b002
commit e51d224da3
12 changed files with 376 additions and 37 deletions

View File

@ -276,7 +276,7 @@ abstract class AbstractQuery<R extends Record> extends AbstractAttachableQueryPa
// in case this Query / Configuration was previously
// deserialised
DefaultExecuteContext ctx = new DefaultExecuteContext(c, this);
ExecuteListener listener = ExecuteListeners.get(ctx);
ExecuteListener listener = ExecuteListeners.get(ctx, true);
int result = 0;
try {

View File

@ -650,7 +650,7 @@ implements
private final int executeCallableStatement() {
ExecuteContext ctx = new DefaultExecuteContext(configuration, this);
ExecuteListener listener = ExecuteListeners.get(ctx);
ExecuteListener listener = ExecuteListeners.get(ctx, true);
try {
// [#8968] Keep start() event inside of lifecycle management

View File

@ -105,7 +105,7 @@ final class BatchMultiple extends AbstractBatch {
return Stream.of(queries).mapToInt(configuration.dsl()::execute).toArray();
DefaultExecuteContext ctx = new DefaultExecuteContext(configuration, BatchMode.MULTIPLE, queries);
ExecuteListener listener = ExecuteListeners.get(ctx);
ExecuteListener listener = ExecuteListeners.get(ctx, true);
try {

View File

@ -199,7 +199,7 @@ final class BatchSingle extends AbstractBatch implements BatchBindStep {
private final int[] executePrepared() {
DefaultExecuteContext ctx = new DefaultExecuteContext(configuration, BatchMode.SINGLE, new Query[] { query });
ExecuteListener listener = ExecuteListeners.get(ctx);
ExecuteListener listener = ExecuteListeners.get(ctx, true);
try {
// [#8968] Keep start() event inside of lifecycle management

View File

@ -41,11 +41,9 @@ import static org.jooq.tools.StringUtils.defaultIfNull;
import java.io.Serializable;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import org.jooq.impl.CacheType;
import org.jooq.Configuration;
/**
@ -74,17 +72,38 @@ final class Cache {
if (!type.category.predicate.test(configuration.settings()))
return operation.get();
Object cacheOrNull = configuration.data(type);
if (cacheOrNull == null) {
synchronized (type) {
cacheOrNull = configuration.data(type);
Object cacheOrNull = null;
DefaultExecuteContext ctx = null;
if (cacheOrNull == null)
configuration.data(type, cacheOrNull = defaultIfNull(
configuration.cacheProvider().provide(new DefaultCacheContext(configuration, type)),
NULL
));
// [#19003] If a cache type allows for being lazy copied, then the current ExecuteContext
// may hold a smaller instance of the cache containing only the data relevant
// to the current execution, not the globally available data. Accessing this
// thread-bound local copy is much faster as there is no contention among threads
// when the data is being accessed repeatedly, as for example DefaultRecordMapper
// instances when using heavily nested collection mapping.
if (type.lazyCopy)
ctx = DefaultExecuteContext.globalExecuteContext();
if (ctx != null)
cacheOrNull = ctx.data(type);
if (cacheOrNull == null) {
cacheOrNull = configuration.data(type);
if (cacheOrNull == null) {
synchronized (type) {
cacheOrNull = configuration.data(type);
if (cacheOrNull == null)
configuration.data(type, cacheOrNull = defaultIfNull(
configuration.cacheProvider().provide(new DefaultCacheContext(configuration, type)),
NULL
));
}
}
if (ctx != null && cacheOrNull != NULL)
ctx.data(type, cacheOrNull = new LazyCopyMap<>((Map<Object, Object>) cacheOrNull));
}
if (cacheOrNull == NULL)
@ -94,8 +113,6 @@ final class Cache {
// contract. However since we cannot use ConcurrentHashMap.computeIfAbsent()
// recursively, we have to revert to double checked locking nonetheless.
// See also: https://stackoverflow.com/q/28840047/521799
// [#18999] Our new ConcurrentReadWriteMap could deadlock when nesting writes
// in internal iterations
Map<Object, Object> cache = (Map<Object, Object>) cacheOrNull;
Object k = key.get();
Object v = cache.get(k);

View File

@ -42,11 +42,13 @@ import static org.jooq.impl.CacheType.CacheCategory.PARSING_CONNECTION;
import static org.jooq.impl.CacheType.CacheCategory.RECORD_MAPPER;
import static org.jooq.impl.CacheType.CacheCategory.REFLECTION;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Predicate;
import org.jooq.CacheProvider;
import org.jooq.Configuration;
import org.jooq.DSLContext;
import org.jooq.ExecuteContext;
import org.jooq.RecordMapper;
import org.jooq.RecordType;
import org.jooq.conf.Settings;
@ -72,63 +74,78 @@ public enum CacheType {
* A reflection cache for lookups of JPA annotated getters in
* {@link DefaultRecordMapper}.
*/
REFLECTION_CACHE_GET_ANNOTATED_GETTER(REFLECTION, "org.jooq.configuration.reflection-cache.get-annotated-getter"),
REFLECTION_CACHE_GET_ANNOTATED_GETTER(REFLECTION, "org.jooq.configuration.reflection-cache.get-annotated-getter", false),
/**
* A reflection cache for lookups of JPA annotated members in
* {@link DefaultRecordMapper}.
*/
REFLECTION_CACHE_GET_ANNOTATED_MEMBERS(REFLECTION, "org.jooq.configuration.reflection-cache.get-annotated-members"),
REFLECTION_CACHE_GET_ANNOTATED_MEMBERS(REFLECTION, "org.jooq.configuration.reflection-cache.get-annotated-members", false),
/**
* A reflection cache for lookups of JPA annotated setters in
* {@link DefaultRecordMapper}.
*/
REFLECTION_CACHE_GET_ANNOTATED_SETTERS(REFLECTION, "org.jooq.configuration.reflection-cache.get-annotated-setters"),
REFLECTION_CACHE_GET_ANNOTATED_SETTERS(REFLECTION, "org.jooq.configuration.reflection-cache.get-annotated-setters", false),
/**
* A reflection cache for lookups of getters matched by name in
* {@link DefaultRecordMapper}.
*/
REFLECTION_CACHE_GET_MATCHING_GETTER(REFLECTION, "org.jooq.configuration.reflection-cache.get-matching-getter"),
REFLECTION_CACHE_GET_MATCHING_GETTER(REFLECTION, "org.jooq.configuration.reflection-cache.get-matching-getter", false),
/**
* A reflection cache for lookups of members matched by name in
* {@link DefaultRecordMapper}.
*/
REFLECTION_CACHE_GET_MATCHING_MEMBERS(REFLECTION, "org.jooq.configuration.reflection-cache.get-matching-members"),
REFLECTION_CACHE_GET_MATCHING_MEMBERS(REFLECTION, "org.jooq.configuration.reflection-cache.get-matching-members", false),
/**
* A reflection cache for lookups of setters matched by name in
* {@link DefaultRecordMapper}.
*/
REFLECTION_CACHE_GET_MATCHING_SETTERS(REFLECTION, "org.jooq.configuration.reflection-cache.get-matching-setters"),
REFLECTION_CACHE_GET_MATCHING_SETTERS(REFLECTION, "org.jooq.configuration.reflection-cache.get-matching-setters", false),
/**
* A reflection cache to check if a type has any JPA annotations at all, in
* {@link DefaultRecordMapper}.
*/
REFLECTION_CACHE_HAS_COLUMN_ANNOTATIONS(REFLECTION, "org.jooq.configuration.reflection-cache.has-column-annotations"),
REFLECTION_CACHE_HAS_COLUMN_ANNOTATIONS(REFLECTION, "org.jooq.configuration.reflection-cache.has-column-annotations", false),
/**
* A cache used by the {@link DefaultRecordMapperProvider} to cache all
* {@link RecordMapper} instances and their possibly expensive
* initialisations per {@link RecordType} and {@link Class} pairs.
*/
CACHE_RECORD_MAPPERS(RECORD_MAPPER, "org.jooq.configuration.cache.record-mappers"),
CACHE_RECORD_MAPPERS(RECORD_MAPPER, "org.jooq.configuration.cache.record-mappers", true),
/**
* [#8334] A cache for SQL to SQL translations in the
* {@link DSLContext#parsingConnection()}, to speed up its usage.
*/
CACHE_PARSING_CONNECTION(PARSING_CONNECTION, "org.jooq.configuration.cache.parsing-connection");
CACHE_PARSING_CONNECTION(PARSING_CONNECTION, "org.jooq.configuration.cache.parsing-connection", false);
/**
* The category of the cache, indicating the logic that enables / disables
* it.
*/
final CacheCategory category;
/**
* The cache key in the backing {@link ConcurrentMap}.
*/
final String key;
CacheType(CacheCategory category, String key) {
/**
* Whether the backing {@link ConcurrentMap} should be lazy copied in the
* scope of an {@link ExecuteContext} to reduce contention.
*/
final boolean lazyCopy;
CacheType(CacheCategory category, String key, boolean lazyCopy) {
this.category = category;
this.key = key;
this.lazyCopy = lazyCopy;
}
enum CacheCategory {

View File

@ -122,7 +122,7 @@ final class CursorImpl<R extends Record> extends AbstractCursor<R> {
super(ctx.configuration(), (AbstractRow<R>) Tools.row0(fields));
this.ctx = ctx;
this.listener = (listener != null ? listener : ExecuteListeners.getAndStart(ctx));
this.listener = (listener != null ? listener : ExecuteListeners.getAndStart(ctx, true));
this.factory = recordFactory(table, type, this.fields);
this.keepStatement = keepStatement;
this.keepResultSet = keepResultSet;

View File

@ -1300,7 +1300,7 @@ public class DefaultDSLContext extends AbstractScope implements DSLContext, Seri
@Override
public Cursor<Record> fetchLazy(ResultSet rs, Field<?>... fields) {
ExecuteContext ctx = new DefaultExecuteContext(configuration());
ExecuteListener listener = ExecuteListeners.getAndStart(ctx);
ExecuteListener listener = ExecuteListeners.getAndStart(ctx, true);
ctx.resultSet(rs);
return new CursorImpl<>(ctx, listener, fields, false, true);

View File

@ -196,6 +196,7 @@ class DefaultExecuteContext implements ExecuteContext {
}
LOCAL_CONNECTION.remove();
GLOBAL_EXECUTE_CONTEXT.remove();
}
/**
@ -244,7 +245,7 @@ class DefaultExecuteContext implements ExecuteContext {
// XXX: Static utility methods for handling Configuration lifecycle
// ------------------------------------------------------------------------
private static final ThreadLocal<ExecuteContext> LOCAL_EXECUTE_CONTEXT = new ThreadLocal<>();
static final ThreadLocal<DefaultExecuteContext> GLOBAL_EXECUTE_CONTEXT = new ThreadLocal<>();
/**
* Get the registered {@link ExecuteContext}.
@ -253,20 +254,41 @@ class DefaultExecuteContext implements ExecuteContext {
* {@link ExecuteContext} has been established, until the statement is
* closed.
*/
static final DefaultExecuteContext globalExecuteContext() {
return GLOBAL_EXECUTE_CONTEXT.get();
}
private static final ThreadLocal<ExecuteContext> LOCAL_EXECUTE_CONTEXT = new ThreadLocal<>();
/**
* Get the registered {@link ExecuteContext}.
*
* @deprecated - [#19003] - 3.21.0 - This is no longer necessary, now that
* we have {@link #globalExecuteContext()}.
*/
@Deprecated
static final ExecuteContext localExecuteContext() {
return LOCAL_EXECUTE_CONTEXT.get();
}
/**
* Run a runnable with a new {@link #localExecuteContext()}.
*
* @deprecated - [#19003] - 3.21.0 - This is no longer necessary, now that
* we have {@link #globalExecuteContext()}.
*/
@Deprecated
static final <E extends Exception> void localExecuteContext(ExecuteContext ctx, ThrowingRunnable<E> runnable) throws E {
localExecuteContext(ctx, () -> { runnable.run(); return null; });
}
/**
* Run a supplier with a new {@link #localExecuteContext()}.
*
* @deprecated - [#19003] - 3.21.0 - This is no longer necessary, now that
* we have {@link #globalExecuteContext()}.
*/
@Deprecated
static final <T, E extends Exception> T localExecuteContext(ExecuteContext ctx, ThrowingSupplier<T, E> supplier) throws E {
ExecuteContext old = localExecuteContext();

View File

@ -39,6 +39,7 @@ package org.jooq.impl;
import static java.lang.Boolean.FALSE;
import static org.jooq.conf.InvocationOrder.REVERSE;
import static org.jooq.impl.DefaultExecuteContext.GLOBAL_EXECUTE_CONTEXT;
import static org.jooq.impl.Tools.EMPTY_EXECUTE_LISTENER;
import java.util.ArrayList;
@ -67,6 +68,7 @@ final class ExecuteListeners implements ExecuteListener {
private final ExecuteListener[][] listeners;
private final boolean threadbound;
// In some setups, these two events may get mixed up chronologically by the
// Cursor. Postpone fetchEnd event until after resultEnd event, if there is
@ -76,14 +78,18 @@ final class ExecuteListeners implements ExecuteListener {
/**
* Initialise the provided {@link ExecuteListener} set and return a wrapper.
*
* @param ctx The {@link ExecuteContext}.
* @param threadbound Whether the {@link ExecuteContext} is thread bound,
* e.g. in ordinary JDBC use-cases, not reactive execution.
*/
static ExecuteListener get(ExecuteContext ctx) {
static final ExecuteListener get(ExecuteContext ctx, boolean threadbound) {
ExecuteListener[][] listeners = listeners(ctx);
if (listeners == null)
return EMPTY_LISTENER;
else
return new ExecuteListeners(listeners);
return new ExecuteListeners(listeners, threadbound);
}
/**
@ -91,9 +97,13 @@ final class ExecuteListeners implements ExecuteListener {
* <p>
* Call this if the {@link ExecuteListener#start(ExecuteContext)} event
* should be triggered eagerly.
*
* @param ctx The {@link ExecuteContext}.
* @param threadbound Whether the {@link ExecuteContext} is thread bound,
* e.g. in ordinary JDBC use-cases, not reactive execution.
*/
static ExecuteListener getAndStart(ExecuteContext ctx) {
ExecuteListener result = get(ctx);
static final ExecuteListener getAndStart(ExecuteContext ctx, boolean threadbound) {
ExecuteListener result = get(ctx, threadbound);
result.start(ctx);
return result;
}
@ -150,12 +160,16 @@ final class ExecuteListeners implements ExecuteListener {
return result == null ? new ArrayList<>() : result;
}
private ExecuteListeners(ExecuteListener[][] listeners) {
private ExecuteListeners(ExecuteListener[][] listeners, boolean threadbound) {
this.listeners = listeners;
this.threadbound = threadbound;
}
@Override
public final void start(ExecuteContext ctx) {
if (threadbound && ctx instanceof DefaultExecuteContext c)
GLOBAL_EXECUTE_CONTEXT.set(c);
for (ExecuteListener listener : listeners[0])
listener.start(ctx);
}
@ -296,6 +310,8 @@ final class ExecuteListeners implements ExecuteListener {
public final void end(ExecuteContext ctx) {
for (ExecuteListener listener : listeners[1])
listener.end(ctx);
GLOBAL_EXECUTE_CONTEXT.remove();
}
@Override

View File

@ -0,0 +1,260 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Other licenses:
* -----------------------------------------------------------------------------
* Commercial licenses for this work are available. These replace the above
* Apache-2.0 license and offer limited warranties, support, maintenance, and
* commercial database integrations.
*
* For more information, please visit: https://www.jooq.org/legal/licensing
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package org.jooq.impl;
import java.util.AbstractMap;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.jooq.ExecuteContext;
/**
* A lazy copy of a backing map, typically to speed up cache access locally, for
* example for the scope of an {@link ExecuteContext}.
* <p>
* The lazy copy map may not be in sync with the backing map, meaning that if
* the backing map is an {@link LRUCache}, or a similar cache like structure
* with eviction policies, it may be the case that an eviction happens in the
* backing map, but not in the local copy. While methods like {@link Map#size()}
* or {@link Map#entrySet()} will call through to the backing data structure,
* {@link Map#get(Object)} or {@link Map#containsKey(Object)} may not, but find
* a stale object that is no longer in the backing cache. The assumption here is
* that in the cache use-case, the backing cache would fill up the the same
* missing value again in case of a {@link Map#get(Object)} call, evicting
* another entry. These caveats will not produce any issues if only "cache
* relevant" methods like {@link Map#get(Object)} and
* {@link Map#put(Object, Object)} are called.
* <p>
* The same is true if the backing map is modified for any other reason.
* <p>
* This map is not thread safe, although it may reference a thread safe backing
* map. It is intended for thread local usage.
* <p>
* This map does not support <code>null</code> keys or values.
*
* @author Lukas Eder
*/
final class LazyCopyMap<K, V> extends AbstractMap<K, V> {
final Map<K, V> copy;
final Map<K, V> delegate;
LazyCopyMap(Map<K, V> delegate) {
this.copy = new HashMap<>();
this.delegate = delegate;
}
@SuppressWarnings("unchecked")
@Override
public V get(Object key) {
if (key == null)
throw new NullPointerException();
V value = copy.get(key);
if (value == null) {
value = delegate.get(key);
if (value != null)
copy.put((K) key, value);
}
return value;
}
@Override
public V put(K key, V value) {
if (key == null || value == null)
throw new NullPointerException();
copy.put(key, value);
return delegate.put(key, value);
}
@Override
public Set<Entry<K, V>> entrySet() {
// [#19003] To keep things simple, Iterator::remove isn't supported
// [#19003] TODO: This currently doesn't copy contents, but it could
return Collections.unmodifiableSet(delegate.entrySet());
}
// keySet() and valueSet() from AbstractMap call through to entrySet()
@Override
public V putIfAbsent(K key, V value) {
if (key == null || value == null)
throw new NullPointerException();
get(key);
copy.putIfAbsent(key, value);
return delegate.putIfAbsent(key, value);
}
@Override
public boolean replace(K key, V oldValue, V newValue) {
if (key == null || oldValue == null || newValue == null)
throw new NullPointerException();
copy.replace(key, oldValue, newValue);
return delegate.replace(key, oldValue, newValue);
}
@Override
public V replace(K key, V value) {
if (key == null || value == null)
throw new NullPointerException();
copy.replace(key, value);
return delegate.replace(key, value);
}
@Override
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
copy.replaceAll(function);
delegate.replaceAll(function);
}
@Override
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
if (key == null)
throw new NullPointerException();
get(key);
copy.computeIfAbsent(key, mappingFunction);
return delegate.computeIfAbsent(key, mappingFunction);
}
@Override
public V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (key == null)
throw new NullPointerException();
get(key);
copy.computeIfPresent(key, remappingFunction);
return delegate.computeIfPresent(key, remappingFunction);
}
@Override
public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (key == null)
throw new NullPointerException();
get(key);
copy.compute(key, remappingFunction);
return delegate.compute(key, remappingFunction);
}
@Override
public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
if (key == null || value == null)
throw new NullPointerException();
get(key);
copy.merge(key, value, remappingFunction);
return delegate.merge(key, value, remappingFunction);
}
@Override
public int size() {
return delegate.size();
}
@Override
public boolean isEmpty() {
return copy.isEmpty()
&& delegate.isEmpty();
}
@Override
public boolean containsValue(Object value) {
return copy.containsValue(value)
|| delegate.containsValue(value);
}
@Override
public boolean containsKey(Object key) {
return copy.containsKey(key)
|| delegate.containsKey(key);
}
@Override
public V remove(Object key) {
copy.remove(key);
return delegate.remove(key);
}
@Override
public boolean remove(Object key, Object value) {
copy.remove(key, value);
return delegate.remove(key, value);
}
@Override
public void putAll(Map<? extends K, ? extends V> m) {
copy.putAll(m);
delegate.putAll(m);
}
@Override
public void clear() {
copy.clear();
delegate.clear();
}
@Override
public boolean equals(Object o) {
return delegate.equals(o);
}
@Override
public int hashCode() {
return delegate.hashCode();
}
@Override
public String toString() {
return delegate.toString();
}
}

View File

@ -7814,11 +7814,18 @@ final class Tools {
}
static final ConverterContext converterContext(Attachable attachable) {
return new DefaultConverterContext(configuration(attachable));
return converterContext(configuration(attachable));
}
static final ConverterContext converterContext(Configuration configuration) {
return new DefaultConverterContext(configuration(configuration));
// [#19003] Allow for accessing a cached instance of the ConverterContext, where available.
DefaultExecuteContext c = DefaultExecuteContext.globalExecuteContext();
if (c != null && c.originalConfiguration() == configuration)
return c.converterContext();
else
return new DefaultConverterContext(configuration(configuration));
}
/**