pgjdbc / r2dbc-postgresql

Postgresql R2DBC Driver

Home Page:https://r2dbc.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Receive "PostgresqlNonTransientResourceException: [08P01] invalid message format" for in clause with too many bindings

cfogrady opened this issue · comments

Bug Report

Versions

  • Driver: 0.9.2.RELEASE
  • Database: 11.1
  • Java: 11
  • OS: Ubuntu 20.04

Current Behavior

When running a simple query containing an in clause with a large number of parameters an invalid message format exception is thrown.
QueryTesting > largeQuery() FAILED
    io.r2dbc.postgresql.ExceptionFactory$PostgresqlNonTransientResourceException: [08P01] invalid message format
        at app//io.r2dbc.postgresql.ExceptionFactory.createException(ExceptionFactory.java:98)
        at app//io.r2dbc.postgresql.ExceptionFactory.createException(ExceptionFactory.java:65)
        at app//io.r2dbc.postgresql.ExceptionFactory.handleErrorResponse(ExceptionFactory.java:132)
        at app//reactor.core.publisher.FluxHandleFuseable$HandleFuseableSubscriber.onNext(FluxHandleFuseable.java:169)
        at app//reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:337)
        at app//reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107)
        at app//reactor.core.publisher.FluxPeekFuseable$PeekConditionalSubscriber.onNext(FluxPeekFuseable.java:854)
        at app//reactor.core.publisher.FluxPeekFuseable$PeekConditionalSubscriber.onNext(FluxPeekFuseable.java:854)
        at app//io.r2dbc.postgresql.util.FluxDiscardOnCancel$FluxDiscardOnCancelSubscriber.onNext(FluxDiscardOnCancel.java:91)
        at app//reactor.core.publisher.FluxDoFinally$DoFinallySubscriber.onNext(FluxDoFinally.java:130)
        at app//reactor.core.publisher.FluxHandle$HandleSubscriber.onNext(FluxHandle.java:119)
        at app//reactor.core.publisher.FluxCreate$BufferAsyncSink.drain(FluxCreate.java:793)
        at app//reactor.core.publisher.FluxCreate$BufferAsyncSink.next(FluxCreate.java:718)
        at app//reactor.core.publisher.FluxCreate$SerializedFluxSink.next(FluxCreate.java:154)
        at app//io.r2dbc.postgresql.client.ReactorNettyClient$Conversation.emit(ReactorNettyClient.java:671)
        at app//io.r2dbc.postgresql.client.ReactorNettyClient$BackendMessageSubscriber.emit(ReactorNettyClient.java:923)
        at app//io.r2dbc.postgresql.client.ReactorNettyClient$BackendMessageSubscriber.onNext(ReactorNettyClient.java:797)
        at app//io.r2dbc.postgresql.client.ReactorNettyClient$BackendMessageSubscriber.onNext(ReactorNettyClient.java:703)
        at app//reactor.core.publisher.FluxHandle$HandleSubscriber.onNext(FluxHandle.java:119)
        at app//reactor.core.publisher.FluxPeekFuseable$PeekConditionalSubscriber.onNext(FluxPeekFuseable.java:854)
        at app//reactor.core.publisher.FluxMap$MapConditionalSubscriber.onNext(FluxMap.java:220)
        at app//reactor.core.publisher.FluxMap$MapConditionalSubscriber.onNext(FluxMap.java:220)
        at app//reactor.netty.channel.FluxReceive.drainReceiver(FluxReceive.java:279)
        at app//reactor.netty.channel.FluxReceive.onInboundNext(FluxReceive.java:388)
        at app//reactor.netty.channel.ChannelOperations.onInboundNext(ChannelOperations.java:404)
        at app//reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:93)
        at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
        at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
        at app//io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
        at app//io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:327)
        at app//io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:299)
        at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
        at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
        at app//io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
        at app//io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
        at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
        at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
        at app//io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
        at app//io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:795)
        at app//io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:480)
        at app//io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:378)
        at app//io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986)
        at app//io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
        at app//io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
        at java.base@11.0.16/java.lang.Thread.run(Thread.java:829)

Table schema

None

Steps to reproduce

See Test Class of working in clause and failing in clause.
public class QueryTesting {

    private static final int maxConnections = 1000;
    private static final String POSTGRES_VERSION = "postgres:11.1";

