quarkusio / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
13.82k stars 2.69k forks source link

Hibernate reactive + Flyway extension causes UnsatisfiedResolutionException #10716

Closed burmanm closed 1 year ago

burmanm commented 4 years ago

Describe the bug Simply adding the quarkus-flyway extension when using Hibernate Reactive causes:

Caused by: javax.enterprise.inject.UnsatisfiedResolutionException: Unsatisfied dependency for type javax.persistence.EntityManagerFactory and qualifiers [@Default]
    - java member: io.quarkus.hibernate.reactive.runtime.ReactiveSessionFactoryProducer#emf
    - declared on CLASS bean [types=[io.quarkus.hibernate.reactive.runtime.ReactiveSessionFactoryProducer, java.lang.Object], qualifiers=[@Default, @Any], target=io.quarkus.hibernate.reactive.runtime.ReactiveSessionFactoryProducer]
    at io.quarkus.arc.processor.Beans.resolveInjectionPoint(Beans.java:487)
    at io.quarkus.arc.processor.BeanInfo.init(BeanInfo.java:362)
    at io.quarkus.arc.processor.BeanDeployment.init(BeanDeployment.java:226)

Expected behavior Flyway should be usable in an application that uses the hibernate-reactive extension.

Actual behavior Will not start.

To Reproduce Steps to reproduce the behavior:

  1. Take hibernate-reactive-quickstart

  2. Add the following dependencies:

        <!-- Flyway specific dependencies -->
        <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-flyway</artifactId>
        </dependency>
    
        <!-- JDBC driver dependencies -->
        <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-jdbc-postgresql</artifactId>
        </dependency>
  3. mvn quarkus:dev

Environment (please complete the following information):

Additional context (Add any other context about the problem here.)

SidMorad commented 1 year ago

Could you be sure that it will work under native as well?

@cdmikechen Yes, I did make native image and this workaround works there too. Thanks to you and @seeseemelk .

arvind-das commented 1 year ago

@andreas-eberle My applications.properties file looks like this:

quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=removed
quarkus.datasource.password=removed
quarkus.datasource.jdbc=false
quarkus.datasource.reactive.url=postgresql://127.0.0.1:5432/mydb

The quarkus.datasource.jdbc=false line is very important. If it's missing, Quarkus won't boot.

Looks like with new versions coming up, it does not work. My quarkus version is 2.13.2.Final.

With above configurations mentioned , it throws following error

org.flywaydb.core.api.FlywayException: No database found to handle vertx-reactive:mysql://localhost:3306/myapp

is there a specific version I should stick to ?

Thanks

humcqc commented 1 year ago

Hello, In 3.0 seems I have a slightly different error for the same use case: 2023-05-03 11:51:18,447 INFO [io.qua.dep.dev.IsolatedDevModeMain] (main) Attempting to start live reload endpoint to recover from previous Quarkus startup failure 2023-05-03 11:51:18,954 ERROR [io.qua.dep.dev.IsolatedDevModeMain] (main) Failed to start quarkus: java.lang.RuntimeException: io.quarkus.builder.BuildException: Build failure: Build failed due to errors [error]: Build step io.quarkus.arc.deployment.SyntheticBeansProcessor#initRuntime threw an exception: java.lang.IllegalStateException: A synthetic bean with identifier 9c40b3d5cbb6ee6bb4cf2cb7bab0a1fda6694fe5 is already registered: SYNTHETIC bean [types=[org.hibernate.SessionFactory, jakarta.persistence.EntityManagerFactory, java.lang.Object], qualifiers=[@jakarta.enterprise.inject.Default, @Any], target=n/a] at io.quarkus.arc.processor.BeanDeployment.addSyntheticBean(BeanDeployment.java:1370) at io.quarkus.arc.processor.BeanDeployment$BeanRegistrationContextImpl.accept(BeanDeployment.java:1610) at io.quarkus.arc.processor.BeanDeployment$BeanRegistrationContextImpl.accept(BeanDeployment.java:1593) at io.quarkus.arc.processor.BeanConfigurator.done(BeanConfigurator.java:95) at io.quarkus.arc.deployment.SyntheticBeansProcessor.configureSyntheticBean(SyntheticBeansProcessor.java:92) at io.quarkus.arc.deployment.SyntheticBeansProcessor.initRuntime(SyntheticBeansProcessor.java:56) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:568) at io.quarkus.deployment.ExtensionLoader$3.execute(ExtensionLoader.java:909) at io.quarkus.builder.BuildContext.run(BuildContext.java:282)

@Sanne , the exception seems not the same, do you think it should be handled in the same issue ?

Seems it involve different components and some assumptions should be reviewed for the hibernate reactive part. Do you plan to work on this soon ? or do you recommend to workaround this for the moment ?

Thanks

wjglerum commented 1 year ago

We also hit this issue recently and came up with relative clean solution. We follow the approach from above, but also make sure everything works nicely in dev mode and production.

@Startup
public class FlywayMigration {

    FlywayMigration(Scheduler scheduler,
                    FlywayConfig flywayConfig,
                    SessionFactory sessionFactory,
                    @ConfigProperty(name = "quarkus.datasource.reactive.url") String datasourceUrl,
                    @ConfigProperty(name = "quarkus.datasource.username") String datasourceUsername,
                    @ConfigProperty(name = "quarkus.datasource.password") String datasourcePassword) {

        Flyway flyway = Flyway
                .configure()
                .dataSource(datasourceUrl.replace("vertx-reactive:", "jdbc:"), datasourceUsername, datasourcePassword)
                .cleanDisabled(!flywayConfig.clean())
                .load();

        if (flywayConfig.migrateAtStart()) {
            scheduler.pause();
            if (flywayConfig.cleanAtStart()) {
                flyway.clean();
            }
            flyway.migrate();
            sessionFactory.getSchemaManager().validateMappedObjects();
            scheduler.resume();
        }
    }

    @ConfigMapping(prefix = "application.database.flyway")
    public interface FlywayConfig {

        boolean migrateAtStart();

        @WithDefault("false")
        boolean cleanAtStart();
    }
}

For this you need the following extensions:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-flyway</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-reactive</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>

<!-- Optional -->
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-scheduler</artifactId>
</dependency>

We have the following config values set:

application.database.flyway.migrate-at-start=true
quarkus.hibernate-orm.database.generation=none

## Explicitly turns off JDBC and Flyway
quarkus.datasource.jdbc=false
quarkus.flyway.enabled=false

And in dev mode we set application.database.flyway.clean-at-start=true to get back the drop-and-create experience from Hibernate in dev mode.

miguelborges99 commented 1 year ago

The fix was released in 3.5, but I still need to do the workaround described above . Actually, to have flyway working in a native image, I need to implement the following code:

@Startup
public class FlywayMigration {

    FlywayMigration(Scheduler scheduler,
                    SessionFactory sessionFactory,
                    FlywayConfig config) {

        final FluentConfiguration configuration = Flyway
                .configure()
                .dataSource("jdbc:".concat(config.getDatasourceUrl()), config.getDatasourceUsername(), config.getDatasourcePassword())
                .locations(config.getLocations())
                .validateMigrationNaming(config.getValidateMigrationNaming())
                .baselineOnMigrate(config.getBaselineOnMigrate())
                .baselineVersion(config.getBaselineVersion())
                .baselineDescription(config.getBaselineDescription())
                .connectRetries(config.getConnectRetries())
                .schemas(config.getSchemas())
                .table(config.getTable());

        if (System.getProperty("org.graalvm.nativeimage.imagecode") != null) {
            configuration.resourceProvider(new GraalVMResourceProvider(configuration.getLocations()));
            configuration.javaMigrationClassProvider(new GraalVMClassProvider());
        }

        final Flyway flyway = configuration.load();

        if (Boolean.TRUE.equals(config.getMigrateAtStart())) {
            scheduler.pause();
            if (config.getCleanAtStart()) {
                flyway.clean();
            }
            flyway.migrate();
            sessionFactory.getSchemaManager().validateMappedObjects();
            scheduler.resume();
        }
    }
}

public class GraalVMResourceProvider implements ResourceProvider {
    private final Location[] locations;

    public GraalVMResourceProvider(Location[] locations) {
        this.locations = Arrays.copyOf(locations, locations.length);
    }

    @Override
    public LoadableResource getResource(String name) {
        if (getClassLoader().getResource(name) == null) {
            return null;
        }
        return new ClassPathResource(null, name, getClassLoader(), StandardCharsets.UTF_8);
    }

