Search code examples
javaspringhibernatesqlitetimestamp

SpringBoot 3, SQLite - IndexOutOfBoundsException when loading Instants


I am using SpringBoot 3 to access a SQLite database. My problem is, that when fetching entities parallely, I get a random ArrayIndexOutOfBoundsException in a Java class that parses a date.

SpringBoot 3.2.5

<dependency>
    <groupId>org.xerial</groupId>
    <artifactId>sqlite-jdbc</artifactId>
    <version>3.42.2.0</version>
</dependency>

The dialect is set to spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect

My Java class looks like this:

public class SomeTable implements Serializable {
  private static final String COLUMN_NAME_CREATED = "CREATED";
  private static final String COLUMN_NAME_MODIFIED = "MODIFIED";
  private static final String COLUMN_NAME_IP = "IP";
  private static final String COLUMN_NAME_USERNAME = "USERNAME";

  @Id
  @Column(length = 36)
  protected String id;

  @NotNull
  @Column(name = "created")
  private Instant created;

  @NotNull
  @Column(name = "modified")
  private Instant modified;
}

The table looks like this:

CREATE TABLE "some_table" (
    "id"    varchar(36) NOT NULL,
    "created"   timestamp NOT NULL,
    "modified"  timestamp NOT NULL
);

This is the stacktrace I am getting:

java.lang.ArrayIndexOutOfBoundsException: Index 18 out of bounds for length 13
at java.base/sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(Unknown Source)
at java.base/java.util.GregorianCalendar.computeFields(Unknown Source)
at java.base/java.util.GregorianCalendar.computeFields(Unknown Source)
at java.base/java.util.Calendar.setTimeInMillis(Unknown Source)
at org.sqlite.jdbc3.JDBC3ResultSet.getTimestamp(JDBC3ResultSet.java:490)
at com.zaxxer.hikari.pool.HikariProxyResultSet.getTimestamp(HikariProxyResultSet.java)
at org.hibernate.type.descriptor.jdbc.TimestampUtcAsJdbcTimestampJdbcType$2.doExtract(TimestampUtcAsJdbcTimestampJdbcType.java:103)
at org.hibernate.type.descriptor.jdbc.BasicExtractor.extract(BasicExtractor.java:44)
at org.hibernate.sql.results.jdbc.internal.JdbcValuesResultSetImpl.getCurrentRowValue(JdbcValuesResultSetImpl.java:302)
at org.hibernate.sql.results.internal.RowProcessingStateStandardImpl.getJdbcValue(RowProcessingStateStandardImpl.java:119)
at org.hibernate.sql.results.graph.basic.BasicResultAssembler.extractRawValue(BasicResultAssembler.java:52)
at org.hibernate.sql.results.graph.basic.BasicResultAssembler.assemble(BasicResultAssembler.java:59)
at org.hibernate.sql.results.graph.DomainResultAssembler.assemble(DomainResultAssembler.java:33)
at org.hibernate.sql.results.graph.entity.AbstractEntityInitializer.extractConcreteTypeStateValues(AbstractEntityInitializer.java:1081)
at org.hibernate.sql.results.graph.entity.AbstractEntityInitializer.initializeEntityInstance(AbstractEntityInitializer.java:838)
at org.hibernate.sql.results.graph.entity.AbstractEntityInitializer.initializeEntity(AbstractEntityInitializer.java:813)
at org.hibernate.sql.results.graph.entity.AbstractEntityInitializer.initializeInstance(AbstractEntityInitializer.java:799)
at org.hibernate.sql.results.internal.InitializersList.initializeInstance(InitializersList.java:70)
at org.hibernate.sql.results.internal.StandardRowReader.coordinateInitializers(StandardRowReader.java:109)
at org.hibernate.sql.results.internal.StandardRowReader.readRow(StandardRowReader.java:86)
at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:181)
at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:33)
at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.doExecuteQuery(JdbcSelectExecutorStandardImpl.java:209)
at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.executeQuery(JdbcSelectExecutorStandardImpl.java:83)
at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:76)
at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:65)
at org.hibernate.loader.ast.internal.SingleIdLoadPlan.load(SingleIdLoadPlan.java:145)
at org.hibernate.loader.ast.internal.SingleIdLoadPlan.load(SingleIdLoadPlan.java:117)
at org.hibernate.loader.ast.internal.SingleIdEntityLoaderStandardImpl.load(SingleIdEntityLoaderStandardImpl.java:75)
at org.hibernate.persister.entity.AbstractEntityPersister.doLoad(AbstractEntityPersister.java:3748)
at org.hibernate.persister.entity.AbstractEntityPersister.load(AbstractEntityPersister.java:3737)
at org.hibernate.event.internal.DefaultLoadEventListener.loadFromDatasource(DefaultLoadEventListener.java:604)
at org.hibernate.event.internal.DefaultLoadEventListener.loadFromCacheOrDatasource(DefaultLoadEventListener.java:590)
at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:560)
at org.hibernate.event.internal.DefaultLoadEventListener.doLoad(DefaultLoadEventListener.java:544)
at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:207)
at org.hibernate.event.internal.DefaultLoadEventListener.loadWithRegularProxy(DefaultLoadEventListener.java:290)
at org.hibernate.event.internal.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:242)
at org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad(DefaultLoadEventListener.java:111)
at org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:68)
at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:138)
at org.hibernate.internal.SessionImpl.fireLoadNoChecks(SessionImpl.java:1222)
at org.hibernate.internal.SessionImpl.internalLoad(SessionImpl.java:1068)
at org.hibernate.sql.results.graph.entity.internal.EntitySelectFetchInitializer.initializeInstance(EntitySelectFetchInitializer.java:195)
at org.hibernate.sql.results.internal.InitializersList.initializeInstance(InitializersList.java:70)
at org.hibernate.sql.results.internal.StandardRowReader.coordinateInitializers(StandardRowReader.java:109)
at org.hibernate.sql.results.internal.StandardRowReader.readRow(StandardRowReader.java:86)
at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:203)
at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:33)
at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.doExecuteQuery(JdbcSelectExecutorStandardImpl.java:209)
at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.executeQuery(JdbcSelectExecutorStandardImpl.java:83)
at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:76)
at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:65)
at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.lambda$new$2(ConcreteSqmSelectQueryPlan.java:137)
at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.withCacheableSqmInterpretation(ConcreteSqmSelectQueryPlan.java:362)
at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.performList(ConcreteSqmSelectQueryPlan.java:303)
at org.hibernate.query.sqm.internal.QuerySqmImpl.doList(QuerySqmImpl.java:509)
at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:427)
at org.hibernate.query.Query.getResultList(Query.java:120)
at org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll(SimpleJpaRepository.java:383)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:354)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:277)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158)
at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:516)
at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:285)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:628)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:168)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:143)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:70)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:392)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:164)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
at jdk.proxy2/jdk.proxy2.$Proxy229.findAll(Unknown Source)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:354)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
at jdk.proxy2/jdk.proxy2.$Proxy229.findAll(Unknown Source)
at com.my.project.services.SomeTableService.getSomeTable(AbstractNetworkRoutingService.java:97)

