/*
* Copyright 2014-2016 the original author or authors.
*
* 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.liquigraph.core.io.lock;
import org.liquigraph.core.exception.LiquigraphLockException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Set;
import java.util.UUID;
import static com.google.common.collect.Sets.newIdentityHashSet;
/**
* Shared lock proxy using references on the connections to create and remove the lock node, more or less like
* performing garbage collection by reference counting. It keeps references instead of simply counting to be able to
* remove the lock using the shutdown hook.
*/
public class LiquigraphLock {
private static final Logger LOGGER = LoggerFactory.getLogger(LiquigraphLock.class);
private final UUID uuid = UUID.randomUUID();
private final Set<Connection> connections = newIdentityHashSet();
private final Thread task = new Thread(new ShutdownTask(this));
void acquire(Connection connection) {
if (addConnection(connection)) {
addShutdownHook();
ensureLockUnicity(connection);
tryWriteLock(connection);
}
}
void release(Connection connection) {
if (removeConnection(connection)) {
removeShutdownHook();
releaseLock(connection);
}
}
void cleanup() {
for (Connection connection : new ArrayList<>(connections)) {
release(connection);
}
}
private boolean addConnection(Connection connection) {
boolean wasEmpty = connections.isEmpty();
connections.add(connection);
return wasEmpty;
}
private boolean removeConnection(Connection connection) {
if (connections.isEmpty()) {
return false;
}
connections.remove(connection);
return connections.isEmpty();
}
private void addShutdownHook() {
Runtime.getRuntime().addShutdownHook(task);
}
private void removeShutdownHook() {
Runtime.getRuntime().removeShutdownHook(task);
}
private void ensureLockUnicity(Connection connection) {
try (Statement statement = connection.createStatement()) {
statement.execute("CREATE CONSTRAINT ON (lock:__LiquigraphLock) ASSERT lock.name IS UNIQUE");
connection.commit();
}
catch (SQLException e) {
throw new LiquigraphLockException(
"Could not ensure __LiquigraphLock unicity\n\t" +
"Please make sure your instance is in a clean state\n\t" +
"No more than 1 lock should be there simultaneously!",
e
);
}
}
private void tryWriteLock(Connection connection) {
try (PreparedStatement statement = connection.prepareStatement(
"CREATE (:__LiquigraphLock {name:'John', uuid:{1}})")) {
statement.setString(1, uuid.toString());
statement.execute();
connection.commit();
}
catch (SQLException e) {
throw new LiquigraphLockException(
"Cannot create __LiquigraphLock lock\n\t" +
"Likely another Liquigraph execution is going on or has crashed.",
e
);
}
}
private void releaseLock(Connection connection) {
try (PreparedStatement statement = connection.prepareStatement(
"MATCH (lock:__LiquigraphLock {uuid:{1}}) DELETE lock")) {
statement.setString(1, uuid.toString());
statement.execute();
connection.commit();
} catch (SQLException e) {
LOGGER.error(
"Cannot remove __LiquigraphLock during cleanup.",
e
);
}
}
}