/* * The MIT License * * Copyright 2014 Jesse Glick. * * 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 org.jenkinsci.plugins.durabletask.executors; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.model.Computer; import hudson.model.Executor; import hudson.model.ExecutorListener; import hudson.model.OneOffExecutor; import hudson.model.Queue; import hudson.slaves.AbstractCloudComputer; import hudson.slaves.AbstractCloudSlave; import hudson.slaves.CloudRetentionStrategy; import hudson.slaves.EphemeralNode; import hudson.util.TimeUnit2; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.security.NotReallyRoleSensitiveCallable; /** * Retention strategy that allows a cloud slave to run only a single build before disconnecting. * A {@link ContinuableExecutable} does not trigger termination. */ public final class OnceRetentionStrategy extends CloudRetentionStrategy implements ExecutorListener { private static final Logger LOGGER = Logger.getLogger(OnceRetentionStrategy.class.getName()); private transient boolean terminating; private int idleMinutes; /** * Creates the retention strategy. * @param idleMinutes number of minutes of idleness after which to kill the slave; serves a backup in case the strategy fails to detect the end of a task */ public OnceRetentionStrategy(int idleMinutes) { super(idleMinutes); this.idleMinutes = idleMinutes; } @Override public long check(final AbstractCloudComputer c) { // When the slave is idle we should disable accepting tasks and check to see if it is already trying to // terminate. If it's not already trying to terminate then lets terminate manually. if (c.isIdle() && !disabled) { final long idleMilliseconds = System.currentTimeMillis() - c.getIdleStartMilliseconds(); if (idleMilliseconds > TimeUnit2.MINUTES.toMillis(idleMinutes)) { LOGGER.log(Level.FINE, "Disconnecting {0}", c.getName()); done(c); } } // Return one because we want to check every minute if idle. return 1; } @Override public void start(AbstractCloudComputer c) { if (c.getNode() instanceof EphemeralNode) { throw new IllegalStateException("May not use OnceRetentionStrategy on an EphemeralNode: " + c); } super.start(c); } @Override public void taskAccepted(Executor executor, Queue.Task task) {} @Override public void taskCompleted(Executor executor, Queue.Task task, long durationMS) { done(executor); } @Override public void taskCompletedWithProblems(Executor executor, Queue.Task task, long durationMS, Throwable problems) { done(executor); } private void done(Executor executor) { final AbstractCloudComputer<?> c = (AbstractCloudComputer) executor.getOwner(); Queue.Executable exec = executor.getCurrentExecutable(); if (executor instanceof OneOffExecutor) { LOGGER.log(Level.FINE, "not terminating {0} because {1} was a flyweight task", new Object[] {c.getName(), exec}); return; } if (exec instanceof ContinuableExecutable && ((ContinuableExecutable) exec).willContinue()) { LOGGER.log(Level.FINE, "not terminating {0} because {1} says it will be continued", new Object[] {c.getName(), exec}); return; } LOGGER.log(Level.FINE, "terminating {0} since {1} seems to be finished", new Object[] {c.getName(), exec}); done(c); } @SuppressFBWarnings(value="SE_BAD_FIELD", justification="not a real Callable") private void done(final AbstractCloudComputer<?> c) { c.setAcceptingTasks(false); // just in case synchronized (this) { if (terminating) { return; } terminating = true; } Computer.threadPoolForRemoting.submit(new Runnable() { @Override public void run() { Queue.withLock(new NotReallyRoleSensitiveCallable<Void,RuntimeException>() { @Override public Void call() { try { AbstractCloudSlave node = c.getNode(); if (node != null) { node.terminate(); } } catch (InterruptedException e) { LOGGER.log(Level.WARNING, "Failed to terminate " + c.getName(), e); synchronized (OnceRetentionStrategy.this) { terminating = false; } } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to terminate " + c.getName(), e); synchronized (OnceRetentionStrategy.this) { terminating = false; } } return null; } }); } }); } }