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 &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}