1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.store; |
7 | |
8 | import java.io.IOException; |
9 | import java.io.OutputStream; |
10 | import java.net.BindException; |
11 | import java.net.ConnectException; |
12 | import java.net.InetAddress; |
13 | import java.net.ServerSocket; |
14 | import java.net.Socket; |
15 | import java.net.UnknownHostException; |
16 | import java.util.Properties; |
17 | import org.h2.Driver; |
18 | import org.h2.api.ErrorCode; |
19 | import org.h2.engine.Constants; |
20 | import org.h2.engine.SessionRemote; |
21 | import org.h2.message.DbException; |
22 | import org.h2.message.Trace; |
23 | import org.h2.message.TraceSystem; |
24 | import org.h2.store.fs.FileUtils; |
25 | import org.h2.util.MathUtils; |
26 | import org.h2.util.NetUtils; |
27 | import org.h2.util.SortedProperties; |
28 | import org.h2.util.StringUtils; |
29 | import org.h2.value.Transfer; |
30 | |
31 | /** |
32 | * The file lock is used to lock a database so that only one process can write |
33 | * to it. It uses a cooperative locking protocol. Usually a .lock.db file is |
34 | * used, but locking by creating a socket is supported as well. |
35 | */ |
36 | public class FileLock implements Runnable { |
37 | |
38 | /** |
39 | * This locking method means no locking is used at all. |
40 | */ |
41 | public static final int LOCK_NO = 0; |
42 | |
43 | /** |
44 | * This locking method means the cooperative file locking protocol should be |
45 | * used. |
46 | */ |
47 | public static final int LOCK_FILE = 1; |
48 | |
49 | /** |
50 | * This locking method means a socket is created on the given machine. |
51 | */ |
52 | public static final int LOCK_SOCKET = 2; |
53 | |
54 | /** |
55 | * This locking method means multiple writers are allowed, and they |
56 | * synchronize themselves. |
57 | */ |
58 | public static final int LOCK_SERIALIZED = 3; |
59 | |
60 | /** |
61 | * Use the file system to lock the file; don't use a separate lock file. |
62 | */ |
63 | public static final int LOCK_FS = 4; |
64 | |
65 | private static final String MAGIC = "FileLock"; |
66 | private static final String FILE = "file"; |
67 | private static final String SOCKET = "socket"; |
68 | private static final String SERIALIZED = "serialized"; |
69 | private static final int RANDOM_BYTES = 16; |
70 | private static final int SLEEP_GAP = 25; |
71 | private static final int TIME_GRANULARITY = 2000; |
72 | |
73 | /** |
74 | * The lock file name. |
75 | */ |
76 | private volatile String fileName; |
77 | |
78 | /** |
79 | * The server socket (only used when using the SOCKET mode). |
80 | */ |
81 | private volatile ServerSocket serverSocket; |
82 | |
83 | /** |
84 | * Whether the file is locked. |
85 | */ |
86 | private volatile boolean locked; |
87 | |
88 | /** |
89 | * The number of milliseconds to sleep after checking a file. |
90 | */ |
91 | private final int sleep; |
92 | |
93 | /** |
94 | * The trace object. |
95 | */ |
96 | private final Trace trace; |
97 | |
98 | /** |
99 | * The last time the lock file was written. |
100 | */ |
101 | private long lastWrite; |
102 | |
103 | private String method, ipAddress; |
104 | private Properties properties; |
105 | private String uniqueId; |
106 | private Thread watchdog; |
107 | |
108 | /** |
109 | * Create a new file locking object. |
110 | * |
111 | * @param traceSystem the trace system to use |
112 | * @param fileName the file name |
113 | * @param sleep the number of milliseconds to sleep |
114 | */ |
115 | public FileLock(TraceSystem traceSystem, String fileName, int sleep) { |
116 | this.trace = traceSystem == null ? |
117 | null : traceSystem.getTrace(Trace.FILE_LOCK); |
118 | this.fileName = fileName; |
119 | this.sleep = sleep; |
120 | } |
121 | |
122 | /** |
123 | * Lock the file if possible. A file may only be locked once. |
124 | * |
125 | * @param fileLockMethod the file locking method to use |
126 | * @throws DbException if locking was not successful |
127 | */ |
128 | public synchronized void lock(int fileLockMethod) { |
129 | checkServer(); |
130 | if (locked) { |
131 | DbException.throwInternalError("already locked"); |
132 | } |
133 | switch (fileLockMethod) { |
134 | case LOCK_FILE: |
135 | lockFile(); |
136 | break; |
137 | case LOCK_SOCKET: |
138 | lockSocket(); |
139 | break; |
140 | case LOCK_SERIALIZED: |
141 | lockSerialized(); |
142 | break; |
143 | case LOCK_FS: |
144 | break; |
145 | } |
146 | locked = true; |
147 | } |
148 | |
149 | /** |
150 | * Unlock the file. The watchdog thread is stopped. This method does nothing |
151 | * if the file is already unlocked. |
152 | */ |
153 | public synchronized void unlock() { |
154 | if (!locked) { |
155 | return; |
156 | } |
157 | locked = false; |
158 | try { |
159 | if (watchdog != null) { |
160 | watchdog.interrupt(); |
161 | } |
162 | } catch (Exception e) { |
163 | trace.debug(e, "unlock"); |
164 | } |
165 | try { |
166 | if (fileName != null) { |
167 | if (load().equals(properties)) { |
168 | FileUtils.delete(fileName); |
169 | } |
170 | } |
171 | if (serverSocket != null) { |
172 | serverSocket.close(); |
173 | } |
174 | } catch (Exception e) { |
175 | trace.debug(e, "unlock"); |
176 | } finally { |
177 | fileName = null; |
178 | serverSocket = null; |
179 | } |
180 | try { |
181 | if (watchdog != null) { |
182 | watchdog.join(); |
183 | } |
184 | } catch (Exception e) { |
185 | trace.debug(e, "unlock"); |
186 | } finally { |
187 | watchdog = null; |
188 | } |
189 | } |
190 | |
191 | /** |
192 | * Add or change a setting to the properties. This call does not save the |
193 | * file. |
194 | * |
195 | * @param key the key |
196 | * @param value the value |
197 | */ |
198 | public void setProperty(String key, String value) { |
199 | if (value == null) { |
200 | properties.remove(key); |
201 | } else { |
202 | properties.put(key, value); |
203 | } |
204 | } |
205 | |
206 | /** |
207 | * Save the lock file. |
208 | * |
209 | * @return the saved properties |
210 | */ |
211 | public Properties save() { |
212 | try { |
213 | OutputStream out = FileUtils.newOutputStream(fileName, false); |
214 | try { |
215 | properties.store(out, MAGIC); |
216 | } finally { |
217 | out.close(); |
218 | } |
219 | lastWrite = FileUtils.lastModified(fileName); |
220 | if (trace.isDebugEnabled()) { |
221 | trace.debug("save " + properties); |
222 | } |
223 | return properties; |
224 | } catch (IOException e) { |
225 | throw getExceptionFatal("Could not save properties " + fileName, e); |
226 | } |
227 | } |
228 | |
229 | private void checkServer() { |
230 | Properties prop = load(); |
231 | String server = prop.getProperty("server"); |
232 | if (server == null) { |
233 | return; |
234 | } |
235 | boolean running = false; |
236 | String id = prop.getProperty("id"); |
237 | try { |
238 | Socket socket = NetUtils.createSocket(server, |
239 | Constants.DEFAULT_TCP_PORT, false); |
240 | Transfer transfer = new Transfer(null); |
241 | transfer.setSocket(socket); |
242 | transfer.init(); |
243 | transfer.writeInt(Constants.TCP_PROTOCOL_VERSION_6); |
244 | transfer.writeInt(Constants.TCP_PROTOCOL_VERSION_15); |
245 | transfer.writeString(null); |
246 | transfer.writeString(null); |
247 | transfer.writeString(id); |
248 | transfer.writeInt(SessionRemote.SESSION_CHECK_KEY); |
249 | transfer.flush(); |
250 | int state = transfer.readInt(); |
251 | if (state == SessionRemote.STATUS_OK) { |
252 | running = true; |
253 | } |
254 | transfer.close(); |
255 | socket.close(); |
256 | } catch (IOException e) { |
257 | return; |
258 | } |
259 | if (running) { |
260 | DbException e = DbException.get( |
261 | ErrorCode.DATABASE_ALREADY_OPEN_1, "Server is running"); |
262 | throw e.addSQL(server + "/" + id); |
263 | } |
264 | } |
265 | |
266 | /** |
267 | * Load the properties file. |
268 | * |
269 | * @return the properties |
270 | */ |
271 | public Properties load() { |
272 | IOException lastException = null; |
273 | for (int i = 0; i < 5; i++) { |
274 | try { |
275 | Properties p2 = SortedProperties.loadProperties(fileName); |
276 | if (trace.isDebugEnabled()) { |
277 | trace.debug("load " + p2); |
278 | } |
279 | return p2; |
280 | } catch (IOException e) { |
281 | lastException = e; |
282 | } |
283 | } |
284 | throw getExceptionFatal( |
285 | "Could not load properties " + fileName, lastException); |
286 | } |
287 | |
288 | private void waitUntilOld() { |
289 | for (int i = 0; i < 2 * TIME_GRANULARITY / SLEEP_GAP; i++) { |
290 | long last = FileUtils.lastModified(fileName); |
291 | long dist = System.currentTimeMillis() - last; |
292 | if (dist < -TIME_GRANULARITY) { |
293 | // lock file modified in the future - |
294 | // wait for a bit longer than usual |
295 | try { |
296 | Thread.sleep(2 * (long) sleep); |
297 | } catch (Exception e) { |
298 | trace.debug(e, "sleep"); |
299 | } |
300 | return; |
301 | } else if (dist > TIME_GRANULARITY) { |
302 | return; |
303 | } |
304 | try { |
305 | Thread.sleep(SLEEP_GAP); |
306 | } catch (Exception e) { |
307 | trace.debug(e, "sleep"); |
308 | } |
309 | } |
310 | throw getExceptionFatal("Lock file recently modified", null); |
311 | } |
312 | |
313 | private void setUniqueId() { |
314 | byte[] bytes = MathUtils.secureRandomBytes(RANDOM_BYTES); |
315 | String random = StringUtils.convertBytesToHex(bytes); |
316 | uniqueId = Long.toHexString(System.currentTimeMillis()) + random; |
317 | properties.setProperty("id", uniqueId); |
318 | } |
319 | |
320 | private void lockSerialized() { |
321 | method = SERIALIZED; |
322 | FileUtils.createDirectories(FileUtils.getParent(fileName)); |
323 | if (FileUtils.createFile(fileName)) { |
324 | properties = new SortedProperties(); |
325 | properties.setProperty("method", String.valueOf(method)); |
326 | setUniqueId(); |
327 | save(); |
328 | } else { |
329 | while (true) { |
330 | try { |
331 | properties = load(); |
332 | } catch (DbException e) { |
333 | // ignore |
334 | } |
335 | return; |
336 | } |
337 | } |
338 | } |
339 | |
340 | private void lockFile() { |
341 | method = FILE; |
342 | properties = new SortedProperties(); |
343 | properties.setProperty("method", String.valueOf(method)); |
344 | setUniqueId(); |
345 | FileUtils.createDirectories(FileUtils.getParent(fileName)); |
346 | if (!FileUtils.createFile(fileName)) { |
347 | waitUntilOld(); |
348 | String m2 = load().getProperty("method", FILE); |
349 | if (!m2.equals(FILE)) { |
350 | throw getExceptionFatal("Unsupported lock method " + m2, null); |
351 | } |
352 | save(); |
353 | sleep(2 * sleep); |
354 | if (!load().equals(properties)) { |
355 | throw getExceptionAlreadyInUse("Locked by another process"); |
356 | } |
357 | FileUtils.delete(fileName); |
358 | if (!FileUtils.createFile(fileName)) { |
359 | throw getExceptionFatal("Another process was faster", null); |
360 | } |
361 | } |
362 | save(); |
363 | sleep(SLEEP_GAP); |
364 | if (!load().equals(properties)) { |
365 | fileName = null; |
366 | throw getExceptionFatal("Concurrent update", null); |
367 | } |
368 | watchdog = new Thread(this, "H2 File Lock Watchdog " + fileName); |
369 | Driver.setThreadContextClassLoader(watchdog); |
370 | watchdog.setDaemon(true); |
371 | watchdog.setPriority(Thread.MAX_PRIORITY - 1); |
372 | watchdog.start(); |
373 | } |
374 | |
375 | private void lockSocket() { |
376 | method = SOCKET; |
377 | properties = new SortedProperties(); |
378 | properties.setProperty("method", String.valueOf(method)); |
379 | setUniqueId(); |
380 | // if this returns 127.0.0.1, |
381 | // the computer is probably not networked |
382 | ipAddress = NetUtils.getLocalAddress(); |
383 | FileUtils.createDirectories(FileUtils.getParent(fileName)); |
384 | if (!FileUtils.createFile(fileName)) { |
385 | waitUntilOld(); |
386 | long read = FileUtils.lastModified(fileName); |
387 | Properties p2 = load(); |
388 | String m2 = p2.getProperty("method", SOCKET); |
389 | if (m2.equals(FILE)) { |
390 | lockFile(); |
391 | return; |
392 | } else if (!m2.equals(SOCKET)) { |
393 | throw getExceptionFatal("Unsupported lock method " + m2, null); |
394 | } |
395 | String ip = p2.getProperty("ipAddress", ipAddress); |
396 | if (!ipAddress.equals(ip)) { |
397 | throw getExceptionAlreadyInUse("Locked by another computer: " + ip); |
398 | } |
399 | String port = p2.getProperty("port", "0"); |
400 | int portId = Integer.parseInt(port); |
401 | InetAddress address; |
402 | try { |
403 | address = InetAddress.getByName(ip); |
404 | } catch (UnknownHostException e) { |
405 | throw getExceptionFatal("Unknown host " + ip, e); |
406 | } |
407 | for (int i = 0; i < 3; i++) { |
408 | try { |
409 | Socket s = new Socket(address, portId); |
410 | s.close(); |
411 | throw getExceptionAlreadyInUse("Locked by another process"); |
412 | } catch (BindException e) { |
413 | throw getExceptionFatal("Bind Exception", null); |
414 | } catch (ConnectException e) { |
415 | trace.debug(e, "socket not connected to port " + port); |
416 | } catch (IOException e) { |
417 | throw getExceptionFatal("IOException", null); |
418 | } |
419 | } |
420 | if (read != FileUtils.lastModified(fileName)) { |
421 | throw getExceptionFatal("Concurrent update", null); |
422 | } |
423 | FileUtils.delete(fileName); |
424 | if (!FileUtils.createFile(fileName)) { |
425 | throw getExceptionFatal("Another process was faster", null); |
426 | } |
427 | } |
428 | try { |
429 | // 0 to use any free port |
430 | serverSocket = NetUtils.createServerSocket(0, false); |
431 | int port = serverSocket.getLocalPort(); |
432 | properties.setProperty("ipAddress", ipAddress); |
433 | properties.setProperty("port", String.valueOf(port)); |
434 | } catch (Exception e) { |
435 | trace.debug(e, "lock"); |
436 | serverSocket = null; |
437 | lockFile(); |
438 | return; |
439 | } |
440 | save(); |
441 | watchdog = new Thread(this, |
442 | "H2 File Lock Watchdog (Socket) " + fileName); |
443 | watchdog.setDaemon(true); |
444 | watchdog.start(); |
445 | } |
446 | |
447 | private static void sleep(int time) { |
448 | try { |
449 | Thread.sleep(time); |
450 | } catch (InterruptedException e) { |
451 | throw getExceptionFatal("Sleep interrupted", e); |
452 | } |
453 | } |
454 | |
455 | private static DbException getExceptionFatal(String reason, Throwable t) { |
456 | return DbException.get( |
457 | ErrorCode.ERROR_OPENING_DATABASE_1, t, reason); |
458 | } |
459 | |
460 | private DbException getExceptionAlreadyInUse(String reason) { |
461 | DbException e = DbException.get( |
462 | ErrorCode.DATABASE_ALREADY_OPEN_1, reason); |
463 | if (fileName != null) { |
464 | try { |
465 | Properties prop = load(); |
466 | String server = prop.getProperty("server"); |
467 | if (server != null) { |
468 | String serverId = server + "/" + prop.getProperty("id"); |
469 | e = e.addSQL(serverId); |
470 | } |
471 | } catch (DbException e2) { |
472 | // ignore |
473 | } |
474 | } |
475 | return e; |
476 | } |
477 | |
478 | /** |
479 | * Get the file locking method type given a method name. |
480 | * |
481 | * @param method the method name |
482 | * @return the method type |
483 | * @throws DbException if the method name is unknown |
484 | */ |
485 | public static int getFileLockMethod(String method) { |
486 | if (method == null || method.equalsIgnoreCase("FILE")) { |
487 | return FileLock.LOCK_FILE; |
488 | } else if (method.equalsIgnoreCase("NO")) { |
489 | return FileLock.LOCK_NO; |
490 | } else if (method.equalsIgnoreCase("SOCKET")) { |
491 | return FileLock.LOCK_SOCKET; |
492 | } else if (method.equalsIgnoreCase("SERIALIZED")) { |
493 | return FileLock.LOCK_SERIALIZED; |
494 | } else if (method.equalsIgnoreCase("FS")) { |
495 | return FileLock.LOCK_FS; |
496 | } else { |
497 | throw DbException.get( |
498 | ErrorCode.UNSUPPORTED_LOCK_METHOD_1, method); |
499 | } |
500 | } |
501 | |
502 | public String getUniqueId() { |
503 | return uniqueId; |
504 | } |
505 | |
506 | @Override |
507 | public void run() { |
508 | try { |
509 | while (locked && fileName != null) { |
510 | // trace.debug("watchdog check"); |
511 | try { |
512 | if (!FileUtils.exists(fileName) || |
513 | FileUtils.lastModified(fileName) != lastWrite) { |
514 | save(); |
515 | } |
516 | Thread.sleep(sleep); |
517 | } catch (OutOfMemoryError e) { |
518 | // ignore |
519 | } catch (InterruptedException e) { |
520 | // ignore |
521 | } catch (NullPointerException e) { |
522 | // ignore |
523 | } catch (Exception e) { |
524 | trace.debug(e, "watchdog"); |
525 | } |
526 | } |
527 | while (serverSocket != null) { |
528 | try { |
529 | trace.debug("watchdog accept"); |
530 | Socket s = serverSocket.accept(); |
531 | s.close(); |
532 | } catch (Exception e) { |
533 | trace.debug(e, "watchdog"); |
534 | } |
535 | } |
536 | } catch (Exception e) { |
537 | trace.debug(e, "watchdog"); |
538 | } |
539 | trace.debug("watchdog end"); |
540 | } |
541 | |
542 | } |