static <T> T doWithBootstrapLock(final Config config,
                                     final String buildVersion,
                                     final Supplier<T> work) {

        Objects.requireNonNull(work);
        // We need to use one of the
        final ConnectionConfig connectionConfig = getClusterLockConnectionConfig(config);

        DbUtil.waitForConnection(connectionConfig);

        final AtomicBoolean doneWork = new AtomicBoolean(false);
        T workOutput;

        final String thisNodeName = NullSafe.getOrElse(
                config,
                Config::getYamlAppConfig,
                AppConfig::getNodeConfig,
                NodeConfig::getNodeName,
                "UNKNOWN NODE");

        try (Connection conn = DbUtil.getSingleConnection(connectionConfig)) {
            // Need read committed so that once we have acquired the lock we can see changes
            // committed by other nodes.
            conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
            final DSLContext context = JooqUtil.createContext(conn);

            workOutput = context.transactionResult(txnConfig -> {
                // make sure we have a populated build_version table to check and lock on
                ensureBuildVersionTable(txnConfig, conn, thisNodeName);

                BootstrapInfo bootstrapInfo = getBootstrapInfoFromDb(txnConfig);

                T output = null;
                if (!isUpgradeRequired(bootstrapInfo, buildVersion)) {
                    handleCorrectBuildVersion(bootstrapInfo, "no lock or DB migration required.");
                    // Set local state so the db modules know not to run flyway when work.get() is called
                    // below
                    DbMigrationState.markBootstrapMigrationsComplete();
                } else {
                    if (bootstrapInfo.getUpgradeOutcome().isPresent()) {
                        LOGGER.info("Found old build version '{}' in table '{}' with upgrade outcome {}. " +
                                    "Bootstrap lock required to upgrade the version.",
                                bootstrapInfo.getBuildVersion(),
                                BUILD_VERSION_TABLE_NAME,
                                bootstrapInfo.getUpgradeOutcome().get());
                    } else {
                        LOGGER.info("Found build version '{}' in table '{}' with empty upgrade outcome. " +
                                    "Bootstrap lock required to re-run the version upgrade.",
                                bootstrapInfo.getBuildVersion(),
                                BUILD_VERSION_TABLE_NAME);
                    }
                    acquireBootstrapLock(connectionConfig, txnConfig);

                    // We now hold a cluster wide lock
                    final Instant startTime = Instant.now();

                    // Re-check the lock table state now we are under lock
                    // For the first node these should be the same as when checked above
                    bootstrapInfo = getBootstrapInfoFromDb(txnConfig);

                    if (!isUpgradeRequired(bootstrapInfo, buildVersion)) {
                        handleCorrectBuildVersion(bootstrapInfo, "releasing lock. No DB migration required.");
                        // Set local state so the db modules know not to run flyway when work.get() is called
                        // below
                        DbMigrationState.markBootstrapMigrationsComplete();
                    } else {
                        if (Objects.equals(bootstrapInfo.getBuildVersion(), buildVersion)) {
                            LOGGER.info("Re-running the upgrade of stroom to version '{}' under cluster wide lock",
                                    buildVersion);
                        } else {
                            LOGGER.info("Upgrading stroom from '{}' to '{}' under cluster wide lock",
                                    bootstrapInfo.getBuildVersion(), buildVersion);
                        }

                        // We hold the lock and the db version is out of date so perform work under lock
                        // including doing all the flyway migrations
                        try {
                            output = work.get();
                        } catch (Exception e) {
                            final String msg = LogUtil.message(
                                    "Error upgrading stroom to {}: {}", buildVersion, e.getMessage(), e);
                            LOGGER.error(msg);
                            // Failure in the migration so need to indicate to other nodes that it has failed
                            // so they can stop
                            updateDbBuildVersion(txnConfig, buildVersion, thisNodeName, UpgradeOutcome.FAILED);
                            LOGGER.info(LogUtil.message("Releasing bootstrap lock after {}",
                                    Duration.between(startTime, Instant.now())));
                            // This will release the bootstrap lock
                            conn.commit();
                            throw new BootstrapFailureException(msg, e);
                        }
                        doneWork.set(true);

                        LOGGER.info(LogUtil.inBoxOnNewLine("Completed all database migrations"));

                        // If anything fails and we don't update/insert these it is fine, it will just
                        // get done on next successful boot
                        updateDbBuildVersion(txnConfig, buildVersion, thisNodeName, UpgradeOutcome.SUCCESS);
                        // Set local state so the db modules know not to run flyway when work.get() is called
                        // below
                        DbMigrationState.markBootstrapMigrationsComplete();
                    }
                    // We are the first node to get the lock for this build version so now release the lock
                    LOGGER.info(LogUtil.message("Releasing bootstrap lock after {}",
                            Duration.between(startTime, Instant.now())));
                }
                return output;
            });
            LOGGER.debug("Closed connection");
        } catch (SQLException e) {
            throw new RuntimeException("Error obtaining bootstrap lock: " + e.getMessage(), e);
        }

        // We didn't execute work under lock, i.e. the db was found to be at the right version, but
        // we still need to set up all the db modules, but each node can now do it concurrently.
        // Flyway will discover that each db module is at the right version so won't do anything either.
        if (!doneWork.get()) {
            workOutput = work.get();
        }
        return workOutput;
    }
Use a table lock on a noddy table created just for this purpose to enforce
a cluster wide lock so we can do all the flyway migrations in isolation.
Flyway migrations should work under lock but we have lots of independent flyway
migrations so it is safer to do it this way.
Can't user the cluster lock service as that is not a thing at this point.
stroom-app/src/main/java/stroom/app/BootstrapUtil.java