1 package org.eclipse.aether.synccontext;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import java.io.File;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.net.InetAddress;
26 import java.net.UnknownHostException;
27 import java.nio.charset.StandardCharsets;
28 import java.nio.file.Files;
29 import java.nio.file.Path;
30 import java.nio.file.Paths;
31 import java.util.Collection;
32 import java.util.Collections;
33 import java.util.Deque;
34 import java.util.Iterator;
35 import java.util.LinkedHashMap;
36 import java.util.LinkedList;
37 import java.util.Map;
38 import java.util.TreeSet;
39
40 import javax.annotation.PreDestroy;
41 import javax.annotation.Priority;
42 import javax.inject.Named;
43 import javax.inject.Singleton;
44
45 import org.eclipse.aether.RepositorySystemSession;
46 import org.eclipse.aether.SyncContext;
47 import org.eclipse.aether.artifact.Artifact;
48 import org.eclipse.aether.impl.SyncContextFactory;
49 import org.eclipse.aether.metadata.Metadata;
50 import org.eclipse.aether.util.ChecksumUtils;
51 import org.eclipse.aether.util.ConfigUtils;
52 import org.redisson.Redisson;
53 import org.redisson.api.RLock;
54 import org.redisson.api.RReadWriteLock;
55 import org.redisson.api.RedissonClient;
56 import org.redisson.config.Config;
57 import org.slf4j.Logger;
58 import org.slf4j.LoggerFactory;
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104 @Named
105 @Priority( Integer.MAX_VALUE )
106 @Singleton
107 public class RedissonSyncContextFactory
108 implements SyncContextFactory
109 {
110
111 private static final String DEFAULT_CONFIG_FILE_NAME = "maven-resolver-redisson.yaml";
112 private static final String DEFAULT_REDIS_ADDRESS = "redis://localhost:6379";
113 private static final String DEFAULT_CLIENT_NAME = "maven-resolver";
114 private static final String DEFAULT_HOSTNAME = "localhost";
115 private static final String DEFAULT_DISCRIMINATOR_DIGEST = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
116
117 private static final String CONFIG_PROP_CONFIG_FILE = "aether.syncContext.redisson.configFile";
118
119 private static final Logger LOGGER = LoggerFactory.getLogger( RedissonSyncContextFactory.class );
120
121
122 private RedissonClient redissonClient;
123 private String hostname;
124
125 public RedissonSyncContextFactory()
126 {
127
128 LOGGER.trace( "TCCL: {}", Thread.currentThread().getContextClassLoader() );
129 LOGGER.trace( "CCL: {}", getClass().getClassLoader() );
130 this.redissonClient = createRedissonClient();
131 this.hostname = getHostname();
132 }
133
134 private RedissonClient createRedissonClient()
135 {
136 Path configFilePath = null;
137
138 String configFile = ConfigUtils.getString( System.getProperties(), null, CONFIG_PROP_CONFIG_FILE );
139 if ( configFile != null && !configFile.isEmpty() )
140 {
141 configFilePath = Paths.get( configFile );
142 if ( Files.notExists( configFilePath ) )
143 {
144 throw new IllegalArgumentException( "The specified Redisson config file does not exist: "
145 + configFilePath );
146 }
147 }
148
149 if ( configFilePath == null )
150 {
151 String mavenConf = ConfigUtils.getString( System.getProperties(), null, "maven.conf" );
152 if ( mavenConf != null && !mavenConf.isEmpty() )
153 {
154 configFilePath = Paths.get( mavenConf, DEFAULT_CONFIG_FILE_NAME );
155 if ( Files.notExists( configFilePath ) )
156 {
157 configFilePath = null;
158 }
159 }
160 }
161
162 Config config = null;
163
164 if ( configFilePath != null )
165 {
166 LOGGER.trace( "Reading Redisson config file from '{}'", configFilePath );
167 try ( InputStream is = Files.newInputStream( configFilePath ) )
168 {
169 config = Config.fromYAML( is );
170 }
171 catch ( IOException e )
172 {
173 throw new IllegalStateException( "Failed to read Redisson config file: " + configFilePath, e );
174 }
175 }
176 else
177 {
178 config = new Config();
179 config.useSingleServer()
180 .setAddress( DEFAULT_REDIS_ADDRESS )
181 .setClientName( DEFAULT_CLIENT_NAME );
182 }
183
184 RedissonClient redissonClient = Redisson.create( config );
185 LOGGER.trace( "Created Redisson client with id '{}'", redissonClient.getId() );
186
187 return redissonClient;
188 }
189
190 private String getHostname()
191 {
192 try
193 {
194 return InetAddress.getLocalHost().getHostName();
195 }
196 catch ( UnknownHostException e )
197 {
198 LOGGER.warn( "Failed to get hostname, using '{}'",
199 DEFAULT_HOSTNAME, e );
200 return DEFAULT_HOSTNAME;
201 }
202 }
203
204 public SyncContext newInstance( RepositorySystemSession session, boolean shared )
205 {
206
207 LOGGER.trace( "Instance: {}", this );
208 return new RedissonSyncContext( session, hostname, redissonClient, shared );
209 }
210
211 @PreDestroy
212 public void shutdown()
213 {
214 LOGGER.trace( "Shutting down Redisson client with id '{}'", redissonClient.getId() );
215 redissonClient.shutdown();
216 }
217
218 static class RedissonSyncContext
219 implements SyncContext
220 {
221
222 private static final String CONFIG_PROP_DISCRIMINATOR = "aether.syncContext.redisson.discriminator";
223
224 private static final String KEY_PREFIX = "maven:resolver:";
225
226 private static final Logger LOGGER = LoggerFactory.getLogger( RedissonSyncContext.class );
227
228 private final RepositorySystemSession session;
229 private final String hostname;
230 private final RedissonClient redissonClient;
231 private final boolean shared;
232 private final Map<String, RReadWriteLock> locks = new LinkedHashMap<>();
233
234 private RedissonSyncContext( RepositorySystemSession session, String hostname,
235 RedissonClient redissonClient, boolean shared )
236 {
237 this.session = session;
238 this.hostname = hostname;
239 this.redissonClient = redissonClient;
240 this.shared = shared;
241 }
242
243 public void acquire( Collection<? extends Artifact> artifacts,
244 Collection<? extends Metadata> metadatas )
245 {
246
247
248 Collection<String> keys = new TreeSet<>();
249 if ( artifacts != null )
250 {
251 for ( Artifact artifact : artifacts )
252 {
253
254 String key = "artifact:" + artifact.getGroupId() + ":"
255 + artifact.getArtifactId() + ":" + artifact.getBaseVersion();
256 keys.add( key );
257 }
258 }
259
260 if ( metadatas != null )
261 {
262 for ( Metadata metadata : metadatas )
263 {
264 StringBuilder key = new StringBuilder( "metadata:" );
265 if ( !metadata.getGroupId().isEmpty() )
266 {
267 key.append( metadata.getGroupId() );
268 if ( !metadata.getArtifactId().isEmpty() )
269 {
270 key.append( ':' ).append( metadata.getArtifactId() );
271 if ( !metadata.getVersion().isEmpty() )
272 {
273 key.append( ':' ).append( metadata.getVersion() );
274 }
275 }
276 }
277 keys.add( key.toString() );
278 }
279 }
280
281 if ( keys.isEmpty() )
282 {
283 return;
284 }
285
286 String discriminator = createDiscriminator();
287 LOGGER.trace( "Using Redis key discriminator '{}' during this session", discriminator );
288
289 LOGGER.trace( "Need {} {} lock(s) for {}", keys.size(), shared ? "read" : "write", keys );
290 int acquiredLockCount = 0;
291 int reacquiredLockCount = 0;
292 for ( String key : keys )
293 {
294 RReadWriteLock rwLock = locks.get( key );
295 if ( rwLock == null )
296 {
297 rwLock = redissonClient
298 .getReadWriteLock( KEY_PREFIX + discriminator + ":" + key );
299 locks.put( key, rwLock );
300 acquiredLockCount++;
301 }
302 else
303 {
304 reacquiredLockCount++;
305 }
306
307 RLock actualLock = shared ? rwLock.readLock() : rwLock.writeLock();
308
309 if ( LOGGER.isTraceEnabled() )
310 {
311 LOGGER.trace( "Acquiring {} lock for '{}' (currently held: {}, already locked: {})",
312 shared ? "read" : "write", key, actualLock.getHoldCount(),
313 actualLock.isLocked() );
314 }
315
316 actualLock.lock();
317 }
318 LOGGER.trace( "Total new locks acquired: {}, total existing locks reacquired: {}",
319 acquiredLockCount, reacquiredLockCount );
320 }
321
322 private String createDiscriminator()
323 {
324 String discriminator = ConfigUtils.getString( session, null, CONFIG_PROP_DISCRIMINATOR );
325
326 if ( discriminator == null || discriminator.isEmpty() )
327 {
328
329 File basedir = session.getLocalRepository().getBasedir();
330 discriminator = hostname + ":" + basedir;
331 try
332 {
333 Map<String, Object> checksums = ChecksumUtils.calc(
334 discriminator.toString().getBytes( StandardCharsets.UTF_8 ),
335 Collections.singletonList( "SHA-1" ) );
336 Object checksum = checksums.get( "SHA-1" );
337
338 if ( checksum instanceof Exception )
339 {
340 throw (Exception) checksum;
341 }
342
343 return String.valueOf( checksum );
344 }
345 catch ( Exception e )
346 {
347
348 LOGGER.trace( "Failed to calculate discriminator digest, using '{}'",
349 DEFAULT_DISCRIMINATOR_DIGEST, e );
350 return DEFAULT_DISCRIMINATOR_DIGEST;
351 }
352 }
353
354 return discriminator;
355 }
356
357 public void close()
358 {
359 if ( locks.isEmpty() )
360 {
361 return;
362 }
363
364
365 Deque<String> keys = new LinkedList<>( locks.keySet() );
366 Iterator<String> keysIter = keys.descendingIterator();
367 while ( keysIter.hasNext() )
368 {
369 String key = keysIter.next();
370 RReadWriteLock rwLock = locks.get( key );
371 RLock actualLock = shared ? rwLock.readLock() : rwLock.writeLock();
372 while ( actualLock.getHoldCount() > 0 )
373 {
374
375 if ( LOGGER.isTraceEnabled() )
376 {
377 LOGGER.trace( "Releasing {} lock for '{}' (currently held: {})",
378 shared ? "read" : "write", key, actualLock.getHoldCount() );
379 }
380 actualLock.unlock();
381 }
382 }
383
384 LOGGER.trace( "Total locks released: {}", locks.size() );
385 locks.clear();
386 }
387
388 }
389
390 }