    private static final AtomicBoolean isStarted = new AtomicBoolean(false);
    public static PostgreSQLContainer CONTAINER;

    public static PostgresqlConnectionFactory connectionFactory;

    @BeforeAll
    static void setupDBContainer() {
        CONTAINER = (PostgreSQLContainer) new PostgreSQLContainer(POSTGRES_VERSION)
                .withDatabaseName("test")
                .withSharedMemorySize(500L * 1000 * 1000)
                .withCommand("postgres", "-c", "fsync=off", "-c", "max_connections=" + maxConnections)
                .withReuse(true);
        if (isStarted.compareAndSet(false, true)) {
            CONTAINER.start();
            connectionFactory = new PostgresqlConnectionFactory(PostgresqlConnectionConfiguration.builder()
                    .host(CONTAINER.getHost())
                    .database(CONTAINER.getDatabaseName())
                    .username(CONTAINER.getUsername())
                    .password(CONTAINER.getPassword())
                    .port(CONTAINER.getFirstMappedPort())
                    .build()
            );
        }
    }

    @Test
    void largeInClauseQuery() {
        int numberOfValues = 70_000;
        Mono<PostgresqlConnection> mono = connectionFactory.create();
        mono.flatMapMany(connection -> {
            StringBuilder builder = new StringBuilder("select 'working' where 1 in (");
            for(int i = 1; i < numberOfValues; i++) {
                builder.append("$").append(i);
                builder.append(", ");
            }
            builder.setLength(builder.length()-2);
            builder.append(")");
            var statement = connection.createStatement(builder.toString());
            for(int i = 1; i < numberOfValues; i++) {
                statement = statement.bind("$" + i, i);
            }
            return statement.execute();
        }).flatMap(this::getStringRowsFromResults).collectList().block();
    }

    @Test
    void smallInClauseQuery() {
        int numberOfValues = 50_000;
        Mono<PostgresqlConnection> mono = connectionFactory.create();
        mono.flatMapMany(connection -> {
            StringBuilder builder = new StringBuilder("select 'working' where 1 in (");
            for(int i = 1; i < numberOfValues; i++) {
                builder.append("$").append(i);
                builder.append(", ");
            }
            builder.setLength(builder.length()-2);
            builder.append(")");
            var statement = connection.createStatement(builder.toString());
            for(int i = 1; i < numberOfValues; i++) {
                statement = statement.bind("$" + i, i);
            }
            return statement.execute();
        }).flatMap(this::getStringRowsFromResults).collectList().block();
    }

    private Flux<String> getStringRowsFromResults(PostgresqlResult result) {
        return result.map((row, rowMetadata) -> row.get(0, String.class));
    }
}

Expected behavior/code

I expect the sql to still execute, or failing that, a clearer error message about the bind parameter limit.

Well the limit is 65_536 so it does look like some postgres limitation and the message you see is the actual backend message. I don't think that we should validate parameter count it on our side and I don't think we should replace backend message either.

Thank you for the reply... I couldn't find a documented limit on the postgres side, but that makes sense. Should have realized it would be 16-bit max int based on the range. I might argue that if this is a known limitation that it be a driver side check instead of the driver assuming infinite binds only to have the backend unable to parse the message because of it. But could also see the argument of leaving as is... My argument for detecting it driver side is that it seems unlikely the backend can detect why it's unable to parse a message to produce a better message in this case... but I don't think I would argue that too strongly... Thanks again for the info!

I couldn't find a documented limit on the postgres side

Yeah, I tried to find something too but I've found only some stackoverflow posts. So I've just tried some numbers near 65536 to be sure.

I might argue that if this is a known limitation that it be a driver side check instead of the driver assuming infinite binds only to have the backend unable to parse the message because of it.

Well you kind of right. Protocol expect parameter length as a short (I've missed that part in first round) so we do can check it somewhere near io.r2dbc.postgresql.PostgresqlStatement constructor since we parse sql now (I don't like that fact btw) and we can throw some exception even without sending the query to backend.

By the way: you can use arrays instead of IN: it's just one parameter and the sql is the same for every array size.

Thanks, I didn't realize that was a possibility. Will definitely look into it for my use case.

Yeah, spring with NamedJdbcTemplate kind of spoiled us all with that ? joining for java List.

Thanks for the discussion. As this is state is controlled entirely from the calling side, we cannot do anything about it so closing the ticket.