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 org.eclipse.aether.named.NamedLockKey;
35  
36  import static org.eclipse.aether.named.support.Retry.retry;
37  
38  /**
39   * Named lock that uses {@link FileLock}. An instance of this class is about ONE LOCK (one file)
40   * and is possibly used by multiple threads. Each thread (if properly coded re boxing) will try to
41   * obtain either shared or exclusive lock. As file locks are JVM-scoped (so one JVM can obtain
42   * same file lock only once), the threads share file lock and synchronize according to it. Still,
43   * as file lock obtain operation does not block (or in other words, the method that does block
44   * cannot be controlled for how long it blocks), we are "simulating" thread blocking using
45   * {@link Retry} utility.
46   * This implementation performs coordination not only on thread (JVM-local) level, but also on
47   * process level, as long as other parties are using this same "advisory" locking mechanism.
48   *
49   * @since 1.7.3
50   */
51  public final class FileLockNamedLock extends NamedLockSupport {
52      private static final long RETRY_SLEEP_MILLIS = 100L;
53  
54      private static final long LOCK_POSITION = 0L;
55  
56      private static final long LOCK_SIZE = 1L;
57  
58      /**
59       * Thread -> steps stack (true = shared, false = exclusive)
60       */
61      private final Map<Thread, Deque<Boolean>> threadSteps;
62  
63      /**
64       * The {@link FileChannel} this instance is about.
65       */
66      private final FileChannel fileChannel;
67  
68      /**
69       * The reference of {@link FileLock}, if obtained.
70       */
71      private final AtomicReference<FileLock> fileLockRef;
72  
73      /**
74       * Lock protecting "critical region": this is where threads are allowed to perform locking but should leave this
75       * region as quick as possible.
76       */
77      private final ReentrantLock criticalRegion;
78  
79      public FileLockNamedLock(
80              final NamedLockKey key, final FileChannel fileChannel, final NamedLockFactorySupport factory) {
81          super(key, factory);
82          this.threadSteps = new HashMap<>();
83          this.fileChannel = fileChannel;
84          this.fileLockRef = new AtomicReference<>(null);
85          this.criticalRegion = new ReentrantLock();
86      }
87  
88      @Override
89      protected boolean doLockShared(final long time, final TimeUnit unit) throws InterruptedException {
90          return retry(time, unit, RETRY_SLEEP_MILLIS, this::doLockShared, null, false);
91      }
92  
93      @Override
94      protected boolean doLockExclusively(final long time, final TimeUnit unit) throws InterruptedException {
95          return retry(time, unit, RETRY_SLEEP_MILLIS, this::doLockExclusively, null, false);
96      }
97  
98      private Boolean doLockShared() {
99          if (criticalRegion.tryLock()) {
100             try {
101                 Deque<Boolean> steps = threadSteps.computeIfAbsent(Thread.currentThread(), k -> new ArrayDeque<>());
102                 FileLock obtainedLock = fileLockRef.get();
103                 if (obtainedLock != null) {
104                     if (obtainedLock.isShared()) {
105                         steps.push(Boolean.TRUE);
106                         return true;
107                     } else {
108                         // if we own exclusive, that's still fine
109                         boolean weOwnExclusive = steps.contains(Boolean.FALSE);
110                         if (weOwnExclusive) {
111                             steps.push(Boolean.TRUE);
112                             return true;
113                         } else {
114                             // someone else owns exclusive, let's wait
115                             return null;
116                         }
117                     }
118                 }
119 
120                 FileLock fileLock = obtainFileLock(true);
121                 if (fileLock != null) {
122                     fileLockRef.set(fileLock);
123                     steps.push(Boolean.TRUE);
124                     return true;
125                 }
126             } finally {
127                 criticalRegion.unlock();
128             }
129         }
130         return null;
131     }
132 
133     private Boolean doLockExclusively() {
134         if (criticalRegion.tryLock()) {
135             try {
136                 Deque<Boolean> steps = threadSteps.computeIfAbsent(Thread.currentThread(), k -> new ArrayDeque<>());
137                 FileLock obtainedLock = fileLockRef.get();
138                 if (obtainedLock != null) {
139                     if (obtainedLock.isShared()) {
140                         // if we own shared, that's attempted upgrade
141                         boolean weOwnShared = steps.contains(Boolean.TRUE);
142                         if (weOwnShared) {
143                             throw new LockUpgradeNotSupportedException(this); // Lock upgrade not supported
144                         } else {
145                             // someone else owns shared, let's wait
146                             return null;
147                         }
148                     } else {
149                         // if we own exclusive, that's fine
150                         boolean weOwnExclusive = steps.contains(Boolean.FALSE);
151                         if (weOwnExclusive) {
152                             steps.push(Boolean.FALSE);
153                             return true;
154                         } else {
155                             // someone else owns exclusive, let's wait
156                             return null;
157                         }
158                     }
159                 }
160 
161                 FileLock fileLock = obtainFileLock(false);
162                 if (fileLock != null) {
163                     fileLockRef.set(fileLock);
164                     steps.push(Boolean.FALSE);
165                     return true;
166                 }
167             } finally {
168                 criticalRegion.unlock();
169             }
170         }
171         return null;
172     }
173 
174     @Override
175     protected void doUnlock() {
176         criticalRegion.lock();
177         try {
178             Deque<Boolean> steps = threadSteps.computeIfAbsent(Thread.currentThread(), k -> new ArrayDeque<>());
179             if (steps.isEmpty()) {
180                 throw new IllegalStateException("Wrong API usage: unlock without lock");
181             }
182             steps.pop();
183             if (steps.isEmpty() && !anyOtherThreadHasSteps()) {
184                 try {
185                     fileLockRef.getAndSet(null).release();
186                 } catch (IOException e) {
187                     throw new UncheckedIOException(e);
188                 }
189             }
190         } finally {
191             criticalRegion.unlock();
192         }
193     }
194 
195     /**
196      * Returns {@code true} if any other than this thread using this instance has any step recorded.
197      */
198     private boolean anyOtherThreadHasSteps() {
199         return threadSteps.entrySet().stream()
200                 .filter(e -> !Thread.currentThread().equals(e.getKey()))
201                 .map(Map.Entry::getValue)
202                 .anyMatch(d -> !d.isEmpty());
203     }
204 
205     /**
206      * Attempts to obtain real {@link FileLock}, returns non-null value is succeeds, or {@code null} if cannot.
207      */
208     private FileLock obtainFileLock(final boolean shared) {
209         FileLock result;
210         try {
211             result = fileChannel.tryLock(LOCK_POSITION, LOCK_SIZE, shared);
212         } catch (OverlappingFileLockException e) {
213             logger.trace("File lock overlap on '{}'", key(), e);
214             return null;
215         } catch (IOException e) {
216             logger.trace("Failure on acquire of file lock for '{}'", key(), e);
217             throw new UncheckedIOException("Failed to acquire lock file channel for '" + key() + "'", e);
218         }
219         return result;
220     }
221 }