View Javadoc
1   package org.eclipse.aether.named.providers;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.IOException;
23  import java.io.UncheckedIOException;
24  import java.nio.channels.FileChannel;
25  import java.nio.file.AccessDeniedException;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.nio.file.Paths;
29  import java.nio.file.StandardOpenOption;
30  import java.util.concurrent.ConcurrentHashMap;
31  import java.util.concurrent.ConcurrentMap;
32  
33  import javax.inject.Named;
34  import javax.inject.Singleton;
35  
36  import org.eclipse.aether.named.support.FileLockNamedLock;
37  import org.eclipse.aether.named.support.NamedLockFactorySupport;
38  import org.eclipse.aether.named.support.NamedLockSupport;
39  
40  import static org.eclipse.aether.named.support.Retry.retry;
41  
42  /**
43   * Named locks factory of {@link FileLockNamedLock}s. This is a bit special implementation, as it
44   * expects locks names to be fully qualified absolute file system paths.
45   *
46   * @since 1.7.3
47   */
48  @Singleton
49  @Named( FileLockNamedLockFactory.NAME )
50  public class FileLockNamedLockFactory
51      extends NamedLockFactorySupport
52  {
53      public static final String NAME = "file-lock";
54  
55      /**
56       * Tweak: on Windows, the presence of {@link StandardOpenOption#DELETE_ON_CLOSE} causes concurrency issues. This
57       * flag allows to have it removed from effective flags, at the cost that lockfile directory becomes crowded
58       * with 0 byte sized lock files that are never cleaned up. Default value is {@code true}.
59       *
60       * @see <a href="https://bugs.openjdk.org/browse/JDK-8252883">JDK-8252883</a>
61       */
62      private static final boolean DELETE_LOCK_FILES = Boolean.parseBoolean(
63              System.getProperty( "aether.named.file-lock.deleteLockFiles", Boolean.TRUE.toString() ) );
64  
65      /**
66       * Tweak: on Windows, the presence of {@link StandardOpenOption#DELETE_ON_CLOSE} causes concurrency issues. This
67       * flag allows to implement similar fix as referenced JDK bug report: retry and hope the best. Default value is
68       * 5 attempts (will retry 4 times).
69       *
70       * @see <a href="https://bugs.openjdk.org/browse/JDK-8252883">JDK-8252883</a>
71       */
72      private static final int ATTEMPTS = Integer.parseInt(
73              System.getProperty( "aether.named.file-lock.attempts", "5" ) );
74  
75      /**
76       * Tweak: When {@link #ATTEMPTS} used, the amount of milliseconds to sleep between subsequent retries. Default
77       * value is 50 milliseconds.
78       */
79      private static final long SLEEP_MILLIS = Long.parseLong(
80              System.getProperty( "aether.named.file-lock.sleepMillis", "50" ) );
81  
82      private final ConcurrentMap<String, FileChannel> fileChannels;
83  
84      public FileLockNamedLockFactory()
85      {
86          this.fileChannels = new ConcurrentHashMap<>();
87      }
88  
89      @Override
90      protected NamedLockSupport createLock( final String name )
91      {
92          Path path = Paths.get( name );
93          FileChannel fileChannel = fileChannels.computeIfAbsent( name, k ->
94          {
95              try
96              {
97                  Files.createDirectories( path.getParent() );
98                  FileChannel channel = retry( ATTEMPTS, SLEEP_MILLIS, () ->
99                  {
100                     try
101                     {
102                         if ( DELETE_LOCK_FILES )
103                         {
104                             return FileChannel.open(
105                                     path,
106                                     StandardOpenOption.READ, StandardOpenOption.WRITE,
107                                     StandardOpenOption.CREATE, StandardOpenOption.DELETE_ON_CLOSE
108                             );
109                         }
110                         else
111                         {
112                             return FileChannel.open(
113                                     path,
114                                     StandardOpenOption.READ, StandardOpenOption.WRITE,
115                                     StandardOpenOption.CREATE
116                             );
117                         }
118                     }
119                     catch ( AccessDeniedException e )
120                     {
121                         return null;
122                     }
123                 }, null, null );
124 
125                 if ( channel == null )
126                 {
127                     throw new IllegalStateException( "Could not open file channel for '"
128                             + name + "' after " + ATTEMPTS + " attempts; giving up" );
129                 }
130                 return channel;
131             }
132             catch ( InterruptedException e )
133             {
134                 Thread.currentThread().interrupt();
135                 throw new RuntimeException( "Interrupted while opening file channel for '"
136                         + name + "'", e );
137             }
138             catch ( IOException e )
139             {
140                 throw new UncheckedIOException( "Failed to open file channel for '"
141                     + name + "'", e );
142             }
143         } );
144         return new FileLockNamedLock( name, fileChannel, this );
145     }
146 
147     @Override
148     protected void destroyLock( final String name )
149     {
150         FileChannel fileChannel = fileChannels.remove( name );
151         if ( fileChannel == null )
152         {
153             throw new IllegalStateException( "File channel expected, but does not exist: " + name );
154         }
155 
156         try
157         {
158             fileChannel.close();
159         }
160         catch ( IOException e )
161         {
162             throw new UncheckedIOException( "Failed to close file channel for '"
163                     + name + "'", e );
164         }
165     }
166 }