001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.eclipse.aether.named.support; 020 021import java.io.IOException; 022import java.io.UncheckedIOException; 023import java.nio.channels.FileChannel; 024import java.nio.channels.FileLock; 025import java.nio.channels.OverlappingFileLockException; 026import java.util.ArrayDeque; 027import java.util.Deque; 028import java.util.HashMap; 029import java.util.Map; 030import java.util.concurrent.TimeUnit; 031import java.util.concurrent.atomic.AtomicReference; 032import java.util.concurrent.locks.ReentrantLock; 033 034import org.eclipse.aether.named.NamedLockKey; 035 036import static org.eclipse.aether.named.support.Retry.retry; 037 038/** 039 * Named lock that uses {@link FileLock}. An instance of this class is about ONE LOCK (one file) 040 * and is possibly used by multiple threads. Each thread (if properly coded re boxing) will try to 041 * obtain either shared or exclusive lock. As file locks are JVM-scoped (so one JVM can obtain 042 * same file lock only once), the threads share file lock and synchronize according to it. Still, 043 * as file lock obtain operation does not block (or in other words, the method that does block 044 * cannot be controlled for how long it blocks), we are "simulating" thread blocking using 045 * {@link Retry} utility. 046 * This implementation performs coordination not only on thread (JVM-local) level, but also on 047 * process level, as long as other parties are using this same "advisory" locking mechanism. 048 * 049 * @since 1.7.3 050 */ 051public final class FileLockNamedLock extends NamedLockSupport { 052 private static final long RETRY_SLEEP_MILLIS = 100L; 053 054 private static final long LOCK_POSITION = 0L; 055 056 private static final long LOCK_SIZE = 1L; 057 058 /** 059 * Thread -> steps stack (true = shared, false = exclusive) 060 */ 061 private final Map<Thread, Deque<Boolean>> threadSteps; 062 063 /** 064 * The {@link FileChannel} this instance is about. 065 */ 066 private final FileChannel fileChannel; 067 068 /** 069 * The reference of {@link FileLock}, if obtained. 070 */ 071 private final AtomicReference<FileLock> fileLockRef; 072 073 /** 074 * Lock protecting "critical region": this is where threads are allowed to perform locking but should leave this 075 * region as quick as possible. 076 */ 077 private final ReentrantLock criticalRegion; 078 079 public FileLockNamedLock( 080 final NamedLockKey key, final FileChannel fileChannel, final NamedLockFactorySupport factory) { 081 super(key, factory); 082 this.threadSteps = new HashMap<>(); 083 this.fileChannel = fileChannel; 084 this.fileLockRef = new AtomicReference<>(null); 085 this.criticalRegion = new ReentrantLock(); 086 } 087 088 @Override 089 protected boolean doLockShared(final long time, final TimeUnit unit) throws InterruptedException { 090 return retry(time, unit, RETRY_SLEEP_MILLIS, () -> doLockSharedPerform(time, unit), null, false); 091 } 092 093 @Override 094 protected boolean doLockExclusively(final long time, final TimeUnit unit) throws InterruptedException { 095 return retry(time, unit, RETRY_SLEEP_MILLIS, () -> doLockExclusivelyPerform(time, unit), null, false); 096 } 097 098 private Boolean doLockSharedPerform(final long time, final TimeUnit unit) throws InterruptedException { 099 if (criticalRegion.tryLock(time, unit)) { 100 try { 101 Deque<Boolean> steps = threadSteps.computeIfAbsent(Thread.currentThread(), k -> new ArrayDeque<>()); 102 FileLock obtainedLock = fileLockRef.get(); 103 if (obtainedLock != null) { 104 if (obtainedLock.isShared()) { 105 steps.push(Boolean.TRUE); 106 return true; 107 } else { 108 // if we own exclusive, that's still fine 109 boolean weOwnExclusive = steps.contains(Boolean.FALSE); 110 if (weOwnExclusive) { 111 steps.push(Boolean.TRUE); 112 return true; 113 } else { 114 // someone else owns exclusive, let's wait 115 return null; 116 } 117 } 118 } 119 120 FileLock fileLock = obtainFileLock(true); 121 if (fileLock != null) { 122 fileLockRef.set(fileLock); 123 steps.push(Boolean.TRUE); 124 return true; 125 } 126 } finally { 127 criticalRegion.unlock(); 128 } 129 } 130 return null; 131 } 132 133 private Boolean doLockExclusivelyPerform(final long time, final TimeUnit unit) throws InterruptedException { 134 if (criticalRegion.tryLock(time, unit)) { 135 try { 136 Deque<Boolean> steps = threadSteps.computeIfAbsent(Thread.currentThread(), k -> new ArrayDeque<>()); 137 FileLock obtainedLock = fileLockRef.get(); 138 if (obtainedLock != null) { 139 if (obtainedLock.isShared()) { 140 // if we own shared, that's attempted upgrade 141 boolean weOwnShared = steps.contains(Boolean.TRUE); 142 if (weOwnShared) { 143 throw new LockUpgradeNotSupportedException(this); // Lock upgrade not supported 144 } else { 145 // someone else owns shared, let's wait 146 return null; 147 } 148 } else { 149 // if we own exclusive, that's fine 150 boolean weOwnExclusive = steps.contains(Boolean.FALSE); 151 if (weOwnExclusive) { 152 steps.push(Boolean.FALSE); 153 return true; 154 } else { 155 // someone else owns exclusive, let's wait 156 return null; 157 } 158 } 159 } 160 161 FileLock fileLock = obtainFileLock(false); 162 if (fileLock != null) { 163 fileLockRef.set(fileLock); 164 steps.push(Boolean.FALSE); 165 return true; 166 } 167 } finally { 168 criticalRegion.unlock(); 169 } 170 } 171 return null; 172 } 173 174 @Override 175 protected void doUnlock() { 176 criticalRegion.lock(); 177 try { 178 Deque<Boolean> steps = threadSteps.computeIfAbsent(Thread.currentThread(), k -> new ArrayDeque<>()); 179 if (steps.isEmpty()) { 180 throw new IllegalStateException("Wrong API usage: unlock without lock"); 181 } 182 steps.pop(); 183 if (steps.isEmpty() && !anyOtherThreadHasSteps()) { 184 try { 185 fileLockRef.getAndSet(null).release(); 186 } catch (IOException e) { 187 throw new UncheckedIOException(e); 188 } 189 } 190 } finally { 191 criticalRegion.unlock(); 192 } 193 } 194 195 /** 196 * Returns {@code true} if any other than this thread using this instance has any step recorded. 197 */ 198 private boolean anyOtherThreadHasSteps() { 199 return threadSteps.entrySet().stream() 200 .filter(e -> !Thread.currentThread().equals(e.getKey())) 201 .map(Map.Entry::getValue) 202 .anyMatch(d -> !d.isEmpty()); 203 } 204 205 /** 206 * Attempts to obtain real {@link FileLock}, returns non-null value is succeeds, or {@code null} if cannot. 207 */ 208 private FileLock obtainFileLock(final boolean shared) { 209 FileLock result; 210 try { 211 result = fileChannel.tryLock(LOCK_POSITION, LOCK_SIZE, shared); 212 } catch (OverlappingFileLockException e) { 213 logger.trace("File lock overlap on '{}'", key(), e); 214 return null; 215 } catch (IOException e) { 216 logger.trace("Failure on acquire of file lock for '{}'", key(), e); 217 throw new UncheckedIOException("Failed to acquire lock file channel for '" + key() + "'", e); 218 } 219 return result; 220 } 221}