AbstractRequestCache.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.maven.impl.cache;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;

import org.apache.maven.api.cache.BatchRequestException;
import org.apache.maven.api.cache.MavenExecutionException;
import org.apache.maven.api.cache.RequestCache;
import org.apache.maven.api.cache.RequestResult;
import org.apache.maven.api.services.Request;
import org.apache.maven.api.services.Result;

/**
 * Abstract implementation of the {@link RequestCache} interface, providing common caching mechanisms
 * for executing and caching request results in Maven.
 * <p>
 * This class implements caching strategies for individual and batch requests, ensuring that results
 * are stored and reused where appropriate to optimize performance.
 * </p>
 *
 * @since 4.0.0
 */
public abstract class AbstractRequestCache implements RequestCache {

    /**
     * Tracks which {@link CachingSupplier} instances are currently being batch-resolved
     * on the current thread. Used by {@link CachingSupplier#apply} to detect re-entrant
     * calls and avoid deadlock: if the current thread is already resolving a CachingSupplier,
     * {@code apply()} will invoke the fallback supplier instead of waiting for
     * {@link CachingSupplier#complete} (which would never arrive on the same thread).
     */
    private static final ThreadLocal<Set<CachingSupplier<?, ?>>> RESOLVING_ON_THREAD =
            ThreadLocal.withInitial(HashSet::new);

    /**
     * Checks whether the given CachingSupplier is currently being batch-resolved
     * on the calling thread.
     *
     * @param cs the CachingSupplier to check
     * @return {@code true} if a batch resolution for {@code cs} is in progress on this thread
     */
    static boolean isResolvingOnCurrentThread(CachingSupplier<?, ?> cs) {
        return RESOLVING_ON_THREAD.get().contains(cs);
    }

    /**
     * Executes and optionally caches a single request.
     * <p>
     * The caching behavior is determined by the specific implementation of {@link #doCache(Request, Function)}.
     * If caching is enabled, the result is retrieved from the cache or computed using the supplier function.
     * </p>
     *
     * @param <REQ> The request type
     * @param <REP> The response type
     * @param req The request object used as the cache key
     * @param supplier The function that provides the response if not cached
     * @return The cached or computed response
     */
    @Override
    @SuppressWarnings("all")
    public <REQ extends Request<?>, REP extends Result<REQ>> REP request(REQ req, Function<REQ, REP> supplier) {
        CachingSupplier<REQ, REP> cs = doCache(req, supplier);
        return cs.apply(req);
    }

