001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.eclipse.aether.named.support; 020 021import java.util.Collection; 022import java.util.Deque; 023import java.util.HashMap; 024import java.util.Map; 025import java.util.concurrent.ConcurrentHashMap; 026import java.util.concurrent.ConcurrentMap; 027import java.util.concurrent.atomic.AtomicBoolean; 028import java.util.concurrent.atomic.AtomicInteger; 029import java.util.function.Supplier; 030import java.util.stream.Collectors; 031 032import org.eclipse.aether.named.NamedLock; 033import org.eclipse.aether.named.NamedLockFactory; 034import org.eclipse.aether.named.NamedLockKey; 035import org.slf4j.Logger; 036import org.slf4j.LoggerFactory; 037 038import static java.util.Objects.requireNonNull; 039 040/** 041 * Support class for {@link NamedLockFactory} implementations providing reference counting. 042 */ 043public abstract class NamedLockFactorySupport implements NamedLockFactory { 044 /** 045 * System property key to enable locking diagnostic collection. 046 * 047 * @since 1.9.11 048 * @configurationSource {@link System#getProperty(String, String)} 049 * @configurationType {@link java.lang.Boolean} 050 * @configurationDefaultValue false 051 */ 052 public static final String SYSTEM_PROP_DIAGNOSTIC_ENABLED = "aether.named.diagnostic.enabled"; 053 054 private static final boolean DIAGNOSTIC_ENABLED = Boolean.getBoolean(SYSTEM_PROP_DIAGNOSTIC_ENABLED); 055 056 protected final Logger logger = LoggerFactory.getLogger(getClass()); 057 058 private final ConcurrentMap<NamedLockKey, NamedLockHolder> locks; 059 060 private final AtomicInteger compositeCounter; 061 062 private final boolean diagnosticEnabled; 063 064 private final AtomicBoolean shutdown = new AtomicBoolean(false); 065 066 public NamedLockFactorySupport() { 067 this(DIAGNOSTIC_ENABLED); 068 } 069 070 public NamedLockFactorySupport(boolean diagnosticEnabled) { 071 this.locks = new ConcurrentHashMap<>(); 072 this.compositeCounter = new AtomicInteger(0); 073 this.diagnosticEnabled = diagnosticEnabled; 074 } 075 076 /** 077 * Returns {@code true} if factory diagnostic collection is enabled. 078 * 079 * @since 1.9.11 080 */ 081 public boolean isDiagnosticEnabled() { 082 return diagnosticEnabled; 083 } 084 085 @Override 086 public final NamedLock getLock(final Collection<NamedLockKey> keys) { 087 requireNonNull(keys, "keys"); 088 if (shutdown.get()) { 089 throw new IllegalStateException("factory already shut down"); 090 } 091 if (keys.isEmpty()) { 092 throw new IllegalArgumentException("empty keys"); 093 } else { 094 return doGetLock(keys); 095 } 096 } 097 098 protected NamedLock doGetLock(final Collection<NamedLockKey> keys) { 099 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 ≤ 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}