001: /*
002: * Copyright 2004-2008 H2 Group. Licensed under the H2 License, Version 1.0
003: * (http://h2database.com/html/license.html).
004: * Initial Developer: H2 Group
005: */
006: package org.h2.store;
007:
008: import java.io.IOException;
009: import java.io.OutputStream;
010: import java.net.BindException;
011: import java.net.ConnectException;
012: import java.net.InetAddress;
013: import java.net.ServerSocket;
014: import java.net.Socket;
015: import java.net.UnknownHostException;
016: import java.sql.SQLException;
017: import java.util.Properties;
018:
019: import org.h2.constant.ErrorCode;
020: import org.h2.constant.SysProperties;
021: import org.h2.message.Message;
022: import org.h2.message.Trace;
023: import org.h2.message.TraceSystem;
024: import org.h2.store.fs.FileSystem;
025: import org.h2.util.ByteUtils;
026: import org.h2.util.FileUtils;
027: import org.h2.util.NetUtils;
028: import org.h2.util.RandomUtils;
029: import org.h2.util.SortedProperties;
030:
031: /**
032: * The file lock is used to lock a database so that only one process can write
033: * to it. Usually a .lock.db file is used, but locking by creating a socket is
034: * supported as well.
035: */
036: public class FileLock {
037:
038: public static final int LOCK_NO = 0, LOCK_FILE = 1,
039: LOCK_SOCKET = 2;
040:
041: // TODO lock: maybe not so secure! what if tread does not have chance to run?
042: // TODO lock: implement locking method using java 1.4 FileLock
043: private static final String MAGIC = "FileLock";
044: private static final String FILE = "file", SOCKET = "socket";
045: private static final int RANDOM_BYTES = 16;
046: private static final int SLEEP_GAP = 25;
047: private static final int TIME_GRANULARITY = 2000;
048:
049: private String method, ipAddress;
050: private int sleep;
051: private long lastWrite;
052: private Properties properties;
053: private FileSystem fs;
054: private volatile String fileName;
055: private volatile ServerSocket socket;
056: private boolean locked;
057: private Trace trace;
058:
059: // private java.nio.channels.FileLock fileLock;
060:
061: public FileLock(TraceSystem traceSystem, int sleep) {
062: this .trace = traceSystem.getTrace(Trace.FILE_LOCK);
063: this .sleep = sleep;
064: }
065:
066: public synchronized void lock(String fileName, boolean allowSocket)
067: throws SQLException {
068: this .fs = FileSystem.getInstance(fileName);
069: this .fileName = fileName;
070: if (locked) {
071: throw Message.getInternalError("already locked");
072: }
073: if (allowSocket) {
074: lockSocket();
075: } else {
076: lockFile();
077: }
078: locked = true;
079: }
080:
081: protected void finalize() {
082: if (!SysProperties.runFinalize) {
083: return;
084: }
085: if (locked) {
086: unlock();
087: }
088: }
089:
090: // void kill() {
091: // socket = null;
092: // file = null;
093: // locked = false;
094: // trace("killed", null);
095: // }
096:
097: // TODO log / messages: use translatable messages!
098: public synchronized void unlock() {
099: if (!locked) {
100: return;
101: }
102: try {
103: if (fileName != null) {
104: if (load().equals(properties)) {
105: fs.delete(fileName);
106: }
107: }
108: if (socket != null) {
109: socket.close();
110: }
111: } catch (Exception e) {
112: trace.debug("unlock", e);
113: }
114: fileName = null;
115: socket = null;
116: locked = false;
117: }
118:
119: void save() throws SQLException {
120: try {
121: OutputStream out = fs.openFileOutputStream(fileName, false);
122: try {
123: properties
124: .setProperty("method", String.valueOf(method));
125: properties.store(out, MAGIC);
126: } finally {
127: out.close();
128: }
129: lastWrite = fs.getLastModified(fileName);
130: trace.debug("save " + properties);
131: } catch (IOException e) {
132: throw getException(e);
133: }
134: }
135:
136: private Properties load() throws SQLException {
137: try {
138: Properties p2 = FileUtils.loadProperties(fileName);
139: trace.debug("load " + p2);
140: return p2;
141: } catch (IOException e) {
142: throw getException(e);
143: }
144: }
145:
146: private void waitUntilOld() throws SQLException {
147: for (int i = 0; i < 10; i++) {
148: long last = fs.getLastModified(fileName);
149: long dist = System.currentTimeMillis() - last;
150: if (dist < -TIME_GRANULARITY) {
151: throw error("Lock file modified in the future: dist="
152: + dist);
153: }
154: if (dist < SLEEP_GAP) {
155: try {
156: Thread.sleep(dist + 1);
157: } catch (Exception e) {
158: trace.debug("sleep", e);
159: }
160: } else {
161: return;
162: }
163: }
164: throw error("Lock file recently modified");
165: }
166:
167: private void lockFile() throws SQLException {
168: method = FILE;
169: properties = new SortedProperties();
170: byte[] bytes = RandomUtils.getSecureBytes(RANDOM_BYTES);
171: String random = ByteUtils.convertBytesToString(bytes);
172: properties.setProperty("id", Long.toHexString(System
173: .currentTimeMillis())
174: + random);
175: if (!fs.createNewFile(fileName)) {
176: waitUntilOld();
177: String m2 = load().getProperty("method", FILE);
178: if (!m2.equals(FILE)) {
179: throw error("Unsupported lock method " + m2);
180: }
181: save();
182: sleep(2 * sleep);
183: if (!load().equals(properties)) {
184: throw error("Locked by another process");
185: }
186: fs.delete(fileName);
187: if (!fs.createNewFile(fileName)) {
188: throw error("Another process was faster");
189: }
190: }
191: save();
192: sleep(SLEEP_GAP);
193: if (!load().equals(properties)) {
194: fileName = null;
195: throw error("Concurrent update");
196: }
197: Thread watchdog = new Thread(new Runnable() {
198: public void run() {
199: try {
200: while (fileName != null) {
201: // trace.debug("watchdog check");
202: try {
203: if (!fs.exists(fileName)
204: || fs.getLastModified(fileName) != lastWrite) {
205: save();
206: }
207: Thread.sleep(sleep);
208: } catch (Exception e) {
209: trace.debug("watchdog", e);
210: }
211: }
212: } catch (Exception e) {
213: trace.debug("watchdog", e);
214: }
215: trace.debug("watchdog end");
216: }
217: });
218: watchdog.setName("H2 File Lock Watchdog " + fileName);
219: watchdog.setDaemon(true);
220: watchdog.setPriority(Thread.MAX_PRIORITY - 1);
221: watchdog.start();
222: }
223:
224: private void lockSocket() throws SQLException {
225: method = SOCKET;
226: properties = new SortedProperties();
227: try {
228: // TODO documentation: if this returns 127.0.0.1,
229: // the computer is probably not networked
230: ipAddress = InetAddress.getLocalHost().getHostAddress();
231: } catch (UnknownHostException e) {
232: throw getException(e);
233: }
234: if (!fs.createNewFile(fileName)) {
235: waitUntilOld();
236: long read = fs.getLastModified(fileName);
237: Properties p2 = load();
238: String m2 = p2.getProperty("method", SOCKET);
239: if (m2.equals(FILE)) {
240: lockFile();
241: return;
242: } else if (!m2.equals(SOCKET)) {
243: throw error("Unsupported lock method " + m2);
244: }
245: String ip = p2.getProperty("ipAddress", ipAddress);
246: if (!ipAddress.equals(ip)) {
247: throw error("Locked by another computer: " + ip);
248: }
249: String port = p2.getProperty("port", "0");
250: int portId = Integer.parseInt(port);
251: InetAddress address;
252: try {
253: address = InetAddress.getByName(ip);
254: } catch (UnknownHostException e) {
255: throw getException(e);
256: }
257: for (int i = 0; i < 3; i++) {
258: try {
259: Socket s = new Socket(address, portId);
260: s.close();
261: throw error("Locked by another process");
262: } catch (BindException e) {
263: throw error("Bind Exception");
264: } catch (ConnectException e) {
265: trace.debug("lockSocket not connected " + port, e);
266: } catch (IOException e) {
267: throw error("IOException");
268: }
269: }
270: if (read != fs.getLastModified(fileName)) {
271: throw error("Concurrent update");
272: }
273: fs.delete(fileName);
274: if (!fs.createNewFile(fileName)) {
275: throw error("Another process was faster");
276: }
277: }
278: try {
279: // 0 to use any free port
280: socket = NetUtils.createServerSocket(0, false);
281: int port = socket.getLocalPort();
282: properties.setProperty("ipAddress", ipAddress);
283: properties.setProperty("port", String.valueOf(port));
284: } catch (Exception e) {
285: trace.debug("lock", e);
286: socket = null;
287: lockFile();
288: return;
289: }
290: save();
291: Thread watchdog = new Thread(new Runnable() {
292: public void run() {
293: while (socket != null) {
294: try {
295: trace.debug("watchdog accept");
296: Socket s = socket.accept();
297: s.close();
298: } catch (Exception e) {
299: trace.debug("watchdog", e);
300: }
301: }
302: trace.debug("watchdog end");
303: }
304: });
305: watchdog.setDaemon(true);
306: watchdog.setName("H2 File Lock Watchdog (Socket) " + fileName);
307: watchdog.start();
308: }
309:
310: private void sleep(int time) throws SQLException {
311: try {
312: Thread.sleep(time);
313: } catch (InterruptedException e) {
314: throw getException(e);
315: }
316: }
317:
318: private SQLException getException(Throwable t) {
319: return Message.getSQLException(
320: ErrorCode.ERROR_OPENING_DATABASE, null, t);
321: }
322:
323: private SQLException error(String reason) {
324: return Message.getSQLException(
325: ErrorCode.DATABASE_ALREADY_OPEN_1, reason);
326: }
327:
328: public static int getFileLockMethod(String method)
329: throws SQLException {
330: if (method == null || method.equalsIgnoreCase("FILE")) {
331: return FileLock.LOCK_FILE;
332: } else if (method.equalsIgnoreCase("NO")) {
333: return FileLock.LOCK_NO;
334: } else if (method.equalsIgnoreCase("SOCKET")) {
335: return FileLock.LOCK_SOCKET;
336: } else {
337: throw Message.getSQLException(
338: ErrorCode.UNSUPPORTED_LOCK_METHOD_1, method);
339: }
340: }
341:
342: }
|