Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RecordMapper cache contention when projecting complex expressions with DefaultRecordMapper #16606

Open
maguro opened this issue Apr 28, 2024 · 19 comments

Comments

@maguro
Copy link

maguro commented Apr 28, 2024

Expected behavior

I have a SQL query that has three CTEs that is essentially

SELECT EXISTS ( WITH ... (..) SELECT 1 FROM .. WHERE ... )

The jOOQ code is executed via

select.fetchOptionalInto(Boolean.class).orElse(false)

I expect the timing of Java/jOOQ code to closely wrap the execution of the SQL code, e.g. if the query takes roughly 100ms, I expect there to roughly be, at most, an additional 100ms.

Actual behavior

What I'm seeing is that the query takes 100ms and there is an additional 2-3 seconds before the above fetch returns. This is occurring on a fully warmed up JVM where this very code has been executed. Debug level log messages from jOOQ show that jOOQ fetches the result but there is a lag before control is returned back to the caller of fetchOptionalInto().

+220.125ms  Fetched row(s) : 1
+1.733s.    MATCHED

What we also see is that if the code is intensely uses, then the latency added to the query becomes 100ms. However, after a period of inactivity, e.g. several house, we start seeing the 2-3 second latency again.

Screenshot 2024-04-27 at 11 48 36 PM

Steps to reproduce the problem

If there's nothing obvious, then I shall embark on creating a reproducible example.

jOOQ Version

jOOQ Profesional Edition 3.18.3

Database product and version

PostgreSQL 12.17 on x86_64-pc-linux-gnu, compiled by Debian clang version 12.0.1, 64-bit

Java Version

java version "21.0.2" 2024-01-16 LTS

JDBC / R2DBC driver name and version (include name if unofficial driver)

42.7.2

@maguro
Copy link
Author

maguro commented Apr 28, 2024

Could this be related to #7431?

@maguro
Copy link
Author

maguro commented Apr 28, 2024

I tried using fetchAnyInto() and things seem to have improved; I'll keep an eye on it for the next few days. However, there still is a 500ms "overhead" after jOOQ has reported the record as fetched.

+35.117ms Fetched row(s) : 1
+591.607ms MATCHED

@lukaseder
Copy link
Member

Thanks for your support request.

The only difference between fetchAny() and fetchOptional() is that the latter needs to fetch the second record (if any) from the JDBC ResultSet in order to assess whether to throw a TooManyRowsException. But that obviously wouldn't account for such a big difference, especially since your query will always only return 1 record.

I doubt that this problem is really caused by jOOQ, unless you have actual evidence (e.g. a profiler session would help)?

I might be able to help you with your performance problem, though. Can you show your complete query, all the DDL to create the tables involved (including indexes!)

@maguro
Copy link
Author

maguro commented Apr 29, 2024

Happy to get you the info that you need. After inspecting several days of telemetry, I feel that I can declare that using fetchAnyInto() does shrink the latency from 2-4s down to 500ms.

It's too bad that jOOQ has no more logging info other than Fetched row(s) : 1, because after that log message there are several seconds of "silence"; the MATCHED is what's immediately logged by my code right after jOOQ returns.

I'm not having issues with the Postgres query per se. As you can see from the above trace, the Postgres query finishes in an acceptable 120.958ms. Manual executions of the query are consistent with what's being reported in the telemetry's traces. The traces were collected via Google Cloud Trace from a Google Cloud SQL Postgres server.

I'm a bit busy and will likely have time to collect the profiling session next weekend.

@maguro
Copy link
Author

maguro commented May 7, 2024

I didn't get a chance to profile this yet, but I have a question. Could there be some kind of Hikari/Postgres JDBC configuration that could cause this kind of behavior?

@lukaseder
Copy link
Member

Could there be some kind of Hikari/Postgres JDBC configuration that could cause this kind of behavior?

If only a profiler could give insight ;-)

Perhaps, if you turn on trace logging for everything, there's a high level hint, though logging comes with its own overhead. A primitive way of "profiling" is to use jstack to extract stack traces from running processes:
https://stackoverflow.com/q/10756105/521799

