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 static org.eclipse.aether.named.support.Retry.retry;
035
036/**
037 * Named lock that uses {@link FileLock}. An instance of this class is about ONE LOCK (one file)
038 * and is possibly used by multiple threads. Each thread (if properly coded re boxing) will try to
039 * obtain either shared or exclusive lock. As file locks are JVM-scoped (so one JVM can obtain
040 * same file lock only once), the threads share file lock and synchronize according to it. Still,
041 * as file lock obtain operation does not block (or in other words, the method that does block
042 * cannot be controlled for how long it blocks), we are "simulating" thread blocking using
043 * {@link Retry} utility.
044 * This implementation performs coordination not only on thread (JVM-local) level, but also on
045 * process level, as long as other parties are using this same "advisory" locking mechanism.
046 *
047 * @since 1.7.3
048 */
049public final class FileLockNamedLock extends NamedLockSupport {
050    private static final long RETRY_SLEEP_MILLIS = 100L;
051
052    private static final long LOCK_POSITION = 0L;
053
054    private static final long LOCK_SIZE = 1L;
055
056    /**
057     * Thread -> steps stack (true = shared, false = exclusive)
058     */
059    private final Map<Thread, Deque<Boolean>> threadSteps;
060
061    /**
062     * The {@link FileChannel} this instance is about.
063     */
064    private final FileChannel fileChannel;
065
066    /**
067     * The reference of {@link FileLock}, if obtained.
068     */
069    private final AtomicReference<FileLock> fileLockRef;
070
071    /**
072     * Lock protecting "critical region": this is where threads are allowed to perform locking but should leave this
073     * region as quick as possible.
074     */
075    private final ReentrantLock criticalRegion;
076
077    public FileLockNamedLock(final String name, final FileChannel fileChannel, final NamedLockFactorySupport factory) {
078        super(name, factory);
079        this.threadSteps = new HashMap<>();
080        this.fileChannel = fileChannel;
081        this.fileLockRef = new AtomicReference<>(null);
082        this.criticalRegion = new ReentrantLock();
083    }
084
085    @Override
086    protected boolean doLockShared(final long time, final TimeUnit unit) throws InterruptedException {
087        return retry(time, unit, RETRY_SLEEP_MILLIS, this::doLockShared, null, false);
088    }
089
090    @Override
091    protected boolean doLockExclusively(final long time, final TimeUnit unit) throws InterruptedException {
092        return retry(time, unit, RETRY_SLEEP_MILLIS, this::doLockExclusively, null, false);
093    }
094
095    private Boolean doLockShared() {
096        if (criticalRegion.tryLock()) {
097            try {
098                Deque<Boolean> steps = threadSteps.computeIfAbsent(Thread.currentThread(), k -> new ArrayDeque<>());
099                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 doLockExclusively() {
131        if (criticalRegion.tryLock()) {
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}