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.internal.impl.synccontext.named;
020
021import java.util.ArrayDeque;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.Deque;
025import java.util.concurrent.TimeUnit;
026import java.util.stream.Collectors;
027
028import org.eclipse.aether.RepositorySystemSession;
029import org.eclipse.aether.SyncContext;
030import org.eclipse.aether.artifact.Artifact;
031import org.eclipse.aether.metadata.Metadata;
032import org.eclipse.aether.named.NamedLock;
033import org.eclipse.aether.named.NamedLockFactory;
034import org.eclipse.aether.named.providers.FileLockNamedLockFactory;
035import org.eclipse.aether.util.ConfigUtils;
036import org.eclipse.aether.util.artifact.ArtifactIdUtils;
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039
040import static java.util.Objects.requireNonNull;
041
042/**
043 * Adapter to adapt {@link NamedLockFactory} and {@link NamedLock} to {@link SyncContext}.
044 */
045public final class NamedLockFactoryAdapter {
046    public static final String TIME_KEY = "aether.syncContext.named.time";
047
048    public static final long DEFAULT_TIME = 900L;
049
050    public static final String TIME_UNIT_KEY = "aether.syncContext.named.time.unit";
051
052    public static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.SECONDS;
053
054    public static final String RETRY_KEY = "aether.syncContext.named.retry";
055
056    public static final int DEFAULT_RETRY = 1;
057
058    public static final String RETRY_WAIT_KEY = "aether.syncContext.named.retry.wait";
059
060    public static final long DEFAULT_RETRY_WAIT = 200L;
061
062    private final NameMapper nameMapper;
063
064    private final NamedLockFactory namedLockFactory;
065
066    public NamedLockFactoryAdapter(final NameMapper nameMapper, final NamedLockFactory namedLockFactory) {
067        this.nameMapper = requireNonNull(nameMapper);
068        this.namedLockFactory = requireNonNull(namedLockFactory);
069        // TODO: this is ad-hoc "validation", experimental and likely to change
070        if (this.namedLockFactory instanceof FileLockNamedLockFactory && !this.nameMapper.isFileSystemFriendly()) {
071            throw new IllegalArgumentException(
072                    "Misconfiguration: FileLockNamedLockFactory lock factory requires FS friendly NameMapper");
073        }
074    }
075
076    public SyncContext newInstance(final RepositorySystemSession session, final boolean shared) {
077        return new AdaptedLockSyncContext(session, shared, nameMapper, namedLockFactory);
078    }
079
080    /**
081     * @since 1.9.1
082     */
083    public NameMapper getNameMapper() {
084        return nameMapper;
085    }
086
087    /**
088     * @since 1.9.1
089     */
090    public NamedLockFactory getNamedLockFactory() {
091        return namedLockFactory;
092    }
093
094    public String toString() {
095        return getClass().getSimpleName()
096                + "(nameMapper=" + nameMapper
097                + ", namedLockFactory=" + namedLockFactory
098                + ")";
099    }
100
101    private static class AdaptedLockSyncContext implements SyncContext {
102        private static final Logger LOGGER = LoggerFactory.getLogger(AdaptedLockSyncContext.class);
103
104        private final RepositorySystemSession session;
105
106        private final boolean shared;
107
108        private final NameMapper lockNaming;
109
110        private final NamedLockFactory namedLockFactory;
111
112        private final long time;
113
114        private final TimeUnit timeUnit;
115
116        private final int retry;
117
118        private final long retryWait;
119
120        private final Deque<NamedLock> locks;
121
122        private AdaptedLockSyncContext(
123                final RepositorySystemSession session,
124                final boolean shared,
125                final NameMapper lockNaming,
126                final NamedLockFactory namedLockFactory) {
127            this.session = session;
128            this.shared = shared;
129            this.lockNaming = lockNaming;
130            this.namedLockFactory = namedLockFactory;
131            this.time = getTime(session);
132            this.timeUnit = getTimeUnit(session);
133            this.retry = getRetry(session);
134            this.retryWait = getRetryWait(session);
135            this.locks = new ArrayDeque<>();
136
137            if (time < 0L) {
138                throw new IllegalArgumentException(TIME_KEY + " value cannot be negative");
139            }
140            if (retry < 0L) {
141                throw new IllegalArgumentException(RETRY_KEY + " value cannot be negative");
142            }
143            if (retryWait < 0L) {
144                throw new IllegalArgumentException(RETRY_WAIT_KEY + " value cannot be negative");
145            }
146        }
147
148        private long getTime(final RepositorySystemSession session) {
149            return ConfigUtils.getLong(session, DEFAULT_TIME, TIME_KEY);
150        }
151
152        private TimeUnit getTimeUnit(final RepositorySystemSession session) {
153            return TimeUnit.valueOf(ConfigUtils.getString(session, DEFAULT_TIME_UNIT.name(), TIME_UNIT_KEY));
154        }
155
156        private int getRetry(final RepositorySystemSession session) {
157            return ConfigUtils.getInteger(session, DEFAULT_RETRY, RETRY_KEY);
158        }
159
160        private long getRetryWait(final RepositorySystemSession session) {
161            return ConfigUtils.getLong(session, DEFAULT_RETRY_WAIT, RETRY_WAIT_KEY);
162        }
163
164        @Override
165        public void acquire(Collection<? extends Artifact> artifacts, Collection<? extends Metadata> metadatas) {
166            Collection<String> keys = lockNaming.nameLocks(session, artifacts, metadatas);
167            if (keys.isEmpty()) {
168                return;
169            }
170
171            final int attempts = retry + 1;
172            final ArrayList<IllegalStateException> illegalStateExceptions = new ArrayList<>();
173            for (int attempt = 1; attempt <= attempts; attempt++) {
174                LOGGER.trace(
175                        "Attempt {}: Need {} {} lock(s) for {}", attempt, keys.size(), shared ? "read" : "write", keys);
176                int acquiredLockCount = 0;
177                try {
178                    if (attempt > 1) {
179                        Thread.sleep(retryWait);
180                    }
181                    for (String key : keys) {
182                        NamedLock namedLock = namedLockFactory.getLock(key);
183                        LOGGER.trace("Acquiring {} lock for '{}'", shared ? "read" : "write", key);
184
185                        boolean locked;
186                        if (shared) {
187                            locked = namedLock.lockShared(time, timeUnit);
188                        } else {
189                            locked = namedLock.lockExclusively(time, timeUnit);
190                        }
191
192                        if (!locked) {
193                            String timeStr = time + " " + timeUnit;
194                            LOGGER.trace(
195                                    "Failed to acquire {} lock for '{}' in {}",
196                                    shared ? "read" : "write",
197                                    key,
198                                    timeStr);
199
200                            namedLock.close();
201                            closeAll();
202                            illegalStateExceptions.add(new IllegalStateException(
203                                    "Attempt " + attempt + ": Could not acquire " + (shared ? "read" : "write")
204                                            + " lock for '" + namedLock.name() + "' in " + timeStr));
205                            break;
206                        } else {
207                            locks.push(namedLock);
208                            acquiredLockCount++;
209                        }
210                    }
211                } catch (InterruptedException e) {
212                    Thread.currentThread().interrupt();
213                    throw new RuntimeException(e);
214                }
215                LOGGER.trace("Attempt {}: Total locks acquired: {}", attempt, acquiredLockCount);
216                if (acquiredLockCount == keys.size()) {
217                    break;
218                }
219            }
220            if (!illegalStateExceptions.isEmpty()) {
221                String message = "Could not acquire " + (shared ? "shared" : "exclusive") + " lock for "
222                        + lockSubjects(artifacts, metadatas) + " in " + time + " " + timeUnit
223                        + "; consider using '" + TIME_KEY
224                        + "' property to increase lock timeout to a value that fits your environment";
225                FailedToAcquireLockException ex = new FailedToAcquireLockException(shared, message);
226                illegalStateExceptions.forEach(ex::addSuppressed);
227                throw namedLockFactory.onFailure(ex);
228            }
229        }
230
231        private String lockSubjects(
232                Collection<? extends Artifact> artifacts, Collection<? extends Metadata> metadatas) {
233            StringBuilder builder = new StringBuilder();
234            if (artifacts != null && !artifacts.isEmpty()) {
235                builder.append("artifacts: ")
236                        .append(artifacts.stream().map(ArtifactIdUtils::toId).collect(Collectors.joining(", ")));
237            }
238            if (metadatas != null && !metadatas.isEmpty()) {
239                if (builder.length() != 0) {
240                    builder.append("; ");
241                }
242                builder.append("metadata: ")
243                        .append(metadatas.stream().map(this::metadataSubjects).collect(Collectors.joining(", ")));
244            }
245            return builder.toString();
246        }
247
248        private String metadataSubjects(Metadata metadata) {
249            String name = "";
250            if (!metadata.getGroupId().isEmpty()) {
251                name += metadata.getGroupId();
252                if (!metadata.getArtifactId().isEmpty()) {
253                    name += ":" + metadata.getArtifactId();
254                    if (!metadata.getVersion().isEmpty()) {
255                        name += ":" + metadata.getVersion();
256                    }
257                }
258            }
259            if (!metadata.getType().isEmpty()) {
260                name += (name.isEmpty() ? "" : ":") + metadata.getType();
261            }
262            return name;
263        }
264
265        private void closeAll() {
266            if (locks.isEmpty()) {
267                return;
268            }
269
270            // Release locks in reverse insertion order
271            int released = 0;
272            while (!locks.isEmpty()) {
273                try (NamedLock namedLock = locks.pop()) {
274                    LOGGER.trace("Releasing {} lock for '{}'", shared ? "read" : "write", namedLock.name());
275                    namedLock.unlock();
276                    released++;
277                }
278            }
279            LOGGER.trace("Total locks released: {}", released);
280        }
281
282        @Override
283        public void close() {
284            closeAll();
285        }
286    }
287}