If you do this frequently enough while this happens, you can get some starter insight into where your process is hanging. This would at least help with some assumptions.

@maguro
Copy link
Author

maguro commented May 10, 2024

Could there be some kind of Hikari/Postgres JDBC configuration that could cause this kind of behavior?

If only a profiler could give insight ;-)

Yep, point taken. I'm being lazy and am hoping that there's some kind of well known, well know to everyone but me :), pathological configuration that wreaks havoc on jOOQ.

Perhaps, if you turn on trace logging for everything, there's a high level hint, though logging comes with its own overhead.

As reported above, I have turned on trace level logging and what it's telling me is that jOOQ logs that it has completed fetching the record and yet the thread of control is not returned back to my code for 2-5 seconds. Hence profiling is the "next step".

A primitive way of "profiling" is to use jstack to extract stack traces from running processes: https://stackoverflow.com/q/10756105/521799

If you do this frequently enough while this happens, you can get some starter insight into where your process is hanging. This would at least help with some assumptions.

Thanks! I'll try this as well.

@lukaseder
Copy link
Member

As reported above, I have turned on trace level logging and what it's telling me is that jOOQ logs that it has completed fetching the record and yet the thread of control is not returned back to my code for 2-5 seconds. Hence profiling is the "next step".

Does this include logging all activities in the connection pool / driver, including calls to ResultSet.close(), Statement.close(), Connection.close()?

@maguro
Copy link
Author

maguro commented May 13, 2024

As reported above, I have turned on trace level logging and what it's telling me is that jOOQ logs that it has completed fetching the record and yet the thread of control is not returned back to my code for 2-5 seconds. Hence profiling is the "next step".

Does this include logging all activities in the connection pool / driver, including calls to ResultSet.close(), Statement.close(), Connection.close()?

It does not. I am only going by what jOOQ logs:

+35.117ms Fetched row(s) : 1

and then there's 2-4 seconds of silence.

TLDR - It's the caching of the record mapper

My jOOQ query is this gigantic SQL statement with layers and layers of CTEs. When the record mapper gets cached, the jOOQ code visits all the elements of the query; I assume to get an accurate key for the cache.

As mentioned above, the jOOQ code is executed via

select.fetchOptionalInto(Boolean.class).orElse(false)

All I want back is a boolean.

Thread Dump

So, I've managed to collect thread dumps, captured every 500ms. The jOOQ logging states that the row was fetched in 100ms, but the thread of control is not returned back to my code until after 6.5s. Here's the distributed trace:

Screenshot 2024-05-13 at 8 59 26 AM

You can see the events that have been generated that mark when the thread dumps occurred. Here is the stack trace.

