diff --git a/.project b/.project index bdc7aa3..65e2d1d 100644 --- a/.project +++ b/.project @@ -5,6 +5,11 @@ + + org.python.pydev.PyDevBuilder + + + org.eclipse.jdt.core.javabuilder @@ -19,5 +24,6 @@ org.eclipse.m2e.core.maven2Nature org.eclipse.jdt.core.javanature + org.python.pydev.pythonNature diff --git a/.pydevproject b/.pydevproject new file mode 100644 index 0000000..a9cca03 --- /dev/null +++ b/.pydevproject @@ -0,0 +1,7 @@ + + + + +Default +python 2.7 + diff --git a/README.txt b/README.txt index 8bfdb28..f023371 100644 --- a/README.txt +++ b/README.txt @@ -1,3 +1,61 @@ -This plugin attempts to integrate Gearman with Jenkins. +--------------------------------------------------------------------- +Copyright 2013 Hewlett-Packard Development Company, L.P. -http://wiki.jenkins-ci.org/display/JENKINS/Gearman+Plugin +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--------------------------------------------------------------------- + +Jenkins does not support multiple masters. You can setup multiple Jenkins +masters but there is no coordination between them. + +One problem with scheduling builds on Jenkins master (“MasterA”) server +is that MasterA only knows about its connected slaves. If all slaves on +MasterA are busy then MasterA will just put the next scheduled build on +its queue. Now MasterA needs to wait for an available slave to run +the build. This will be very in-efficient if your builds take a long +time to run. So..what if there is another Jenkins master (“MasterB”) +that has free slaves to service the next scheduled build? Your probably +saying “Then slaves on MasterB should run the build”. However MasterB +will never service the builds on MasterA's queue. The client that schedules +the builds must know about MasterB and then schedule builds on MasterB. +This is what we mean by lack of coordination between masters. +The gearman-plugin attempts to fill this gap. + +This plugin integrates Gearman with Jenkins and will make it so that +any Jenkins slave on any Jenkins master can service a job in the queue. +It will essentially replace the Jenkins build queue with the Gearman +job queue. The job should stay in the gearman queue until there is a +Jenkins node that can run that job. + + +This is the typical workflow: + +1) On a 'Launch Workers', we spawn a Gearman worker for each Jenkins +executor. We'll call these "executor worker threads". + +Each executor worker thread is associated 1:1 with a Jenkins node (slave or master) + +2) Now we register jobs for each Gearman executor depending on +projects and nodes. View the image to see the mapping. + +3) On a 'Launch Workers', we spawn one more thread to be a Gearman +worker to handle job management for this Jenkins master. We'll call +it the "management worker thread" and register the following function: + + stop:$hostname + +4) Any Gearman client can connect to the Gearman server and send a +request to build a Jenkins project or cancel a project. View the +examples to see how this can be done. + +5) The Gearman workers will service any client request that come +through to start/cancel a Jenkins build. diff --git a/pom.xml b/pom.xml index 5e91814..bda1cef 100644 --- a/pom.xml +++ b/pom.xml @@ -29,6 +29,7 @@ gearman-plugin hpi + 0.0.1 Gearman Plugin http://wiki.jenkins-ci.org/display/JENKINS/Gearman+Plugin diff --git a/src/main/java/hudson/plugins/gearman/StopJobWorker.java b/src/main/java/hudson/plugins/gearman/StopJobWorker.java index f734788..b3c867c 100644 --- a/src/main/java/hudson/plugins/gearman/StopJobWorker.java +++ b/src/main/java/hudson/plugins/gearman/StopJobWorker.java @@ -19,13 +19,11 @@ package hudson.plugins.gearman; +import hudson.model.AbstractBuild; import hudson.model.Computer; import hudson.model.Executor; -import hudson.model.Label; import hudson.model.Node; import hudson.model.Queue; -import hudson.model.Queue.Executable; -import hudson.model.queue.SubTask; import java.io.UnsupportedEncodingException; import java.util.List; @@ -45,7 +43,6 @@ import com.google.gson.reflect.TypeToken; /** * This is a gearman function that will cancel/abort jenkins builds * - * * @author Khai Do */ public class StopJobWorker extends AbstractGearmanFunction { @@ -80,7 +77,6 @@ public class StopJobWorker extends AbstractGearmanFunction { // need to pass on uuid from client. // temporarily passing uuid as a build parameter due to // issue: https://answers.launchpad.net/gearman-java/+question/218865 - String inUuid = null; for (Map.Entry entry : inParams.entrySet()) { if (entry.getKey().equals("uuid")) { @@ -90,39 +86,44 @@ public class StopJobWorker extends AbstractGearmanFunction { } - - // Cancel jenkins jobs that contain matching uuid from client - - boolean jobResult = cancelBuild(inUuid); - String jobResultMsg = null; - if (jobResult){ - jobResultMsg = "Canceled jenkins build " + inUuid; - - } else { - jobResultMsg = "Could not cancel build " + inUuid; - + boolean abortResult = false; + if (inUuid != null) { + // Abort running jenkins build that contain matching uuid + abortResult = abortBuild(inUuid); } + //TODO: build might be on gearman queue if it's not currently + // running by jenkins, need to check the gearman queue for the + // job and remove it. + + String jobResultMsg = ""; + String jobResultEx = ""; + boolean jobResult = true; + if (abortResult){ + jobResultMsg = "Canceled jenkins build " + inUuid; + } else { + jobResultMsg = "Did not cancel jenkins build " + inUuid; + jobResultEx = "Could not cancel build " + inUuid; + } GearmanJobResult gjr = new GearmanJobResultImpl(this.jobHandle, jobResult, - jobResultMsg.getBytes(), new byte[0], new byte[0], 0, 0); + jobResultMsg.getBytes(), new byte[0], jobResultEx.getBytes(), 0, 0); return gjr; } /** * Function to cancel a jenkins build from the jenkins queue * - * @param id - * The build Id + * @param uuid + * The build uuid * @return * true if build was cancel, otherwise false */ - private boolean cancelBuild (String id) { + private boolean cancelBuild (String uuid) { - // Cancel jenkins job from the jenkins queue Queue queue = Jenkins.getInstance().getQueue(); - if (id.isEmpty() || id == null){ // error checking + if (uuid.isEmpty() || uuid == null){ //NOOP return false; } @@ -130,6 +131,7 @@ public class StopJobWorker extends AbstractGearmanFunction { return false; } + // locate the build with matching uuid Queue.Item[] qItems = queue.getItems(); for (Queue.Item qi : qItems) { List actions = qi @@ -139,7 +141,8 @@ public class StopJobWorker extends AbstractGearmanFunction { String jenkinsJobId = gpa.getUuid(); - if (jenkinsJobId.equals(id)) { + if (jenkinsJobId.equals(uuid)) { + // Cancel jenkins job from the jenkins queue logger.info("---- Cancelling Jenkins build " + jenkinsJobId + " -------"); return queue.cancel(qi); @@ -150,62 +153,97 @@ public class StopJobWorker extends AbstractGearmanFunction { } /** - * Function to abort a running jenkins build + * Function to abort a currently running Jenkins build + * Running Jenkins builds are builds that actively being + * executed by Jenkins * - * @param id - * The build Id + * @param uuid + * The build UUID * @return * true if build was aborted, otherwise false */ - private boolean abortBuild (String id) { + private boolean abortBuild (String uuid) { - if (id.isEmpty() || id == null){ // error checking + if (uuid.isEmpty() || uuid == null){ //NOOP return false; } - /* - * iterate over the executors on all the nodes then find the build - * on that executor with the specified id. - * - * I'm able to iterate across the executors but not able to get - * the build object from the executor to lookup the id parameter value + * iterate over the executors on master and slave nodes to find the + * build on the executor with the matching uuid */ + // look at executors on master + Node masterNode = Computer.currentComputer().getNode(); + Computer masterComp = masterNode.toComputer(); + if (!masterComp.isIdle()) { // ignore idle master + List masterExecutors = masterComp.getExecutors(); + for (Executor executor: masterExecutors) { - // abort running jenkins job + if (executor.isIdle()) { // ignore idle executors + continue; + } + + // lookup the running build with matching uuid + Queue.Executable executable = executor.getCurrentExecutable(); + AbstractBuild currBuild = (AbstractBuild) executable; + int buildNum = currBuild.getNumber(); + String buildId = currBuild.getId(); + String runNodeName = currBuild.getBuiltOn().getNodeName(); + NodeParametersAction param = currBuild.getAction(NodeParametersAction.class); + String buildParams = param.getParameters().toString(); + + if (param.getUuid().equals(uuid)) { + + logger.info("Aborting build : "+buildNum+": "+buildId+" on " + runNodeName + +" with UUID " + uuid + " and build params " + buildParams); + + // abort the running jenkins build + if (!executor.isInterrupted()) { + executor.interrupt(); + return true; + } + } + } + } + + // look at executors on slave nodes List nodes = Jenkins.getInstance().getNodes(); - - if (nodes.isEmpty()) { + if (nodes.isEmpty()) { //NOOP return false; } for (Node node: nodes){ - Computer computer = node.toComputer(); - if (computer.isIdle()) { // ignore all idle slaves + Computer slave = node.toComputer(); + if (slave.isIdle()) { // ignore all idle slaves continue; } - List executors = computer.getExecutors(); - + List executors = slave.getExecutors(); for (Executor executor: executors) { - if (executor.isIdle()) { + if (executor.isIdle()) { // ignore idle executors continue; } - // lookup the running build with the id - Executable executable = executor.getCurrentExecutable(); - SubTask subtask = executable.getParent(); - Label label = subtask.getAssignedLabel(); - List params = label.getActions(NodeParametersAction.class); + // lookup the running build with matching uuid + Queue.Executable executable = executor.getCurrentExecutable(); + AbstractBuild currBuild = (AbstractBuild) executable; + int buildNum = currBuild.getNumber(); + String buildId = currBuild.getId(); + String runNodeName = currBuild.getBuiltOn().getNodeName(); + NodeParametersAction param = currBuild.getAction(NodeParametersAction.class); + String buildParams = param.getParameters().toString(); - for (NodeParametersAction param: params){ - if (param.getUuid().equals(id)){ + if (param.getUuid().equals(uuid)) { + + logger.info("Aborting build : "+buildNum+": "+buildId+" on " + runNodeName + +" with UUID " + uuid + " and build params " + buildParams); + + // abort the running jenkins build + if (!executor.isInterrupted()) { executor.interrupt(); - if (executor.interrupted()){ - return true; - } + return true; } } } diff --git a/src/main/java/hudson/plugins/gearman/example/StartJobClient.py b/src/main/java/hudson/plugins/gearman/example/StartJobClient.py index 6b16693..5f6bdf1 100644 --- a/src/main/java/hudson/plugins/gearman/example/StartJobClient.py +++ b/src/main/java/hudson/plugins/gearman/example/StartJobClient.py @@ -32,7 +32,7 @@ server = '127.0.0.1:4730' client = GearmanClient([server]) function = 'build:pep8:precise' build_id = uuid.uuid4().hex -build_params = {'param1':"red",'param2':"white",'param3':"blue"} +build_params = {'param1':"red", 'param2':"white", 'param3':"blue"} # Submit a synchronous job request to the job server print 'Sending job ' + build_id + ' to ' + server + ' with params ' + str(build_params) @@ -41,4 +41,5 @@ request = client.submit_job(function, poll_timeout=60, unique=build_id) + print request.result