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 steps.push( Boolean.TRUE ); 116 return true; 117 } 118 else 119 { 120 // if we own exclusive, that's still fine 121 boolean weOwnExclusive = steps.contains( Boolean.FALSE ); 122 if ( weOwnExclusive ) 123 { 124 steps.push( Boolean.TRUE ); 125 return true; 126 } 127 else 128 { 129 // someone else owns exclusive, let's wait 130 return null; 131 } 132 } 133 } 134 135 FileLock fileLock = obtainFileLock( true ); 136 if ( fileLock != null ) 137 { 138 fileLockRef.set( fileLock ); 139 steps.push( Boolean.TRUE ); 140 return true; 141 } 142 } 143 finally 144 { 145 criticalRegion.unlock(); 146 } 147 } 148 return null; 149 } 150 151 private Boolean doLockExclusively() 152 { 153 if ( criticalRegion.tryLock() ) 154 { 155 try 156 { 157 Deque<Boolean> steps = threadSteps.computeIfAbsent( Thread.currentThread(), k -> new ArrayDeque<>() ); 158 FileLock obtainedLock = fileLockRef.get(); 159 if ( obtainedLock != null ) 160 { 161 if ( obtainedLock.isShared() ) 162 { 163 // if we own shared, that's attempted upgrade 164 boolean weOwnShared = steps.contains( Boolean.TRUE ); 165 if ( weOwnShared ) 166 { 167 return false; // Lock upgrade not supported 168 } 169 else 170 { 171 // someone else owns shared, let's wait 172 return null; 173 } 174 } 175 else 176 { 177 // if we own exclusive, that's fine 178 boolean weOwnExclusive = steps.contains( Boolean.FALSE ); 179 if ( weOwnExclusive ) 180 { 181 steps.push( Boolean.FALSE ); 182 return true; 183 } 184 else 185 { 186 // someone else owns exclusive, let's wait 187 return null; 188 } 189 } 190 } 191 192 FileLock fileLock = obtainFileLock( false ); 193 if ( fileLock != null ) 194 { 195 fileLockRef.set( fileLock ); 196 steps.push( Boolean.FALSE ); 197 return true; 198 } 199 } 200 finally 201 { 202 criticalRegion.unlock(); 203 } 204 } 205 return null; 206 } 207 208 @Override 209 public void unlock() 210 { 211 criticalRegion.lock(); 212 try 213 { 214 Deque<Boolean> steps = threadSteps.computeIfAbsent( Thread.currentThread(), k -> new ArrayDeque<>() ); 215 if ( steps.isEmpty() ) 216 { 217 throw new IllegalStateException( "Wrong API usage: unlock without lock" ); 218 } 219 steps.pop(); 220 if ( steps.isEmpty() && !anyOtherThreadHasSteps() ) 221 { 222 try 223 { 224 fileLockRef.getAndSet( null ).release(); 225 } 226 catch ( IOException e ) 227 { 228 throw new UncheckedIOException( e ); 229 } 230 } 231 } 232 finally 233 { 234 criticalRegion.unlock(); 235 } 236 } 237 238 /** 239 * Returns {@code true} if any other than this thread using this instance has any step recorded. 240 */ 241 private boolean anyOtherThreadHasSteps() 242 { 243 return threadSteps.entrySet().stream() 244 .filter( e -> !Thread.currentThread().equals( e.getKey() ) ) 245 .map( Map.Entry::getValue ) 246 .anyMatch( d -> !d.isEmpty() ); 247 } 248 249 /** 250 * Attempts to obtain real {@link FileLock}, returns non-null value is succeeds, or {@code null} if cannot. 251 */ 252 private FileLock obtainFileLock( final boolean shared ) 253 { 254 FileLock result; 255 try 256 { 257 result = fileChannel.tryLock( LOCK_POSITION, LOCK_SIZE, shared ); 258 } 259 catch ( OverlappingFileLockException e ) 260 { 261 logger.trace( "File lock overlap on '{}'", name(), e ); 262 return null; 263 } 264 catch ( IOException e ) 265 { 266 logger.trace( "Failure on acquire of file lock for '{}'", name(), e ); 267 throw new UncheckedIOException( "Failed to acquire lock file channel for '" + name() + "'", e ); 268 } 269 return result; 270 } 271}