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