/*
 * Copyright © Red Gate Software Ltd 2010-2021
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.flywaydb.core.internal.database;

import org.flywaydb.core.api.logging.Log;
import org.flywaydb.core.api.logging.LogFactory;
import org.flywaydb.core.internal.jdbc.JdbcTemplate;
import org.flywaydb.core.internal.jdbc.Results;

import java.math.BigInteger;
import java.sql.SQLException;
import java.time.Duration;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;

public class InsertRowLock {
    private static final Log LOG = LogFactory.getLog(InsertRowLock.class);
    private static final Random random = new Random();

    /**
     * A random string used as an ID for this instance of Flyway.
     */
    private final String tableLockString = getNextRandomString();
    private final JdbcTemplate jdbcTemplate;
    public final int lockTimeoutMins;
    private Timer timer;

    public InsertRowLock(JdbcTemplate jdbcTemplate, int lockTimeoutMins) {
        this.jdbcTemplate = jdbcTemplate;
        this.lockTimeoutMins = lockTimeoutMins;
    }

    public void doLock(String insertStatementTemplate, String updateLockStatement, String deleteExpiredLockStatement, String booleanTrue) throws SQLException {
        int retryCount = 0;
        while (true) {
            try {
                jdbcTemplate.execute(deleteExpiredLockStatement);
                if (insertLockingRow(insertStatementTemplate, booleanTrue)) {
                    startLockWatchingThread(String.format(updateLockStatement.replace("?", "%s"), tableLockString));
                    return;
                }
                if (retryCount < 50) {
                    retryCount++;
                    LOG.debug("Waiting for lock on Flyway schema history table");
                } else {
                    LOG.error("Waiting for lock on Flyway schema history table. Application may be deadlocked. Lock row may require manual removal " +
                            "from the schema history table.");
                }
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
                // Ignore - if interrupted, we still need to wait for lock to become available
            }
        }
    }

    private boolean insertLockingRow(String insertStatementTemplate, String booleanTrue) {
        String insertStatement = String.format(insertStatementTemplate.replace("?", "%s"),
                -100,
                "'" + tableLockString + "'",
                "'flyway-lock'",
                "''",
                "''",
                0,
                "''",
                0,
                booleanTrue
        );

        // Insert the locking row - the primary key-ness of 'installed_rank' will prevent us having two
        Results results = jdbcTemplate.executeStatement(insertStatement);

        // Succeed if there were no errors
        return results.getException() == null;
    }

    public void doUnlock(String deleteLockTemplate) throws SQLException {
        stopLockWatchingThread();
        String deleteLockStatement = String.format(deleteLockTemplate.replace("?", "%s"), tableLockString);
        jdbcTemplate.execute(deleteLockStatement);
    }

    private String getNextRandomString(){
        return new BigInteger(128, random).toString(16);
    }

    private void startLockWatchingThread(String updateLockStatement) {
        TimerTask lockWatcherTask = new TimerTask() {
            @Override
            public void run() {
                LOG.debug("Updating lock in Flyway schema history table");
                jdbcTemplate.executeStatement(updateLockStatement);
            }
        };
        timer = new Timer();
        timer.schedule(lockWatcherTask, 0, Duration.ofMinutes(lockTimeoutMins / 2).toMillis());
    }

    private void stopLockWatchingThread() {
        timer.cancel();
    }
}
@@ -923,98 +923,195 @@
 ava.
+u
 ti
-me.Duration;%0Aimport java.util.Random;%0Aimport java.util.Timer;%0Aimport java.util.TimerTask
+l.Random;%0Aimport java.util.concurrent.Executors;%0Aimport java.util.concurrent.ScheduledExecutorService;%0Aimport java.util.concurrent.ScheduledFuture;%0Aimport java.util.concurrent.TimeUnit
 ;%0A%0Ap
@@ -1267,16 +1267,62 @@
 andom();
+%0A    private static final int NUM_THREADS = 2;
 %0A%0A    /*
@@ -1562,19 +1562,95 @@
 ate 
-Timer timer
+final ScheduledExecutorService executor;%0A    private ScheduledFuture%3C?%3E scheduledFuture
 ;%0A%0A 
@@ -1809,24 +1809,75 @@
 imeoutMins;%0A
+        this.executor = createScheduledExecutor();%0A
     %7D%0A%0A    p
@@ -2263,16 +2263,34 @@
         
+ scheduledFuture =
  startLo
@@ -4145,171 +4145,352 @@
 ate 
-void startLockWatchingThread(String updateLockStatement) %7B%0A        TimerTask lockWatcherTask = new TimerTask() %7B%0A            @Override%0A            public void run(
+ScheduledExecutorService createScheduledExecutor() %7B%0A        return Executors.newScheduledThreadPool(NUM_THREADS, r -%3E %7B%0A            Thread t = Executors.defaultThreadFactory().newThread(r);%0A            t.setDaemon(true);%0A            return t;%0A        %7D);%0A    %7D%0A%0A    private ScheduledFuture%3C?%3E startLockWatchingThread(String updateLockStatement
 ) %7B%0A
@@ -4486,32 +4486,71 @@
 atement) %7B%0A     
+  
+ Runnable lockUpdatingTask = () -%3E %7B%0A
            LOG.d
@@ -4536,32 +4536,33 @@
 -%3E %7B%0A           
+ 
 LOG.debug(%22Updat
@@ -4608,36 +4608,32 @@
 %22);%0A            
-    
 jdbcTemplate.exe
@@ -4668,30 +4668,16 @@
 ement);%0A
-            %7D%0A
         
@@ -4691,91 +4691,65 @@
     
-timer = new Timer();%0A        timer.schedule(lockWatcherTask, 0, Duration.ofMinutes(
+return executor.scheduleAtFixedRate(lockUpdatingTask, 0, 
 lock
@@ -4767,20 +4767,26 @@
  / 2
-).toMillis()
+, TimeUnit.MINUTES
 );%0A 
@@ -4847,21 +4847,31 @@
     
-timer
+scheduledFuture
 .cancel(
 );%0A 
@@ -4866,16 +4866,20 @@
 .cancel(
+true
 );%0A    %7D

Use Executor for task scheduling instead of Timer