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

com.coditory.sherlock.sql.rxjava.SqlDistributedLockConnector Maven / Gradle / Ivy

package com.coditory.sherlock.sql.rxjava;

import com.coditory.sherlock.LockRequest;
import com.coditory.sherlock.SherlockException;
import com.coditory.sherlock.connector.AcquireResult;
import com.coditory.sherlock.connector.InitializationResult;
import com.coditory.sherlock.connector.ReleaseResult;
import com.coditory.sherlock.rxjava.DistributedLockConnector;
import com.coditory.sherlock.sql.BindingMapper;
import com.coditory.sherlock.sql.SqlLockQueries;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.Statement;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
import org.jetbrains.annotations.NotNull;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.function.Function;

import static com.coditory.sherlock.Preconditions.expectNonEmpty;
import static com.coditory.sherlock.Preconditions.expectNonNull;

class SqlDistributedLockConnector implements DistributedLockConnector {
    private final SqlTableInitializer sqlTableInitializer;
    private final SqlLockQueries sqlQueries;
    private final Clock clock;
    private final BindingMapper bindingMapper;

    SqlDistributedLockConnector(
        @NotNull ConnectionFactory connectionFactory,
        @NotNull String tableName,
        @NotNull Clock clock,
        @NotNull BindingMapper bindingMapper
    ) {
        expectNonNull(connectionFactory, "connectionFactory");
        expectNonEmpty(tableName, "tableName");
        expectNonNull(clock, "clock");
        expectNonNull(bindingMapper, "bindingMapper");
        this.clock = clock;
        this.sqlQueries = new SqlLockQueries(tableName, bindingMapper);
        this.sqlTableInitializer = new SqlTableInitializer(connectionFactory, sqlQueries);
        this.bindingMapper = bindingMapper;
    }

    @Override
    @NotNull
    public Single initialize() {
        return sqlTableInitializer.getInitializedConnection()
            .flatMap(c -> closeConnection(c).map(__ -> InitializationResult.of(true)))
            .onErrorResumeNext(e -> Single.error(new SherlockException("Could not initialize SQL table", e)));
    }

    @Override
    @NotNull
    public Single acquire(@NotNull LockRequest lockRequest) {
        Instant now = now();
        return connectionFlatMap(connection ->
            updateReleasedLock(connection, lockRequest, now)
                .flatMap(updated -> updated
                    ? Single.just(true)
                    : insertLock(connection, lockRequest, now))
        )
            .onErrorResumeNext(e -> Single.error(new SherlockException("Could not acquire lock: " + lockRequest, e)))
            .map(AcquireResult::of);
    }

    @Override
    @NotNull
    public Single acquireOrProlong(@NotNull LockRequest lockRequest) {
        Instant now = now();
        return connectionFlatMap(connection ->
            updateAcquiredOrReleasedLock(connection, lockRequest, now)
                .flatMap(updated -> updated
                    ? Single.just(true)
                    : insertLock(connection, lockRequest, now))
        )
            .onErrorResumeNext(e -> Single.error(new SherlockException("Could not acquire or prolong lock: " + lockRequest, e)))
            .map(AcquireResult::of);
    }

    @Override
    @NotNull
    public Single forceAcquire(@NotNull LockRequest lockRequest) {
        Instant now = now();
        return connectionFlatMap(connection ->
            updateLockById(connection, lockRequest, now)
                .flatMap(updated -> updated
                    ? Single.just(true)
                    : insertLock(connection, lockRequest, now))
        )
            .onErrorResumeNext(e -> Single.error(new SherlockException("Could not force acquire lock: " + lockRequest, e)))
            .map(AcquireResult::of);
    }

    private Single updateReleasedLock(Connection connection, LockRequest lockRequest, Instant now) {
        String lockId = lockRequest.lockId();
        Instant expiresAt = expiresAt(now, lockRequest.duration());
        return statementBinder(connection, sqlQueries.updateReleasedLock())
            .bindOwnerId(lockRequest.ownerId())
            .bindNow(now)
            .bindExpiresAt(expiresAt)
            .bindLockId(lockId)
            .bindNow(now)
            .executeAndGetUpdated()
            .map(updatedRows -> updatedRows > 0);
    }

    private Single updateAcquiredOrReleasedLock(Connection connection, LockRequest lockRequest, Instant now) {
        String lockId = lockRequest.lockId();
        Instant expiresAt = expiresAt(now, lockRequest.duration());
        return statementBinder(connection, sqlQueries.updateAcquiredOrReleasedLock())
            .bindOwnerId(lockRequest.ownerId())
            .bindNow(now)
            .bindExpiresAt(expiresAt)
            .bindLockId(lockId)
            .bindOwnerId(lockRequest.ownerId())
            .bindNow(now)
            .executeAndGetUpdated()
            .map(updatedRows -> updatedRows > 0);
    }

    private Single updateLockById(Connection connection, LockRequest lockRequest, Instant now) {
        String lockId = lockRequest.lockId();
        Instant expiresAt = expiresAt(now, lockRequest.duration());
        return statementBinder(connection, sqlQueries.updateLockById())
            .bindOwnerId(lockRequest.ownerId())
            .bindNow(now)
            .bindExpiresAt(expiresAt)
            .bindLockId(lockId)
            .executeAndGetUpdated()
            .map(updatedRows -> updatedRows > 0);
    }

    private Single insertLock(Connection connection, LockRequest lockRequest, Instant now) {
        String lockId = lockRequest.lockId();
        Instant expiresAt = expiresAt(now, lockRequest.duration());
        return statementBinder(connection, sqlQueries.insertLock())
            .bindLockId(lockId)
            .bindOwnerId(lockRequest.ownerId())
            .bindNow(now)
            .bindExpiresAt(expiresAt)
            .executeAndGetUpdated()
            .map(updatedRows -> updatedRows > 0)
            .onErrorResumeNext(e -> Single.just(false));
    }

    @Override
    @NotNull
    public Single release(@NotNull String lockId, @NotNull String ownerId) {
        return statementBinder(sqlQueries.deleteAcquiredByIdAndOwnerId(), binder ->
            binder
                .bindLockId(lockId)
                .bindOwnerId(ownerId)
                .bindNow(now())
                .executeAndGetUpdated()
                .map(updated -> ReleaseResult.of(updated > 0))
        ).onErrorResumeNext(e -> Single.error(new SherlockException("Could not release lock: " + lockId + ", owner: " + ownerId, e)));
    }

    @Override
    @NotNull
    public Single forceRelease(@NotNull String lockId) {
        return statementBinder(sqlQueries.deleteAcquiredById(), binder ->
            binder
                .bindOwnerId(lockId)
                .bindNow(now())
                .executeAndGetUpdated()
                .map(updated -> ReleaseResult.of(updated > 0))
        ).onErrorResumeNext(e -> Single.error(new SherlockException("Could not force release lock: " + lockId, e)));
    }

    @Override
    @NotNull
    public Single forceReleaseAll() {
        return connectionFlatMap(connection -> {
            Statement statement = connection.createStatement(sqlQueries.deleteAll());
            return Single.fromPublisher(statement.execute())
                .flatMap(result -> Single.fromPublisher(result.getRowsUpdated()))
                .map(updated -> ReleaseResult.of(updated > 0));
        })
            .onErrorResumeNext(e -> Single.error(new SherlockException("Could not force release all locks", e)));
    }

    private Instant now() {
        return clock.instant();
    }

    private Instant expiresAt(Instant now, Duration duration) {
        if (duration == null) {
            return null;
        }
        return now.plus(duration);
    }

    private  Single statementBinder(String sql, Function> action) {
        return connectionFlatMap(connection -> {
            SqlStatementBinder binder = statementBinder(connection, sql);
            return action.apply(binder);
        });
    }

    private SqlStatementBinder statementBinder(Connection connection, String sql) {
        Statement statement = connection.createStatement(sql);
        return new SqlStatementBinder(statement, bindingMapper);
    }

    private  Single connectionFlatMap(Function> action) {
        return sqlTableInitializer.getInitializedConnection()
            .flatMap(connection ->
                action.apply(connection)
                    .flatMap(v -> closeConnection(connection).map(__ -> v))
                    .onErrorResumeNext(e -> closeConnection(connection).flatMap(wer -> Single.error(e)))
            );
    }

    private Single closeConnection(Connection connection) {
        return Flowable.fromPublisher(connection.close())
            .firstElement()
            .map(__ -> true)
            .defaultIfEmpty(true)
            .onErrorResumeNext(e -> Single.error(new SherlockException("Could not close connection", e)));
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy