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