Chapter 13Lesson 02~155 minutes

Groovy CPS, Serialization, @NonCPS, Pipeline State, Restarts, and Common Pipeline Programming Pitfalls: Guided Hands-On Workflow and Core Operations

Run a disposable CPS laboratory: prove simple state survives suspension, trigger and diagnose a non-serializable-object failure, repair it with a pure @NonCPS helper, and verify the same Pipeline run resumes after a controller restart.

Hands-onSerializationMatcher@NonCPSRestartEvidence

Learning objectives

  • Create a synthetic SCM-backed Pipeline and capture a reproducible CPS/plugin baseline.
  • Prove a simple serializable state object survives sleep and controller restart.
  • Trigger a controlled NotSerializableException by retaining a regex Matcher across a suspension point.
  • Repair the problem by confining the non-serializable object inside a pure @NonCPS helper.
  • Verify restart continuity using the same build number, source SHA, and persisted run evidence.

1. Lab preflight and safety boundary

Use a disposable Jenkins controller and a disposable agent labeled lab-linux. The controller must use persistent JENKINS_HOME. Do not run this experiment on a shared production controller because the lesson deliberately restarts Jenkins and deliberately fails builds.

Guard: if your controller is not named jenkins-ch13-controller and dedicated to this lab, adapt the restart command only after proving you are targeting the disposable instance. Never use an uncontrolled docker restart $(...) pattern.
# On the disposable controller host
printf 'controller=%s\n' jenkins-ch13-controller
docker inspect --format '{{.Name}} {{.State.Status}}' jenkins-ch13-controller
# Record the image identity and mounted JENKINS_HOME volume before starting.

Inside Jenkins, record core/Java and plugin versions, agent status, and the job’s exact SCM URL and revision.

2. Create a synthetic repository

mkdir jenkins-ch13-cps-lab && cd jenkins-ch13-cps-lab
git init
git config user.name "Jenkins Chapter 13 Lab"
git config user.email "jenkins-ch13@example.invalid"
printf 'chapter13-cps-lab\n' > README.txt
git add README.txt
git commit -m 'ch13: baseline'
git rev-parse HEAD

Create one Pipeline job from SCM, for example training/ch13-cps-lab. Keep sandbox enabled. Use no real credentials; a local/public synthetic repository is sufficient.

3. First run: prove simple state survives a suspension

node('lab-linux') {
  checkout scm
  def state = [operation: 'cps-safe-001', revision: sh(script: 'git rev-parse HEAD', returnStdout: true).trim()]
  echo "before=${state}"
  sleep 5
  echo "after=${state}"
  writeFile file: 'evidence-safe.txt', text: "${state}\n"
  archiveArtifacts artifacts: 'evidence-safe.txt', fingerprint: true
}

Record the build URL, build number, source SHA, node, workspace, and both log lines. The map contains strings and is intentionally small.

4. Broken run: retain a regex matcher across sleep

Commit this intentionally broken revision:

node('lab-linux') {
  checkout scm
  def matcher = ('release build-42 ready' =~ /build-(\d+)/)
  assert matcher.find()
  echo "before-suspend=${matcher.group(1)}"
  sleep 3
  echo "after-suspend=${matcher.group(1)}"
}

A regex matcher is a runtime object rather than durable Pipeline data. When Jenkins persists the continuation around the suspension, the run can fail with a serialization error such as java.io.NotSerializableException. Preserve the exact exception, build URL, source SHA, and nearest Pipeline line before editing the code.

Why “can fail” rather than “must fail at exactly this line”? Persistence timing can vary with Pipeline durability and execution details. The design defect is retaining the non-serializable matcher across a suspension, regardless of the exact moment Jenkins reports it.

5. Repair: confine native objects inside @NonCPS

@NonCPS
String buildIdFrom(String text) {
  def m = text =~ /build-(\d+)/
  return m.find() ? m.group(1) : 'none'
}

