001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.named.support;
020
021import java.io.IOException;
022import java.io.UncheckedIOException;
023import java.nio.channels.FileChannel;
024import java.nio.channels.FileLock;
025import java.nio.channels.OverlappingFileLockException;
026import java.util.ArrayDeque;
027import java.util.Deque;
028import java.util.HashMap;
029import java.util.Map;
030import java.util.concurrent.TimeUnit;
031import java.util.concurrent.atomic.AtomicReference;
032import java.util.concurrent.locks.ReentrantLock;
033
034import org.eclipse.aether.named.NamedLockKey;
035
036import static org.eclipse.aether.named.support.Retry.retry;
037
038/**
039 * Named lock that uses {@link FileLock}. An instance of this class is about ONE LOCK (one file)
040 * and is possibly used by multiple threads. Each thread (if properly coded re boxing) will try to
041 * obtain either shared or exclusive lock. As file locks are JVM-scoped (so one JVM can obtain
042 * same file lock only once), the threads share file lock and synchronize according to it. Still,
043 * as file lock obtain operation does not block (or in other words, the method that does block
044 * cannot be controlled for how long it blocks), we are "simulating" thread blocking using
045 * {@link Retry} utility.
046 * This implementation performs coordination not only on thread (JVM-local) level, but also on
047 * process level, as long as other parties are using this same "advisory" locking mechanism.
048 *
049 * @since 1.7.3
050 */
051public final class FileLockNamedLock extends NamedLockSupport {
052    private static final long RETRY_SLEEP_MILLIS = 100L;
053
054    private static final long LOCK_POSITION = 0L;
055
056    private static final long LOCK_SIZE = 1L;
057
058    /**
059     * Thread -> steps stack (true = shared, false = exclusive)
060     */
061    private final Map<Thread, Deque<Boolean>> threadSteps;
062
063    /**
064     * The {@link FileChannel} this instance is about.
065     */
066    private final FileChannel fileChannel;
067
068    /**
069     * The reference of {@link FileLock}, if obtained.
070     */
071    private final AtomicReference<FileLock> fileLockRef;
072
073    /**
074     * Lock protecting "critical region": this is where threads are allowed to perform locking but should leave this
075     * region as quick as possible.
076     */
077    private final ReentrantLock criticalRegion;
078
079    public FileLockNamedLock(
080            final NamedLockKey key, final FileChannel fileChannel, final NamedLockFactorySupport factory) {
081        super(key, factory);
082        this.threadSteps = new HashMap<>();
083        this.fileChannel = fileChannel;
084        this.fileLockRef = new AtomicReference<>(null);
085        this.criticalRegion = new ReentrantLock();
086    }
087
088    @Override
089    protected boolean doLockShared(final long time, final TimeUnit unit) throws InterruptedException {
090        return retry(time, unit, RETRY_SLEEP_MILLIS, () -> doLockSharedPerform(time, unit), null, false);
091    }
092
093    @Override
094    protected boolean doLockExclusively(final long time, final TimeUnit unit) throws InterruptedException {
095        return retry(time, unit, RETRY_SLEEP_MILLIS, () -> doLockExclusivelyPerform(time, unit), null, false);
096    }
097
098    private Boolean doLockSharedPerform(final long time, final TimeUnit unit) throws InterruptedException {
099        if (criticalRegion.tryLock(time, unit)) {
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 doLockExclusivelyPerform(final long time, final TimeUnit unit) throws InterruptedException {
134        if (criticalRegion.tryLock(time, unit)) {
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}