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