    /**
     * Executes and optionally caches a batch of requests.
     * <p>
     * This method processes a list of requests, utilizing caching where applicable and executing
     * only the non-cached requests using the provided supplier function.
     * </p>
     * <p>
     * This implementation uses {@link CachingSupplier#complete} with {@code wait/notifyAll}
     * to coordinate batch results, and a {@link ThreadLocal} re-entrancy guard to prevent
     * deadlocks when batch resolution triggers nested calls (e.g., parent POM resolution
     * during artifact resolution).
     * </p>
     * <p>
     * <b>Concurrent calls</b> for the same request on different threads: the first thread
     * performs the resolution; the second thread's {@link CachingSupplier#apply} blocks
     * (via {@code Object.wait()}) until {@code complete()} is called, avoiding duplicate work.
     * </p>
     * <p>
     * <b>Re-entrant calls</b> on the same thread: detected via {@link #RESOLVING_ON_THREAD}.
     * {@link CachingSupplier#apply} skips the wait and invokes the fallback supplier directly
     * so the inner call can complete without waiting for the outer call's batch result.
     * </p>
     *
     * @param <REQ> The request type
     * @param <REP> The response type
     * @param reqs List of requests to process
     * @param supplier Function to execute the batch of requests
     * @return List of results corresponding to the input requests
     * @throws BatchRequestException if any request in the batch fails
     */
    @Override
    @SuppressWarnings("unchecked")
    public <REQ extends Request<?>, REP extends Result<REQ>> List<REP> requests(
            List<REQ> reqs, Function<List<REQ>, List<REP>> supplier) {
        // Create a fallback supplier that can resolve individual requests independently.
        // This is stored in newly created CachingSupplier instances and used only when
        // a re-entrant call on the same thread needs to resolve a request whose batch
        // resolution is still in progress higher up the call stack.
        Function<REQ, REP> singleSupplier = req -> supplier.apply(List.of(req)).get(0);

        List<CachingSupplier<REQ, REP>> suppliers = new ArrayList<>(reqs.size());
        List<REQ> nonCached = new ArrayList<>();
        for (REQ req : reqs) {
            CachingSupplier<REQ, REP> cs = doCache(req, singleSupplier);
            suppliers.add(cs);
            if (cs.getValue() == null) {
                nonCached.add(req);
            }
        }

        // Resolve non-cached requests in batch and directly set results on CachingSuppliers
        if (!nonCached.isEmpty()) {
            // Use IdentityHashMap: request objects may have unstable hashCode() due to
            // mutable RequestTrace/ModelBuilderRequest data that changes during batch resolution.
            // Since we always use the same object references for put and get, identity is safe.
            Map<REQ, Integer> reqToIndex = new IdentityHashMap<>();
            List<CachingSupplier<REQ, REP>> nonCachedSuppliers = new ArrayList<>(nonCached.size());
            for (int i = 0; i < reqs.size(); i++) {
                if (suppliers.get(i).getValue() == null) {
                    reqToIndex.put(reqs.get(i), i);
                    nonCachedSuppliers.add(suppliers.get(i));
                }
            }

            // Mark these CachingSuppliers as being batch-resolved, and register them
            // on this thread so that re-entrant apply() calls skip the wait.
            Set<CachingSupplier<?, ?>> resolving = RESOLVING_ON_THREAD.get();
            for (CachingSupplier<REQ, REP> cs : nonCachedSuppliers) {
                cs.setBatchResolving(true);
                resolving.add(cs);
            }
            try {
                try {
                    List<REP> reps = supplier.apply(nonCached);
                    for (int i = 0; i < reps.size(); i++) {
                        Integer idx = reqToIndex.get(nonCached.get(i));
                        if (idx != null) {
                            suppliers.get(idx).complete(reps.get(i));
                        }
                    }
                } catch (MavenExecutionException e) {
                    // If batch request fails, mark all non-cached requests as failed
                    CachingSupplier.AltRes failure = new CachingSupplier.AltRes(e.getCause());
                    for (REQ req : nonCached) {
                        Integer idx = reqToIndex.get(req);
                        if (idx != null) {
                            suppliers.get(idx).complete(failure);
                        }
                    }
                } catch (Throwable e) {
                    // Ensure waiting concurrent threads are unblocked on unexpected errors.
                    // We mark all non-cached requests as failed and fall through to the
                    // collection loop, which produces a consistent BatchRequestException
                    // with per-request RequestResult details ��� same as MavenExecutionException.
                    CachingSupplier.AltRes failure = new CachingSupplier.AltRes(e);
                    for (REQ req : nonCached) {
                        Integer idx = reqToIndex.get(req);
                        if (idx != null) {
                            suppliers.get(idx).complete(failure);
                        }
                    }
                }
            } finally {
                for (CachingSupplier<REQ, REP> cs : nonCachedSuppliers) {
                    cs.setBatchResolving(false);
                    resolving.remove(cs);
                }
            }
        }

        // Collect results in original order
        List<RequestResult<REQ, REP>> allResults = new ArrayList<>(reqs.size());
        boolean hasFailures = false;
        for (int i = 0; i < reqs.size(); i++) {
            REQ req = reqs.get(i);
            CachingSupplier<REQ, REP> cs = suppliers.get(i);
            try {
                REP value = cs.apply(req);
                allResults.add(new RequestResult<>(req, value, null));
            } catch (Throwable t) {
                hasFailures = true;
                allResults.add(new RequestResult<>(req, null, t));
            }
        }

        if (hasFailures) {
            BatchRequestException exception = new BatchRequestException("One or more requests failed", allResults);
            // Add all individual exceptions as suppressed exceptions to preserve stack traces
            for (RequestResult<REQ, REP> result : allResults) {
                if (result.error() != null) {
                    exception.addSuppressed(result.error());
                }
            }
            throw exception;
        }

        return allResults.stream().map(RequestResult::result).toList();
    }

    /**
     * Abstract method to be implemented by subclasses to handle caching logic.
     * <p>
     * This method is responsible for determining whether a request result should be cached,
     * retrieving it from cache if available, or executing the supplier function if necessary.
     * </p>
     *
     * @param <REQ> The request type
     * @param <REP> The response type
     * @param req The request object
     * @param supplier The function that provides the response
     * @return A caching supplier that handles caching logic for the request
     */
    protected abstract <REQ extends Request<?>, REP extends Result<REQ>> CachingSupplier<REQ, REP> doCache(
            REQ req, Function<REQ, REP> supplier);

    @SuppressWarnings("unchecked")
    protected static <T extends Throwable> void uncheckedThrow(Throwable t) throws T {
        throw (T) t; // rely on vacuous cast
    }
}