View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.named.support;
20  
21  import java.util.Collection;
22  import java.util.Deque;
23  import java.util.HashMap;
24  import java.util.Map;
25  import java.util.concurrent.ConcurrentHashMap;
26  import java.util.concurrent.ConcurrentMap;
27  import java.util.concurrent.atomic.AtomicBoolean;
28  import java.util.concurrent.atomic.AtomicInteger;
29  import java.util.function.Supplier;
30  import java.util.stream.Collectors;
31  
32  import org.eclipse.aether.named.NamedLock;
33  import org.eclipse.aether.named.NamedLockFactory;
34  import org.eclipse.aether.named.NamedLockKey;
35  import org.slf4j.Logger;
36  import org.slf4j.LoggerFactory;
37  
38  import static java.util.Objects.requireNonNull;
39  
40  /**
41   * Support class for {@link NamedLockFactory} implementations providing reference counting.
42   */
43  public abstract class NamedLockFactorySupport implements NamedLockFactory {
44      /**
45       * System property key to enable locking diagnostic collection.
46       *
47       * @since 1.9.11
48       * @configurationSource {@link System#getProperty(String, String)}
49       * @configurationType {@link java.lang.Boolean}
50       * @configurationDefaultValue false
51       */
52      public static final String SYSTEM_PROP_DIAGNOSTIC_ENABLED = "aether.named.diagnostic.enabled";
53  
54      private static final boolean DIAGNOSTIC_ENABLED = Boolean.getBoolean(SYSTEM_PROP_DIAGNOSTIC_ENABLED);
55  
56      protected final Logger logger = LoggerFactory.getLogger(getClass());
57  
58      private final ConcurrentMap<NamedLockKey, NamedLockHolder> locks;
59  
60      private final AtomicInteger compositeCounter;
61  
62      private final boolean diagnosticEnabled;
63  
64      private final AtomicBoolean shutdown = new AtomicBoolean(false);
65  
66      public NamedLockFactorySupport() {
67          this(DIAGNOSTIC_ENABLED);
68      }
69  
70      public NamedLockFactorySupport(boolean diagnosticEnabled) {
71          this.locks = new ConcurrentHashMap<>();
72          this.compositeCounter = new AtomicInteger(0);
73          this.diagnosticEnabled = diagnosticEnabled;
74      }
75  
76      /**
77       * Returns {@code true} if factory diagnostic collection is enabled.
78       *
79       * @since 1.9.11
80       */
81      public boolean isDiagnosticEnabled() {
82          return diagnosticEnabled;
83      }
84  
85      @Override
86      public final NamedLock getLock(final Collection<NamedLockKey> keys) {
87          requireNonNull(keys, "keys");
88          if (shutdown.get()) {
89              throw new IllegalStateException("factory already shut down");
90          }
91          if (keys.isEmpty()) {
92              throw new IllegalArgumentException("empty keys");
93          } else {
94              return doGetLock(keys);
95          }
96      }
97  
98      protected NamedLock doGetLock(final Collection<NamedLockKey> keys) {
99          if (keys.size() == 1) {
100             NamedLockKey key = keys.iterator().next();
101             return getLockAndRefTrack(key, () -> createLock(key));
102         } else {
103             return new CompositeNamedLock(
104                     NamedLockKey.of(
105                             "composite-" + compositeCounter.incrementAndGet(),
106                             keys.stream()
107                                     .map(NamedLockKey::resources)
108                                     .flatMap(Collection::stream)
109                                     .collect(Collectors.toList())),
110                     this,
111                     keys.stream()
112                             .map(k -> getLockAndRefTrack(k, () -> createLock(k)))
113                             .collect(Collectors.toList()));
114         }
115     }
116 
117     protected NamedLock getLockAndRefTrack(final NamedLockKey key, Supplier<NamedLockSupport> supplier) {
118         if (shutdown.get()) {
119             throw new IllegalStateException("factory already shut down");
120         }
121         // Fast path: lock-free volatile read + atomic CAS increment.
122         // ConcurrentHashMap.get() is a volatile read — no bucket locking.
123         // In the common case (lock already exists), this avoids compute()'s
124         // per-bucket exclusive lock entirely.
125         NamedLockHolder holder = locks.get(key);
126         if (holder != null && holder.tryIncRef()) {
127             return holder.namedLock;
128         }
129         // Slow path: holder absent or being closed (refcount hit 0).
130         // Use compute() to atomically create a new holder.
131         return locks.compute(key, (k, v) -> {
132                     if (shutdown.get()) {
133                         throw new IllegalStateException("factory already shut down");
134                     }
135                     if (v == null || !v.tryIncRef()) {
136                         v = new NamedLockHolder(supplier.get());
137                         v.incRef();
138                     }
139                     return v;
140                 })
141                 .namedLock;
142     }
143 
144     @Override
145     public void shutdown() {
146         if (shutdown.compareAndSet(false, true)) {
147             doShutdown();
148         }
149     }
150 
151     protected void doShutdown() {
152         // override if needed
153     }
154 
155     @Override
156     public <E extends Throwable> E onFailure(E failure) {
157         if (isDiagnosticEnabled()) {
158             Map<NamedLockKey, NamedLockHolder> locks = new HashMap<>(this.locks); // copy
159             int activeLocks = locks.size();
160             logger.info("Diagnostic dump of lock factory");
161             logger.info("===============================");
162             logger.info("Implementation: {}", getClass().getName());
163             logger.info("Active locks: {}", activeLocks);
164             logger.info("");
165             if (activeLocks > 0) {
166                 for (Map.Entry<NamedLockKey, NamedLockHolder> entry : locks.entrySet()) {
167                     NamedLockKey key = entry.getKey();
168                     int refCount = entry.getValue().referenceCount.get();
169                     NamedLockSupport lock = entry.getValue().namedLock;
170                     logger.info("Name: {}", key.name());
171                     logger.info("RefCount: {}", refCount);
172                     logger.info("Resources:");
173                     key.resources().forEach(r -> logger.info(" - {}", r));
174                     Map<Thread, Deque<String>> diag = lock.diagnosticState();
175                     logger.info("State:");
176                     diag.forEach((k, v) -> logger.info("  {} -> {}", k, v));
177                 }
178                 logger.info("");
179             }
180         }
181         return failure;
182     }
183 
184     public void closeLock(final NamedLockKey key) {
185         locks.compute(key, (k, v) -> {
186             if (v != null && v.decRef() == 0) {
187                 // Mark as closed to prevent a concurrent tryIncRef (lock-free fast path)
188                 // from reviving this holder. CAS ensures atomicity: if tryIncRef already
189                 // incremented from 0→1, our CAS fails and we keep the holder alive.
190                 if (v.referenceCount.compareAndSet(0, Integer.MIN_VALUE)) {
191                     destroyLock(v.namedLock);
192                     return null;
193                 }
194                 // A concurrent tryIncRef succeeded — holder is still in use, keep it
195             }
196             return v;
197         });
198     }
199 
200     /**
201      * Implementations shall create and return {@link NamedLockSupport} for given {@code name}, this method must never
202      * return {@code null}.
203      */
204     protected abstract NamedLockSupport createLock(NamedLockKey key);
205 
206     /**
207      * Implementation may override this (empty) method to perform some sort of implementation specific cleanup for
208      * given lock name. Invoked when reference count for given name drops to zero and named lock was removed.
209      */
210     protected void destroyLock(final NamedLock namedLock) {
211         // override if needed
212     }
213 
214     private static final class NamedLockHolder {
215         private final NamedLockSupport namedLock;
216 
217         private final AtomicInteger referenceCount;
218 
219         private NamedLockHolder(final NamedLockSupport namedLock) {
220             this.namedLock = requireNonNull(namedLock);
221             this.referenceCount = new AtomicInteger(0);
222         }
223 
224         private NamedLockHolder incRef() {
225             referenceCount.incrementAndGet();
226             return this;
227         }
228 
229         /**
230          * Atomically tries to increment the reference count. Returns {@code false} if the
231          * holder has been closed (refcount &le; 0), preventing revival of a destroyed lock.
232          * Used by the lock-free fast path in {@link #getLockAndRefTrack}.
233          */
234         private boolean tryIncRef() {
235             while (true) {
236                 int current = referenceCount.get();
237                 if (current <= 0) {
238                     return false;
239                 }
240                 if (referenceCount.compareAndSet(current, current + 1)) {
241                     return true;
242                 }
243             }
244         }
245 
246         private int decRef() {
247             return referenceCount.decrementAndGet();
248         }
249 
250         @Override
251         public String toString() {
252             return "[refCount=" + referenceCount.get() + ", lock=" + namedLock + "]";
253         }
254     }
255 }