Liquibase 4.21 onwards not creating the changelog. DATABASECHANGELOG is empty

690 Views Asked by At

I have recently upgraded my application to spring boot 3 and using the latest version of liquibase i.e. 4.22 After the upgrade, liquibase has stopped executing the changelogs & the DATABASECHANGELOG table is empty in the DB.

My application supports multiple tenants i.e. multiple DBs are created at runtime based on the subscription to the applciation and the liquibase changelog is ran once the DB is created.

public void addTenant(final String dbKey) {
        log.info("Setting up tenant for - {}", dbKey);
        String url = StringUtils.replace(dbUrl, defaultDbName, dbKey);
        DataSource tenantDataSource = DataSourceBuilder.create()
                .driverClassName(dbDriver)
                .url(url)
                .username(dbUser)
                .password(dbPwd)
                .build();

        // Check that new connection is 'live'. If not - throw exception
        try (Connection c = tenantDataSource.getConnection()) {

            final SpringLiquibase liquibase = new SpringLiquibase();
            liquibase.setResourceLoader(resourceLoader);
            liquibase.setDataSource(tenantDataSource);
            liquibase.setChangeLog(liquibaseProperties.getChangeLog());
            liquibase.setContexts(liquibaseProperties.getContexts());
            liquibase.afterPropertiesSet();

            tenantRoutingDataSourceWrapper.addTargetDataSource(dbKey, new CustomDataSource(url, dbUser, dbPwd, dbDriver));
        } catch (Exception ex) {
            log.error("Unable to add new tenant - {}.", dbKey, ex);
            throw new RuntimeException(GenericError.UNSPECIFIC);
        }
    }

I tried downgrading the liquibase version and somehow it works till 4.20 version. All the version higher than 4.21 has this issue.

1

There are 1 best solutions below

1
Rameshwar Singh On

After a bit of digging finally found out the root cause of this issue. The Liquibase update command was not getting executed as Liquibase introduced a "fast check" cache to minimize the run of the changeset.

In the logs, I found these two statements:

  1. Executing internal command update merge
  2. Database is up to date, no changesets to execute

In the AbstractUpdateCommandStep class, a new method isUpToDateFastCheck is introduced for the fast check.

The API doc is as follows:

Performs check of the historyService to determine if there is no unrun changesets without obtaining an exclusive write lock. This allows multiple peer services to boot in parallel in the common case where there are no changelogs to run.

If we see that there is nothing in the changelog to run and this returns true, then regardless of the lock status we already know we are "done" and can finish up without waiting for the lock.

But, if there are changelogs that might have to be ran and this returns false, you MUST get a lock and do a real check to know what changesets actually need to run.

NOTE: to reduce the number of queries to the databasehistory table, this method will cache the "fast check" results within this instance under the assumption that the total changesets will not change within this instance.

This method checks for any existing key in the cache and returns true if it exists.

private boolean isUpToDateFastCheck(CommandScope commandScope, Database database, DatabaseChangeLog databaseChangeLog, Contexts contexts, LabelExpression labelExpression) throws LiquibaseException {
        String cacheKey = contexts + "/" + labelExpression;
        if (!upToDateFastCheck.containsKey(cacheKey)) {
            try {
                ....
        }
        return upToDateFastCheck.get(cacheKey);
    }

In my case, when the Liquibase runs for multiple DBs in the same instance, the same cache key was used (i.e. '/()'), and hence the changeset was not executed.

As a fix, added a label (i.e. setLabelFilter()) for each DB so that it creates a unique cache key.

final SpringLiquibase liquibase = new SpringLiquibase();
liquibase.setResourceLoader(resourceLoader);
liquibase.setDataSource(tenantDataSource);
liquibase.setChangeLog(liquibaseProperties.getChangeLog());
liquibase.setContexts(liquibaseProperties.getContexts());
liquibase.setLabelFilter(dbKey);
liquibase.afterPropertiesSet();