001package org.eclipse.aether.named.support;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *  http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import java.io.IOException;
023import java.io.UncheckedIOException;
024import java.nio.channels.FileChannel;
025import java.nio.channels.FileLock;
026import java.nio.channels.OverlappingFileLockException;
027import java.util.ArrayDeque;
028import java.util.Deque;
029import java.util.HashMap;
030import java.util.Map;
031import java.util.concurrent.TimeUnit;
032import java.util.concurrent.atomic.AtomicReference;
033import java.util.concurrent.locks.ReentrantLock;
034
035import static org.eclipse.aether.named.support.Retry.retry;
036
037/**
038 * Named lock that uses {@link FileLock}. An instance of this class is about ONE LOCK (one file)
039 * and is possibly used by multiple threads. Each thread (if properly coded re boxing) will try to
040 * obtain either shared or exclusive lock. As file locks are JVM-scoped (so one JVM can obtain
041 * same file lock only once), the threads share file lock and synchronize according to it. Still,
042 * as file lock obtain operation does not block (or in other words, the method that does block
043 * cannot be controlled for how long it blocks), we are "simulating" thread blocking using
044 * {@link Retry} utility.
045 * This implementation performs coordination not only on thread (JVM-local) level, but also on
046 * process level, as long as other parties are using this same "advisory" locking mechanism.
047 *
048 * @since 1.7.3
049 */
050public final class FileLockNamedLock
051    extends NamedLockSupport
052{
053    private static final long RETRY_SLEEP_MILLIS = 100L;
054
055    private static final long LOCK_POSITION = 0L;
056
057    private static final long LOCK_SIZE = 1L;
058
059    /**
060     * Thread -> steps stack (true = shared, false = exclusive)
061     */
062    private final Map<Thread, Deque<Boolean>> threadSteps;
063
064    /**
065     * The {@link FileChannel} this instance is about.
066     */
067    private final FileChannel fileChannel;
068
069    /**
070     * The reference of {@link FileLock}, if obtained.
071     */
072    private final AtomicReference<FileLock> fileLockRef;
073
074    /**
075     * Lock protecting "critical region": this is where threads are allowed to perform locking but should leave this
076     * region as quick as possible.
077     */
078    private final ReentrantLock criticalRegion;
079
080    public FileLockNamedLock( final String name,
081                              final FileChannel fileChannel,
082                              final NamedLockFactorySupport factory )
083    {
084        super( name, factory );
085        this.threadSteps = new HashMap<>();
086        this.fileChannel = fileChannel;
087        this.fileLockRef = new AtomicReference<>( null );
088        this.criticalRegion = new ReentrantLock();
089    }
090
091    @Override
092    public boolean lockShared( final long time, final TimeUnit unit ) throws InterruptedException
093    {
094        return retry( time, unit, RETRY_SLEEP_MILLIS, this::doLockShared, null, false );
095    }
096
097    @Override
098    public boolean lockExclusively( final long time, final TimeUnit unit ) throws InterruptedException
099    {
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                        // TODO No counterpart in other lock impls, drop or make consistent?
116                        logger.trace( "{} lock (shared={})", name(), true );
117                        steps.push( Boolean.TRUE );
118                        return true;
119                    }
120                    else
121                    {
122                        // if we own exclusive, that's still fine
123                        boolean weOwnExclusive = steps.contains( Boolean.FALSE );
124                        if ( weOwnExclusive )
125                        {
126                            // TODO No counterpart in other lock impls, drop or make consistent?
127                            logger.trace( "{} lock (shared={})", name(), true );
128                            steps.push( Boolean.TRUE );
129                            return true;
130                        }
131                        else
132                        {
133                            // someone else owns exclusive, let's wait
134                            return null;
135                        }
136                    }
137                }
138
139                // TODO No counterpart in other lock impls, drop or make consistent?
140                logger.trace( "{} no obtained lock: obtain shared file lock", name() );
141                FileLock fileLock = obtainFileLock( true );
142                if ( fileLock != null )
143                {
144                    fileLockRef.set( fileLock );
145                    steps.push( Boolean.TRUE );
146                    return true;
147                }
148            }
149            finally
150            {
151                criticalRegion.unlock();
152            }
153        }
154        return null;
155    }
156
157    private Boolean doLockExclusively()
158    {
159        if ( criticalRegion.tryLock() )
160        {
161            try
162            {
163                Deque<Boolean> steps = threadSteps.computeIfAbsent( Thread.currentThread(), k -> new ArrayDeque<>() );
164                FileLock obtainedLock = fileLockRef.get();
165                if ( obtainedLock != null )
166                {
167                    if ( obtainedLock.isShared() )
168                    {
169                        // if we own shared, that's attempted upgrade
170                        boolean weOwnShared = steps.contains( Boolean.TRUE );
171                        if ( weOwnShared )
172                        {
173                            // TODO No counterpart in other lock impls, drop or make consistent?
174                            logger.trace(
175                                    "{} steps not empty, has not exclusive lock: lock-upgrade not supported", name()
176                            );
177                            return false; // Lock upgrade not supported
178                        }
179                        else
180                        {
181                            // someone else owns shared, let's wait
182                            return null;
183                        }
184                    }
185                    else
186                    {
187                        // if we own exclusive, that's fine
188                        boolean weOwnExclusive = steps.contains( Boolean.FALSE );
189                        if ( weOwnExclusive )
190                        {
191                            // TODO No counterpart in other lock impls, drop or make consistent?
192                            logger.trace( "{} lock (shared={})", name(), false );
193                            steps.push( Boolean.FALSE );
194                            return true;
195                        }
196                        else
197                        {
198                            // someone else owns exclusive, let's wait
199                            return null;
200                        }
201                    }
202                }
203
204                // TODO No counterpart in other lock impls, drop or make consistent?
205                logger.trace( "{} no obtained lock: obtain exclusive file lock", name() );
206                FileLock fileLock = obtainFileLock( false );
207                if ( fileLock != null )
208                {
209                    fileLockRef.set( fileLock );
210                    steps.push( Boolean.FALSE );
211                    return true;
212                }
213            }
214            finally
215            {
216                criticalRegion.unlock();
217            }
218        }
219        return null;
220    }
221
222    @Override
223    public void unlock()
224    {
225        criticalRegion.lock();
226        try
227        {
228            Deque<Boolean> steps = threadSteps.computeIfAbsent( Thread.currentThread(), k -> new ArrayDeque<>() );
229            if ( steps.isEmpty() )
230            {
231                throw new IllegalStateException( "Wrong API usage: unlock without lock" );
232            }
233            Boolean shared = steps.pop();
234            // TODO No counterpart in other lock impls, drop or make consistent?
235            logger.trace( "{} unlock (shared = {})", name(), shared );
236            if ( steps.isEmpty() && !anyOtherThreadHasSteps() )
237            {
238                try
239                {
240                    fileLockRef.getAndSet( null ).release();
241                }
242                catch ( IOException e )
243                {
244                    throw new UncheckedIOException( e );
245                }
246            }
247        }
248        finally
249        {
250            criticalRegion.unlock();
251        }
252    }
253
254    /**
255     * Returns {@code true} if any other than this thread using this instance has any step recorded.
256     */
257    private boolean anyOtherThreadHasSteps()
258    {
259        return threadSteps.entrySet().stream()
260                .filter( e -> !Thread.currentThread().equals( e.getKey() ) )
261                .map( Map.Entry::getValue )
262                .anyMatch( d -> !d.isEmpty() );
263    }
264
265    /**
266     * Attempts to obtain real {@link FileLock}, returns non-null value is succeeds, or {@code null} if cannot.
267     */
268    private FileLock obtainFileLock( final boolean shared )
269    {
270        FileLock result;
271        try
272        {
273            result = fileChannel.tryLock( LOCK_POSITION, LOCK_SIZE, shared );
274        }
275        catch ( OverlappingFileLockException e )
276        {
277            logger.trace( "File lock overlap on '{}'", name(), e );
278            return null;
279        }
280        catch ( IOException e )
281        {
282            logger.trace( "Failure on acquire of file lock for '{}'", name(), e );
283            throw new UncheckedIOException( "Failed to acquire lock file channel for '" + name() + "'", e );
284        }
285        return result;
286    }
287}