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                         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 }