node('lab-linux') {
  checkout scm
  def buildId = buildIdFrom('release build-42 ready')
  echo "before-suspend=${buildId}"
  sleep 3
  echo "after-suspend=${buildId}"
}

The matcher exists only while the native helper is executing. The CPS program retains a plain string. Do not move echo or sleep into buildIdFrom; Pipeline steps do not belong inside @NonCPS.

6. Observe persisted run state without editing it

While a build is paused at input or sleep, inspect only metadata on the disposable controller:

# Example for a simple top-level job; folder jobs have nested jobs/ directories.
find "$JENKINS_HOME/jobs" -path '*/builds/*/program.dat' -type f \
  -printf '%TY-%Tm-%TdT%TH:%TM:%TS %s %p\n' 2>/dev/null | tail -n 10

Do not copy a program.dat between builds and do not modify it. Its value here is evidence that the run has persisted Pipeline program state.

7. Controlled restart during a safe suspension

Commit a revision with an explicit input gate outside any agent allocation:

def runIdentity = [job: env.JOB_NAME, build: env.BUILD_NUMBER, url: env.BUILD_URL]
echo "before_restart=${runIdentity}"
timeout(time: 15, unit: 'MINUTES') {
  input message: 'Chapter 13 restart checkpoint — resume after controller returns', ok: 'Continue'
}
echo "after_restart=${runIdentity}"

node('lab-linux') {
  checkout scm
  sh 'git rev-parse HEAD > restart-source-sha.txt'
  archiveArtifacts artifacts: 'restart-source-sha.txt', fingerprint: true
}

Wait until the build is visibly paused at the input step. Record its build number and URL. Then restart only the disposable controller:

docker restart jenkins-ch13-controller

After Jenkins returns, open the same build URL. Confirm it is still waiting at the same input step. Approve the input and verify the same build number continues; it must not become a new build merely because the controller restarted.

8. Groovy versus agent process: prove the boundary

def labels = ['alpha', 'beta', 'gamma']
def compact = labels.collect { it.toUpperCase() }.join(',')  // controller-side Groovy
echo "labels=${compact}"

node('lab-linux') {
  sh 'printf "agent_pid=%s\\n" "$$" > process-evidence.txt' // agent process
  archiveArtifacts artifacts: 'process-evidence.txt', fingerprint: true
}

The agent executes the shell process. The Groovy collection operation is controller-side even though both appear in one Jenkinsfile.

9. Challenge: choose the right layer

You need to parse a 200 MB JSON test report and retain only three totals. Which layer should do it?

Expected design: run a parser/tool on an agent against the file, write a tiny summary, then read only that summary into Pipeline state. Do not load the 200 MB report into a Groovy map on the controller, and do not mark a giant parser @NonCPS merely to make it faster.

10. Cleanup

Download the evidence, stop/delete only the Chapter 13 job/repository and disposable controller resources you created, and leave shared plugins/agents untouched. Preserve the broken build record until the checkpoint review is complete.

Next lesson

Configuration, Design Choices, and Tradeoffs

Choose between CPS-safe data, @NonCPS helpers, external tools, and agent-side computation while balancing durability, performance, security, and maintainability.

Knowledge check

What is the defect in retaining a regex Matcher across sleep?

Why does the repaired helper return a String?

What proves restart continuity?

Should you edit program.dat after a serialization failure?

Where should a large report transformation run?

Official references and version notes

Version and compatibility note

Rechecked on 2026-09-16. Examples assume Jenkins 2.568.3 LTS (tested with Java 21 and 25), Pipeline: Groovy 4380.v6eb_8378b_9647, Pipeline: Supporting APIs 1015.v785e5a_b_b_8b_22, and Script Security 1422.v06869826dd9b_. The mandatory path is local/disposable, uses synthetic state and fake identities, and requires no commercial service. Plugin releases are independent of Jenkins core, so record the versions actually installed on your controller before applying these lessons.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.