From c24b11fc818d73d082f56987ee52923343b9fc17 Mon Sep 17 00:00:00 2001 From: Ahmad Faraj Date: Sat, 4 Jul 2026 22:49:58 +0300 Subject: [PATCH 1/2] fix(controller): stop starting the next stage after a cancel command When a cancel arrives while a stage is finishing, the stage can still report success and the scheduler continues to the next stage. The next select then receives nil from the closed cancelledCh and falls through without cancelling or waiting for the just-started stage, leaving it running while rollback begins. Make a received cancel command terminate the deployment even when the stage reported success, and treat a nil receive as a cancellation. Signed-off-by: Ahmad Faraj --- pkg/app/piped/controller/scheduler.go | 14 ++++++++++++-- pkg/app/pipedv1/controller/scheduler.go | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/pkg/app/piped/controller/scheduler.go b/pkg/app/piped/controller/scheduler.go index 6c5b888803..7a5574eb92 100644 --- a/pkg/app/piped/controller/scheduler.go +++ b/pkg/app/piped/controller/scheduler.go @@ -373,17 +373,27 @@ func (s *scheduler) Run(ctx context.Context) error { <-doneCh case cmd := <-s.cancelledCh: + // A nil command means the channel was closed after delivering + // a cancel command, so treat it as a cancellation as well. if cmd != nil { cancelCommand = cmd cancelCommander = cmd.Commander - handler.Cancel() - <-doneCh } + handler.Cancel() + <-doneCh case <-doneCh: break } + // A received cancel command wins even if the stage finished successfully, + // otherwise the loop would continue to the next stage after the user cancelled. + if cancelCommand != nil { + deploymentStatus = model.DeploymentStatus_DEPLOYMENT_CANCELLED + statusReason = fmt.Sprintf("Cancelled by %s while executing stage %s", cancelCommander, ps.Id) + break + } + // If all operations of the stage were completed successfully or skipped by a web user // handle the next stage. if result == model.StageStatus_STAGE_SUCCESS || result == model.StageStatus_STAGE_SKIPPED { diff --git a/pkg/app/pipedv1/controller/scheduler.go b/pkg/app/pipedv1/controller/scheduler.go index 72a0fec59d..464e0f3fc8 100644 --- a/pkg/app/pipedv1/controller/scheduler.go +++ b/pkg/app/pipedv1/controller/scheduler.go @@ -370,12 +370,14 @@ func (s *scheduler) Run(ctx context.Context) error { <-doneCh case cmd := <-s.cancelledCh: + // A nil command means the channel was closed after delivering + // a cancel command, so treat it as a cancellation as well. if cmd != nil { cancelCommand = cmd cancelCommander = cmd.Commander - handler.Cancel() - <-doneCh } + handler.Cancel() + <-doneCh case <-timeout: handler.Timeout() @@ -385,6 +387,14 @@ func (s *scheduler) Run(ctx context.Context) error { break } + // A received cancel command wins even if the stage finished successfully, + // otherwise the loop would continue to the next stage after the user cancelled. + if cancelCommand != nil { + deploymentStatus = model.DeploymentStatus_DEPLOYMENT_CANCELLED + statusReason = fmt.Sprintf("Cancelled by %s while executing stage %s", cancelCommander, ps.Id) + break + } + // If all operations of the stage were completed successfully or skipped by a web user // handle the next stage. if result == model.StageStatus_STAGE_SUCCESS || result == model.StageStatus_STAGE_SKIPPED { From 33f962011745237f4c5528be769a28eb0066787c Mon Sep 17 00:00:00 2001 From: Ahmad Faraj Date: Sun, 5 Jul 2026 00:38:58 +0300 Subject: [PATCH 2/2] fix(controller): also check for a waiting cancel command after the stage completes The select can pick doneCh while a cancel command is already buffered, which would let the next stage start before the cancel is observed. Drain the channel non-blocking after the stage completes. Signed-off-by: Ahmad Faraj --- pkg/app/piped/controller/scheduler.go | 13 +++++++++++++ pkg/app/pipedv1/controller/scheduler.go | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/pkg/app/piped/controller/scheduler.go b/pkg/app/piped/controller/scheduler.go index 7a5574eb92..79ec256399 100644 --- a/pkg/app/piped/controller/scheduler.go +++ b/pkg/app/piped/controller/scheduler.go @@ -386,6 +386,19 @@ func (s *scheduler) Run(ctx context.Context) error { break } + // The select may have picked doneCh while a cancel command was already + // waiting, so check it before deciding to move on. + if cancelCommand == nil { + select { + case cmd := <-s.cancelledCh: + if cmd != nil { + cancelCommand = cmd + cancelCommander = cmd.Commander + } + default: + } + } + // A received cancel command wins even if the stage finished successfully, // otherwise the loop would continue to the next stage after the user cancelled. if cancelCommand != nil { diff --git a/pkg/app/pipedv1/controller/scheduler.go b/pkg/app/pipedv1/controller/scheduler.go index 464e0f3fc8..8a5e97199e 100644 --- a/pkg/app/pipedv1/controller/scheduler.go +++ b/pkg/app/pipedv1/controller/scheduler.go @@ -387,6 +387,19 @@ func (s *scheduler) Run(ctx context.Context) error { break } + // The select may have picked doneCh while a cancel command was already + // waiting, so check it before deciding to move on. + if cancelCommand == nil { + select { + case cmd := <-s.cancelledCh: + if cmd != nil { + cancelCommand = cmd + cancelCommander = cmd.Commander + } + default: + } + } + // A received cancel command wins even if the stage finished successfully, // otherwise the loop would continue to the next stage after the user cancelled. if cancelCommand != nil {