This is an automated email from the ASF dual-hosted git repository.
wilfred-s pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/yunikorn-k8shim.git
The following commit(s) were added to refs/heads/master by this push:
new e666a782 [YUNIKORN-3316] Handle AssumePod failures in shim (#1052)
e666a782 is described below
commit e666a782fa62098ae767528b9900e385068d1844
Author: Aditya Maheshwari <[email protected]>
AuthorDate: Mon Aug 10 22:41:43 2026 +1000
[YUNIKORN-3316] Handle AssumePod failures in shim (#1052)
During AssumePod cache update if there are any failures in the API call
the task goes to failed state. The application is completed if the failed
task is the last task for the application.
Yunikorn should retry the task and possibly select a different node that
does not fail the AssumePod.
As part of these changes following changes are done -
* Shim will send a SCHEDULING_FAILED_ON_RM termination type to core after
exhausting all retries on assume pod failures.
* Task will be kept in scheduling state so that it can be retries again.
* Task will clear node assignment, clear pvc bindings from cache and will
remove itself from node (forgetPod).
Core will rollback based on the core changes apache/yunikorn-core#1104
Closes: #1052
Signed-off-by: Wilfred Spiegelenburg <[email protected]>
---
go.mod | 4 +-
go.sum | 8 +--
pkg/cache/context.go | 31 ++++++++++++
pkg/cache/context_test.go | 97 ++++++++++++++++++++++++++++++++++++
pkg/cache/scheduler_callback.go | 11 +++-
pkg/cache/scheduler_callback_test.go | 58 ++++++++++++++++++++-
pkg/cache/task.go | 49 ++++++++++++++++++
pkg/cache/task_test.go | 93 ++++++++++++++++++++++++++++++++++
pkg/common/test/volumebinder_mock.go | 20 ++++++--
pkg/shim/scheduler_test.go | 31 +++++++++---
10 files changed, 383 insertions(+), 19 deletions(-)
diff --git a/go.mod b/go.mod
index 414d6156..349efd5e 100644
--- a/go.mod
+++ b/go.mod
@@ -21,8 +21,8 @@ module github.com/apache/yunikorn-k8shim
go 1.26.0
require (
- github.com/apache/yunikorn-core v0.0.0-20260605065500-fae6e256cd5c
- github.com/apache/yunikorn-scheduler-interface
v0.0.0-20260528033204-c474acff6d53
+ github.com/apache/yunikorn-core v0.0.0-20260728123950-900bd603df82
+ github.com/apache/yunikorn-scheduler-interface
v0.0.0-20260727092410-674338955bdf
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/looplab/fsm v1.0.3
diff --git a/go.sum b/go.sum
index 08489887..974ceb67 100644
--- a/go.sum
+++ b/go.sum
@@ -10,10 +10,10 @@ github.com/alexbrainman/sspi
v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktp
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod
h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/antlr4-go/antlr/v4 v4.13.1
h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod
h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
-github.com/apache/yunikorn-core v0.0.0-20260605065500-fae6e256cd5c
h1:iuShQ3op12fL+jQ420ZQLsgyDp/1L1/ygi9ZG8awvko=
-github.com/apache/yunikorn-core v0.0.0-20260605065500-fae6e256cd5c/go.mod
h1:yxn4stqkqgiSNYqOyulBHf9IgyTBM+rFrzWcyicueKU=
-github.com/apache/yunikorn-scheduler-interface
v0.0.0-20260528033204-c474acff6d53
h1:zodKoODatR57zSGoQGdQ8EZjJgQqy9nh5/yxmI9hWpI=
-github.com/apache/yunikorn-scheduler-interface
v0.0.0-20260528033204-c474acff6d53/go.mod
h1:XygReHrRd3TtfNS3uVtRQI0sCOfprcLUfhlmv/LdCmk=
+github.com/apache/yunikorn-core v0.0.0-20260728123950-900bd603df82
h1:uvaD7WchXd9nLkXArGjdul1OQfxXO+ukvI0VJJ0hAwk=
+github.com/apache/yunikorn-core v0.0.0-20260728123950-900bd603df82/go.mod
h1:Z/8pYK5VeDk9Q74vTCm3FnCVm1DtciaGYjl5/P7kO+M=
+github.com/apache/yunikorn-scheduler-interface
v0.0.0-20260727092410-674338955bdf
h1:IXEpAeqZgCXODJtvB6Ib5CCcpRyvF0mqcicz80rA3Cc=
+github.com/apache/yunikorn-scheduler-interface
v0.0.0-20260727092410-674338955bdf/go.mod
h1:qb739Bdm82PH7gsfEYabulGF90xKGNQ1hWmf197rDfw=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod
h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
diff --git a/pkg/cache/context.go b/pkg/cache/context.go
index e353f5bc..9b99be02 100644
--- a/pkg/cache/context.go
+++ b/pkg/cache/context.go
@@ -897,6 +897,37 @@ func (ctx *Context) ForgetPod(name string) {
log.Log(log.ShimContext).Debug("unable to forget pod: not found in
cache", zap.String("pod", name))
}
+// RevertPodVolumeAssumptions undoes any PV/PVC assumptions made by the volume
binder
+// for the given pod on the given node. This is idempotent and safe to call
even if
+// AssumePodVolumes was never called or already reverted internally.
+func (ctx *Context) RevertPodVolumeAssumptions(podName, nodeID string) {
+ ctx.lock.Lock()
+ defer ctx.lock.Unlock()
+ pod := ctx.schedulerCache.GetPod(podName)
+ if pod == nil {
+ return
+ }
+ node := ctx.schedulerCache.GetNode(nodeID)
+ if node == nil {
+ return
+ }
+ podVolumeClaims, err :=
ctx.apiProvider.GetAPIs().VolumeBinder.GetPodVolumeClaims(ctx.klogger, pod)
+ if err != nil {
+ log.Log(log.ShimContext).Error("RevertPodVolumeAssumptions:
failed to get pod volume claims",
+ zap.String("pod", podName), zap.Error(err))
+ return
+ }
+ podVolumes, _, err :=
ctx.apiProvider.GetAPIs().VolumeBinder.FindPodVolumes(ctx.klogger, pod,
podVolumeClaims, node.Node())
+ if err != nil || podVolumes == nil {
+ log.Log(log.ShimContext).Error("RevertPodVolumeAssumptions:
failed to find pod volumes",
+ zap.String("pod", podName), zap.Error(err))
+ return
+ }
+
ctx.apiProvider.GetAPIs().VolumeBinder.RevertAssumedPodVolumes(podVolumes)
+ log.Log(log.ShimContext).Info("reverted assumed pod volumes",
+ zap.String("pod", podName), zap.String("node", nodeID))
+}
+
func (ctx *Context) notifyTaskComplete(app *Application, taskID string) {
if app == nil {
log.Log(log.ShimContext).Debug("In notifyTaskComplete but app
is nil",
diff --git a/pkg/cache/context_test.go b/pkg/cache/context_test.go
index d4a7d669..226dee53 100644
--- a/pkg/cache/context_test.go
+++ b/pkg/cache/context_test.go
@@ -2325,6 +2325,35 @@ func TestAssumePod_PodNotFound(t *testing.T) {
assert.Equal(t, podInCache.Spec.NodeName, "", "NodeName in pod spec was
set unexpectedly")
}
+func TestAssumePod_VolumesNotFullyBound(t *testing.T) {
+ binder := test.NewVolumeBinderMock()
+ binder.SetAllBound(false)
+ context := initAssumePodTest(binder)
+ defer dispatcher.UnregisterAllEventHandlers()
+ defer dispatcher.Stop()
+
+ err := context.AssumePod(pod1UID, fakeNodeName)
+ assert.NilError(t, err)
+ assert.Assert(t,
!context.schedulerCache.ArePodVolumesAllBound(pod1UID), "volumes should not be
reported as fully bound")
+ assumedPod := context.schedulerCache.GetPod(pod1UID)
+ assert.Assert(t, assumedPod != nil, "pod not found in cache")
+ assert.Equal(t, assumedPod.Spec.NodeName, fakeNodeName)
+ assert.Assert(t, context.schedulerCache.IsAssumedPod(pod1UID))
+}
+
+func TestAssumePod_NodeNotFound(t *testing.T) {
+ context := initAssumePodTest(test.NewVolumeBinderMock())
+ defer dispatcher.UnregisterAllEventHandlers()
+ defer dispatcher.Stop()
+
+ err := context.AssumePod(pod1UID, "nonexistent-node")
+ assert.NilError(t, err)
+ assert.Assert(t, !context.schedulerCache.IsAssumedPod(pod1UID))
+ podInCache := context.schedulerCache.GetPod(pod1UID)
+ assert.Assert(t, podInCache != nil, "pod not found in cache")
+ assert.Equal(t, podInCache.Spec.NodeName, "", "NodeName in pod spec was
set unexpectedly")
+}
+
// TestOriginatorPodAfterRestart Test to ensure originator pod remains same
even after restart. After restart, ordering of pods may change which can lead to
// incorrect originator pod selection. Instead of doing actual restart, create
a situation where in pods are being processed in any random order.
// For example, placeholders are processed first and then real driver pod.
@@ -2452,6 +2481,74 @@ func initAssumePodTest(binder *test.VolumeBinderMock)
*Context {
return context
}
+func TestRevertPodVolumeAssumptions(t *testing.T) {
+ binder := test.NewVolumeBinderMock()
+ binder.SetPodVolumes(&volumebinding.PodVolumes{})
+ context := initAssumePodTest(binder)
+ defer dispatcher.UnregisterAllEventHandlers()
+ defer dispatcher.Stop()
+
+ context.RevertPodVolumeAssumptions(pod1UID, fakeNodeName)
+ assert.Equal(t, binder.RevertCalledCount(), 1)
+}
+
+func TestRevertPodVolumeAssumptions_PodNotFound(t *testing.T) {
+ binder := test.NewVolumeBinderMock()
+ binder.SetPodVolumes(&volumebinding.PodVolumes{})
+ context := initAssumePodTest(binder)
+ defer dispatcher.UnregisterAllEventHandlers()
+ defer dispatcher.Stop()
+
+ context.RevertPodVolumeAssumptions("nonexistent-pod", fakeNodeName)
+ assert.Equal(t, binder.RevertCalledCount(), 0)
+}
+
+func TestRevertPodVolumeAssumptions_NodeNotFound(t *testing.T) {
+ binder := test.NewVolumeBinderMock()
+ binder.SetPodVolumes(&volumebinding.PodVolumes{})
+ context := initAssumePodTest(binder)
+ defer dispatcher.UnregisterAllEventHandlers()
+ defer dispatcher.Stop()
+
+ context.RevertPodVolumeAssumptions(pod1UID, "nonexistent-node")
+ assert.Equal(t, binder.RevertCalledCount(), 0)
+}
+
+func TestRevertPodVolumeAssumptions_GetPodVolumeClaimsError(t *testing.T) {
+ binder := test.NewVolumeBinderMock()
+ binder.SetPodVolumes(&volumebinding.PodVolumes{})
+ binder.EnableVolumeClaimsError("volume claims error")
+ context := initAssumePodTest(binder)
+ defer dispatcher.UnregisterAllEventHandlers()
+ defer dispatcher.Stop()
+
+ context.RevertPodVolumeAssumptions(pod1UID, fakeNodeName)
+ assert.Equal(t, binder.RevertCalledCount(), 0)
+}
+
+func TestRevertPodVolumeAssumptions_FindPodVolumesError(t *testing.T) {
+ binder := test.NewVolumeBinderMock()
+ binder.SetPodVolumes(&volumebinding.PodVolumes{})
+ binder.EnableFindPodVolumesError("find volumes error")
+ context := initAssumePodTest(binder)
+ defer dispatcher.UnregisterAllEventHandlers()
+ defer dispatcher.Stop()
+
+ context.RevertPodVolumeAssumptions(pod1UID, fakeNodeName)
+ assert.Equal(t, binder.RevertCalledCount(), 0)
+}
+
+func TestRevertPodVolumeAssumptions_NilPodVolumes(t *testing.T) {
+ // By default the mock returns nil podVolumes; RevertAssumedPodVolumes
must not be called.
+ binder := test.NewVolumeBinderMock()
+ context := initAssumePodTest(binder)
+ defer dispatcher.UnregisterAllEventHandlers()
+ defer dispatcher.Stop()
+
+ context.RevertPodVolumeAssumptions(pod1UID, fakeNodeName)
+ assert.Equal(t, binder.RevertCalledCount(), 0)
+}
+
func waitForNodeAcceptedEvent(recorder *k8sEvents.FakeRecorder) error {
// fetch the "node accepted" event
err := utils.WaitForCondition(func() bool {
diff --git a/pkg/cache/scheduler_callback.go b/pkg/cache/scheduler_callback.go
index ab93e6f9..a2cdaf0a 100644
--- a/pkg/cache/scheduler_callback.go
+++ b/pkg/cache/scheduler_callback.go
@@ -77,8 +77,15 @@ func (callback *AsyncRMCallback) UpdateAllocation(response
*si.AllocationRespons
return callback.context.AssumePod(alloc.AllocationKey,
alloc.NodeID)
})
if err != nil {
- task.FailWithEvent(err.Error(), "AssumePodError")
- return err
+ if task.IsPlaceholder() {
+ // Placeholder tasks do not have volume
bindings, so AssumePod failure
+ // is unexpected and unrecoverable; wrap the
error with context.
+ wrappedErr := fmt.Errorf("placeholder task does
not have volume bindings, AssumePod failed: %w", err)
+ task.FailWithEvent(wrappedErr.Error(),
"AssumePodError")
+ return wrappedErr
+ }
+ task.rollbackOnAssumePodFailure(alloc.AllocationKey,
alloc.NodeID)
+ continue
}
if utils.IsAssignedPod(task.GetTaskPod()) {
diff --git a/pkg/cache/scheduler_callback_test.go
b/pkg/cache/scheduler_callback_test.go
index 22b8c37e..cd4d7980 100644
--- a/pkg/cache/scheduler_callback_test.go
+++ b/pkg/cache/scheduler_callback_test.go
@@ -94,6 +94,49 @@ func TestUpdateAllocation_NewTask_AssumePodFails(t
*testing.T) {
binder.SetAssumePodVolumesError(errMsg)
setVolumeBinder(context, binder)
+ var rollbackSent atomic.Bool
+ //nolint:errcheck
+
context.apiProvider.(*client.MockedAPIProvider).MockSchedulerAPIUpdateAllocationFn(func(request
*si.AllocationRequest) error {
+ if request.Releases != nil {
+ for _, rel := range
request.Releases.AllocationsToRelease {
+ if rel.TerminationType ==
si.TerminationType_SCHEDULING_FAILED_ON_RM {
+ rollbackSent.Store(true)
+ }
+ }
+ }
+ return nil
+ })
+
+ err := callback.UpdateAllocation(&si.AllocationResponse{
+ New: []*si.Allocation{
+ {
+ ApplicationID: appID,
+ AllocationKey: taskUID1,
+ NodeID: fakeNodeName,
+ },
+ },
+ })
+ assert.NilError(t, err, "error updating allocation")
+ assert.Assert(t, !context.schedulerCache.IsAssumedPod(taskUID1))
+ assert.Assert(t, rollbackSent.Load(), "rollback release was not sent to
scheduler")
+ task := context.getTask(appID, taskUID1)
+ assert.Equal(t, "", task.GetAllocationKey(), "allocation key should be
cleared after rollback")
+ assert.Equal(t, TaskStates().Scheduling, task.GetTaskState(), "task
should remain in Scheduling state")
+}
+
+func TestUpdateAllocation_PlaceholderTask_AssumePodFails(t *testing.T) {
+ callback, context := initCallbackTest(t, false, true)
+ defer dispatcher.UnregisterAllEventHandlers()
+ defer dispatcher.Stop()
+ binder := test.NewVolumeBinderMock()
+ const errMsg = "error assuming pod volumes"
+ binder.SetAssumePodVolumesError(errMsg)
+ setVolumeBinder(context, binder)
+ NewPlaceholderManager(context.apiProvider.GetAPIs())
+ recorder := k8sEvents.NewFakeRecorder(1024)
+ events.SetRecorder(recorder)
+ defer events.SetRecorder(events.NewMockedRecorder())
+
err := callback.UpdateAllocation(&si.AllocationResponse{
New: []*si.Allocation{
{
@@ -103,13 +146,24 @@ func TestUpdateAllocation_NewTask_AssumePodFails(t
*testing.T) {
},
},
})
- assert.Error(t, err, errMsg)
+ assert.ErrorContains(t, err, "placeholder task does not have volume
bindings, AssumePod failed", "error should contain placeholder context")
+ assert.ErrorContains(t, err, errMsg, "error should wrap the original
AssumePod error")
assert.Assert(t, !context.schedulerCache.IsAssumedPod(taskUID1))
task := context.getTask(appID, taskUID1)
err = utils.WaitForCondition(func() bool {
return task.GetTaskState() == TaskStates().Failed
}, 10*time.Millisecond, time.Second)
- assert.NilError(t, err, "task has not transitioned to Failed state")
+ assert.NilError(t, err, "placeholder task has not transitioned to
Failed state")
+ // FailWithEvent emits a Warning/"AssumePodError" event; beforeTaskFail
emits a Normal/"TaskFailed" event.
+ assert.Equal(t, 2, len(recorder.Events), "expected two K8s events to be
recorded")
+ assumePodErrorFound := false
+ for i := 0; i < 2; i++ {
+ event := <-recorder.Events
+ if strings.Contains(event, "AssumePodError") {
+ assumePodErrorFound = true
+ }
+ }
+ assert.Assert(t, assumePodErrorFound, "no event with reason
'AssumePodError' was recorded")
}
func TestUpdateAllocation_NewTask_PodAlreadyAssigned(t *testing.T) {
diff --git a/pkg/cache/task.go b/pkg/cache/task.go
index e68b7b54..595b8c0c 100644
--- a/pkg/cache/task.go
+++ b/pkg/cache/task.go
@@ -620,6 +620,55 @@ func (task *Task) failWithEvent(errorMessage, actionReason
string) {
dispatcher.Dispatch(NewFailTaskEvent(task.applicationID, task.taskID,
errorMessage))
}
+// rollbackOnAssumePodFailure is called when AssumePod fails after all retries.
+// It resets task state and notifies the core to move the allocation back to a
+// pending ask so it can be re-scheduled on a different node.
+// Must be called without holding the task lock.
+func (task *Task) rollbackOnAssumePodFailure(allocationKey, nodeID string) {
+ // Read fields needed for event posting and release request.
+ // Clear stale node assignment under write lock so the task is clean
for the next allocation.
+ task.lock.Lock()
+ podCopy := task.pod.DeepCopy()
+ alias := task.alias
+ appID := task.applicationID
+ partition := task.application.partition
+ task.allocationKey = ""
+ task.nodeName = ""
+ task.lock.Unlock()
+
+ // Post a warning event so operators can see the retry via kubectl
describe pod.
+ events.GetRecorder().Eventf(podCopy, nil,
+ v1.EventTypeWarning, "AssumePodFailed", "AssumePodFailed",
+ "Node assignment failed for %s on node %s, it will be retried",
alias, nodeID)
+
+ // Revert any PV/PVC assumptions made by the volume binder. Idempotent:
safe to call
+ // even if AssumePodVolumes was never reached or already cleaned up
internally.
+ task.context.RevertPodVolumeAssumptions(allocationKey, nodeID)
+
+ // ForgetPod is idempotent: removes the pod from the assumed-pods cache.
+ task.context.ForgetPod(allocationKey)
+
+ // Notify the core to roll back the allocation to a pending ask.
+ if schedulerAPI := task.context.apiProvider.GetAPIs().SchedulerAPI;
schedulerAPI != nil {
+ releaseRequest := common.CreateReleaseRequestForTask(
+ appID,
+ allocationKey,
+ partition,
+ si.TerminationType_SCHEDULING_FAILED_ON_RM,
+ )
+ if err := schedulerAPI.UpdateAllocation(releaseRequest); err !=
nil {
+ log.Log(log.ShimCacheTask).Error("failed to send
rollback request to scheduler",
+ zap.String("appID", appID),
+ zap.String("allocationKey", allocationKey),
+ zap.Error(err))
+ }
+ }
+
+ log.Log(log.ShimCacheTask).Info("task allocation rolled back, will
retry on a different node",
+ zap.String("appID", appID),
+ zap.String("allocationKey", allocationKey))
+}
+
func (task *Task) SetTaskPod(pod *v1.Pod) {
task.lock.Lock()
defer task.lock.Unlock()
diff --git a/pkg/cache/task_test.go b/pkg/cache/task_test.go
index c8a51609..98184b2b 100644
--- a/pkg/cache/task_test.go
+++ b/pkg/cache/task_test.go
@@ -19,6 +19,9 @@
package cache
import (
+ "fmt"
+ "strings"
+ "sync/atomic"
"testing"
"time"
@@ -27,6 +30,7 @@ import (
schedulingv1 "k8s.io/api/scheduling/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ k8sEvents "k8s.io/client-go/tools/events"
"github.com/apache/yunikorn-k8shim/pkg/client"
"github.com/apache/yunikorn-k8shim/pkg/common/constants"
@@ -930,3 +934,92 @@ func TestCheckPodMetadataBeforeScheduling(t *testing.T) {
})
}
}
+
+// newRollbackTask creates a task in Scheduling state with allocationKey and
nodeName set,
+// ready to exercise rollbackOnAssumePodFailure.
+func newRollbackTask(ctx *Context, allocationKey, nodeID string) *Task {
+ app := NewApplication(appID1, queueNameA, testUser, testGroups,
map[string]string{},
+ ctx.apiProvider.GetAPIs().SchedulerAPI)
+ pod := &v1.Pod{
+ TypeMeta: metav1.TypeMeta{Kind: "Pod", APIVersion: "v1"},
+ ObjectMeta: metav1.ObjectMeta{Name: "rollback-pod", UID:
"rollback-uid"},
+ }
+ task := NewTask(allocationKey, app, ctx, pod)
+ task.sm.SetState(TaskStates().Scheduling)
+ task.allocationKey = allocationKey
+ task.nodeName = nodeID
+ return task
+}
+
+// TestRollbackOnAssumePodFailure_ClearsStateAndSendsRelease verifies the
happy-path:
+// allocationKey and nodeName are cleared, a Warning "AssumePodFailed" event
is posted,
+// and a SCHEDULING_FAILED_ON_RM release request is sent to the scheduler core.
+func TestRollbackOnAssumePodFailure_ClearsStateAndSendsRelease(t *testing.T) {
+ mockedContext, apiProvider := initContextAndAPIProviderForTest()
+
+ recorder := k8sEvents.NewFakeRecorder(1024)
+ events.SetRecorder(recorder)
+ defer events.SetRecorder(events.NewMockedRecorder())
+
+ var rollbackSent atomic.Bool
+ apiProvider.MockSchedulerAPIUpdateAllocationFn(func(request
*si.AllocationRequest) error {
+ if request.Releases != nil {
+ for _, rel := range
request.Releases.AllocationsToRelease {
+ if rel.TerminationType ==
si.TerminationType_SCHEDULING_FAILED_ON_RM &&
+ rel.AllocationKey == taskUID1 {
+ rollbackSent.Store(true)
+ }
+ }
+ }
+ return nil
+ })
+
+ task := newRollbackTask(mockedContext, taskUID1, fakeNodeName)
+ task.rollbackOnAssumePodFailure(taskUID1, fakeNodeName)
+
+ assert.Equal(t, "", task.GetAllocationKey(), "allocationKey should be
cleared after rollback")
+ assert.Equal(t, "", task.GetNodeName(), "nodeName should be cleared
after rollback")
+ assert.Assert(t, rollbackSent.Load(), "SCHEDULING_FAILED_ON_RM release
request should be sent to scheduler")
+
+ assert.Equal(t, 1, len(recorder.Events), "expected one AssumePodFailed
event")
+ event := <-recorder.Events
+ assert.Assert(t, strings.Contains(event, "AssumePodFailed"), "event
should contain AssumePodFailed reason")
+}
+
+// TestRollbackOnAssumePodFailure_UpdateAllocationError verifies that if the
scheduler
+// API returns an error, the task state (allocationKey, nodeName) is still
cleaned up
+// and the function does not panic.
+func TestRollbackOnAssumePodFailure_UpdateAllocationError(t *testing.T) {
+ mockedContext, apiProvider := initContextAndAPIProviderForTest()
+ events.SetRecorder(events.NewMockedRecorder())
+ defer events.SetRecorder(events.NewMockedRecorder())
+
+ apiProvider.MockSchedulerAPIUpdateAllocationFn(func(request
*si.AllocationRequest) error {
+ return fmt.Errorf("scheduler unavailable")
+ })
+
+ task := newRollbackTask(mockedContext, taskUID1, fakeNodeName)
+ // Must not panic even when UpdateAllocation fails.
+ task.rollbackOnAssumePodFailure(taskUID1, fakeNodeName)
+
+ assert.Equal(t, "", task.GetAllocationKey(), "allocationKey should be
cleared even when UpdateAllocation errors")
+ assert.Equal(t, "", task.GetNodeName(), "nodeName should be cleared
even when UpdateAllocation errors")
+}
+
+// TestRollbackOnAssumePodFailure_NilSchedulerAPI verifies that
rollbackOnAssumePodFailure
+// does not panic when the scheduler API is nil, and still clears the
allocation state.
+func TestRollbackOnAssumePodFailure_NilSchedulerAPI(t *testing.T) {
+ mockedContext, _ := initContextAndAPIProviderForTest()
+ events.SetRecorder(events.NewMockedRecorder())
+ defer events.SetRecorder(events.NewMockedRecorder())
+
+ // Simulate a context where the scheduler API is not available.
+ mockedContext.apiProvider.GetAPIs().SchedulerAPI = nil
+
+ task := newRollbackTask(mockedContext, taskUID1, fakeNodeName)
+ // Must not panic with a nil SchedulerAPI.
+ task.rollbackOnAssumePodFailure(taskUID1, fakeNodeName)
+
+ assert.Equal(t, "", task.GetAllocationKey(), "allocationKey should be
cleared even with nil SchedulerAPI")
+ assert.Equal(t, "", task.GetNodeName(), "nodeName should be cleared
even with nil SchedulerAPI")
+}
diff --git a/pkg/common/test/volumebinder_mock.go
b/pkg/common/test/volumebinder_mock.go
index 3bc38276..149d60a6 100644
--- a/pkg/common/test/volumebinder_mock.go
+++ b/pkg/common/test/volumebinder_mock.go
@@ -37,9 +37,10 @@ type VolumeBinderMock struct {
bindError error
conflictReasons volumebinding.ConflictReasons
- podVolumeClaim *volumebinding.PodVolumeClaims
- podVolumes *volumebinding.PodVolumes
- allBound bool
+ podVolumeClaim *volumebinding.PodVolumeClaims
+ podVolumes *volumebinding.PodVolumes
+ allBound bool
+ revertCalledCount int
}
func NewVolumeBinderMock() *VolumeBinderMock {
@@ -81,6 +82,15 @@ func (v *VolumeBinderMock) AssumePodVolumes(_ klog.Logger, _
*v1.Pod, _ string,
}
func (v *VolumeBinderMock) RevertAssumedPodVolumes(_
*volumebinding.PodVolumes) {
+ v.revertCalledCount++
+}
+
+func (v *VolumeBinderMock) RevertCalledCount() int {
+ return v.revertCalledCount
+}
+
+func (v *VolumeBinderMock) SetPodVolumes(podVolumes *volumebinding.PodVolumes)
{
+ v.podVolumes = podVolumes
}
func (v *VolumeBinderMock) BindPodVolumes(_ context.Context, _ *v1.Pod, _
*volumebinding.PodVolumes) error {
@@ -106,3 +116,7 @@ func (v *VolumeBinderMock) SetConflictReasons(reasons
...string) {
func (v *VolumeBinderMock) SetAssumePodVolumesError(message string) {
v.assumeVolumeError = errors.New(message)
}
+
+func (v *VolumeBinderMock) SetAllBound(allBound bool) {
+ v.allBound = allBound
+}
diff --git a/pkg/shim/scheduler_test.go b/pkg/shim/scheduler_test.go
index afdcf92c..ecfb8d4b 100644
--- a/pkg/shim/scheduler_test.go
+++ b/pkg/shim/scheduler_test.go
@@ -34,6 +34,7 @@ import (
"github.com/apache/yunikorn-k8shim/pkg/common"
"github.com/apache/yunikorn-k8shim/pkg/common/constants"
"github.com/apache/yunikorn-k8shim/pkg/common/test"
+ "github.com/apache/yunikorn-k8shim/pkg/common/utils"
"github.com/apache/yunikorn-scheduler-interface/lib/go/api"
siCommon "github.com/apache/yunikorn-scheduler-interface/lib/go/common"
"github.com/apache/yunikorn-scheduler-interface/lib/go/si"
@@ -248,12 +249,30 @@ func TestAssumePodError(t *testing.T) {
pod1 := createTestPod("root.a", "app0001", "task0001", taskResource)
cluster.AddPod(pod1)
- // expect app to enter Completing state with allocation+ask removed
- err = cluster.waitForApplicationStateInCore("app0001", partitionName,
"Completing")
- assert.NilError(t, err)
- app := cluster.getApplicationFromCore("app0001", partitionName)
- assert.Equal(t, 0, len(app.GetAllRequests()), "asks were not removed
from the application")
- assert.Equal(t, 0, len(app.GetAllAllocations()), "allocations were not
removed from the application")
+ // wait for core to deliver the first allocation to the shim; the
allocationKey is
+ // set in the callback before the AssumePod retry loop starts, so it
becomes
+ // visible immediately — well before the long exponential backoff runs
to completion.
+ err = utils.WaitForCondition(func() bool {
+ shimApp := cluster.context.GetApplication("app0001")
+ if shimApp == nil {
+ return false
+ }
+ task := shimApp.GetTask("task0001")
+ return task != nil && task.GetAllocationKey() != ""
+ }, time.Second, 30*time.Second)
+ assert.NilError(t, err, "task allocation key was never set; core did
not allocate the ask")
+
+ // with the rollback-on-AssumePod-failure implementation the task must
NOT fail;
+ // instead, each failed AssumePod attempt rolls the allocation back to
a pending
+ // ask in core so the scheduler can retry on a different node.
+ shimApp := cluster.context.GetApplication("app0001")
+ assert.Assert(t, shimApp != nil, "application should still be present
in the shim")
+ task := shimApp.GetTask("task0001")
+ assert.Assert(t, task != nil, "task should still be present in the
shim")
+ assert.Check(t, task.GetTaskState() == cache.TaskStates().Scheduling,
+ "task must be in Scheduling state while AssumePod retries are
in flight")
+ assert.Equal(t, 0, len(cluster.GetBoundPods(false)),
+ "no pods should be bound when AssumePod always fails")
}
func TestForeignPodTracking(t *testing.T) {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]