Caused by: java.lang.ArrayIndexOutOfBoundsException: Index 27 out of bounds for length 13
at java.base/sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(Unknown Source)
at java.base/java.util.GregorianCalendar.computeFields(Unknown Source)
at java.base/java.util.GregorianCalendar.computeFields(Unknown Source)
at java.base/java.util.Calendar.setTimeInMillis(Unknown Source)
at org.sqlite.jdbc3.JDBC3ResultSet.getTimestamp(JDBC3ResultSet.java:490)
at com.zaxxer.hikari.pool.HikariProxyResultSet.getTimestamp(HikariProxyResultSet.java)
at org.hibernate.type.descriptor.jdbc.TimestampUtcAsJdbcTimestampJdbcType$2.doExtract(TimestampUtcAsJdbcTimestampJdbcType.java:103)
at org.hibernate.type.descriptor.jdbc.BasicExtractor.extract(BasicExtractor.java:44)
at org.hibernate.sql.results.jdbc.internal.JdbcValuesResultSetImpl.getCurrentRowValue(JdbcValuesResultSetImpl.java:302)
at org.hibernate.sql.results.internal.RowProcessingStateStandardImpl.getJdbcValue(RowProcessingStateStandardImpl.java:119)
at org.hibernate.sql.results.graph.basic.BasicResultAssembler.extractRawValue(BasicResultAssembler.java:52)
at org.hibernate.sql.results.graph.basic.BasicResultAssembler.assemble(BasicResultAssembler.java:59)
at org.hibernate.sql.results.graph.DomainResultAssembler.assemble(DomainResultAssembler.java:33)
at org.hibernate.sql.results.graph.entity.AbstractEntityInitializer.extractConcreteTypeStateValues(AbstractEntityInitializer.java:1081)
at org.hibernate.sql.results.graph.entity.AbstractEntityInitializer.initializeEntityInstance(AbstractEntityInitializer.java:838)
at org.hibernate.sql.results.graph.entity.AbstractEntityInitializer.initializeEntity(AbstractEntityInitializer.java:813)
at org.hibernate.sql.results.graph.entity.AbstractEntityInitializer.initializeInstance(AbstractEntityInitializer.java:799)
at org.hibernate.sql.results.internal.InitializersList.initializeInstance(InitializersList.java:70)
at org.hibernate.sql.results.internal.StandardRowReader.coordinateInitializers(StandardRowReader.java:109)
at org.hibernate.sql.results.internal.StandardRowReader.readRow(StandardRowReader.java:86)
...
com.my.project.services.SomeTableService.getSomeTable(AbstractNetworkRoutingService.java:97)

