All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.harishb2k.easy.database.mysql.lock.MySqlDistributedLock Maven / Gradle / Ivy

package io.github.harishb2k.easy.database.mysql.lock;

import com.zaxxer.hikari.HikariDataSource;
import io.gitbub.harishb2k.easy.helper.Safe;
import io.github.harishb2k.easy.lock.IDistributedLock;
import io.github.harishb2k.easy.lock.IDistributedLockService.ExistingLockWithNoOp;
import io.github.harishb2k.easy.lock.config.LockConfig;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import javax.inject.Inject;
import javax.inject.Named;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

@Slf4j
public class MySqlDistributedLock implements IDistributedLock {
    private static final ThreadLocal lockInCurrentThread = new ThreadLocal<>();
    private final DataSource dataSource;
    private final String lockTableName;
    private LockConfig lockConfig;

    @Inject
    public MySqlDistributedLock(
            @Named("lock_table_data_source") DataSource dataSource,
            @Named("lock_table_name") String lockTableName
    ) {
        this.dataSource = dataSource;
        this.lockTableName = lockTableName;
    }

    @Override
    public void setup(LockConfig lockConfig) {
        this.lockConfig = lockConfig;
    }

    @Override
    public void tearDown() {
        if (dataSource instanceof HikariDataSource) {
            Safe.safe(((HikariDataSource) dataSource)::close);
        }
    }

    @Override
    public Lock achieveLock(LockRequest request) {

        // If we already have a lock in this thread and same request id made - then just give a no-op lock
        if (lockInCurrentThread.get() != null) {
            if (Objects.equals(lockInCurrentThread.get().request, request)) {
                return new ExistingLockWithNoOp(lockInCurrentThread.get());
            }
        }

        InternalLock internalLock = new InternalLock(dataSource, request, lockTableName, lockConfig);
        internalLock.lock();
        lockInCurrentThread.set(internalLock);

        return internalLock;
    }

    @Override
    public void releaseLock(Lock lock, LockRequest lockRequest) {
        try {
            lock.unlock();
        } finally {
            lockInCurrentThread.set(null);
        }
    }

    @Data
    private static class InternalLock implements Lock {
        private final DataSource dataSource;
        private final LockRequest request;
        private final String lockTableName;
        private Connection connection;
        private Connection insertConnection;
        private PreparedStatement insertStatement;
        private PreparedStatement lockStatement;
        private final LockConfig lockConfig;

        private InternalLock(DataSource dataSource, LockRequest request, String lockTableName, LockConfig lockConfig) {
            this.dataSource = dataSource;
            this.request = request;
            this.lockTableName = lockTableName;
            this.lockConfig = lockConfig;
        }

        @Override
        public void lock() {
            try {

                String lockIdToUse = request.getUniqueLockIdForLocking();

                // Make sure we do have a entry in table for locking
                log.debug("Try to insert lock: id={}", lockIdToUse);
                try (Connection connection = dataSource.getConnection(); PreparedStatement statement = connection.prepareStatement(String.format("INSERT IGNORE INTO %s(lock_id) VALUES(?)", lockTableName))) {
                    statement.setQueryTimeout(lockConfig.getTimeoutInSec());
                    statement.setString(1, lockIdToUse);
                    statement.execute();
                    log.debug("Inserted lock: id={}", lockIdToUse);
                } catch (Exception e) {
                    log.error("Failed to insert lock for the first time", e);
                }

                log.debug("Try to lock with select for update: id={}", lockIdToUse);
                connection = dataSource.getConnection();
                connection.setAutoCommit(false);
                lockStatement = connection.prepareStatement(String.format("SELECT * FROM %s WHERE lock_id=? FOR UPDATE", lockTableName));
                lockStatement.setQueryTimeout(lockConfig.getTimeoutInSec());
                lockStatement.setString(1, lockIdToUse);
                lockStatement.executeQuery();
                log.debug("Lock taken with select for update: id={}", lockIdToUse);

            } catch (Throwable e) {
                Safe.safe(() -> {
                    lockStatement.close();
                    lockStatement = null;
                });
                Safe.safe(() -> {
                    connection.setAutoCommit(true);
                    connection.rollback();
                    connection.close();
                    connection = null;
                });
                throw new RuntimeException(String.format("lock cannot be taken: name=%s, id=%s", request.getName(), request.getLockId()), e);
            }
        }

        @Override
        public void unlock() {
            log.debug("Try to unlock lock: id={}", request.getUniqueLockIdForLocking());
            try {
                Safe.safe(() -> {
                    lockStatement.close();
                    lockStatement = null;
                });
                connection.commit();
                connection.setAutoCommit(true);
                connection.close();
                connection = null;
                log.debug("Unlock done: id={}", request.getUniqueLockIdForLocking());
            } catch (Exception e) {
                throw new RuntimeException(String.format("lock cannot be released: name=%s, id=%s", request.getName(), request.getLockId()), e);
            }
        }

        @Override
        public void lockInterruptibly() throws InterruptedException {
            throw new RuntimeException("Not implemented");
        }

        @Override
        public boolean tryLock() {
            throw new RuntimeException("Not implemented");
        }

        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            throw new RuntimeException("Not implemented");
        }

        @Override
        public Condition newCondition() {
            throw new RuntimeException("Not implemented");
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy