'What can cause flyway to not auto-detect migrations at startup, but then be able to do so at runtime?

Question:

What can cause flyway to not auto-detect migrations from the default path, and prevent resolution of migrations from a custom location, during startup only?

Given the following:

  • io.micronaut.flyway:micronaut-flyway uses flyway 6.4.4
  • Flyway, when run at application startup by micronaut, is unable to auto-detect migrations
  • Flyway, when run during bean initialization (i.e. in the constructor of the controller bean), is able to auto-detect migrations
  • Flyway is able to pick up and apply the migrations during startup during integration-testing. This gives me confidence that it is configured correctly. I can break it in expected ways by messing with the config / file location.
  • Migration file is certainly on the classpath during runtime on prod at the expected location, as evidenced by runtime-logs.

Context

I want to setup flyway migrations for my Kotlin-Micronaut-GoogleCloudFunction. As described in the docs, I have my migrations under src/main/resources/db/migration, named like V1__create_xyz_table.sql.

I verified that the migration is on the classpath at runtime, by logging it in the function body:

val fileContent = FunctionController::class.java.getResource("/db/migration/V1__create_xyz_table.sql").readText()   
println(fileContent) // "create table xyz(id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY)"

This works, and logs the contents of the file to stdout as expected.

My integration tests run fine. Migrations are automatically detected and applied to the mysql-testcontainer instance. Data is written to and read from the dockerized DB.

However, when I start the application locally, or deploy it, the application warns me:

No migrations found. Are your locations set up correctly?

Unsurprisingly, triggering the function results in errors like "Table xyz does not exist".

Besides the actual db-credentials, my test and production setup share the following config:

# application.yml
datasources:
  mysql:
    url: <url>
    username: <user>
    password: <pw>
flyway:
  datasources:
    mysql:
      enabled: true

Other things I have tried:

  • Using a Java-Based migration (same result)
  • Using the custom locations config (same result)

What "works":

  • When I autowire the datasource into the function controller, and apply the migrations inside the constructor it works: Successfully validated 1 migration.
init {
  Flyway.configure().dataSource(mysqlDS).load().migrate()
}

This confirms, that all the necessary files are present and discoverable by flyway. Why would this not work during application startup?

I attached a debugger and found that different ClassLoaders are used to discover the resources:

  • During startup: AppClassLoader
  • During function execution: FunctionClassLoader


Solution 1:[1]

I was having the same issue. Debugging, I realized that the method that triggers the flyway migration wasn't running. This method lives inside a Micronaut BeanCreatedEventListener which listens on the creation of DataSource type beans.

What left me scratching my head was that the DataSource type bean was created successfully, which I confirmed on runtime by fetching it from the application context. So why wasn't the even listener triggering?

This is because the bean was being created before the event listener was even initialized. Why was this happening? Because I had another custom even listener in my app that injected a Jdbi bean. The jdbi bean subsequently injected the DataSource bean. This means that my custom event listener was injecting the DataSource bean, so it was impossible for it to be initialized before the bean was created.

I suggest setting a breakpoint in this method to check if it's being triggered. If it's not, it's possible the cause of your issue was similar to mine.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 niebula