AndiH
AndiH

Reputation: 602

How to delay Jenkins job until a certain time was reached?

In our current project setup we have the Jobs A, B, C.

When A, B or C is built successfully, we want to deploy the resulting artifacts to our development server. Therefore we use job D.

Because the deployment recreates the development database, we only want to run the job at every full hour. Our testers are familiar with this schedule and work this way.

Actually when D gets triggered by A, B, or C, a script is started that waits until the server minute is 00. If one of the trigger jobs triggers D again while its waiting, the formerly waiting script gets canceled and restarted. If the script reaches 00 of the hour, the deployment happens.

The main problem is, that the job D blocks a building slot for 59 minutes in the worst case.

Running the job hourly by default is not an option, because then the deployment would happen, even if nothing changed.

Running the job after something changed is also bad, because the testers are used to the hourly deployment.

I know there is a 'quiet time' option, but that only enables me to set a waiting time relative to the trigger time. What I need is a 'quiet time' that delays a job until a certain time has reached. Has anybody an advice on how to reach this goal?

Upvotes: 4

Views: 3541

Answers (1)

Gerold Broser
Gerold Broser

Reputation: 14762

I developed the following solution:

1) Use the Conditional BuildStep Plugin in jobs A, B and C to set the cron schedule in job D's config.xml:

  • Build
    • Conditional step (single)
      • Run?: Execute shell   [just for testing, use your build step(s) here]
        • Command

 

#!/bin/bash
echo "  This script is a placeholder to represent a build step that can succeed or not succeed"
true   # to test build success → Groovy script should be executed
#false  # to test not successful build → nothing should be done
 
  • Builder: Execute system Groovy script
    • ◉ Groovy command

 

// From: How to delay Jenkins job until a certain time was reached?
//       http://stackoverflow.com/questions/27952216/1744774

// -----------------------------------------------------------
// Adapt these according to your environment
final String DOWNSTREAM_NAME = 'SO-27952216-Downstream-job'
final String CRON_SCHEDULE = '0 * * * *'
// -----------------------------------------------------------

final String SEPARATOR = new String(new char[8]).replace('\0', '-')
println("  ${SEPARATOR} Adapting configuration of ${DOWNSTREAM_NAME} ${SEPARATOR}")

import jenkins.model.*
import hudson.model.*

final Project DOWNSTREAM_JOB = Jenkins.instance.getItem(DOWNSTREAM_NAME)
final String DOWNSTREAM_CONFIG = DOWNSTREAM_JOB.getRootDir().getPath() + "/config.xml"  

//import hudson.triggers.*
//DOWNSTREAM_JOB.getTrigger(TimerTrigger.class).spec = CRON_SCHEDULE
// leads to:
//   ERROR: Build step failed with exception groovy.lang.ReadOnlyPropertyException: 
//       Cannot set readonly property: spec for class: hudson.triggers.TimerTrigger

import org.w3c.dom.*
import javax.xml.parsers.*
import javax.xml.xpath.*

println("  Reading ${DOWNSTREAM_CONFIG}")
Document doc = 
    DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(DOWNSTREAM_CONFIG)
XPathExpression expr = XPathFactory.newInstance().newXPath()
    .compile("/project/triggers/hudson.triggers.TimerTrigger/spec");
final org.w3c.dom.Node SCHEDULE_NODE = expr.evaluate(doc, XPathConstants.NODE)

println(String.format(
    "  Changing Build Triggers → Build periodically → Schedule from '%s' to ' %s'",
    SCHEDULE_NODE.getTextContent(), CRON_SCHEDULE))
SCHEDULE_NODE.setTextContent(CRON_SCHEDULE)

import javax.xml.transform.*
import javax.xml.transform.dom.*
import javax.xml.transform.stream.*

println("  Writing ${DOWNSTREAM_CONFIG}")
Transformer transformer = TransformerFactory.newInstance().newTransformer()
transformer.setOutputProperty(OutputKeys.INDENT, "yes")
transformer.transform(new DOMSource(doc), new StreamResult(new File(DOWNSTREAM_CONFIG)))

println("  ${SEPARATOR} Adapted configuration of ${DOWNSTREAM_NAME} ${SEPARATOR}")
 

 

2) Use Post-build ActionGroovy Postbuild in job D to reset the cron schedule in its config.xml:

  • Build
    • Execute shell   [just for testing, use your deployment step(s) here]
      • Command

 

#!/bin/bash
echo "  This script is a placeholder to represent a build step"
 
  • Post-build Actions
    • Groovy Postbuild
      • Groovy Script

 

// From: How to delay Jenkins job until a certain time was reached?
//       http://stackoverflow.com/questions/27952216/1744774

// -----------------------------------------------------------
// Adapt these according to your environment
final String THIS_JOB_NAME = 'SO-27952216-Downstream-job'
final String CRON_SCHEDULE = ''
// -----------------------------------------------------------

final Object LOG = manager.listener.logger
final String SEPARATOR = new String(new char[8]).replace('\0', '-')
LOG.println("  ${SEPARATOR} Adapting configuration of ${THIS_JOB_NAME} ${SEPARATOR}")

import jenkins.model.*
import hudson.model.*

final Project THIS_JOB = Jenkins.instance.getItem(THIS_JOB_NAME)
final String THIS_JOB_CONFIG = THIS_JOB.getRootDir().getPath() + "/config.xml"  

//import hudson.triggers.*
//THIS_JOB.getTrigger(TimerTrigger.class).spec = CRON_SCHEDULE
// leads to:
//   ERROR: Build step failed with exception groovy.lang.ReadOnlyPropertyException: 
//       Cannot set readonly property: spec for class: hudson.triggers.TimerTrigger

import org.w3c.dom.*;
import javax.xml.parsers.*
import javax.xml.xpath.*

LOG.println("  Reading ${THIS_JOB_CONFIG}")
Document doc = 
    DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(THIS_JOB_CONFIG)
XPathExpression expr = XPathFactory.newInstance().newXPath()
    .compile("/project/triggers/hudson.triggers.TimerTrigger/spec")
final org.w3c.dom.Node SCHEDULE_NODE = expr.evaluate(doc, XPathConstants.NODE)

LOG.println(String.format(
    "  Changing Build Triggers → Build periodically → Schedule from '%s' to ' %s'",
    SCHEDULE_NODE.getTextContent(), CRON_SCHEDULE))
SCHEDULE_NODE.setTextContent(CRON_SCHEDULE)

import javax.xml.transform.*
import javax.xml.transform.dom.*
import javax.xml.transform.stream.*

LOG.println("  Writing ${THIS_JOB_CONFIG}")
Transformer transformer = TransformerFactory.newInstance().newTransformer()
transformer.setOutputProperty(OutputKeys.INDENT, "yes")
transformer.transform(new DOMSource(doc), new StreamResult(new File(THIS_JOB_CONFIG)))

LOG.println("  ${SEPARATOR} Adapted configuration of ${THIS_JOB_NAME} ${SEPARATOR}")
 

Hints are welcome why println() doesn't print anything to Jenkin's Console Output in this Post-build Action.

UPDATE: Got it! According to [JENKINS-18651 – Enable correct writing to the job's log from a post build script] it's manager.listener.logger.println() instead of just println().


It should be noted that there are three situations where this solution can cause issues:

  • Jobs A, B, C or D write to config.xml and a user saves D's configuration via the UI at the same moment → concurrent write access.
    • If the UI is first to open config.xml we can cover this with exception handling and a retry after a few seconds in our scripts.
    • If our script is first to open config.xml we're out of luck → the UI will probably show an error under the regime of the "diabolic" Jenkins:
      .
  • Jobs A or B or C write to config.xml at the same moment as D, or vice versa → we can cover this with exception handling and a retry after a few seconds in our scripts.
  • Jobs A, B, C or D write to config.xml and Jenkins' TimerTrigger reads the file (it does that once a minute) at the same moment → reading incorrect data due to write not yet finished completely.

I leave it as a challenge for the reader to avoid code duplication in case of more than one upstream job by:

  • Moving the Groovy code of the upstream job to:
    http://jenkins/userContent/scripts/JobCronScheduleConfigurator.groovy
  • Using the following in any of the upstream jobs:
    • Build
      • Conditional step (single)
        • Builder
          • ◉ Groovy script file
            http://jenkins/userContent/scripts/JobCronScheduleConfigurator.groovy

Unfortunately Post-build ActionGroovy Postbuild doesn't support Groovy script file so there remains code duplication in this respect (and the logging, see above, is different anyway).

Upvotes: 2

Related Questions