/******************************************************************************* * Copyright (c) 2012-2015 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.api.vfs.server; /** * Advisory file locks. It does not prevent access to the file from other programs. * <p/> * Usage: * <pre> * PathLockFactory lockFactory = ... * * public void doSomething(Path path) * { * PathLock exclusiveLock = lockFactory.getLock(path, true).acquire(30000); * try * { * ... // do something * } * finally * { * exclusiveLock.release(); * } * } * </pre> * * @author <a href="mailto:andrew00x@gmail.com">Andrey Parfonov</a> */ public final class PathLockFactory { private static final int MAX_RECURSIVE_LOCKS = (1 << 10) - 1; /** Max number of threads allowed to access file. */ private final int maxThreads; // Tail of the "lock table". private final Node tail = new Node(null, 0, null); /** * @param maxThreads * the max number of threads are allowed to access one file. Typically this parameter should be big enough to * avoid blocking threads that need to obtain NOT exclusive lock. */ public PathLockFactory(int maxThreads) { if (maxThreads < 1) { throw new IllegalArgumentException(); } this.maxThreads = maxThreads; } public PathLock getLock(Path path, boolean exclusive) { return new PathLock(path, exclusive ? maxThreads : 1); } private synchronized void acquire(Path path, int permits) { while (!tryAcquire(path, permits)) { try { wait(); } catch (InterruptedException e) { notifyAll(); throw new RuntimeException(e); } } } private synchronized void acquire(Path path, int permits, long timeoutMilliseconds) { final long endTime = System.currentTimeMillis() + timeoutMilliseconds; long waitTime = timeoutMilliseconds; while (!tryAcquire(path, permits)) { try { wait(waitTime); } catch (InterruptedException e) { notifyAll(); throw new RuntimeException(e); } long now = System.currentTimeMillis(); if (now >= endTime) { throw new RuntimeException(String.format("Get lock timeout for '%s'. ", path)); } waitTime = endTime - now; } } private synchronized void release(Path path, int permits) { Node node = tail; while (node != null) { Node prev = node.prev; if (prev == null) { break; } if (prev.path.equals(path)) { if (prev.threadDeep == 1) { // If last recursive lock. prev.permits += permits; if (prev.permits >= maxThreads) { // remove node.prev = prev.prev; prev.prev = null; } } else { --prev.threadDeep; } } node = node.prev; } notifyAll(); //System.err.printf(">>>>> release: %s : %d%n", path, permits); } private boolean tryAcquire(Path path, int permits) { //System.err.printf(">>>>> acquire: %s : %d%n", path, permits); Node node = tail.prev; final Thread current = Thread.currentThread(); while (node != null) { if (node.path.equals(path)) { if (node.threadId == current.getId()) { // Current thread already has direct lock for this path if (node.threadDeep > MAX_RECURSIVE_LOCKS) { throw new Error("Max number of recursive locks exceeded. "); } ++node.threadDeep; return true; } if (node.permits > permits) { // Lock already exists and current thread is not owner of this lock, // but lock is not exclusive and we can "share" it for other thread. node.permits -= permits; // decrement number of allowed concurrent threads return true; } // Lock is exclusive or max number of allowed concurrent thread is reached. return false; } else if ((node.path.isChild(path) || path.isChild(node.path)) && node.permits <= permits) { // Found some path which already has lock that prevents us to get required permits. // There is two possibilities: // 1. Parent of the path we try to lock already locked // 2. Child of the path we try to lock already locked // Need to check is such lock obtained by current thread or not. // If such lock obtained by other thread stop here immediately there is no reasons to continue. if (node.threadId != current.getId()) { return false; } } node = node.prev; } // If we are here there is no lock for path yet. tail.prev = new Node(path, maxThreads - permits, tail.prev); return true; } public synchronized void checkClean() { assert tail.prev == null; } /* =============================================== */ private static class Node { final Path path; final long threadId = Thread.currentThread().getId(); int permits; int threadDeep; Node prev; Node(Path path, int permits, Node prev) { this.path = path; this.permits = permits; this.prev = prev; threadDeep = 1; } @Override public String toString() { return "Node{" + "path=" + path + ", threadId=" + threadId + ", permits=" + permits + ", prev=" + prev + '}'; } } public final class PathLock { private final Path path; private final int permits; private PathLock(Path path, int permits) { this.path = path; this.permits = permits; } /** * Acquire permit for file. Method is blocked until permit available. * * @return this PathLock instance */ public PathLock acquire() { PathLockFactory.this.acquire(path, permits); return this; } /** * Acquire permit for file if it becomes available within the given timeout. It is the same as method {@link * #acquire()} but with waiting timeout. If waiting timeout reached then PathLockTimeoutException thrown. * * @param timeoutMilliseconds * maximum time (in milliseconds) to wait for access permit * @return this PathLock instance * @throws RuntimeException * if waiting timeout reached */ public PathLock acquire(long timeoutMilliseconds) { PathLockFactory.this.acquire(path, permits, timeoutMilliseconds); return this; } /** Release file permit. */ public void release() { PathLockFactory.this.release(path, permits); } /** Returns <code>true</code> if this lock is exclusive and <code>false</code> otherwise. */ public boolean isExclusive() { return permits == PathLockFactory.this.maxThreads; } } }