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 static org.eclipse.aether.named.support.Retry.retry; 035 036/** 037 * Named lock that uses {@link FileLock}. An instance of this class is about ONE LOCK (one file) 038 * and is possibly used by multiple threads. Each thread (if properly coded re boxing) will try to 039 * obtain either shared or exclusive lock. As file locks are JVM-scoped (so one JVM can obtain 040 * same file lock only once), the threads share file lock and synchronize according to it. Still, 041 * as file lock obtain operation does not block (or in other words, the method that does block 042 * cannot be controlled for how long it blocks), we are "simulating" thread blocking using 043 * {@link Retry} utility. 044 * This implementation performs coordination not only on thread (JVM-local) level, but also on 045 * process level, as long as other parties are using this same "advisory" locking mechanism. 046 * 047 * @since 1.7.3 048 */ 049public final class FileLockNamedLock extends NamedLockSupport { 050 private static final long RETRY_SLEEP_MILLIS = 100L; 051 052 private static final long LOCK_POSITION = 0L; 053 054 private static final long LOCK_SIZE = 1L; 055 056 /** 057 * Thread -> steps stack (true = shared, false = exclusive) 058 */ 059 private final Map<Thread, Deque<Boolean>> threadSteps; 060 061 /** 062 * The {@link FileChannel} this instance is about. 063 */ 064 private final FileChannel fileChannel; 065 066 /** 067 * The reference of {@link FileLock}, if obtained. 068 */ 069 private final AtomicReference<FileLock> fileLockRef; 070 071 /** 072 * Lock protecting "critical region": this is where threads are allowed to perform locking but should leave this 073 * region as quick as possible. 074 */ 075 private final ReentrantLock criticalRegion; 076 077 public FileLockNamedLock(final String name, final FileChannel fileChannel, final NamedLockFactorySupport factory) { 078 super(name, factory); 079 this.threadSteps = new HashMap<>(); 080 this.fileChannel = fileChannel; 081 this.fileLockRef = new AtomicReference<>(null); 082 this.criticalRegion = new ReentrantLock(); 083 } 084 085 @Override 086 protected boolean doLockShared(final long time, final TimeUnit unit) throws InterruptedException { 087 return retry(time, unit, RETRY_SLEEP_MILLIS, this::doLockShared, null, false); 088 } 089 090 @Override 091 protected boolean doLockExclusively(final long time, final TimeUnit unit) throws InterruptedException { 092 return retry(time, unit, RETRY_SLEEP_MILLIS, this::doLockExclusively, null, false); 093 } 094 095 private Boolean doLockShared() { 096 if (criticalRegion.tryLock()) { 097 try { 098 Deque<Boolean> steps = threadSteps.computeIfAbsent(Thread.currentThread(), k -> new ArrayDeque<>()); 099 FileLock obtainedLock = fileLockRef.get(); 100 if (obtainedLock != null) { 101 if (obtainedLock.isShared()) { 102 steps.push(Boolean.TRUE); 103 return true; 104 } else { 105 // if we own exclusive, that's still fine 106 boolean weOwnExclusive = steps.contains(Boolean.FALSE); 107 if (weOwnExclusive) { 108 steps.push(Boolean.TRUE); 109 return true; 110 } else { 111 // someone else owns exclusive, let's wait 112 return null; 113 } 114 } 115 } 116 117 FileLock fileLock = obtainFileLock(true); 118 if (fileLock != null) { 119 fileLockRef.set(fileLock); 120 steps.push(Boolean.TRUE); 121 return true; 122 } 123 } finally { 124 criticalRegion.unlock(); 125 } 126 } 127 return null; 128 } 129 130 private Boolean doLockExclusively() { 131 if (criticalRegion.tryLock()) { 132 try { 133 Deque<Boolean> steps = threadSteps.computeIfAbsent(Thread.currentThread(), k -> new ArrayDeque<>()); 134 FileLock obtainedLock = fileLockRef.get(); 135 if (obtainedLock != null) { 136 if (obtainedLock.isShared()) { 137 // if we own shared, that's attempted upgrade 138 boolean weOwnShared = steps.contains(Boolean.TRUE); 139 if (weOwnShared) { 140 throw new LockUpgradeNotSupportedException(this); // Lock upgrade not supported 141 } else { 142 // someone else owns shared, let's wait 143 return null; 144 } 145 } else { 146 // if we own exclusive, that's fine 147 boolean weOwnExclusive = steps.contains(Boolean.FALSE); 148 if (weOwnExclusive) { 149 steps.push(Boolean.FALSE); 150 return true; 151 } else { 152 // someone else owns exclusive, let's wait 153 return null; 154 } 155 } 156 } 157 158 FileLock fileLock = obtainFileLock(false); 159 if (fileLock != null) { 160 fileLockRef.set(fileLock); 161 steps.push(Boolean.FALSE); 162 return true; 163 } 164 } finally { 165 criticalRegion.unlock(); 166 } 167 } 168 return null; 169 } 170 171 @Override 172 protected void doUnlock() { 173 criticalRegion.lock(); 174 try { 175 Deque<Boolean> steps = threadSteps.computeIfAbsent(Thread.currentThread(), k -> new ArrayDeque<>()); 176 if (steps.isEmpty()) { 177 throw new IllegalStateException("Wrong API usage: unlock without lock"); 178 } 179 steps.pop(); 180 if (steps.isEmpty() && !anyOtherThreadHasSteps()) { 181 try { 182 fileLockRef.getAndSet(null).release(); 183 } catch (IOException e) { 184 throw new UncheckedIOException(e); 185 } 186 } 187 } finally { 188 criticalRegion.unlock(); 189 } 190 } 191 192 /** 193 * Returns {@code true} if any other than this thread using this instance has any step recorded. 194 */ 195 private boolean anyOtherThreadHasSteps() { 196 return threadSteps.entrySet().stream() 197 .filter(e -> !Thread.currentThread().equals(e.getKey())) 198 .map(Map.Entry::getValue) 199 .anyMatch(d -> !d.isEmpty()); 200 } 201 202 /** 203 * Attempts to obtain real {@link FileLock}, returns non-null value is succeeds, or {@code null} if cannot. 204 */ 205 private FileLock obtainFileLock(final boolean shared) { 206 FileLock result; 207 try { 208 result = fileChannel.tryLock(LOCK_POSITION, LOCK_SIZE, shared); 209 } catch (OverlappingFileLockException e) { 210 logger.trace("File lock overlap on '{}'", name(), e); 211 return null; 212 } catch (IOException e) { 213 logger.trace("Failure on acquire of file lock for '{}'", name(), e); 214 throw new UncheckedIOException("Failed to acquire lock file channel for '" + name() + "'", e); 215 } 216 return result; 217 } 218}