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