"pool-10-thread-30" Id=635 RUNNABLE
	at app//org.jooq.impl.DefaultRenderContext.sql(DefaultRenderContext.java:417)
	at app//org.jooq.impl.DefaultRenderContext.sql(DefaultRenderContext.java:91)
	at app//org.jooq.impl.QueryPartCollectionView.accept(QueryPartCollectionView.java:203)
	at app//org.jooq.impl.DefaultRenderContext.visit0(DefaultRenderContext.java:726)
	at app//org.jooq.impl.AbstractContext.visit(AbstractContext.java:350)
	at app//org.jooq.impl.SelectQueryImpl.toSQLReference0(SelectQueryImpl.java:2362)
	at app//org.jooq.impl.SelectQueryImpl.lambda$toSQLReferenceLimitDefault$37(SelectQueryImpl.java:2016)
	at app//org.jooq.impl.SelectQueryImpl$$Lambda/0x00000007c0ec8838.accept(Unknown Source)
	at app//org.jooq.impl.AbstractContext.toggle(AbstractContext.java:393)
	at app//org.jooq.impl.AbstractContext.data(AbstractContext.java:404)
	at app//org.jooq.impl.SelectQueryImpl.toSQLReferenceLimitDefault(SelectQueryImpl.java:2016)
	at app//org.jooq.impl.SelectQueryImpl.accept0(SelectQueryImpl.java:1911)
	at app//org.jooq.impl.SelectQueryImpl.accept(SelectQueryImpl.java:1539)
	at app//org.jooq.impl.DefaultRenderContext.visit0(DefaultRenderContext.java:726)
	at app//org.jooq.impl.AbstractContext.visit(AbstractContext.java:350)
	at app//org.jooq.impl.AbstractDelegatingQuery.accept(AbstractDelegatingQuery.java:90)
	at app//org.jooq.impl.DefaultRenderContext.visit0(DefaultRenderContext.java:726)
	at app//org.jooq.impl.AbstractContext.visit(AbstractContext.java:325)
	at app//org.jooq.impl.Tools.visitSubquery(Tools.java:2858)
	at app//org.jooq.impl.Tools.visitSubquery(Tools.java:2831)
	at app//org.jooq.impl.CommonTableExpressionImpl.accept(CommonTableExpressionImpl.java:147)
	at app//org.jooq.impl.DefaultRenderContext.visit0(DefaultRenderContext.java:726)
	at app//org.jooq.impl.AbstractContext.visit(AbstractContext.java:350)
	at app//org.jooq.impl.QueryPartCollectionView.acceptElement(QueryPartCollectionView.java:253)
	at app//org.jooq.impl.QueryPartCollectionView.accept(QueryPartCollectionView.java:223)
	at app//org.jooq.impl.CommonTableExpressionList.lambda$accept$0(CommonTableExpressionList.java:56)
	at app//org.jooq.impl.CommonTableExpressionList$$Lambda/0x00000007c0f3f208.accept(Unknown Source)
	at app//org.jooq.impl.CommonTableExpressionList.markTopLevelCteAndAccept(CommonTableExpressionList.java:64)
	at app//org.jooq.impl.CommonTableExpressionList.accept(CommonTableExpressionList.java:56)
	at app//org.jooq.impl.DefaultRenderContext.visit0(DefaultRenderContext.java:726)
	at app//org.jooq.impl.AbstractContext.visit(AbstractContext.java:350)
	at app//org.jooq.impl.WithImpl.lambda$accept$0(WithImpl.java:243)
	at app//org.jooq.impl.WithImpl$$Lambda/0x00000007c0f3eb98.accept(Unknown Source)
	at app//org.jooq.impl.AbstractContext.toggle(AbstractContext.java:379)
	at app//org.jooq.impl.AbstractContext.declareCTE(AbstractContext.java:694)
	at app//org.jooq.impl.WithImpl.lambda$accept$1(WithImpl.java:243)
	at app//org.jooq.impl.WithImpl$$Lambda/0x00000007c0f2bc38.accept(Unknown Source)
	at app//org.jooq.impl.AbstractContext.toggle(AbstractContext.java:393)
	at app//org.jooq.impl.AbstractContext.data(AbstractContext.java:404)
	at app//org.jooq.impl.WithImpl.accept(WithImpl.java:240)
	at app//org.jooq.impl.DefaultRenderContext.visit0(DefaultRenderContext.java:726)
	at app//org.jooq.impl.AbstractContext.visit(AbstractContext.java:350)
	at app//org.jooq.impl.SelectQueryImpl.accept0(SelectQueryImpl.java:1736)
	at app//org.jooq.impl.SelectQueryImpl.accept(SelectQueryImpl.java:1539)
	at app//org.jooq.impl.DefaultRenderContext.visit0(DefaultRenderContext.java:726)
	at app//org.jooq.impl.AbstractContext.visit(AbstractContext.java:350)
	at app//org.jooq.impl.AbstractDelegatingQuery.accept(AbstractDelegatingQuery.java:90)
	at app//org.jooq.impl.DefaultRenderContext.visit0(DefaultRenderContext.java:726)
	at app//org.jooq.impl.AbstractContext.visit(AbstractContext.java:350)
	at app//org.jooq.impl.DefaultDSLContext.renderInlined(DefaultDSLContext.java:749)
	at app//org.jooq.impl.AbstractQueryPart.equals(AbstractQueryPart.java:170)
	at app//org.jooq.tools.StringUtils.equals(StringUtils.java:1315)
	at app//org.jooq.impl.Exists.equals(Exists.java:163)
	at java.base@21.0.2/java.util.Objects.equals(Objects.java:64)
	at java.base@21.0.2/java.util.Arrays.equals(Arrays.java:2980)
	at app//org.jooq.impl.FieldsImpl.equals(FieldsImpl.java:625)
	at java.base@21.0.2/java.util.Objects.equals(Objects.java:64)
	at java.base@21.0.2/java.lang.invoke.DirectMethodHandle$Holder.invokeStatic(DirectMethodHandle$Holder)
	at java.base@21.0.2/java.lang.invoke.LambdaForm$MH/0x00000007c0f14000.invoke(LambdaForm$MH)
	at java.base@21.0.2/java.lang.invoke.LambdaForm$MH/0x00000007c0f15400.guard(LambdaForm$MH)
	at java.base@21.0.2/java.lang.invoke.DelegatingMethodHandle$Holder.delegate(DelegatingMethodHandle$Holder)
	at java.base@21.0.2/java.lang.invoke.LambdaForm$MH/0x00000007c0f15800.invoke(LambdaForm$MH)
	at java.base@21.0.2/java.lang.invoke.DelegatingMethodHandle$Holder.delegate(DelegatingMethodHandle$Holder)
	at java.base@21.0.2/java.lang.invoke.LambdaForm$MH/0x00000007c0f15400.guard(LambdaForm$MH)
	at java.base@21.0.2/java.lang.invoke.DelegatingMethodHandle$Holder.delegate(DelegatingMethodHandle$Holder)
	at java.base@21.0.2/java.lang.invoke.LambdaForm$MH/0x00000007c0f15400.guard(LambdaForm$MH)
	at java.base@21.0.2/java.lang.invoke.LambdaForm$MH/0x00000007c0f15c00.linkToTargetMethod(LambdaForm$MH)
	at app//org.jooq.impl.Cache$Key2.equals(Cache.java:125)
	at java.base@21.0.2/java.util.concurrent.ConcurrentHashMap$TreeNode.findTreeNode(ConcurrentHashMap.java:2743)
	at java.base@21.0.2/java.util.concurrent.ConcurrentHashMap$TreeNode.findTreeNode(ConcurrentHashMap.java:2753)
	at java.base@21.0.2/java.util.concurrent.ConcurrentHashMap$TreeNode.findTreeNode(ConcurrentHashMap.java:2753)
	at java.base@21.0.2/java.util.concurrent.ConcurrentHashMap$TreeNode.findTreeNode(ConcurrentHashMap.java:2753)
	at java.base@21.0.2/java.util.concurrent.ConcurrentHashMap$TreeBin.putTreeVal(ConcurrentHashMap.java:2945)
	at java.base@21.0.2/java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1055)
	-  locked java.util.concurrent.ConcurrentHashMap$TreeBin@563dbd4b
	at java.base@21.0.2/java.util.concurrent.ConcurrentHashMap.put(ConcurrentHashMap.java:1006)
	at app//org.jooq.impl.Cache.run(Cache.java:103)
	-  locked java.util.concurrent.ConcurrentHashMap@79ad7ab6
	at app//org.jooq.impl.DefaultRecordMapperProvider.provide(DefaultRecordMapperProvider.java:77)
	at app//org.jooq.impl.FieldsImpl.mapper(FieldsImpl.java:194)
	at app//org.jooq.impl.AbstractRecord.into(AbstractRecord.java:812)
	at app//org.jooq.impl.ResultQueryTrait.fetchAnyInto(ResultQueryTrait.java:825)
	at app//com.acme.sql.ApplicationSql.findMatch(ApplicationSql.java:374)
	at app//com.acme.sql.ApplicationSql.lambda$matchStuff$0(ApplicationSql.java:241)
	at app//com.acme.sql.ApplicationSql$$Lambda/0x00000007c0f1fd10.call(Unknown Source)
	at app//io.opentelemetry.context.Context.lambda$wrap$2(Context.java:224)
	at app//io.opentelemetry.context.Context$$Lambda/0x00000007c0f232c8.call(Unknown Source)
	at app//com.acme.logging.slf4j.LoggingContextExecutorService.lambda$submit$0(LoggingContextExecutorService.java:28)
	at app//com.acme.logging.slf4j.LoggingContextExecutorService$$Lambda/0x00000007c0f234e8.call(Unknown Source)
	at java.base@21.0.2/java.util.concurrent.FutureTask.run(FutureTask.java:317)
	at java.base@21.0.2/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
	at java.base@21.0.2/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
	at java.base@21.0.2/java.lang.Thread.runWith(Thread.java:1596)
	at java.base@21.0.2/java.lang.Thread.run(Thread.java:1583)

	Number of locked synchronizers = 1
	- java.util.concurrent.ThreadPoolExecutor$Worker@4b91a410

