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-core.git
The following commit(s) were added to refs/heads/master by this push:
new 900bd603 [YUNIKORN-3316] Handle AssumePod failures in core (#1104)
900bd603 is described below
commit 900bd603df82609152d30de2f4811daf109380ed
Author: Aditya Maheshwari <[email protected]>
AuthorDate: Tue Jul 28 22:39:50 2026 +1000
[YUNIKORN-3316] Handle AssumePod failures in core (#1104)
During UpdateAllocation, shim tries to call AssumePod which updates
in-memory cache of k8s for pod volume bindings (not the actual binding).
We have retries in place to retry any failure during this step. But if
all retries get exhausted we fail the task and ask core to release the
allocation.
Instead of failing the task, we should retry again the task after all
the cleanup.
Core changes to handle updates of tasks with termination type
SCHEDULING_FAILED_ON_RM
Remove allocation from node, application, update and clear resources on
queue, update partition allocation count.
Closes: #1104
Signed-off-by: Wilfred Spiegelenburg <[email protected]>
---
go.mod | 2 +-
go.sum | 4 +-
pkg/scheduler/objects/application.go | 52 +++++++
pkg/scheduler/objects/application_test.go | 245 ++++++++++++++++++++++++++++++
pkg/scheduler/partition.go | 109 +++++++++----
pkg/scheduler/partition_test.go | 136 +++++++++++++++++
6 files changed, 517 insertions(+), 31 deletions(-)
diff --git a/go.mod b/go.mod
index c83ea670..76556ee2 100644
--- a/go.mod
+++ b/go.mod
@@ -22,7 +22,7 @@ module github.com/apache/yunikorn-core
go 1.25.0
require (
- github.com/apache/yunikorn-scheduler-interface
v0.0.0-20260528033204-c474acff6d53
+ github.com/apache/yunikorn-scheduler-interface
v0.0.0-20260727092410-674338955bdf
github.com/go-ldap/ldap/v3 v3.4.13
github.com/google/btree v1.1.3
github.com/google/go-cmp v0.7.0
diff --git a/go.sum b/go.sum
index d8100c43..10e3c159 100644
--- a/go.sum
+++ b/go.sum
@@ -2,8 +2,8 @@ github.com/Azure/go-ntlmssp v0.1.1
h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDP
github.com/Azure/go-ntlmssp v0.1.1/go.mod
h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e
h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod
h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
-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-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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod
h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0
h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
diff --git a/pkg/scheduler/objects/application.go
b/pkg/scheduler/objects/application.go
index 694924a4..d22f094e 100644
--- a/pkg/scheduler/objects/application.go
+++ b/pkg/scheduler/objects/application.go
@@ -514,6 +514,17 @@ func (sa *Application) GetAllocationAsk(allocationKey
string) *Allocation {
return sa.requests[allocationKey]
}
+// GetAllocationNodeID returns the node ID for a confirmed allocation
identified by allocationKey.
+// Returns an empty string if the allocation does not exist.
+func (sa *Application) GetAllocationNodeID(allocationKey string) string {
+ sa.RLock()
+ defer sa.RUnlock()
+ if alloc := sa.allocations[allocationKey]; alloc != nil {
+ return alloc.GetNodeID()
+ }
+ return ""
+}
+
// GetAllocatedResource returns the currently allocated resources for this
application
func (sa *Application) GetAllocatedResource() *resources.Resource {
sa.RLock()
@@ -799,6 +810,47 @@ func (sa *Application) DeallocateAsk(allocKey string)
(*resources.Resource, erro
return nil, fmt.Errorf("failed to locate ask with key %s", allocKey)
}
+// RollbackAllocation atomically reverts an allocated ask back to pending
state.
+// This is used when the shim fails to bind a pod after the core has allocated
it,
+// allowing the task to be re-scheduled on a different node.
+// The application must be in Accepted or Running state for rollback to
succeed.
+func (sa *Application) RollbackAllocation(allocKey string)
(*resources.Resource, error) {
+ sa.Lock()
+ defer sa.Unlock()
+
+ if !sa.IsAccepted() && !sa.IsRunning() {
+ return nil, fmt.Errorf("cannot rollback allocation %s:
application %s is in state %s", allocKey, sa.ApplicationID, sa.CurrentState())
+ }
+
+ ask := sa.allocations[allocKey]
+ if ask == nil {
+ return nil, fmt.Errorf("failed to locate allocation with key %s
for rollback", allocKey)
+ }
+
+ // Restore pending FIRST. This ensures hasZeroAllocations() sees
pending > 0 when
+ // we later decrement allocatedResource, preventing a spurious
CompleteApplication event.
+ if _, err := sa.deallocateAsk(ask); err != nil {
+ return nil, fmt.Errorf("failed to deallocate ask %s during
rollback on app %s: %w", allocKey, sa.ApplicationID, err)
+ }
+
+ // Clear stale node assignment on the ask so it is re-schedulable
cleanly.
+ ask.SetNodeID("")
+
+ res := ask.GetAllocatedResource()
+ sa.allocatedResource = resources.Sub(sa.allocatedResource, res)
+ sa.allocatedResource.Prune()
+ sa.decUserResourceUsage(res, false)
+ delete(sa.allocations, allocKey)
+
+ sa.appEvents.SendRemoveAllocationEvent(sa.ApplicationID, allocKey, res,
si.TerminationType_SCHEDULING_FAILED_ON_RM)
+
+ log.Log(log.SchedApplication).Info("allocation rolled back to pending
ask",
+ zap.String("appID", sa.ApplicationID),
+ zap.String("allocationKey", allocKey))
+
+ return res, nil
+}
+
func (sa *Application) allocateAsk(ask *Allocation) (*resources.Resource,
error) {
if !ask.allocate() {
return nil, fmt.Errorf("unable to allocate previously allocated
ask %s on app %s", ask.GetAllocationKey(), sa.ApplicationID)
diff --git a/pkg/scheduler/objects/application_test.go
b/pkg/scheduler/objects/application_test.go
index 7233788d..2d21f250 100644
--- a/pkg/scheduler/objects/application_test.go
+++ b/pkg/scheduler/objects/application_test.go
@@ -4012,6 +4012,251 @@ func TestAppSubmissionTime(t *testing.T) {
assert.Equal(t, app.submissionTime, time.Unix(0, 30), "app submission
time is not set properly")
}
+// TestGetAllocationNodeID verifies the node ID lookup for confirmed
allocations.
+func TestGetAllocationNodeID(t *testing.T) {
+ app := newApplication(appID1, "default", "root.unknown")
+
+ // Unknown key returns empty string.
+ assert.Equal(t, "", app.GetAllocationNodeID("nonexistent"), "unknown
key should return empty string")
+
+ // Empty key returns empty string.
+ assert.Equal(t, "", app.GetAllocationNodeID(""), "empty key should
return empty string")
+
+ // Existing allocation returns the correct node ID.
+ res :=
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 5})
+ alloc := newAllocationWithKey(aKey, appID1, nodeID1, res)
+ app.allocations[aKey] = alloc
+ assert.Equal(t, nodeID1, app.GetAllocationNodeID(aKey), "existing
allocation should return its node ID")
+
+ // A second allocation with a different node returns its own node ID
independently.
+ alloc2 := newAllocationWithKey(aKey2, appID1, nodeID2, res)
+ app.allocations[aKey2] = alloc2
+ assert.Equal(t, nodeID2, app.GetAllocationNodeID(aKey2), "second
allocation should return its own node ID")
+ assert.Equal(t, nodeID1, app.GetAllocationNodeID(aKey), "first
allocation node ID should be unchanged")
+
+ // After removing an allocation, the key returns empty string again.
+ delete(app.allocations, aKey)
+ assert.Equal(t, "", app.GetAllocationNodeID(aKey), "removed allocation
should return empty string")
+}
+
+// TestRollbackAllocationInvalidState verifies that RollbackAllocation returns
an error
+// when the application is not in Accepted or Running state.
+func TestRollbackAllocationInvalidState(t *testing.T) {
+ setupUGM()
+ queue, err := createRootQueue(nil)
+ assert.NilError(t, err, "queue create failed")
+ res :=
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 5})
+
+ for _, state := range []string{New.String(), Completing.String(),
Completed.String(), Rejected.String(), Failed.String()} {
+ app := newApplication(appID1, "default", "root.unknown")
+ app.queue = queue
+ app.SetState(state)
+ // inject a fake ask and allocation so the state check is the
only failure path
+ ask := newAllocationAsk(aKey, appID1, res)
+ app.requests[aKey] = ask
+ alloc := newAllocationWithKey(aKey, appID1, nodeID1, res)
+ app.allocations[aKey] = alloc
+
+ _, rollbackErr := app.RollbackAllocation(aKey)
+ assert.Assert(t, rollbackErr != nil, "expected error for state
%s", state)
+ assert.Assert(t, strings.Contains(rollbackErr.Error(), state),
"error should mention the invalid state %q, got: %s", state,
rollbackErr.Error())
+ }
+}
+
+// TestRollbackAllocationAskNotFound verifies that RollbackAllocation returns
an error
+// when the ask key does not exist in the application's request map.
+func TestRollbackAllocationAskNotFound(t *testing.T) {
+ setupUGM()
+ queue, err := createRootQueue(nil)
+ assert.NilError(t, err, "queue create failed")
+ app := newApplication(appID1, "default", "root.unknown")
+ app.queue = queue
+ app.SetState(Accepted.String())
+
+ _, err = app.RollbackAllocation("nonexistent-key")
+ assert.Assert(t, err != nil, "expected error for missing ask key")
+ assert.Assert(t, strings.Contains(err.Error(), "nonexistent-key"),
"error should mention the missing key")
+}
+
+// TestRollbackAllocationAllocationNotFound verifies that RollbackAllocation
returns an
+// error when the ask exists in requests but no corresponding allocation has
been recorded.
+func TestRollbackAllocationAllocationNotFound(t *testing.T) {
+ setupUGM()
+ queue, err := createRootQueue(nil)
+ assert.NilError(t, err, "queue create failed")
+ app := newApplication(appID1, "default", "root.unknown")
+ app.queue = queue
+ res :=
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 5})
+
+ ask := newAllocationAsk(aKey, appID1, res)
+ err = app.AddAllocationAsk(ask)
+ assert.NilError(t, err, "ask should have been added to app")
+ assert.Assert(t, app.IsAccepted(), "app should be in Accepted state")
+ // ask is in requests but no allocation has been added
+
+ _, err = app.RollbackAllocation(aKey)
+ assert.Assert(t, err != nil, "expected error when no allocation exists
for the ask")
+ assert.Assert(t, strings.Contains(err.Error(), aKey), "error should
mention the missing allocation key")
+}
+
+// TestRollbackAllocationFromAccepted verifies a successful rollback when the
application
+// is in Accepted state. The allocation is injected directly into internal
state to avoid
+// the Running-state transition triggered by AddAllocation.
+func TestRollbackAllocationFromAccepted(t *testing.T) {
+ setupUGM()
+ queue, err := createRootQueue(nil)
+ assert.NilError(t, err, "queue create failed")
+ app := newApplication(appID1, "default", "root.unknown")
+ app.queue = queue
+ res :=
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 5})
+
+ // Add ask → app moves to Accepted, ask enters requests.
+ ask := newAllocationAsk(aKey, appID1, res)
+ err = app.AddAllocationAsk(ask)
+ assert.NilError(t, err, "ask should have been added to app")
+ assert.Assert(t, app.IsAccepted(), "app should be in Accepted state")
+
+ // Simulate the core allocating the ask: mark it allocated and record
the allocation
+ // in internal state without triggering the Running state transition.
+ _, err = app.AllocateAsk(aKey)
+ assert.NilError(t, err, "AllocateAsk should succeed")
+ ask.SetNodeID(nodeID1)
+
+ app.Lock()
+ app.allocations[aKey] = ask
+ app.allocatedResource = resources.Add(app.allocatedResource, res)
+ app.incUserResourceUsage(res)
+ app.Unlock()
+
+ assert.Assert(t, app.IsAccepted(), "app should still be in Accepted
state before rollback")
+
+ // Execute rollback.
+ returned, err := app.RollbackAllocation(aKey)
+ assert.NilError(t, err, "rollback should succeed from Accepted state")
+ assert.Assert(t, resources.Equals(returned, res), "returned resource
should match allocation resource, got %v", returned)
+
+ // Allocation is removed from the map.
+ assert.Assert(t, app.allocations[aKey] == nil, "allocation should have
been removed from map")
+
+ // Pending resource is restored.
+ assert.Assert(t, resources.Equals(app.GetPendingResource(), res),
"pending resource should be restored, got %v", app.GetPendingResource())
+
+ // Ask still exists in requests (rolled back to pending, not deleted).
+ assert.Assert(t, app.GetAllocationAsk(aKey) != nil, "ask should still
exist in requests after rollback")
+
+ // allocatedResource is back to zero.
+ assert.Assert(t, resources.IsZero(app.GetAllocatedResource()),
"allocated resource should be zero after rollback, got %v",
app.GetAllocatedResource())
+
+ // Node ID is cleared on the ask so it can be rescheduled on a
different node.
+ assert.Equal(t, app.GetAllocationAsk(aKey).GetNodeID(), "", "ask node
ID should be cleared after rollback")
+
+ // App remains in Accepted state (pending > 0 prevents Completing
transition).
+ assert.Assert(t, app.IsAccepted(), "app should remain in Accepted state
after rollback")
+}
+
+// TestRollbackAllocationFromRunning verifies a successful rollback from
Running state,
+// including full resource accounting and UGM tracking validation.
+func TestRollbackAllocationFromRunning(t *testing.T) {
+ setupUGM()
+ queue, err := createRootQueue(nil)
+ assert.NilError(t, err, "queue create failed")
+ app := newApplication(appID1, "default", "root.unknown")
+ app.queue = queue
+ res :=
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 5})
+
+ // Standard allocation flow: ask → Accepted, allocate → Running.
+ ask := newAllocationAsk(aKey, appID1, res)
+ err = app.AddAllocationAsk(ask)
+ assert.NilError(t, err, "ask should have been added to app")
+ assert.Assert(t, app.IsAccepted(), "app should be in Accepted state
after adding ask")
+
+ delta, err := app.AllocateAsk(aKey)
+ assert.NilError(t, err, "AllocateAsk should succeed")
+ assert.Assert(t, resources.Equals(delta, res), "AllocateAsk delta
should equal res, got %v", delta)
+ ask.SetNodeID(nodeID1)
+
+ app.AddAllocation(ask)
+ assert.Assert(t, app.IsRunning(), "app should be in Running state after
AddAllocation")
+ assert.Assert(t, resources.IsZero(app.GetPendingResource()), "pending
should be zero before rollback")
+ assert.Assert(t, resources.Equals(app.GetAllocatedResource(), res),
"allocated should equal res before rollback")
+ assertUserGroupResource(t, getTestUserGroup(), res)
+
+ // Execute rollback.
+ returned, err := app.RollbackAllocation(aKey)
+ assert.NilError(t, err, "rollback should succeed from Running state")
+ assert.Assert(t, resources.Equals(returned, res), "returned resource
should match allocation resource, got %v", returned)
+
+ // Allocation is removed.
+ assert.Assert(t, app.allocations[aKey] == nil, "allocation should have
been removed from map")
+
+ // Pending resource is restored.
+ assert.Assert(t, resources.Equals(app.GetPendingResource(), res),
"pending resource should be restored after rollback, got %v",
app.GetPendingResource())
+
+ // Ask still exists in requests.
+ assert.Assert(t, app.GetAllocationAsk(aKey) != nil, "ask should still
exist in requests after rollback")
+
+ // allocatedResource is back to zero.
+ assert.Assert(t, resources.IsZero(app.GetAllocatedResource()),
"allocated resource should be zero after rollback, got %v",
app.GetAllocatedResource())
+
+ // Node ID is cleared so the ask can land on a different node.
+ assert.Equal(t, app.GetAllocationAsk(aKey).GetNodeID(), "", "ask node
ID should be cleared after rollback")
+
+ // UGM resource usage is decremented.
+ assertUserGroupResource(t, getTestUserGroup(), nil)
+}
+
+// TestRollbackAllocationPartial verifies that rolling back one of several
allocations
+// leaves the remaining allocations and their accounting untouched.
+func TestRollbackAllocationPartial(t *testing.T) {
+ setupUGM()
+ queue, err := createRootQueue(nil)
+ assert.NilError(t, err, "queue create failed")
+ app := newApplication(appID1, "default", "root.unknown")
+ app.queue = queue
+ res :=
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 5})
+
+ // Allocate first ask.
+ ask1 := newAllocationAsk(aKey, appID1, res)
+ err = app.AddAllocationAsk(ask1)
+ assert.NilError(t, err, "ask1 should have been added")
+ _, err = app.AllocateAsk(aKey)
+ assert.NilError(t, err, "AllocateAsk for ask1 should succeed")
+ ask1.SetNodeID(nodeID1)
+ app.AddAllocation(ask1)
+
+ // Allocate second ask.
+ ask2 := newAllocationAsk(aKey2, appID1, res)
+ err = app.AddAllocationAsk(ask2)
+ assert.NilError(t, err, "ask2 should have been added")
+ _, err = app.AllocateAsk(aKey2)
+ assert.NilError(t, err, "AllocateAsk for ask2 should succeed")
+ ask2.SetNodeID(nodeID2)
+ app.AddAllocation(ask2)
+
+ doubleRes := resources.Multiply(res, 2)
+ assert.Assert(t, app.IsRunning(), "app should be Running with two
allocations")
+ assert.Assert(t, resources.Equals(app.GetAllocatedResource(),
doubleRes), "allocated should be 2x res, got %v", app.GetAllocatedResource())
+ assertUserGroupResource(t, getTestUserGroup(), doubleRes)
+
+ // Roll back only the first allocation.
+ returned, err := app.RollbackAllocation(aKey)
+ assert.NilError(t, err, "partial rollback should succeed")
+ assert.Assert(t, resources.Equals(returned, res), "returned resource
should equal res, got %v", returned)
+
+ // Second allocation is untouched.
+ assert.Assert(t, app.allocations[aKey2] != nil, "second allocation
should still exist")
+ assert.Assert(t, resources.Equals(app.GetAllocatedResource(), res),
"allocated should be 1x res after partial rollback, got %v",
app.GetAllocatedResource())
+
+ // First ask is back to pending; second ask remains allocated.
+ assert.Assert(t, resources.Equals(app.GetPendingResource(), res),
"pending should be restored to res for the rolled-back ask, got %v",
app.GetPendingResource())
+
+ // UGM reflects only the remaining allocation.
+ assertUserGroupResource(t, getTestUserGroup(), res)
+
+ // App remains Running because the second allocation is still active.
+ assert.Assert(t, app.IsRunning(), "app should remain Running after
partial rollback")
+}
+
func TestApplicationBackoff(t *testing.T) {
rootQ, err := createRootQueue(map[string]string{"first": "10"})
assert.NilError(t, err, "unexpected error when creating root queue")
diff --git a/pkg/scheduler/partition.go b/pkg/scheduler/partition.go
index 28a4859f..3a8d2ddc 100644
--- a/pkg/scheduler/partition.go
+++ b/pkg/scheduler/partition.go
@@ -1504,6 +1504,14 @@ func (pc *PartitionContext) removeAllocation(release
*si.AllocationRelease) ([]*
return nil, nil
}
+ // Handle shim-initiated scheduling failure: roll the allocation back
to a pending ask
+ // so the core can re-schedule it on a different node. This bypasses
the normal
+ // remove-and-destroy path entirely. We return nil,nil to suppress the
echo back to
+ // the shim (the shim initiated this release; no confirmation is
needed).
+ if release.TerminationType ==
si.TerminationType_SCHEDULING_FAILED_ON_RM {
+ return pc.rollbackAllocation(appID, allocationKey, app)
+ }
+
// **** DO NOT MOVE **** this must be called before any allocations are
released.
// Processing a removal while in the Completing state could race with
the state change. The race occurs between
// removing the allocation and updating the queue after node
processing. If the state change removes the queue link
@@ -1528,34 +1536,7 @@ func (pc *PartitionContext) removeAllocation(release
*si.AllocationRelease) ([]*
continue
}
if release.TerminationType ==
si.TerminationType_PLACEHOLDER_REPLACED {
- confirmed = alloc.GetRelease()
- // we need to check the resources equality
- delta :=
resources.Sub(confirmed.GetAllocatedResource(), alloc.GetAllocatedResource())
- // Any negative value in the delta means that at least
one of the requested resource in the
- // placeholder is larger than the real allocation. The
node and queue need adjusting.
- // The reverse case is handled during allocation.
- if delta.HasNegativeValue() {
- // This looks incorrect but the delta is
negative and the result will be an increase of the
- // total tracked. The total will later be
deducted from the queue usage.
- total.SubFrom(delta)
- log.Log(log.SchedPartition).Warn("replacing
placeholder: placeholder is larger than real allocation",
- zap.String("allocationKey",
confirmed.GetAllocationKey()),
- zap.Stringer("requested resource",
confirmed.GetAllocatedResource()),
- zap.String("placeholderKey",
alloc.GetAllocationKey()),
- zap.Stringer("placeholder resource",
alloc.GetAllocatedResource()))
- }
- // replacements could be on a different node and
different size handle all cases
- if confirmed.GetNodeID() == alloc.GetNodeID() {
- // this is the real swap on the node, adjust
usage if needed
-
node.ReplaceAllocation(alloc.GetAllocationKey(), confirmed, delta)
- } else {
- // we have already added the real allocation to
the new node, just remove the placeholder
- node.RemoveAllocation(alloc.GetAllocationKey())
- }
- log.Log(log.SchedPartition).Info("replacing placeholder
allocation on node",
- zap.String("nodeID", alloc.GetNodeID()),
- zap.String("allocationKey",
alloc.GetAllocationKey()),
- zap.String("allocation nodeID",
confirmed.GetNodeID()))
+ confirmed = pc.replacePlaceholderOnNode(alloc, node,
total)
} else if node.RemoveAllocation(alloc.GetAllocationKey()) !=
nil {
// all non replacement are real removes: must update
the queue usage
total.AddTo(alloc.GetAllocatedResource())
@@ -1605,6 +1586,78 @@ func (pc *PartitionContext) removeAllocation(release
*si.AllocationRelease) ([]*
return released, confirmed
}
+// replacePlaceholderOnNode handles a placeholder replacement on a node. It
adjusts the total resource tracking
+// when the placeholder is larger than the real allocation, updates the node
accordingly, and returns the confirmed
+// real allocation.
+func (pc *PartitionContext) replacePlaceholderOnNode(alloc
*objects.Allocation, node *objects.Node, total *resources.Resource)
*objects.Allocation {
+ confirmed := alloc.GetRelease()
+ // we need to check the resources equality
+ delta := resources.Sub(confirmed.GetAllocatedResource(),
alloc.GetAllocatedResource())
+ // Any negative value in the delta means that at least one of the
requested resource in the
+ // placeholder is larger than the real allocation. The node and queue
need adjusting.
+ // The reverse case is handled during allocation.
+ if delta.HasNegativeValue() {
+ // This looks incorrect but the delta is negative and the
result will be an increase of the
+ // total tracked. The total will later be deducted from the
queue usage.
+ total.SubFrom(delta)
+ log.Log(log.SchedPartition).Warn("replacing placeholder:
placeholder is larger than real allocation",
+ zap.String("allocationKey",
confirmed.GetAllocationKey()),
+ zap.Stringer("requested resource",
confirmed.GetAllocatedResource()),
+ zap.String("placeholderKey", alloc.GetAllocationKey()),
+ zap.Stringer("placeholder resource",
alloc.GetAllocatedResource()))
+ }
+ // replacements could be on a different node and different size handle
all cases
+ if confirmed.GetNodeID() == alloc.GetNodeID() {
+ // this is the real swap on the node, adjust usage if needed
+ node.ReplaceAllocation(alloc.GetAllocationKey(), confirmed,
delta)
+ } else {
+ // we have already added the real allocation to the new node,
just remove the placeholder
+ node.RemoveAllocation(alloc.GetAllocationKey())
+ }
+ log.Log(log.SchedPartition).Info("replacing placeholder allocation on
node",
+ zap.String("nodeID", alloc.GetNodeID()),
+ zap.String("allocationKey", alloc.GetAllocationKey()),
+ zap.String("allocation nodeID", confirmed.GetNodeID()))
+ return confirmed
+}
+
+// rollbackAllocation handles a shim-initiated scheduling failure by rolling
the allocation back to a pending ask
+// so the core can re-schedule it on a different node.
+// NOTE: this is a lock free call. It must NOT be called holding the
PartitionContext lock.
+func (pc *PartitionContext) rollbackAllocation(appID, allocationKey string,
app *objects.Application) ([]*objects.Allocation, *objects.Allocation) {
+ queue := app.GetQueue()
+ // Retrieve node ID before rolling back (RollbackAllocation clears it
on the ask).
+ nodeID := app.GetAllocationNodeID(allocationKey)
+ res, err := app.RollbackAllocation(allocationKey)
+ if err != nil {
+ log.Log(log.SchedPartition).Warn("failed to rollback
allocation",
+ zap.String("appID", appID),
+ zap.String("allocationKey", allocationKey),
+ zap.Error(err))
+ return nil, nil
+ }
+ if node := pc.GetNode(nodeID); node != nil {
+ node.RemoveAllocation(allocationKey)
+ } else {
+ log.Log(log.SchedPartition).Warn("node not found while rolling
back allocation",
+ zap.String("appID", appID),
+ zap.String("allocationKey", allocationKey),
+ zap.String("nodeID", nodeID))
+ }
+ if err := queue.DecAllocatedResource(res); err != nil {
+ log.Log(log.SchedPartition).Warn("failed to release resources
from queue during rollback",
+ zap.String("appID", appID),
+ zap.String("allocationKey", allocationKey),
+ zap.Error(err))
+ }
+ pc.updateAllocationCount(-1)
+ log.Log(log.SchedPartition).Info("allocation rolled back to pending
ask",
+ zap.String("appID", appID),
+ zap.String("allocationKey", allocationKey),
+ zap.String("nodeID", nodeID))
+ return nil, nil
+}
+
// updatePhAllocationCount checks the released allocations and updates the
partition context counter of allocated
// placeholders.
func (pc *PartitionContext) updatePhAllocationCount(released
[]*objects.Allocation) {
diff --git a/pkg/scheduler/partition_test.go b/pkg/scheduler/partition_test.go
index f832f2b4..3368ae0c 100644
--- a/pkg/scheduler/partition_test.go
+++ b/pkg/scheduler/partition_test.go
@@ -5426,3 +5426,139 @@ func TestUpdateResolver(t *testing.T) {
assert.NilError(t, err, "unable to update partition config")
assert.Equal(t, partition.GetUserGroupResolverType(), "test", "resolver
cannot be updated on running partition")
}
+
+// TestRemoveAllocationSchedulingFailedOnRM verifies that a
SCHEDULING_FAILED_ON_RM release rolls the
+// allocation back to a pending ask: allocation count decrements, node
allocation is removed, and the
+// queue allocated resource is decremented. The function must return nil, nil
(no echo to the shim).
+func TestRemoveAllocationSchedulingFailedOnRM(t *testing.T) {
+ setupUGM()
+ partition, err := newBasePartition()
+ assert.NilError(t, err, "partition create failed")
+ defer partition.userGroupCache.Stop()
+
+ nodeRes :=
resources.NewResourceFromMap(map[string]resources.Quantity{"vcore": 10})
+ node := setupNode(t, nodeID1, partition, nodeRes)
+
+ app := newApplication(appID1, "default", defQueue)
+ err = partition.AddApplication(app)
+ assert.NilError(t, err, "add application failed")
+
+ askRes :=
resources.NewResourceFromMap(map[string]resources.Quantity{"vcore": 1})
+
+ // Add a pending ask, then transition it to allocated on nodeID1.
+ ask := newAllocationAsk(allocKey, appID1, askRes)
+ _, _, err = partition.UpdateAllocation(ask)
+ assert.NilError(t, err, "add ask failed")
+
+ alloc := newAllocation(allocKey, appID1, nodeID1, askRes)
+ _, allocCreated, err := partition.UpdateAllocation(alloc)
+ assert.NilError(t, err, "transition to allocated failed")
+ assert.Check(t, allocCreated, "allocation should have been created")
+
+ assert.Equal(t, 1, partition.GetTotalAllocationCount(), "allocation
count should be 1 before rollback")
+ assert.Assert(t, node.GetAllocation(allocKey) != nil, "node should have
the allocation before rollback")
+ queueAllocBefore :=
partition.GetQueue(defQueue).GetAllocatedResource().Clone()
+ assert.Assert(t, resources.StrictlyGreaterThanZero(queueAllocBefore),
"queue should have allocated resources before rollback")
+
+ release := &si.AllocationRelease{
+ PartitionName: "test",
+ ApplicationID: appID1,
+ AllocationKey: allocKey,
+ TerminationType: si.TerminationType_SCHEDULING_FAILED_ON_RM,
+ }
+ released, confirmed := partition.removeAllocation(release)
+
+ assert.Assert(t, released == nil, "SCHEDULING_FAILED_ON_RM should not
echo released allocations to the shim")
+ assert.Assert(t, confirmed == nil, "SCHEDULING_FAILED_ON_RM should not
return a confirmed allocation")
+ assert.Equal(t, 0, partition.GetTotalAllocationCount(), "allocation
count should be 0 after rollback")
+ assert.Assert(t, node.GetAllocation(allocKey) == nil, "node should not
have the allocation after rollback")
+ assert.Assert(t,
resources.IsZero(partition.GetQueue(defQueue).GetAllocatedResource()), "queue
allocated resource should be zero after rollback")
+ assert.Assert(t,
resources.StrictlyGreaterThanZero(app.GetPendingResource()), "ask should be
pending again after rollback")
+}
+
+// TestRemoveAllocationSchedulingFailedOnRMRollbackError verifies that when
RollbackAllocation returns an
+// error (allocation key not found in app), removeAllocation logs a warning
and returns nil, nil without
+// modifying the allocation count.
+func TestRemoveAllocationSchedulingFailedOnRMRollbackError(t *testing.T) {
+ setupUGM()
+ partition, err := newBasePartition()
+ assert.NilError(t, err, "partition create failed")
+ defer partition.userGroupCache.Stop()
+
+ nodeRes :=
resources.NewResourceFromMap(map[string]resources.Quantity{"vcore": 10})
+ setupNode(t, nodeID1, partition, nodeRes)
+
+ app := newApplication(appID1, "default", defQueue)
+ err = partition.AddApplication(app)
+ assert.NilError(t, err, "add application failed")
+
+ askRes :=
resources.NewResourceFromMap(map[string]resources.Quantity{"vcore": 1})
+ ask := newAllocationAsk(allocKey, appID1, askRes)
+ _, _, err = partition.UpdateAllocation(ask)
+ assert.NilError(t, err, "add ask failed")
+
+ alloc := newAllocation(allocKey, appID1, nodeID1, askRes)
+ _, allocCreated, err := partition.UpdateAllocation(alloc)
+ assert.NilError(t, err, "transition to allocated failed")
+ assert.Check(t, allocCreated)
+
+ // Use a non-existent allocation key to force RollbackAllocation to
fail.
+ release := &si.AllocationRelease{
+ PartitionName: "test",
+ ApplicationID: appID1,
+ AllocationKey: "does-not-exist",
+ TerminationType: si.TerminationType_SCHEDULING_FAILED_ON_RM,
+ }
+ released, confirmed := partition.removeAllocation(release)
+
+ assert.Assert(t, released == nil, "should return nil on rollback error")
+ assert.Assert(t, confirmed == nil, "should return nil on rollback
error")
+ // Allocation count must be unchanged since the rollback failed.
+ assert.Equal(t, 1, partition.GetTotalAllocationCount(), "allocation
count should be unchanged after failed rollback")
+}
+
+// TestRemoveAllocationSchedulingFailedOnRMNodeNotFound verifies that when the
node referenced by the
+// allocation has already been removed, the rollback still succeeds: the
allocation is removed from the
+// app and queue, the allocation count is decremented, and the function
returns nil, nil.
+func TestRemoveAllocationSchedulingFailedOnRMNodeNotFound(t *testing.T) {
+ setupUGM()
+ partition, err := newBasePartition()
+ assert.NilError(t, err, "partition create failed")
+ defer partition.userGroupCache.Stop()
+
+ nodeRes :=
resources.NewResourceFromMap(map[string]resources.Quantity{"vcore": 10})
+ setupNode(t, nodeID1, partition, nodeRes)
+
+ app := newApplication(appID1, "default", defQueue)
+ err = partition.AddApplication(app)
+ assert.NilError(t, err, "add application failed")
+
+ askRes :=
resources.NewResourceFromMap(map[string]resources.Quantity{"vcore": 1})
+ ask := newAllocationAsk(allocKey, appID1, askRes)
+ _, _, err = partition.UpdateAllocation(ask)
+ assert.NilError(t, err, "add ask failed")
+
+ alloc := newAllocation(allocKey, appID1, nodeID1, askRes)
+ _, allocCreated, err := partition.UpdateAllocation(alloc)
+ assert.NilError(t, err, "transition to allocated failed")
+ assert.Check(t, allocCreated)
+
+ assert.Equal(t, 1, partition.GetTotalAllocationCount(), "allocation
count should be 1 before rollback")
+
+ // Remove the node to simulate it disappearing before the shim sends
the failure release.
+ partition.nodes.RemoveNode(nodeID1)
+
+ release := &si.AllocationRelease{
+ PartitionName: "test",
+ ApplicationID: appID1,
+ AllocationKey: allocKey,
+ TerminationType: si.TerminationType_SCHEDULING_FAILED_ON_RM,
+ }
+ released, confirmed := partition.removeAllocation(release)
+
+ assert.Assert(t, released == nil, "SCHEDULING_FAILED_ON_RM should not
echo released allocations to the shim")
+ assert.Assert(t, confirmed == nil, "SCHEDULING_FAILED_ON_RM should not
return a confirmed allocation")
+ assert.Equal(t, 0, partition.GetTotalAllocationCount(), "allocation
count should be decremented even when node is missing")
+ assert.Assert(t,
resources.IsZero(partition.GetQueue(defQueue).GetAllocatedResource()), "queue
resource should be zero after rollback")
+ assert.Assert(t,
resources.StrictlyGreaterThanZero(app.GetPendingResource()), "ask should be
pending again after rollback")
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]