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.Collection;
023import java.util.Deque;
024import java.util.concurrent.TimeUnit;
025import java.util.stream.Collectors;
026
027import org.eclipse.aether.ConfigurationProperties;
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.NamedLockKey;
035import org.eclipse.aether.named.providers.FileLockNamedLockFactory;
036import org.eclipse.aether.util.ConfigUtils;
037import org.eclipse.aether.util.artifact.ArtifactIdUtils;
038import org.slf4j.Logger;
039import org.slf4j.LoggerFactory;
040
041import static java.util.Objects.requireNonNull;
042
043/**
044 * Adapter to adapt {@link NamedLockFactory} and {@link NamedLock} to {@link SyncContext}.
045 */
046public final class NamedLockFactoryAdapter {
047    public static final String CONFIG_PROPS_PREFIX = ConfigurationProperties.PREFIX_SYNC_CONTEXT + "named.";
048
049    /**
050     * The maximum of time amount to be blocked to obtain lock.
051     *
052     * @since 1.7.0
053     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
054     * @configurationType {@link java.lang.Long}
055     * @configurationDefaultValue {@link #DEFAULT_TIME}
056     */
057    public static final String CONFIG_PROP_TIME = CONFIG_PROPS_PREFIX + "time";
058
059    public static final long DEFAULT_TIME = 900L;
060
061    /**
062     * The unit of maximum time amount to be blocked to obtain lock. Use TimeUnit enum names.
063     *
064     * @since 1.7.0
065     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
066     * @configurationType {@link java.lang.String}
067     * @configurationDefaultValue {@link #DEFAULT_TIME_UNIT}
068     */
069    public static final String CONFIG_PROP_TIME_UNIT = CONFIG_PROPS_PREFIX + "time.unit";
070
071    public static final String DEFAULT_TIME_UNIT = "SECONDS";
072
073    /**
074     * The amount of retries on time-out.
075     *
076     * @since 1.7.0
077     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
078     * @configurationType {@link java.lang.Integer}
079     * @configurationDefaultValue {@link #DEFAULT_RETRY}
080     */
081    public static final String CONFIG_PROP_RETRY = CONFIG_PROPS_PREFIX + "retry";
082
083    public static final int DEFAULT_RETRY = 1;
084
085    /**
086     * The amount of milliseconds to wait between retries on time-out.
087     *
088     * @since 1.7.0
089     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
090     * @configurationType {@link java.lang.Long}
091     * @configurationDefaultValue {@link #DEFAULT_RETRY_WAIT}
092     */
093    public static final String CONFIG_PROP_RETRY_WAIT = CONFIG_PROPS_PREFIX + "retry.wait";
094
095    public static final long DEFAULT_RETRY_WAIT = 200L;
096
097    private final NameMapper nameMapper;
098
099    private final NamedLockFactory namedLockFactory;
100
101    public NamedLockFactoryAdapter(final NameMapper nameMapper, final NamedLockFactory namedLockFactory) {
102        this.nameMapper = requireNonNull(nameMapper);
103        this.namedLockFactory = requireNonNull(namedLockFactory);
104        // TODO: this is ad-hoc "validation", experimental and likely to change
105        if (this.namedLockFactory instanceof FileLockNamedLockFactory && !this.nameMapper.isFileSystemFriendly()) {
106            throw new IllegalArgumentException(
107                    "Misconfiguration: FileLockNamedLockFactory lock factory requires FS friendly NameMapper");
108        }
109    }
110
111    public SyncContext newInstance(final RepositorySystemSession session, final boolean shared) {
112        return new AdaptedLockSyncContext(session, shared, nameMapper, namedLockFactory);
113    }
114
115    /**
116     * @since 1.9.1
117     */
118    public NameMapper getNameMapper() {
119        return nameMapper;
120    }
121
122    /**
123     * @since 1.9.1
124     */
125    public NamedLockFactory getNamedLockFactory() {
126        return namedLockFactory;
127    }
128
129    public String toString() {
130        return getClass().getSimpleName()
131                + "(nameMapper=" + nameMapper
132                + ", namedLockFactory=" + namedLockFactory
133                + ")";
134    }
135
136    private static class AdaptedLockSyncContext implements SyncContext {
137        private static final Logger LOGGER = LoggerFactory.getLogger(AdaptedLockSyncContext.class);
138
139        private final RepositorySystemSession session;
140
141        private final boolean shared;
142
143        private final NameMapper lockNaming;
144
145        private final NamedLockFactory namedLockFactory;
146
147        private final long time;
148
149        private final TimeUnit timeUnit;
150
151        private final int retry;
152
153        private final long retryWait;
154
155        private final Deque<NamedLock> locks;
156
157        private AdaptedLockSyncContext(
158                final RepositorySystemSession session,
159                final boolean shared,
160                final NameMapper lockNaming,
161                final NamedLockFactory namedLockFactory) {
162            this.session = session;
163            this.shared = shared;
164            this.lockNaming = lockNaming;
165            this.namedLockFactory = namedLockFactory;
166            this.time = getTime(session, DEFAULT_TIME, CONFIG_PROP_TIME);
167            this.timeUnit = getTimeUnit(session);
168            this.retry = getRetry(session);
169            this.retryWait = getRetryWait(session);
170            this.locks = new ArrayDeque<>();
171
172            if (time < 0L) {
173                throw new IllegalArgumentException(CONFIG_PROP_TIME + " value cannot be negative");
174            }
175            if (retry < 0L) {
176                throw new IllegalArgumentException(CONFIG_PROP_RETRY + " value cannot be negative");
177            }
178            if (retryWait < 0L) {
179                throw new IllegalArgumentException(CONFIG_PROP_RETRY_WAIT + " value cannot be negative");
180            }
181        }
182
183        private long getTime(final RepositorySystemSession session, long defaultValue, String... keys) {
184            return ConfigUtils.getLong(session, defaultValue, keys);
185        }
186
187        private TimeUnit getTimeUnit(final RepositorySystemSession session) {
188            return TimeUnit.valueOf(ConfigUtils.getString(session, DEFAULT_TIME_UNIT, CONFIG_PROP_TIME_UNIT));
189        }
190
191        private int getRetry(final RepositorySystemSession session) {
192            return ConfigUtils.getInteger(session, DEFAULT_RETRY, CONFIG_PROP_RETRY);
193        }
194
195        private long getRetryWait(final RepositorySystemSession session) {
196            return ConfigUtils.getLong(session, DEFAULT_RETRY_WAIT, CONFIG_PROP_RETRY_WAIT);
197        }
198
199        @Override
200        public void acquire(Collection<? extends Artifact> artifacts, Collection<? extends Metadata> metadatas) {
201            Collection<NamedLockKey> keys = lockNaming.nameLocks(session, artifacts, metadatas);
202            if (keys.isEmpty()) {
203                return;
204            }
205
206            final String timeStr = time + " " + timeUnit;
207            final String lockKind = shared ? "shared" : "exclusive";
208            final NamedLock namedLock = namedLockFactory.getLock(keys);
209            if (LOGGER.isTraceEnabled()) {
210                LOGGER.trace(
211                        "Need {} lock for {} from {}",
212                        lockKind,
213                        namedLock.key().resources(),
214                        namedLock.key().name());
215            }
216
217            final int attempts = retry + 1;
218            for (int attempt = 1; attempt <= attempts; attempt++) {
219                if (LOGGER.isTraceEnabled()) {
220                    LOGGER.trace(
221                            "Attempt {}: Acquire {} lock from {}",
222                            attempt,
223                            lockKind,
224                            namedLock.key().name());
225                }
226                try {
227                    if (attempt > 1) {
228                        Thread.sleep(retryWait);
229                    }
230                    boolean locked;
231                    if (shared) {
232                        locked = namedLock.lockShared(time, timeUnit);
233                    } else {
234                        locked = namedLock.lockExclusively(time, timeUnit);
235                    }
236
237                    if (locked) {
238                        // we are done, get out
239                        locks.push(namedLock);
240                        return;
241                    }
242
243                    // we failed; retry
244                    if (LOGGER.isTraceEnabled()) {
245                        LOGGER.trace(
246                                "Failed to acquire {} lock for '{}' in {}",
247                                lockKind,
248                                namedLock.key().name(),
249                                timeStr);
250                    }
251                } catch (InterruptedException e) {
252                    // if we are here, means we were interrupted: fail
253                    close();
254                    Thread.currentThread().interrupt();
255                    throw new RuntimeException(e);
256                }
257            }
258            // if we are here, means all attempts were unsuccessful: fail
259            close();
260            String message = "Could not acquire " + lockKind + " lock for "
261                    + lockSubjects(artifacts, metadatas) + " in " + timeStr
262                    + "; consider using '" + CONFIG_PROP_TIME
263                    + "' property to increase lock timeout to a value that fits your environment";
264            FailedToAcquireLockException ex = new FailedToAcquireLockException(shared, message);
265            throw namedLockFactory.onFailure(ex);
266        }
267
268        private String lockSubjects(
269                Collection<? extends Artifact> artifacts, Collection<? extends Metadata> metadatas) {
270            StringBuilder builder = new StringBuilder();
271            if (artifacts != null && !artifacts.isEmpty()) {
272                builder.append("artifacts: ")
273                        .append(artifacts.stream().map(ArtifactIdUtils::toId).collect(Collectors.joining(", ")));
274            }
275            if (metadatas != null && !metadatas.isEmpty()) {
276                if (builder.length() != 0) {
277                    builder.append("; ");
278                }
279                builder.append("metadata: ")
280                        .append(metadatas.stream().map(this::metadataSubjects).collect(Collectors.joining(", ")));
281            }
282            return builder.toString();
283        }
284
285        private String metadataSubjects(Metadata metadata) {
286            String name = "";
287            if (!metadata.getGroupId().isEmpty()) {
288                name += metadata.getGroupId();
289                if (!metadata.getArtifactId().isEmpty()) {
290                    name += ":" + metadata.getArtifactId();
291                    if (!metadata.getVersion().isEmpty()) {
292                        name += ":" + metadata.getVersion();
293                    }
294                }
295            }
296            if (!metadata.getType().isEmpty()) {
297                name += (name.isEmpty() ? "" : ":") + metadata.getType();
298            }
299            return name;
300        }
301
302        @Override
303        public void close() {
304            while (!locks.isEmpty()) {
305                try (NamedLock namedLock = locks.pop()) {
306                    namedLock.unlock();
307                    if (LOGGER.isTraceEnabled()) {
308                        LOGGER.trace(
309                                "Unlocked and closed {} lock of {}", shared ? "shared" : "exclusive", namedLock.key());
310                    }
311                }
312            }
313        }
314    }
315}