@lukaseder
Copy link
Member

Thanks for the additional investigation, that's very useful. I recently thought about this possibility of there being a potential slowdown in this area. The expression needs to be evaluated once to render the query, but it should obviously be avoided to repeat this operation for every record that is being mapped.

Can you work around this by either:

  • Aliasing the expression (in case of which the rendering is much more simple)
  • Not using the DefaultRecordMapper (e.g. fetchOptional(r -> r.value1()) instead of fetchOptional(Boolean.class))

@maguro
Copy link
Author

maguro commented May 14, 2024

Thanks. I did the latter and everyone is quite happy. :)

@lukaseder
Copy link
Member

Looking into this now. I don't think that an individual rendering is really the problem here. The rendering has already been done when generating the query, and there it wasn't a problem (or else, jOOQ would have very significant problems in general). But what's going on here is that the cache access is synchronized, e.g.:

-  locked java.util.concurrent.ConcurrentHashMap@79ad7ab6

So it might be the case that multiple of these computations block each other as each one takes maybe 1ms, which starts adding up.

@lukaseder
Copy link
Member

I wonder if we should cache record mappers at all, if they contain projections of expressions like these, at all. We have a marker interface org.jooq.impl.SimpleQueryPart which marks all QueryPart types that are "simple" for formatting purposes. That could be a good starting point to decide whether caching of record mappers is reasonable.

