ebean icon indicating copy to clipboard operation
ebean copied to clipboard

Provide API to run ddl and migration manually (e.g. with offline DB)

Open rPraml opened this issue 2 years ago • 2 comments

This change provides API methods to run DDL and migration manually. This is useful, if you have a setup where you

  • use TenantMode.DB / SCHEMA or CATALOG
  • expect, that the DB may be not available on startup

How to use

If you use a multi-tenancy mode, you will have to iterate over the available tenants and call dbMigration for each tenant. (You'll have to set the tenant in the currentTenantProvider, so that the multiTenantDbSupplier will switch to the correct tenant

for (int tenant : tenants) {
    currentTenantProvider.set(tenant)
    db.runMigration();
}

Note: Ebean and Ebean-datasource will recover properly, if there are connection problems after the first successful connect. But the default configuration does not allow you, to construct a io.ebean.Database instance, when the first connect fails.

If you want to start an ebean server where the datasource could be unreachable, you will have to disable all DB access and checks at startup (datasource.failOnStart=false, skipDataSourceCheck=true, ddl.run=false and migration.run=false) in this case, ebean will also come up, if the database is not accessible. This allows you, to show a "Database not available" message when you start your application.

Sample code If you use io.ebean.datasource you may register a dataSourceAlert and do the migration when the database comes up the first time. Refer to the "TestServerOffline" or the following code draft, how to use this feature.

class MyDbFactory implements DataSourceAlert {
   boolean initialized = false; 
   public Database create() {
       ...
       dataSourceConfig.setAlert(this);
       ...
       db = DatabaseFactory.create(config)
       if (((ConnectionPool)db.dataSource()).isDataSourceUp()) {
          boot();
       }
       return db;
   }
   void synchronized boot() { // synchronized: dataSourceUp is invoked from different thread
       if (init) return; 
       db.runMigration();
       init = true; // run only once after success
   }
   public void dataSourceUp(final DataSource dataSource) {
       boot(); 
   }
}

rPraml avatar Dec 17 '21 12:12 rPraml

We use Ebean in multi tenant mode and here DDL/DbMigration has to be completely done by the application, because ebean does neither run DbMigration on start up, nor provide an API to initialize the database for a new tenant.

We have the use case, that tenants are created (and also removed) while the application runs. Our app performs the following steps:

  • Admin enters the new DB credentials for the tenant in a web-form,
  • The connection is registered in our MultiTenantDataSourceProvider
  • we have a CurrentTenantProvider where we set the new tenant (a ThreadLocal) and run ddl-generation by invoking server.start() for this thread.
  • it is no option for us to do tenant initializion on ebean startup

I don't think they can become plugins due to ordering ... the ddl ought to run before any/all "plugins" are run etc

that contradicts itself when a multi-tenant mode is active. So plugins (at least the default ones) have to deal with that situation

For the "ordering" problem, I've introduced a new lifecycle method start in Plugin, so that online will called once, when the server is created and start is called for each tenant. This will work for our use cases.

I also extracted DDL run and DbMigration run in separate plugins in our branch, so I've the following plugins in the classpath

  • DdlPlugin generates in the execute method the DDL and applies the DDL in the start method (for each tenant)
  • DbMigrationPlugin generates the DbMigration in the online method. (for this, I've made DbMigration to be fully configurable from properties. See https://github.com/FOCONIS/ebean/commit/9d1f4194aa2d5a9c248f958bf633115caddc422b)
  • DbRunMigrationPlugin applies the DDL scripts

This would allow to remove all DdlGenerator and MigrationRun code from DefaultServer https://github.com/FOCONIS/ebean/commit/699f7a8449d09c84f73c0a4730a5ba31e0430b1b#diff-0804968369579e408eea483a9e582485cd7faf94e5d78dc8f7a918c729962a70 https://github.com/FOCONIS/ebean/commit/208e987ab542f83f377dd2957f0a7895f848f1da#diff-0804968369579e408eea483a9e582485cd7faf94e5d78dc8f7a918c729962a70

So if we introduce more lifecycle methods in the Plugin interface we could achieve that we can offload all the DDL stuff from defaultserver.

How would I change the plugin interface:

public interface Plugin {
  // called after Server is constructed. Tasks that can be done here
  // - read configs etc from server
  // - this.server = server
  void configure(SpiServer server);

  // called when the server comes 'online', means
  // - all plugins are configured
  // - you can use all features of the server, like background executor
  // - you can not access the database (at least when multi-tenant mode is used)
  void online(boolean online);
  
  // called when the datasource is available. You may access the database the first time here
  // this is called for each tenant.
  default void start() { };

  // called once, when the server is shut down
  void shutdown();
}

Of course, I see the ordering problem. So we can either add an int order() method and sort the plugins or add a third lifecycle method e.g. void initDatabase(). (or do both). So the initializion workflow could look like this pseudocode

plugins = discoverPluginsViaServiceLocator()
sort(plugins, Plugin::order)

for (plugin : plugins)
   plugin.configure(server)

for (plugin : plugins)
   plugin.online(online)

for (tenant : tenants) // is done by application when multi tenant is active
   server.start()
     // this does effectively
     for (plugin : plugins)
        plugin.initDatabase()  // migration plugins can apply DDL or whatever
     for (plugin : plugins)
        plugin.start()  // can fully access the database

// when server stops
for (plugin : plugins)
   plugin.shutdown()

When we add the default methods, we would not break the Plugin-API. We would only introduce the breaking change that the DB is not fully initialized in online method (which is also the case when multi-tenant mode is used) - as far as I see the existing plugins will have no problems with that.

@rbygrave I sometimes find it difficult to convey the overall picture and may need some feedback if my text is understandable and what you think about this change. Maybe you can give me some tips, how I should proceed with this PR.

rPraml avatar Jan 25 '22 09:01 rPraml

Note to myself: This is still on my to-do list ;)

rPraml avatar Feb 07 '22 16:02 rPraml

This PR is probably obsolete

rPraml avatar Aug 09 '23 15:08 rPraml