Skip to content

Spring @Transactional support for Virtual Thread #1349

@iyanging

Description

@iyanging

As described in #448, when Spring GraphQL receives JPA entities from a DataFetcher, attempting to touch FetchType.LAZY fields triggers:

LazyInitializationException: Cannot lazily initialize collection of role '???' (no session)

Following the latest documentation(transaction-management), I tried enabling "global transaction" with this configuration:

@Bean
public GraphQlSourceBuilderCustomizer customizer(
    FederationSchemaFactory factory,
    ObjectProvider<DataFetcherExceptionResolver> resolvers) {

  final var exceptionHandler =
      DataFetcherExceptionResolver.createExceptionHandler(resolvers.stream().toList());

  return schemaBuilder ->
      schemaBuilder
          .configureGraphQl(
              gqlBuilder ->
                  gqlBuilder
                      .queryExecutionStrategy(new AsyncSerialExecutionStrategy(exceptionHandler))
                      .mutationExecutionStrategy(new AsyncSerialExecutionStrategy(exceptionHandler)));
}

@Component
@RequiredArgsConstructor
public static class GraphQLTransactionalInstrumentation extends SimplePerformantInstrumentation {

  private final PlatformTransactionManager txManager;

  @Override
  public @Nullable InstrumentationContext<ExecutionResult> beginExecuteOperation(
      InstrumentationExecuteOperationParameters parameters, InstrumentationState state) {

    final var tx = txManager.getTransaction(null);

    return new SimpleInstrumentationContext<>() {

      @Override
      public void onCompleted(@Nullable ExecutionResult result, @Nullable Throwable t) {
        if (t == null && (result == null || result.getErrors().isEmpty())) {
          txManager.commit(tx);

        } else {
          txManager.rollback(tx);
        }
      }
    };
  }
}

However, this only works when spring.threads.virtual.enabled = false.

Upon investigation, I found that with virtual threads, AnnotatedControllerConfigurer attempts to configure SchemaMappingDataFetcher/BatchLoaderHandlerMethod with invokeAsync = true. If this succeeds, field resolution always runs in the executor, resulting in a transaction-less context (transactions don't auto-propagate to new threads).

To override this, I tried a BeanPostProcessor to modify shouldInvokeAsync() behavior:

@Component
public static class AnnotatedControllerConfigurerPostProcessor
    implements BeanPostProcessor, Ordered {

  @Override
  public @Nullable Object postProcessAfterInitialization(Object bean, String beanName)
      throws BeansException {
    if (bean instanceof AnnotatedControllerConfigurer configurer) {
      configurer.setBlockingMethodPredicate(ignored -> false);
    }

    return bean;
  }

  @Override
  public int getOrder() {
    return Ordered.HIGHEST_PRECEDENCE;
  }
}

While this works, is there a more robust way to configure this without tightly coupling to Spring GraphQL's internals?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions