View Javadoc
1   package org.eclipse.aether.named.support;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.IOException;
23  import java.io.UncheckedIOException;
24  import java.nio.channels.FileChannel;
25  import java.nio.channels.FileLock;
26  import java.nio.channels.OverlappingFileLockException;
27  import java.util.ArrayDeque;
28  import java.util.Deque;
29  import java.util.HashMap;
30  import java.util.Map;
31  import java.util.concurrent.TimeUnit;
32  import java.util.concurrent.atomic.AtomicReference;
33  import java.util.concurrent.locks.ReentrantLock;
34  
35  import static org.eclipse.aether.named.support.Retry.retry;
36  
37  /**
38   * Named lock that uses {@link FileLock}. An instance of this class is about ONE LOCK (one file)
39   * and is possibly used by multiple threads. Each thread (if properly coded re boxing) will try to
40   * obtain either shared or exclusive lock. As file locks are JVM-scoped (so one JVM can obtain
41   * same file lock only once), the threads share file lock and synchronize according to it. Still,
42   * as file lock obtain operation does not block (or in other words, the method that does block
43   * cannot be controlled for how long it blocks), we are "simulating" thread blocking using
44   * {@link Retry} utility.
45   * This implementation performs coordination not only on thread (JVM-local) level, but also on
46   * process level, as long as other parties are using this same "advisory" locking mechanism.
47   *
48   * @since 1.7.3
49   */
50  public final class FileLockNamedLock
51      extends NamedLockSupport
52  {
53      private static final long RETRY_SLEEP_MILLIS = 100L;
54  
55      private static final long LOCK_POSITION = 0L;
56  
57      private static final long LOCK_SIZE = 1L;
58  
59      /**
60       * Thread -> steps stack (true = shared, false = exclusive)
61       */
62      private final Map<Thread, Deque<Boolean>> threadSteps;
63  
64      /**
65       * The {@link FileChannel} this instance is about.
66       */
67      private final FileChannel fileChannel;
68  
69      /**
70       * The reference of {@link FileLock}, if obtained.
71       */
72      private final AtomicReference<FileLock> fileLockRef;
73  
74      /**
75       * Lock protecting "critical region": this is where threads are allowed to perform locking but should leave this
76       * region as quick as possible.
77       */
78      private final ReentrantLock criticalRegion;
79  
80      public FileLockNamedLock( final String name,
81                                final FileChannel fileChannel,
82                                final NamedLockFactorySupport factory )
83      {
84          super( name, factory );
85          this.threadSteps = new HashMap<>();
86          this.fileChannel = fileChannel;
87          this.fileLockRef = new AtomicReference<>( null );
88          this.criticalRegion = new ReentrantLock();
89      }
90  
91      @Override
92      public boolean lockShared( final long time, final TimeUnit unit ) throws InterruptedException
93      {
94          return retry( time, unit, RETRY_SLEEP_MILLIS, this::doLockShared, null, false );
95      }
96  
97      @Override
98      public boolean lockExclusively( final long time, final TimeUnit unit ) throws InterruptedException
99      {
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 }