/*
 * Copyright 2004-2019 H2 Group. Multiple-Licensed under the MPL 2.0,
 * and the EPL 1.0 (http://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.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.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.
/** * 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 String SERIALIZED = "serialized"; 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.
/** * The lock file name. */
private volatile String fileName;
The server socket (only used when using the SOCKET mode).
/** * The server socket (only used when using the SOCKET mode). */
private volatile ServerSocket serverSocket;
Whether the file is locked.
/** * Whether the file is locked. */
private volatile boolean locked;
The number of milliseconds to sleep after checking a file.
/** * The number of milliseconds to sleep after checking a file. */
private final int sleep;
The trace object.
/** * The trace object. */
private final Trace trace;
The last time the lock file was written.
/** * 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.
Params:
  • traceSystem – the trace system to use
  • fileName – the file name
  • sleep – the number of milliseconds to sleep
/** * 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.
Params:
  • fileLockMethod – the file locking method to use
Throws:
/** * 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) { DbException.throwInternalError("already locked"); } switch (fileLockMethod) { case FILE: lockFile(); break; case SOCKET: lockSocket(); break; case SERIALIZED: lockSerialized(); 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.
/** * 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.
Params:
  • key – the key
  • value – the value
/** * 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.
Returns:the saved properties
/** * Save the lock file. * * @return the saved properties */
public Properties save() { try { try (OutputStream out = FileUtils.newOutputStream(fileName, false)) { properties.store(out, MAGIC); } lastWrite = FileUtils.lastModified(fileName); if (trace.isDebugEnabled()) { trace.debug("save " + properties); } return properties; } catch (IOException e) { throw getExceptionFatal("Could not save properties " + fileName, e); } } 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.
Returns:the properties
/** * 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 = FileUtils.lastModified(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 lockSerialized() { method = SERIALIZED; FileUtils.createDirectories(FileUtils.getParent(fileName)); if (FileUtils.createFile(fileName)) { properties = new SortedProperties(); properties.setProperty("method", String.valueOf(method)); setUniqueId(); save(); } else { while (true) { try { properties = load(); } catch (DbException e) { // ignore } return; } } } 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 = FileUtils.lastModified(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 != FileUtils.lastModified(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.
Params:
  • method – the method name
Throws:
Returns:the method type
/** * 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("SERIALIZED")) { return FileLockMethod.SERIALIZED; } 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) || FileUtils.lastModified(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"); } }