The dates in the table look like this: screenshot of suspect columns

We tried to use a custom converter that uses Instant.ofEpochMillis() but the problem still appeared.

My current assumption is a race condition. Limiting the number of connections to 1 via Spring settings solves the problem - but having to do that because of this seems just wrong, performance-wise this is not optimal so I was thinking someone might have an idea on some other way.


Solution

  • This is a bug now reported to Hibernate: HHH-18239. We implemented a custom type as a temporary solution:

    /**
     * This is a custom implementation of a Hibernate type, that maps a {@link Types#TIMESTAMP} to a
     * {@link java.time.Instant}. This is necessary, because of this bug in Hibernate:
     * <a href="https://hibernate.atlassian.net/browse/HHH-18239">Hibernate ORM Jira HHH-18239</a>.
     */
    public class CustomThreadSafeInstantType implements UserType<Instant> {
    
      @Override
      public int getSqlType() {
        return Types.TIMESTAMP;
      }
    
      @Override
      public Class<Instant> returnedClass() {
        return Instant.class;
      }
    
      @Override
      public boolean equals(Instant instant, Instant j1) {
        return Objects.equals(instant, j1);
      }
    
      @Override
      public int hashCode(Instant instant) {
        return instant.hashCode();
      }
    
      @Override
      public Instant nullSafeGet(ResultSet resultSet, int i, SharedSessionContractImplementor sharedSessionContractImplementor, Object o) throws SQLException {
        var timestamp = resultSet.getTimestamp(i);
    
        if (timestamp == null) {
          return null;
        }
    
        return timestamp.toInstant();
      }
    
      @Override
      public void nullSafeSet(PreparedStatement preparedStatement, Instant instant, int i, SharedSessionContractImplementor sharedSessionContractImplementor)
          throws SQLException {
        if (instant == null) {
          preparedStatement.setNull(i, Types.TIMESTAMP);
          return;
        }
    
        preparedStatement.setTimestamp(i, Timestamp.from(instant));
      }
    
      @Override
      public Instant deepCopy(Instant instant) {
        return instant;
      }
    
      @Override
      public boolean isMutable() {
        return false;
      }
    
      @Override
      public Serializable disassemble(Instant instant) {
        return instant;
      }
    
      @Override
      public Instant assemble(Serializable serializable, Object o) {
        return (Instant) serializable;
      }
    
    
    }
    

    This type is used like so:

    public class SomeTable implements Serializable {
      private static final String COLUMN_NAME_CREATED = "CREATED";
      private static final String COLUMN_NAME_MODIFIED = "MODIFIED";
      private static final String COLUMN_NAME_IP = "IP";
      private static final String COLUMN_NAME_USERNAME = "USERNAME";
    
      @Id
      @Column(length = 36)
      protected String id;
    
      @NotNull
      @Column(name = "created")
      @Type(CustomThreadSafeInstantType.class)
      private Instant created;
    
      @NotNull
      @Column(name = "modified")
      @Type(CustomThreadSafeInstantType.class)
      private Instant modified;
    }