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                        steps.push( Boolean.TRUE );
116                        return true;
117                    }
118                    else
119                    {
120                        // if we own exclusive, that's still fine
121                        boolean weOwnExclusive = steps.contains( Boolean.FALSE );
122                        if ( weOwnExclusive )
123                        {
124                            steps.push( Boolean.TRUE );
125                            return true;
126                        }
127                        else
128                        {
129                            // someone else owns exclusive, let's wait
130                            return null;
131                        }
132                    }
133                }
134
135                FileLock fileLock = obtainFileLock( true );
136                if ( fileLock != null )
137                {
138                    fileLockRef.set( fileLock );
139                    steps.push( Boolean.TRUE );
140                    return true;
141                }
142            }
143            finally
144            {
145                criticalRegion.unlock();
146            }
147        }
148        return null;
149    }
150
151    private Boolean doLockExclusively()
152    {
153        if ( criticalRegion.tryLock() )
154        {
155            try
156            {
157                Deque<Boolean> steps = threadSteps.computeIfAbsent( Thread.currentThread(), k -> new ArrayDeque<>() );
158                FileLock obtainedLock = fileLockRef.get();
159                if ( obtainedLock != null )
160                {
161                    if ( obtainedLock.isShared() )
162                    {
163                        // if we own shared, that's attempted upgrade
164                        boolean weOwnShared = steps.contains( Boolean.TRUE );
165                        if ( weOwnShared )
166                        {
167                            return false; // Lock upgrade not supported
168                        }
169                        else
170                        {
171                            // someone else owns shared, let's wait
172                            return null;
173                        }
174                    }
175                    else
176                    {
177                        // if we own exclusive, that's fine
178                        boolean weOwnExclusive = steps.contains( Boolean.FALSE );
179                        if ( weOwnExclusive )
180                        {
181                            steps.push( Boolean.FALSE );
182                            return true;
183                        }
184                        else
185                        {
186                            // someone else owns exclusive, let's wait
187                            return null;
188                        }
189                    }
190                }
191
192                FileLock fileLock = obtainFileLock( false );
193                if ( fileLock != null )
194                {
195                    fileLockRef.set( fileLock );
196                    steps.push( Boolean.FALSE );
197                    return true;
198                }
199            }
200            finally
201            {
202                criticalRegion.unlock();
203            }
204        }
205        return null;
206    }
207
208    @Override
209    public void unlock()
210    {
211        criticalRegion.lock();
212        try
213        {
214            Deque<Boolean> steps = threadSteps.computeIfAbsent( Thread.currentThread(), k -> new ArrayDeque<>() );
215            if ( steps.isEmpty() )
216            {
217                throw new IllegalStateException( "Wrong API usage: unlock without lock" );
218            }
219            steps.pop();
220            if ( steps.isEmpty() && !anyOtherThreadHasSteps() )
221            {
222                try
223                {
224                    fileLockRef.getAndSet( null ).release();
225                }
226                catch ( IOException e )
227                {
228                    throw new UncheckedIOException( e );
229                }
230            }
231        }
232        finally
233        {
234            criticalRegion.unlock();
235        }
236    }
237
238    /**
239     * Returns {@code true} if any other than this thread using this instance has any step recorded.
240     */
241    private boolean anyOtherThreadHasSteps()
242    {
243        return threadSteps.entrySet().stream()
244                .filter( e -> !Thread.currentThread().equals( e.getKey() ) )
245                .map( Map.Entry::getValue )
246                .anyMatch( d -> !d.isEmpty() );
247    }
248
249    /**
250     * Attempts to obtain real {@link FileLock}, returns non-null value is succeeds, or {@code null} if cannot.
251     */
252    private FileLock obtainFileLock( final boolean shared )
253    {
254        FileLock result;
255        try
256        {
257            result = fileChannel.tryLock( LOCK_POSITION, LOCK_SIZE, shared );
258        }
259        catch ( OverlappingFileLockException e )
260        {
261            logger.trace( "File lock overlap on '{}'", name(), e );
262            return null;
263        }
264        catch ( IOException e )
265        {
266            logger.trace( "Failure on acquire of file lock for '{}'", name(), e );
267            throw new UncheckedIOException( "Failed to acquire lock file channel for '" + name() + "'", e );
268        }
269        return result;
270    }
271}