    @Override
    public Collection<LoadableResource> getResources(String prefix, String[] suffixes) {
        try (FileSystem fileSystem = FileSystems.newFileSystem(URI.create("resource:/"), Map.of())) {
            final List<LoadableResource> result = new ArrayList<>();
            for (Location location : locations) {
                final Path path = fileSystem.getPath(location.getPath());
                try (Stream<Path> files = Files.walk(path)) {
                    files.filter(Files::isRegularFile)
                            .filter(file -> file.getFileName().toString().startsWith(prefix))
                            .filter(file -> hasSuffix(file.getFileName().toString(), suffixes))
                            .map(file -> (LoadableResource) new ClassPathResource(null,
                                    file.toString(), getClassLoader(), StandardCharsets.UTF_8))
                            .forEach(result::add);
                }
            }

            // Sort DB migration files
            final List<LoadableResource> resources = result.stream()
                    .sorted().collect(Collectors.toList());

            return resources;
        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }
    }

    private boolean hasSuffix(String input, String[] suffixes) {
        for (String suffix : suffixes) {
            if (input.endsWith(suffix) || input.toUpperCase().endsWith(suffix.toUpperCase())) {
                return true;
            }
        }
        return false;
    }

    private static ClassLoader getClassLoader() {
        return GraalVMResourceProvider.class.getClassLoader();
    }
}

public class GraalVMClassProvider implements ClassProvider<JavaMigration> {

    @Override
    public Collection<Class<? extends JavaMigration>> getClasses() {
        return Collections.emptySet();
    }
}

Do I need to have the workaround described? What properties should I have to have Hibernate reactive and flyway working at the same time?

yrodiere commented 1 year ago

@miguelborges99 We have a (simple) integration test that works with this configuration.

Several things to take care about:

If you still encounter problems, can you please open another issue with a reproducer? Thank you.

humcqc commented 1 year ago

Hi @yrodiere , "Hibernate Reactive can only use the default datasource", does it mean that for multi-tenancy in reactive we cannot use multiple datasource, one per tenant ?

Thanks

yrodiere commented 1 year ago

"Hibernate Reactive can only use the default datasource"

Yes, and that's documented here: https://quarkus.io/guides/hibernate-reactive#hr-limitations

does it mean that for multi-tenancy in reactive we cannot use multiple datasource, one per tenant ?

It seems the Hibernate Reactive extension for Quarkus doesn't support multi-tenancy at the moment: #15959

"static" configuration with named datasources certainly won't work due to this limitation.

A more dynamic approach where you create your own connections could work in theory, but I'm pretty sure it won't in practice because of the missing configuration mentioned in #15959.

Schema-based multitenancy is more likely to work to with Hibernate Reactive, FWIW. Though I'm not entirely sure either, since this I couldn't find relevant tests.

humcqc commented 1 year ago

Thanks for the quick reply, i will continue the discussion on : https://github.com/quarkusio/quarkus/discussions/33342

arvind-das commented 12 months ago

The solution provided in the thread to set datasource.jdbc: false works but then in the dev ui, there is no option to generate the initial migration script, is it happening with me only or others are also facing the same issue.

erickloss commented 9 months ago

Hello all,

I ran into the same Issue not being able to use flyway migration in combination with a reactive data source. The workaround above works fine for me (running flyway manually on application startup).

Just FYI: I constantly got exceptions when I open the Quarkus DEV UI:

(executor-thread-1) Error in JsonRPC Call: java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
    at io.smallrye.mutiny.unchecked.UncheckedSupplier.lambda$toSupplier$0(UncheckedSupplier.java:45)
    at io.smallrye.context.impl.wrappers.SlowContextualSupplier.get(SlowContextualSupplier.java:21)
    at io.smallrye.mutiny.operators.uni.builders.UniCreateFromItemSupplier.subscribe(UniCreateFromItemSupplier.java:28)
    at io.smallrye.mutiny.operators.AbstractUni.subscribe(AbstractUni.java:36)
    at io.smallrye.mutiny.operators.uni.UniRunSubscribeOn.lambda$subscribe$0(UniRunSubscribeOn.java:27)
    at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:582)
    at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
    at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
    at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at io.quarkus.devui.runtime.comms.JsonRpcRouter.lambda$invoke$0(JsonRpcRouter.java:98)
    at io.smallrye.mutiny.unchecked.UncheckedSupplier.lambda$toSupplier$0(UncheckedSupplier.java:41)
    ... 11 more
Caused by: java.lang.NullPointerException: Cannot invoke "io.quarkus.agroal.runtime.DataSources.getActiveDataSourceNames()" because the return value of "io.quarkus.arc.InstanceHandle.get()" is null
    at io.quarkus.flyway.runtime.FlywayContainerUtil.getActiveFlywayContainers(FlywayContainerUtil.java:26)
    at io.quarkus.flyway.runtime.FlywayContainersSupplier.get(FlywayContainersSupplier.java:16)
    at io.quarkus.flyway.runtime.devui.FlywayJsonRpcService.getNumberOfDatasources(FlywayJsonRpcService.java:165)
    at io.quarkus.flyway.runtime.devui.FlywayJsonRpcService_ClientProxy.getNumberOfDatasources(Unknown Source)
    ... 17 more

