/* * The MIT License * * Copyright (c) 2004-2009, Sun Microsystems, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package hudson.slaves; import hudson.FilePath; import hudson.Functions; import jenkins.util.SystemProperties; import hudson.model.Computer; import hudson.model.DirectoryBrowserSupport; import java.io.Closeable; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nonnull; /** * Used by {@link Computer} to keep track of workspaces that are actively in use. * * @author Kohsuke Kawaguchi * @since 1.319 * @see Computer#getWorkspaceList() */ public final class WorkspaceList { private static final class AllocationAt extends Exception { @Override public String toString() { return "Allocation Point"; } } /** * Book keeping for workspace allocation. */ public static final class Entry { /** * Who acquired this workspace? */ public final Thread holder = Thread.currentThread(); /** * When? */ public final long time = System.currentTimeMillis(); /** * From where? */ public final Exception source = new AllocationAt(); /** * True makes the caller of {@link WorkspaceList#allocate(FilePath)} wait * for this workspace. */ public final boolean quick; public final @Nonnull FilePath path; /** * Multiple threads can acquire the same lock if they share the same context object. */ public final Object context; public int lockCount=1; private Entry(@Nonnull FilePath path, boolean quick) { this(path,quick,new Object()); // unique context } private Entry(@Nonnull FilePath path, boolean quick, Object context) { this.path = path; this.quick = quick; this.context = context; } @Override public String toString() { String s = path+" owned by "+holder.getName()+" from "+new Date(time); if(quick) s+=" (quick)"; s+="\n"+Functions.printThrowable(source); return s; } } /** * Represents a leased workspace that needs to be returned later. */ public static abstract class Lease implements /*Auto*/Closeable { public final @Nonnull FilePath path; protected Lease(@Nonnull FilePath path) { path.getRemote(); // null check this.path = path; } /** * Releases this lease. */ public abstract void release(); /** * By default, calls {@link #release}, but should be idempotent. * @since 1.600 */ @Override public void close() { release(); } /** * Creates a dummy {@link Lease} object that does no-op in the release. */ public static Lease createDummyLease(@Nonnull FilePath p) { return new Lease(p) { public void release() { // noop } }; } /** * Creates a {@link Lease} object that points to the specified path, but the lock * is controlled by the given parent lease object. */ public static Lease createLinkedDummyLease(@Nonnull FilePath p, final Lease parent) { return new Lease(p) { public void release() { parent.release(); } }; } } private final Map<FilePath,Entry> inUse = new HashMap<FilePath,Entry>(); public WorkspaceList() { } /** * Allocates a workspace by adding some variation to the given base to make it unique. * * <p> * This method doesn't block prolonged amount of time. Whenever a desired workspace * is in use, the unique variation is added. */ public synchronized Lease allocate(@Nonnull FilePath base) throws InterruptedException { return allocate(base,new Object()); } /** * See {@link #allocate(FilePath)} * * @param context * Threads that share the same context can re-acquire the same lock (which will just increment the lock count.) * This allows related executors to share the same workspace. */ public synchronized Lease allocate(@Nonnull FilePath base, Object context) throws InterruptedException { for (int i=1; ; i++) { FilePath candidate = i==1 ? base : base.withSuffix(COMBINATOR+i); Entry e = inUse.get(candidate); if(e!=null && !e.quick && e.context!=context) continue; return acquire(candidate,false,context); } } /** * Just record that this workspace is being used, without paying any attention to the synchronization support. */ public synchronized Lease record(@Nonnull FilePath p) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "recorded " + p, new Throwable("from " + this)); } Entry old = inUse.put(p, new Entry(p, false)); if (old!=null) throw new AssertionError("Tried to record a workspace already owned: "+old); return lease(p); } /** * Releases an allocated or acquired workspace. */ private synchronized void _release(@Nonnull FilePath p) { Entry old = inUse.get(p); if (old==null) throw new AssertionError("Releasing unallocated workspace "+p); if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "releasing " + p + " with lock count " + old.lockCount, new Throwable("from " + this)); } old.lockCount--; if (old.lockCount==0) inUse.remove(p); notifyAll(); } /** * Acquires the given workspace. If necessary, this method blocks until it's made available. * * @return * The same {@link FilePath} as given to this method. */ public synchronized Lease acquire(@Nonnull FilePath p) throws InterruptedException { return acquire(p,false); } /** * See {@link #acquire(FilePath)} * * @param quick * If true, indicates that the acquired workspace will be returned quickly. * This makes other calls to {@link #allocate(FilePath)} to wait for the release of this workspace. */ public synchronized Lease acquire(@Nonnull FilePath p, boolean quick) throws InterruptedException { return acquire(p,quick,new Object()); } /** * See {@link #acquire(FilePath,boolean)} * * @param context * Threads that share the same context can re-acquire the same lock (which will just increment the lock count.) * This allows related executors to share the same workspace. */ public synchronized Lease acquire(@Nonnull FilePath p, boolean quick, Object context) throws InterruptedException { Entry e; Thread t = Thread.currentThread(); String oldName = t.getName(); t.setName("Waiting to acquire "+p+" : "+t.getName()); try { while (true) { e = inUse.get(p); if (e==null || e.context==context) break; wait(); } } finally { t.setName(oldName); } if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "acquired " + p + (e == null ? "" : " with lock count " + e.lockCount), new Throwable("from " + this)); } if (e!=null) e.lockCount++; else inUse.put(p,new Entry(p,quick,context)); return lease(p); } /** * Wraps a path into a valid lease. */ private Lease lease(@Nonnull FilePath p) { return new Lease(p) { final AtomicBoolean released = new AtomicBoolean(); public void release() { _release(path); } @Override public void close() { if (released.compareAndSet(false, true)) { release(); } } }; } /** * Locates a conventional temporary directory to be associated with a workspace. * <p>This directory is suitable for temporary files to be deleted later in the course of a build, * or caches and local repositories which should persist across builds done in the same workspace. * (If multiple workspaces are present for a single job built concurrently, via {@link #allocate(FilePath)}, each will get its own temporary directory.) * <p>It may also be used for security-sensitive files which {@link DirectoryBrowserSupport} ought not serve, * acknowledging that these will be readable by builds of other jobs done on the same node. * <p>Each plugin using this directory is responsible for specifying sufficiently unique subdirectory/file names. * {@link FilePath#createTempFile} may be used for this purpose if desired. * <p>The resulting directory may not exist; you may call {@link FilePath#mkdirs} on it if you need it to. * It may be deleted alongside the workspace itself during cleanup actions. * @param ws a directory such as a build workspace * @return a sibling directory, for example {@code …/something@tmp} for {@code …/something} * @since 1.652 */ public static FilePath tempDir(FilePath ws) { return ws.sibling(ws.getName() + COMBINATOR + "tmp"); } private static final Logger LOGGER = Logger.getLogger(WorkspaceList.class.getName()); /** * The token that combines the project name and unique number to create unique workspace directory. */ private static final String COMBINATOR = SystemProperties.getString(WorkspaceList.class.getName(),"@"); }