Alternatively, instead of using the RecordType itself as a cache key, we could prepare the cache key in string form (outside of the synchronised block) and use that instead - at least in cases where we don't have SimpleQueryPart

@lukaseder
Copy link
Member

Caching really is essential, though. With this benchmark:

@Fork(value = 1, jvmArgsAppend = "-Dorg.jooq.no-logo=true")
@Warmup(iterations = 3, time = 3)
@Measurement(iterations = 7, time = 3)
@State(Scope.Benchmark)
public class BenchmarkRecordMapper {

    @State(Scope.Benchmark)
    public static class BenchmarkState {

        Connection connection;
        DSLContext ctx1;
        DSLContext ctx2;
        Result<?>  result1;
        Result<?>  result2;

        @Setup(Level.Trial)
        public void setup() throws Exception {
            try (InputStream is = BenchmarkState.class.getResourceAsStream("/config.properties")) {
                Properties p = new Properties();
                p.load(is);
                connection = DriverManager.getConnection(
                    p.getProperty("db.url"),
                    p.getProperty("db.username"),
                    p.getProperty("db.password")
                );
            }

            ctx1 = DSL.using(connection, new Settings().withExecuteLogging(false).withRenderQuotedNames(NEVER).withCacheRecordMappers(false));
            result1 = ctx1
                .select(AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
                .from(AUTHOR)
                .fetch();

            ctx2 = DSL.using(connection, new Settings().withExecuteLogging(false).withRenderQuotedNames(NEVER).withCacheRecordMappers(true));
            result2 = ctx2
                .select(AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
                .from(AUTHOR)
                .fetch();
        }
    }

    static class A1 {
        final String firstName;
        final String lastName;

        A1(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }
    }

    @Benchmark
    public Object intoImmutablePojoNoCache(BenchmarkState state) {
        return state.result1.into(A1.class);
    }

    @Benchmark
    public Object intoImmutablePojoCache(BenchmarkState state) {
        return state.result2.into(A1.class);
    }
}

When benchmarking only the mapping as above, then there's a 10x performance improvement with the cache!

Benchmark                                        Mode  Cnt        Score      Error  Units
BenchmarkRecordMapper.intoImmutablePojoCache    thrpt    7  5032935.575 ┬▒ 6223.952  ops/s
BenchmarkRecordMapper.intoImmutablePojoNoCache  thrpt    7   567510.541 ┬▒  757.831  ops/s

This is significant for large result sets, where the mapping time can be significant compared to the execution time (this isn't the case for small result sets)

@lukaseder
Copy link
Member

As expected, while there's some overhead rendering the SQL all the time in order to check for equality, it's not as significant (in a benchmark) compared to turning off the cache:

Benchmark                                                   Mode  Cnt        Score       Error  Units
BenchmarkRecordMapper.intoImmutablePojoCache               thrpt    7  4974804.245 ┬▒  9739.575  ops/s
BenchmarkRecordMapper.intoImmutablePojoCacheExpressions    thrpt    7  5332782.464 ┬▒ 11768.427  ops/s
BenchmarkRecordMapper.intoImmutablePojoNoCache             thrpt    7   568255.368 ┬▒  2335.386  ops/s
BenchmarkRecordMapper.intoImmutablePojoNoCacheExpressions  thrpt    7   579122.476 ┬▒  1930.823  ops/s
BenchmarkRecordMapper.intoMutablePojoCache                 thrpt    7  2892917.527 ┬▒  1834.731  ops/s
BenchmarkRecordMapper.intoMutablePojoCacheExpressions      thrpt    7  7655053.274 ┬▒  9104.239  ops/s
BenchmarkRecordMapper.intoMutablePojoNoCache               thrpt    7  2049306.612 ┬▒  2863.606  ops/s
BenchmarkRecordMapper.intoMutablePojoNoCacheExpressions    thrpt    7  3250343.911 ┬▒ 11582.067  ops/s
BenchmarkRecordMapper.intoTableCache                       thrpt    7  2391084.577 ┬▒  4693.724  ops/s
BenchmarkRecordMapper.intoTableCacheExpressions            thrpt    7  2785289.121 ┬▒  6222.785  ops/s
BenchmarkRecordMapper.intoTableNoCache                     thrpt    7  2493489.815 ┬▒  3302.672  ops/s
BenchmarkRecordMapper.intoTableNoCacheExpressions          thrpt    7  2882355.753 ┬▒  3104.791  ops/s
BenchmarkRecordMapper.intoTableRecordCache                 thrpt    7  1592415.482 ┬▒  2516.453  ops/s
BenchmarkRecordMapper.intoTableRecordCacheExpressions      thrpt    7  2138659.818 ┬▒  3321.041  ops/s
BenchmarkRecordMapper.intoTableRecordNoCache               thrpt    7  1656402.145 ┬▒  1454.052  ops/s
BenchmarkRecordMapper.intoTableRecordNoCacheExpressions    thrpt    7  2123466.089 ┬▒  3562.513  ops/s

The types of queries are these:

private Result<?> queryTableFields(DSLContext ctx) {
    return ctx
        .select(AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
        .from(AUTHOR)
        .fetch();
}

private Result<?> queryExpressions(DSLContext ctx) {
    return ctx
        .select(
            field(select(field(select(field(select(field(select(field(select(AUTHOR.FIRST_NAME)))))))))),
            field(select(field(select(field(select(field(select(field(select(AUTHOR.LAST_NAME))))))))))
        )
        .from(AUTHOR)
        .fetch();
}

Comparing the immutable pojo mapper with the other ones, there does appear to be an overhead worth looking into separately, as that overhead doesn't reproduce to the same extent with other algorithms (especially compared to the mutable pojos).

The benchmark doesn't fully reflect your situation, as we only run 2 queries instead of hundreds to thousands. But it does hint at this being more of a concurrency problem (synchronisation on the cache) than something else.

@lukaseder lukaseder changed the title 2-3 second lag to process one record RecordMapper cache contention when projecting complex expressions with DefaultRecordMapper May 23, 2024
@lukaseder
Copy link
Member

There's also room for improvement here at a different level. The actual RecordMapper implementation being chosen here isn't related to immutable POJOs as in the benchmark, but the ValueTypeMapper, which applies to any Record1<T> to Class<E> conversion that is covered by the DefaultConverterProvider based on the type of T, not the name of the field in the record. It applies first, before all other record mappers are resolved.

In other words, there are RecordMapper implementations that only depend on the <T> types of the fields. These could be cached more simply than e.g. the MutablePOJOMapper, or some of the others, which may depend on the field name.

On the other hand, mappers that depend on the field name only really work if the field is not an expression, but a SimpleQueryPart. So, a MutablePOJOMapper query will typically either have:

  • TableField references from generated code
  • Aliased expressions

The case you ran into should simply never happen, because an Exists expression doesn't need to be placed like that in the cache. I think I know how to fix this now:

  • If all fields are SimpleQueryPart, then store the RecordType as a key in the cache
  • If at least one field is not, then store a List<Class<?>> or similar as a key in the cache

@lukaseder
Copy link
Member

There's an even better internal marker interface: org.jooq.impl.NamedField

@lukaseder
Copy link
Member

Interestingly, this "improvement" negatively affects almost all mapping benchmarks, possibly due to the extra allocation for each lookup?

Benchmark                                                   Mode  Cnt        Score       Error  Units
BenchmarkRecordMapper.intoImmutablePojoCache               thrpt    7  4573655.657 ┬▒ 30398.235  ops/s
BenchmarkRecordMapper.intoImmutablePojoCacheExpressions    thrpt    7  3262516.398 ┬▒  8839.853  ops/s
BenchmarkRecordMapper.intoImmutablePojoNoCache             thrpt    7   555928.084 ┬▒  1391.702  ops/s
BenchmarkRecordMapper.intoImmutablePojoNoCacheExpressions  thrpt    7   571143.394 ┬▒  1504.525  ops/s
BenchmarkRecordMapper.intoMutablePojoCache                 thrpt    7  2774243.043 ┬▒ 14771.764  ops/s
BenchmarkRecordMapper.intoMutablePojoCacheExpressions      thrpt    7  5182079.842 ┬▒  8571.298  ops/s
BenchmarkRecordMapper.intoMutablePojoNoCache               thrpt    7  1699648.608 ┬▒  3622.007  ops/s
BenchmarkRecordMapper.intoMutablePojoNoCacheExpressions    thrpt    7  2541825.411 ┬▒  5980.735  ops/s
BenchmarkRecordMapper.intoTableCache                       thrpt    7  2453724.896 ┬▒  3512.330  ops/s
BenchmarkRecordMapper.intoTableCacheExpressions            thrpt    7  2859495.154 ┬▒  5816.447  ops/s
BenchmarkRecordMapper.intoTableNoCache                     thrpt    7  2431799.685 ┬▒  6032.299  ops/s
BenchmarkRecordMapper.intoTableNoCacheExpressions          thrpt    7  2838402.981 ┬▒  4978.002  ops/s
BenchmarkRecordMapper.intoTableRecordCache                 thrpt    7  1558224.532 ┬▒  2939.188  ops/s
BenchmarkRecordMapper.intoTableRecordCacheExpressions      thrpt    7  1894549.926 ┬▒  4446.415  ops/s
BenchmarkRecordMapper.intoTableRecordNoCache               thrpt    7  1676464.190 ┬▒  2494.321  ops/s
BenchmarkRecordMapper.intoTableRecordNoCacheExpressions    thrpt    7  2165480.651 ┬▒ 13871.759  ops/s

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

2 participants