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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions