blob: 9c40fb34c020299ac5d11ac98c8c3139017d155f [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.ivy.plugins.lock;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.ivy.util.Message;
public abstract class FileBasedLockStrategy extends AbstractLockStrategy {
private static final int SLEEP_TIME = 100;
private static final long DEFAULT_TIMEOUT = 2 * 60 * 1000;
/**
* The locker to use to make file lock attempts.
*/
private FileLocker locker;
private long timeout = DEFAULT_TIMEOUT;
/**
* Lock counter list must be static: locks are implicitly shared to the entire process, so the
* list too much be.
*/
private static final ConcurrentMap<File, ConcurrentMap<Thread, Integer>> currentLockHolders = new ConcurrentHashMap<>();
protected FileBasedLockStrategy() {
this(new CreateFileLocker(false), false);
}
protected FileBasedLockStrategy(boolean debugLocking) {
this(new CreateFileLocker(debugLocking), debugLocking);
}
protected FileBasedLockStrategy(FileLocker locker, boolean debugLocking) {
super(debugLocking);
this.locker = locker;
}
protected boolean acquireLock(File file) throws InterruptedException {
Thread currentThread = Thread.currentThread();
if (isDebugLocking()) {
debugLocking("acquiring lock on " + file);
}
long start = System.currentTimeMillis();
do {
synchronized (currentLockHolders) {
if (isDebugLocking()) {
debugLocking("entered synchronized area (locking)");
}
int lockCount = hasLock(file, currentThread);
if (isDebugLocking()) {
debugLocking("current status for " + file + " is " + lockCount
+ " held locks: " + getCurrentLockHolderNames(file));
}
if (lockCount < 0) {
/* Another thread in this process holds the lock; we need to wait */
if (isDebugLocking()) {
debugLocking("waiting for another thread to release the lock: "
+ getCurrentLockHolderNames(file));
}
} else if (lockCount > 0) {
int holdLocks = incrementLock(file, currentThread);
if (isDebugLocking()) {
debugLocking("reentrant lock acquired on " + file + " in "
+ (System.currentTimeMillis() - start) + "ms" + " - hold locks = "
+ holdLocks);
}
return true;
} else {
/* No prior lock on this file is held at all */
if (locker.tryLock(file)) {
if (isDebugLocking()) {
debugLocking("lock acquired on " + file + " in "
+ (System.currentTimeMillis() - start) + "ms");
}
incrementLock(file, currentThread);
return true;
}
}
}
if (isDebugLocking()) {
debugLocking("failed to acquire lock; sleeping for retry...");
}
Thread.sleep(SLEEP_TIME);
} while (System.currentTimeMillis() - start < timeout);
return false;
}
protected void releaseLock(File file) {
Thread currentThread = Thread.currentThread();
if (isDebugLocking()) {
debugLocking("releasing lock on " + file);
}
synchronized (currentLockHolders) {
if (isDebugLocking()) {
debugLocking("entered synchronized area (unlocking)");
}
int holdLocks = decrementLock(file, currentThread);
if (holdLocks == 0) {
locker.unlock(file);
if (isDebugLocking()) {
debugLocking("lock released on " + file);
}
} else {
if (isDebugLocking()) {
debugLocking("reentrant lock released on " + file + " - hold locks = "
+ holdLocks);
}
}
}
}
private static void debugLocking(String msg) {
Message.info(Thread.currentThread() + " " + System.currentTimeMillis() + " " + msg);
}
/**
* Determine the state of the lockfile.
*
* Must be called from within a synchronized block.
*
* Three possibilities exist: - The lock is held by the current thread (>0) - The lock is held
* by one or more different threads (-1) - The lock is not held at all (0).
*
* @param file
* file to lock
* @param forThread
* thread for which lock status is being queried
*/
private int hasLock(File file, Thread forThread) {
ConcurrentMap<Thread, Integer> locksPerThread = currentLockHolders.get(file);
if (locksPerThread == null) {
return 0;
}
if (locksPerThread.isEmpty()) {
return 0;
}
Integer counterObj = locksPerThread.get(forThread);
int counter = (counterObj == null) ? 0 : counterObj;
if (counter > 0) {
return counter;
} else {
return -1;
}
}
/**
* Record that this thread holds the lock.
*
* Asserts that the lock has been previously grabbed by this thread. Must be called from a
* synchronized block in which the lock was grabbed.
*
* @param file
* file which has been locked
* @param forThread
* thread for which locking occurred
* @return number of times this thread has grabbed the lock
*/
private int incrementLock(File file, Thread forThread) {
ConcurrentMap<Thread, Integer> locksPerThread = currentLockHolders.get(file);
if (locksPerThread == null) {
locksPerThread = new ConcurrentHashMap<>();
currentLockHolders.put(file, locksPerThread);
}
Integer c = locksPerThread.get(forThread);
int holdLocks = (c == null) ? 1 : c + 1;
locksPerThread.put(forThread, holdLocks);
return holdLocks;
}
/**
* Decrease depth of this thread's lock.
*
* Must be called within a synchronized block.
*
* If this returns 0, the caller is responsible for releasing the lock within that same block.
*
* @param file
* file for which lock depth is being decreased
* @param forThread
* thread for which lock depth is being decreased
* @return remaining depth of this lock
*/
private int decrementLock(File file, Thread forThread) {
ConcurrentMap<Thread, Integer> locksPerThread = currentLockHolders.get(file);
if (locksPerThread == null) {
throw new RuntimeException("Calling decrementLock on a thread which holds no locks");
}
Integer c = locksPerThread.get(forThread);
int oldHeldLocks = (c == null) ? 0 : c;
if (oldHeldLocks <= 0) {
throw new RuntimeException("Calling decrementLock on a thread which holds no locks");
}
int newHeldLocks = oldHeldLocks - 1;
if (newHeldLocks > 0) {
locksPerThread.put(forThread, newHeldLocks);
} else {
locksPerThread.remove(forThread);
}
return newHeldLocks;
}
/**
* Return a string naming the threads which currently hold this lock.
*
* @param file File
* @return String
*/
protected String getCurrentLockHolderNames(File file) {
StringBuilder sb = new StringBuilder();
ConcurrentMap<Thread, Integer> m = currentLockHolders.get(file);
if (m == null) {
return "(NULL)";
}
for (Thread t : m.keySet()) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(t.toString());
}
return sb.toString();
}
public interface FileLocker {
boolean tryLock(File f);
void unlock(File f);
}
/**
* "locks" a file by creating it if it doesn't exist, relying on the
* {@link File#createNewFile()} atomicity.
*/
public static class CreateFileLocker implements FileLocker {
private boolean debugLocking;
public CreateFileLocker(boolean debugLocking) {
this.debugLocking = debugLocking;
}
public boolean tryLock(File file) {
try {
if (file.getParentFile().exists() || file.getParentFile().mkdirs()) {
if (file.createNewFile()) {
DeleteOnExitHook.add(file);
return true;
} else {
if (debugLocking) {
debugLocking("file creation failed " + file);
}
}
}
} catch (IOException e) {
// ignored
Message.verbose("file creation failed due to an exception: " + e.getMessage()
+ " (" + file + ")");
}
return false;
}
public void unlock(File file) {
file.delete();
DeleteOnExitHook.remove(file);
}
}
/**
* Locks a file using the {@link FileLock} mechanism.
*/
public static class NIOFileLocker implements FileLocker {
private ConcurrentMap<File, LockData> locks = new ConcurrentHashMap<>();
private boolean debugLocking;
public NIOFileLocker(boolean debugLocking) {
this.debugLocking = debugLocking;
}
private static class LockData {
private RandomAccessFile raf;
private FileLock l;
LockData(RandomAccessFile raf, FileLock l) {
this.raf = raf;
this.l = l;
}
}
@SuppressWarnings("resource")
public boolean tryLock(File file) {
try {
if (file.getParentFile().exists() || file.getParentFile().mkdirs()) {
// this must not be closed until unlock
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileLock l = raf.getChannel().tryLock();
if (l != null) {
synchronized (this) {
locks.put(file, new LockData(raf, l));
}
return true;
} else {
if (debugLocking) {
debugLocking("failed to acquire lock on " + file);
}
}
}
} catch (IOException e) {
// ignored
Message.verbose("file lock failed due to an exception: " + e.getMessage() + " ("
+ file + ")");
}
return false;
}
public void unlock(File file) {
synchronized (this) {
LockData data = locks.get(file);
if (data == null) {
throw new IllegalArgumentException("file not previously locked: " + file);
}
try {
locks.remove(file);
data.l.release();
data.raf.close();
} catch (IOException e) {
Message.error("problem while releasing lock on " + file + ": " + e.getMessage());
}
}
}
}
}