Skip to content

plank: enforce pending timeout when build cluster is unreachable - #817

Open
Prucek wants to merge 1 commit into
kubernetes-sigs:mainfrom
Prucek:fix-pending-timeout-unreachable-cluster
Open

plank: enforce pending timeout when build cluster is unreachable#817
Prucek wants to merge 1 commit into
kubernetes-sigs:mainfrom
Prucek:fix-pending-timeout-unreachable-cluster

Conversation

@Prucek

@Prucek Prucek commented Jul 29, 2026

Copy link
Copy Markdown
Member

Summary

When a build cluster becomes unreachable, syncPendingJob enters a retry loop that never terminates:

  1. r.pod() does an informer cache lookup — pod not found (cache is empty after plank restart while the cluster was down)
  2. Enters !podExists branch → calls startPod()client.Create() (direct API write)
  3. Cluster unreachable → network error (e.g. connection refused)
  4. isRequestError() returns false — it only matches 4xx API responses, not raw network errors
  5. Returns error → controller-runtime requeues with backoff → repeats forever

The pod_pending_timeout is never evaluated because the timeout check only runs after the pod is found in the informer cache and observed in a PodPending phase. When the pod is missing from the cache entirely, neither the scheduling nor the pending timeout code paths are reached.

Fix: In the !podExists path, when startPod() fails with a non-request error, check the ProwJob's pendingTime against the configured pod_pending_timeout. If the timeout has been exceeded, mark the job as errored instead of retrying forever. If the timeout hasn't elapsed yet, continue retrying (the cluster may recover).

Also extracts maxPodPendingTimeout helper to deduplicate the timeout resolution logic between the pod-missing and pod-pending code paths (addresses review feedback from the previous revision).

Example: https://prow.ci.openshift.org/view/gs/test-platform-results/logs/periodic-build-farm-canary-build06/2080695972983214080 — this canary job was stuck in pending state since July 24 because build06 was temporarily unreachable. Despite pod_pending_timeout: 30m being configured globally, it was never enforced. The prowjob.json confirms: state: "pending", pod_name set (pod was originally created), no finished.json or build-log.txt.

Why the informer cache read doesn't help

As confirmed in review discussion: buildClient.Get() reads from the informer cache — a purely in-memory lookup that never fails with network errors. When a build cluster goes down:

  • If plank keeps running: the stale pod stays in cache, existing timeout logic works
  • If plank restarts: fresh cache can't list from the unreachable cluster → pod is missing → startPod() network error loop (this fix)

Changes

  • pkg/plank/reconciler.go: Add pending timeout check in the !podExists + startPod failure path for non-request errors. Extract maxPodPendingTimeout helper.
  • pkg/plank/controller_test.go: Add ExpectError field to test struct. Add two test cases: "build cluster unreachable, pending timeout exceeded" (expects ErrorState) and "build cluster unreachable, pending timeout not yet exceeded" (expects retry error).

@netlify

netlify Bot commented Jul 29, 2026

Copy link
Copy Markdown

Deploy Preview for k8s-prow ready!

