// Copyright © 2015 HSL <https://www.hsl.fi>
// This program is dual-licensed under the EUPL v1.2 and AGPLv3 licenses.
package fi.hsl.parkandride.back;
import com.querydsl.core.QueryException;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.MappingProjection;
import com.querydsl.sql.postgresql.PostgreSQLQueryFactory;
import fi.hsl.parkandride.back.sql.QLock;
import fi.hsl.parkandride.core.back.LockRepository;
import fi.hsl.parkandride.core.domain.Lock;
import fi.hsl.parkandride.core.domain.LockAcquireFailedException;
import fi.hsl.parkandride.core.domain.LockException;
import fi.hsl.parkandride.core.service.ValidationService;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
public class LockDao implements LockRepository {
private static final QLock qLock = QLock.lock;
private static final MappingProjection<Lock> lockMapping = new MappingProjection<Lock>(Lock.class, qLock.all()) {
@Override
protected Lock map(Tuple row) {
return new Lock(row.get(qLock.name), row.get(qLock.owner), row.get(qLock.validUntil));
}
};
private final PostgreSQLQueryFactory queryFactory;
private final ValidationService validationService;
private final String ownerName;
public LockDao(PostgreSQLQueryFactory queryFactory, ValidationService validationService, String lockOwnerName) {
this.queryFactory = queryFactory;
this.validationService = validationService;
this.ownerName = lockOwnerName;
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE)
public Lock acquireLock(String lockName, Duration lockDuration) {
Optional<Lock> lock = selectLockIfExists(lockName);
if (lock.isPresent()) {
Lock existingLock = lock.get();
if (!existingLock.validUntil.isAfter(DateTime.now())) {
return claimExpiredLock(existingLock, lockDuration);
} else {
throw new LockAcquireFailedException("Existing lock " + existingLock + " is still valid");
}
}
return insertLock(lockName, lockDuration);
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE)
public boolean releaseLock(Lock lock) {
validationService.validate(lock);
if (ownerName.equals(lock.owner)) {
return deleteLock(lock) == 1;
} else {
throw new LockException("Cannot release lock. Lock is not owned by this node.");
}
}
// Following mehods are protected to allow testing
protected Optional<Lock> selectLockIfExists(String lockName) {
return Optional.ofNullable(queryFactory.from(qLock)
.where(qLock.name.eq(lockName))
.select(lockMapping)
.fetchOne());
}
protected Lock claimExpiredLock(Lock existingLock, Duration lockDuration) {
try {
final DateTime newValidUntil = DateTime.now().plus(lockDuration);
long rowsUpdated = queryFactory.update(qLock)
.where(qLock.name.eq(existingLock.name))
.where(qLock.owner.eq(existingLock.owner))
.where(qLock.validUntil.eq(existingLock.validUntil))
.set(qLock.owner, ownerName)
.set(qLock.validUntil, newValidUntil)
.execute();
if (rowsUpdated > 0) {
return new Lock(existingLock.name, ownerName, newValidUntil);
} else {
throw getFailedToClaimExpiredLockException(existingLock, null);
}
} catch (QueryException e) {
throw getFailedToClaimExpiredLockException(existingLock, e);
}
}
private LockAcquireFailedException getFailedToClaimExpiredLockException(Lock existingLock, QueryException e) {
return new LockAcquireFailedException("Failed to claim expired lock " + existingLock + " for " + ownerName, e);
}
protected Lock insertLock(String lockName, Duration lockDuration) {
final DateTime validUntil = DateTime.now().plus(lockDuration);
final Lock newLock = new Lock(lockName, ownerName, validUntil);
try {
long rowsInserted = queryFactory.insert(qLock)
.columns(qLock.name, qLock.owner, qLock.validUntil)
.values(lockName, ownerName, validUntil)
.execute();
if (rowsInserted > 0) {
return newLock;
} else {
throw getInsertLockFailedException(lockName, null);
}
} catch (QueryException e) {
throw getInsertLockFailedException(lockName, e);
}
}
private LockAcquireFailedException getInsertLockFailedException(String lockName, QueryException e) {
return new LockAcquireFailedException("Failed to acquire lock '" + lockName + "' (lost acquisition race to another node)", e);
}
protected long deleteLock(Lock lock) {
return queryFactory.delete(qLock)
.where(qLock.name.eq(lock.name))
.where(qLock.owner.eq(lock.owner))
.where(qLock.validUntil.eq(lock.validUntil))
.execute();
}
}