I fixed that by removing the quarkus-flyway extension from the project. That also means there is no flyway tile in the Dev UI, but that's fine for me.

Cheers

yrodiere commented 9 months ago

I ran into the same Issue not being able to use flyway migration in combination with a reactive data source.

This was supposed to be fixed in https://github.com/quarkusio/quarkus/pull/36012, in Quarkus 3.5.0.CR1.

Can you please open a new issue with a reproducer?

Just FYI: I constantly got exceptions when I open the Quarkus DEV UI:

This looks like a bug too. Let's see what your reproducer looks like though, there must be something else at play.

duartevinicius91 commented 6 months ago

Hi guys I'm using the following config in my environment (and it works ;D) I just tried to use a different datasource configuration for the migrations

`quarkus.datasource.db-kind = postgresql quarkus.datasource.username = postgres quarkus.datasource.password = postgres quarkus.datasource.reactive.url = vertx-reactive:postgresql://localhost:5432/mydb quarkus.datasource.jdbc = false

quarkus.flyway."flyway-migration".active = true quarkus.flyway."flyway-migration".migrate-at-start = true quarkus.datasource."flyway-migration".db-kind = postgresql quarkus.datasource."flyway-migration".jdbc.url = jdbc:postgresql://localhost:5432/mydb quarkus.datasource."flyway-migration".username = postgres quarkus.datasource."flyway-migration".password = postgres`

yrodiere commented 6 months ago

Hey, FWIW you don't need two datasources, so the right configuration would be something like this:

quarkus.datasource.db-kind = postgresql
quarkus.datasource.username = postgres
quarkus.datasource.password = postgres
quarkus.datasource.reactive.url = vertx-reactive:postgresql://localhost:5432/mydb
quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:5432/mydb
quarkus.datasource.jdbc = false

quarkus.flyway.migrate-at-start = true

Or this if you use dev services:

quarkus.datasource.db-kind = postgresql
quarkus.datasource.username = postgres
quarkus.datasource.password = postgres
%prod.quarkus.datasource.reactive.url = vertx-reactive:postgresql://localhost:5432/mydb
%prod.quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:5432/mydb
quarkus.datasource.jdbc = false

quarkus.flyway.migrate-at-start = true

I updated the documentation to make this clearer: https://quarkus.io/version/main/guides/flyway#reactive-datasources

rovio-deepak commented 2 months ago

Hi @yrodiere - I'm a recent victim of this scenario as well. I need Flyway working for local development and for other environments as well. Setting

quarkus.datasource.jdbc = false

Prevented Flyway from working locally for me. Is that expected? My final configuration looks like

quarkus.hibernate-orm.enabled=true
quarkus.hibernate-orm.database.generation=none
quarkus.hibernate-orm.log.sql=true
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=<username>
quarkus.flyway.migrate-at-start=true

%stage.quarkus.datasource.credentials-provider=my-ssm-credentials-provider
%stage.quarkus.datasource.db-version=13.12
%stage.quarkus.datasource.reactive.url=vertx-reactive:postgresql://<host>
%stage.quarkus.datasource.jdbc.url=jdbc:postgresql://<host>

But now I fear that Hibernate Reactive does not use the correct reactive driver anymore (since JDBC is not disabled). Does this look correct to you? Thanks!

yrodiere commented 2 months ago

Hello

Setting

quarkus.datasource.jdbc = false

Prevented Flyway from working locally for me. Is that expected?

Yes. Flyway uses JDBC. You disable JDBC, Flyway doesn't work.

My final configuration looks like

quarkus.hibernate-orm.enabled=true
quarkus.hibernate-orm.database.generation=none
quarkus.hibernate-orm.log.sql=true
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=<username>
quarkus.flyway.migrate-at-start=true

%stage.quarkus.datasource.credentials-provider=my-ssm-credentials-provider
%stage.quarkus.datasource.db-version=13.12
%stage.quarkus.datasource.reactive.url=vertx-reactive:postgresql://<host>
%stage.quarkus.datasource.jdbc.url=jdbc:postgresql://<host>

But now I fear that Hibernate Reactive does not use the correct reactive driver anymore (since JDBC is not disabled).

Then fear not: Hibernate Reactive is absolutely incapable of using JDBC.

Does this look correct to you?

Yes.