Name Link
🔨 Latest commit e9b0c93
🔍 Latest deploy log https://app.netlify.com/projects/k8s-prow/deploys/6a734b7f42ab1a0008a2949a
😎 Deploy Preview https://deploy-preview-817--k8s-prow.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@kubernetes-prow

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: Prucek
Once this PR has been reviewed and has the lgtm label, please assign cjwagner for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubernetes-prow kubernetes-prow Bot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/M Denotes a PR that changes 30-99 lines, ignoring generated files. labels Jul 29, 2026
Comment thread pkg/plank/reconciler.go Outdated
Comment on lines +459 to +462
maxPodPending := r.config().Plank.PodPendingTimeout.Duration
if pj.Spec.DecorationConfig != nil && pj.Spec.DecorationConfig.PodPendingTimeout != nil {
maxPodPending = pj.Spec.DecorationConfig.PodPendingTimeout.Duration
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is duplicated from original lines 551-553. I think we could use a helper here incase the logic needs to merge something else in the future.

Comment thread pkg/plank/reconciler.go Outdated
Comment on lines +464 to +474
pj.SetComplete()
pj.Status.State = prowv1.ErrorState
pj.Status.Description = fmt.Sprintf("Pod pending timeout: could not get pod from build cluster: %v.", err)
r.log.WithFields(pjutil.ProwJobFields(pj)).WithError(err).Info("Marked job as errored: build cluster unreachable and pending timeout exceeded.")
pj.Status.URL, err = pjutil.JobURL(r.config().Plank, *pj, r.log)
if err != nil {
r.log.WithFields(pjutil.ProwJobFields(pj)).WithError(err).Warn("failed to get jobURL")
}
if patchErr := r.pjClient.Patch(ctx, pj.DeepCopy(), ctrlruntimeclient.MergeFrom(prevPJ)); patchErr != nil {
return nil, fmt.Errorf("patch prowjob: %w", patchErr)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also seems like something that must be duplicated. Can we get a helper to error a pj that is used here?

@Prucek
Prucek force-pushed the fix-pending-timeout-unreachable-cluster branch from 6e36518 to 3ac48b9 Compare August 3, 2026 11:39
@kubernetes-prow kubernetes-prow Bot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. and removed size/M Denotes a PR that changes 30-99 lines, ignoring generated files. labels Aug 3, 2026
@petr-muller

Copy link
Copy Markdown
Contributor

Isn't this too strict? Single failed Get - where the condition can be transient - will make Plank assume the Pod never actually started and kill any job that went Pending (=we created a pod) before a fairly short period of time. I guess the problem is that we do not distinguish the Pending jobs where we never saw the payload pod started (where we'd want to apply pod_pending_timeout) from the Pending jobs where we did (where we'd want to apply a different timeout, like the job timeout itself).

Still I feel something like a grace period is in order.

@petr-muller petr-muller left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dug into the Get() failure signal and have several more thoughts - I don't consider them blocking but I'd like them to be considered.

  1. I wonder if instead of handling the error in-situ it should be re-translated to a TerminalError that's handled almost identically the call chain? We already have the "stop reconciling, set to abort" behavior, just at different place.
  2. We don't have the "wait for the change to propagate into the informer cache behavior" in the TerminalError handler though -> that kinda defeats (1) but I wonder if the TerminalError handler should do the wait too (and then we could do (1))

And lastly, I really wonder if the Pod Get() really actually errors if a build cluster dies - if I read the code correctly it is an informer cache-backed read, which is local and not affected by build farm outages - so the Get IMO never errors, you just stop getting new state and you stop getting reconcile events about the Pod.

Comment thread pkg/plank/reconciler.go Outdated
return maxPodPending
}

func (r *reconciler) updateProwJobStatus(ctx context.Context, pj *prowv1.ProwJob, prevPJ *prowv1.ProwJob) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper could use a godoc that describes how exactly is the the prowjob status updated, because it is absolutely unclear without reading the code. It should probably retain the comment that was at its original location because the wait could be surprising and a reader can just guess why it is there.

When a build cluster becomes unreachable, syncPendingJob enters a retry
loop that never terminates. The pod lookup (informer cache read) returns
not-found, startPod attempts client.Create which fails with a network
error, and because isRequestError only matches 4xx API responses, the
error is returned for requeue — indefinitely.

Check the ProwJob's pendingTime against pod_pending_timeout when
startPod fails with a non-request error. If the timeout is exceeded,
mark the job as errored instead of retrying forever.

Also extract maxPodPendingTimeout helper to deduplicate the timeout
resolution logic between the pod-missing and pod-pending code paths.
@Prucek
Prucek force-pushed the fix-pending-timeout-unreachable-cluster branch from 3ac48b9 to e9b0c93 Compare August 5, 2026 14:41
@kubernetes-prow kubernetes-prow Bot added size/M Denotes a PR that changes 30-99 lines, ignoring generated files. and removed size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Aug 5, 2026
@Prucek

Prucek commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

/retest

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/M Denotes a PR that changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants