org.h2.store.FileLock Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of h2-mvstore Show documentation
Show all versions of h2-mvstore Show documentation
Fork of h2database to maintain Java 8 compatibility
The newest version!
/*
* Copyright 2004-2023 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (https://h2database.com/html/license.html).
* Initial Developer: H2 Group
*/
package org.h2.store;
import java.io.IOException;
import java.io.OutputStream;
import java.net.BindException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Properties;
import org.h2.Driver;
import org.h2.api.ErrorCode;
import org.h2.engine.Constants;
import org.h2.engine.SessionRemote;
import org.h2.message.DbException;
import org.h2.message.Trace;
import org.h2.message.TraceSystem;
import org.h2.store.fs.FilePath;
import org.h2.store.fs.FileUtils;
import org.h2.util.MathUtils;
import org.h2.util.NetUtils;
import org.h2.util.SortedProperties;
import org.h2.util.StringUtils;
import org.h2.value.Transfer;
/**
* The file lock is used to lock a database so that only one process can write
* to it. It uses a cooperative locking protocol. Usually a .lock.db file is
* used, but locking by creating a socket is supported as well.
*/
public class FileLock implements Runnable {
private static final String MAGIC = "FileLock";
private static final String FILE = "file";
private static final String SOCKET = "socket";
private static final int RANDOM_BYTES = 16;
private static final int SLEEP_GAP = 25;
private static final int TIME_GRANULARITY = 2000;
/**
* The lock file name.
*/
private volatile String fileName;
/**
* The server socket (only used when using the SOCKET mode).
*/
private volatile ServerSocket serverSocket;
/**
* Whether the file is locked.
*/
private volatile boolean locked;
/**
* The number of milliseconds to sleep after checking a file.
*/
private final int sleep;
/**
* The trace object.
*/
private final Trace trace;
/**
* The last time the lock file was written.
*/
private long lastWrite;
private String method;
private Properties properties;
private String uniqueId;
private Thread watchdog;
/**
* Create a new file locking object.
*
* @param traceSystem the trace system to use
* @param fileName the file name
* @param sleep the number of milliseconds to sleep
*/
public FileLock(TraceSystem traceSystem, String fileName, int sleep) {
this.trace = traceSystem == null ?
null : traceSystem.getTrace(Trace.FILE_LOCK);
this.fileName = fileName;
this.sleep = sleep;
}
/**
* Lock the file if possible. A file may only be locked once.
*
* @param fileLockMethod the file locking method to use
* @throws DbException if locking was not successful
*/
public synchronized void lock(FileLockMethod fileLockMethod) {
checkServer();
if (locked) {
throw DbException.getInternalError("already locked");
}
switch (fileLockMethod) {
case FILE:
lockFile();
break;
case SOCKET:
lockSocket();
break;
case FS:
case NO:
break;
}
locked = true;
}
/**
* Unlock the file. The watchdog thread is stopped. This method does nothing
* if the file is already unlocked.
*/
public synchronized void unlock() {
if (!locked) {
return;
}
locked = false;
try {
if (watchdog != null) {
watchdog.interrupt();
}
} catch (Exception e) {
trace.debug(e, "unlock");
}
try {
if (fileName != null) {
if (load().equals(properties)) {
FileUtils.delete(fileName);
}
}
if (serverSocket != null) {
serverSocket.close();
}
} catch (Exception e) {
trace.debug(e, "unlock");
} finally {
fileName = null;
serverSocket = null;
}
try {
if (watchdog != null) {
watchdog.join();
}
} catch (Exception e) {
trace.debug(e, "unlock");
} finally {
watchdog = null;
}
}
/**
* Add or change a setting to the properties. This call does not save the
* file.
*
* @param key the key
* @param value the value
*/
public void setProperty(String key, String value) {
if (value == null) {
properties.remove(key);
} else {
properties.put(key, value);
}
}
/**
* Save the lock file.
*
* @return the saved properties
*/
public Properties save() {
try {
try (OutputStream out = FileUtils.newOutputStream(fileName, false)) {
properties.store(out, MAGIC);
}
lastWrite = aggressiveLastModified(fileName);
if (trace.isDebugEnabled()) {
trace.debug("save " + properties);
}
return properties;
} catch (IOException e) {
throw getExceptionFatal("Could not save properties " + fileName, e);
}
}
/**
* Aggressively read last modified time, to work-around remote filesystems.
*
* @param fileName file name to check
* @return last modified date/time in milliseconds UTC
*/
private static long aggressiveLastModified(String fileName) {
/*
* Some remote filesystem, e.g. SMB on Windows, can cache metadata for
* 5-10 seconds. To work around that, do a one-byte read from the
* underlying file, which has the effect of invalidating the metadata
* cache.
*/
try {
try (FileChannel f = FilePath.get(fileName).open("rws")) {
ByteBuffer b = ByteBuffer.wrap(new byte[1]);
f.read(b);
}
} catch (IOException ignoreEx) {}
return FileUtils.lastModified(fileName);
}
private void checkServer() {
Properties prop = load();
String server = prop.getProperty("server");
if (server == null) {
return;
}
boolean running = false;
String id = prop.getProperty("id");
try {
Socket socket = NetUtils.createSocket(server,
Constants.DEFAULT_TCP_PORT, false);
Transfer transfer = new Transfer(null, socket);
transfer.init();
transfer.writeInt(Constants.TCP_PROTOCOL_VERSION_MIN_SUPPORTED);
transfer.writeInt(Constants.TCP_PROTOCOL_VERSION_MAX_SUPPORTED);
transfer.writeString(null);
transfer.writeString(null);
transfer.writeString(id);
transfer.writeInt(SessionRemote.SESSION_CHECK_KEY);
transfer.flush();
int state = transfer.readInt();
if (state == SessionRemote.STATUS_OK) {
running = true;
}
transfer.close();
socket.close();
} catch (IOException e) {
return;
}
if (running) {
DbException e = DbException.get(
ErrorCode.DATABASE_ALREADY_OPEN_1, "Server is running");
throw e.addSQL(server + "/" + id);
}
}
/**
* Load the properties file.
*
* @return the properties
*/
public Properties load() {
IOException lastException = null;
for (int i = 0; i < 5; i++) {
try {
Properties p2 = SortedProperties.loadProperties(fileName);
if (trace.isDebugEnabled()) {
trace.debug("load " + p2);
}
return p2;
} catch (IOException e) {
lastException = e;
}
}
throw getExceptionFatal(
"Could not load properties " + fileName, lastException);
}
private void waitUntilOld() {
for (int i = 0; i < 2 * TIME_GRANULARITY / SLEEP_GAP; i++) {
long last = aggressiveLastModified(fileName);
long dist = System.currentTimeMillis() - last;
if (dist < -TIME_GRANULARITY) {
// lock file modified in the future -
// wait for a bit longer than usual
try {
Thread.sleep(2 * (long) sleep);
} catch (Exception e) {
trace.debug(e, "sleep");
}
return;
} else if (dist > TIME_GRANULARITY) {
return;
}
try {
Thread.sleep(SLEEP_GAP);
} catch (Exception e) {
trace.debug(e, "sleep");
}
}
throw getExceptionFatal("Lock file recently modified", null);
}
private void setUniqueId() {
byte[] bytes = MathUtils.secureRandomBytes(RANDOM_BYTES);
String random = StringUtils.convertBytesToHex(bytes);
uniqueId = Long.toHexString(System.currentTimeMillis()) + random;
properties.setProperty("id", uniqueId);
}
private void lockFile() {
method = FILE;
properties = new SortedProperties();
properties.setProperty("method", String.valueOf(method));
setUniqueId();
FileUtils.createDirectories(FileUtils.getParent(fileName));
if (!FileUtils.createFile(fileName)) {
waitUntilOld();
String m2 = load().getProperty("method", FILE);
if (!m2.equals(FILE)) {
throw getExceptionFatal("Unsupported lock method " + m2, null);
}
save();
sleep(2 * sleep);
if (!load().equals(properties)) {
throw getExceptionAlreadyInUse("Locked by another process: " + fileName);
}
FileUtils.delete(fileName);
if (!FileUtils.createFile(fileName)) {
throw getExceptionFatal("Another process was faster", null);
}
}
save();
sleep(SLEEP_GAP);
if (!load().equals(properties)) {
fileName = null;
throw getExceptionFatal("Concurrent update", null);
}
locked = true;
watchdog = new Thread(this, "H2 File Lock Watchdog " + fileName);
Driver.setThreadContextClassLoader(watchdog);
watchdog.setDaemon(true);
watchdog.setPriority(Thread.MAX_PRIORITY - 1);
watchdog.start();
}
private void lockSocket() {
method = SOCKET;
properties = new SortedProperties();
properties.setProperty("method", String.valueOf(method));
setUniqueId();
// if this returns 127.0.0.1,
// the computer is probably not networked
String ipAddress = NetUtils.getLocalAddress();
FileUtils.createDirectories(FileUtils.getParent(fileName));
if (!FileUtils.createFile(fileName)) {
waitUntilOld();
long read = aggressiveLastModified(fileName);
Properties p2 = load();
String m2 = p2.getProperty("method", SOCKET);
if (m2.equals(FILE)) {
lockFile();
return;
} else if (!m2.equals(SOCKET)) {
throw getExceptionFatal("Unsupported lock method " + m2, null);
}
String ip = p2.getProperty("ipAddress", ipAddress);
if (!ipAddress.equals(ip)) {
throw getExceptionAlreadyInUse("Locked by another computer: " + ip);
}
String port = p2.getProperty("port", "0");
int portId = Integer.parseInt(port);
InetAddress address;
try {
address = InetAddress.getByName(ip);
} catch (UnknownHostException e) {
throw getExceptionFatal("Unknown host " + ip, e);
}
for (int i = 0; i < 3; i++) {
try {
Socket s = new Socket(address, portId);
s.close();
throw getExceptionAlreadyInUse("Locked by another process");
} catch (BindException e) {
throw getExceptionFatal("Bind Exception", null);
} catch (ConnectException e) {
trace.debug(e, "socket not connected to port " + port);
} catch (IOException e) {
throw getExceptionFatal("IOException", null);
}
}
if (read != aggressiveLastModified(fileName)) {
throw getExceptionFatal("Concurrent update", null);
}
FileUtils.delete(fileName);
if (!FileUtils.createFile(fileName)) {
throw getExceptionFatal("Another process was faster", null);
}
}
try {
// 0 to use any free port
serverSocket = NetUtils.createServerSocket(0, false);
int port = serverSocket.getLocalPort();
properties.setProperty("ipAddress", ipAddress);
properties.setProperty("port", Integer.toString(port));
} catch (Exception e) {
trace.debug(e, "lock");
serverSocket = null;
lockFile();
return;
}
save();
locked = true;
watchdog = new Thread(this,
"H2 File Lock Watchdog (Socket) " + fileName);
watchdog.setDaemon(true);
watchdog.start();
}
private static void sleep(int time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
throw getExceptionFatal("Sleep interrupted", e);
}
}
private static DbException getExceptionFatal(String reason, Throwable t) {
return DbException.get(
ErrorCode.ERROR_OPENING_DATABASE_1, t, reason);
}
private DbException getExceptionAlreadyInUse(String reason) {
DbException e = DbException.get(
ErrorCode.DATABASE_ALREADY_OPEN_1, reason);
if (fileName != null) {
try {
Properties prop = load();
String server = prop.getProperty("server");
if (server != null) {
String serverId = server + "/" + prop.getProperty("id");
e = e.addSQL(serverId);
}
} catch (DbException e2) {
// ignore
}
}
return e;
}
/**
* Get the file locking method type given a method name.
*
* @param method the method name
* @return the method type
* @throws DbException if the method name is unknown
*/
public static FileLockMethod getFileLockMethod(String method) {
if (method == null || method.equalsIgnoreCase("FILE")) {
return FileLockMethod.FILE;
} else if (method.equalsIgnoreCase("NO")) {
return FileLockMethod.NO;
} else if (method.equalsIgnoreCase("SOCKET")) {
return FileLockMethod.SOCKET;
} else if (method.equalsIgnoreCase("FS")) {
return FileLockMethod.FS;
} else {
throw DbException.get(ErrorCode.UNSUPPORTED_LOCK_METHOD_1, method);
}
}
public String getUniqueId() {
return uniqueId;
}
@Override
public void run() {
try {
while (locked && fileName != null) {
// trace.debug("watchdog check");
try {
if (!FileUtils.exists(fileName) ||
aggressiveLastModified(fileName) != lastWrite) {
save();
}
Thread.sleep(sleep);
} catch (OutOfMemoryError | NullPointerException | InterruptedException e) {
// ignore
} catch (Exception e) {
trace.debug(e, "watchdog");
}
}
while (true) {
// take a copy so we don't get an NPE between checking it and using it
ServerSocket local = serverSocket;
if (local == null) {
break;
}
try {
trace.debug("watchdog accept");
Socket s = local.accept();
s.close();
} catch (Exception e) {
trace.debug(e, "watchdog");
}
}
} catch (Exception e) {
trace.debug(e, "watchdog");
}
trace.debug("watchdog end");
}
}