Skip to content

Commit f674ea9

Browse files
committed
[YUNIKORN-287] reserved ask release double release (#185)
When an ask gets released that is reserved and being allocated the counters tracking reservations can be updated twice for one ask release. The counters keep track of the number of reservations on a queue or partition. This can lead to other reservations being ignored. Nodes that have been reserved by other asks for the same app will be skipped during scheduling. In a small cluster this could lead to resource starvation Fixes: #185
1 parent 4ea58b5 commit f674ea9

10 files changed

+189
-129
lines changed

pkg/scheduler/scheduling_application.go

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -149,16 +149,17 @@ func (sa *SchedulingApplication) removeAllocationAsk(allocKey string) int {
149149
if allocKey == "" {
150150
// cleanup all reservations
151151
for key, reserve := range sa.reservations {
152-
_, err := reserve.unReserve()
152+
releases, err := sa.unReserveInternal(reserve.node, reserve.ask)
153153
if err != nil {
154154
log.Logger().Warn("Removal of reservation failed while removing all allocation asks",
155155
zap.String("appID", sa.ApplicationInfo.ApplicationID),
156156
zap.String("reservationKey", key),
157157
zap.Error(err))
158+
continue
158159
}
159160
// clean up the queue reservation (one at a time)
160-
sa.queue.unReserve(sa.ApplicationInfo.ApplicationID)
161-
toRelease++
161+
sa.queue.unReserve(sa.ApplicationInfo.ApplicationID, releases)
162+
toRelease += releases
162163
}
163164
// Cleanup total pending resource
164165
deltaPendingResource = sa.pending
@@ -167,16 +168,18 @@ func (sa *SchedulingApplication) removeAllocationAsk(allocKey string) int {
167168
} else {
168169
// cleanup the reservation for this allocation
169170
for _, key := range sa.isAskReserved(allocKey) {
170-
_, err := sa.reservations[key].unReserve()
171+
reserve := sa.reservations[key]
172+
releases, err := sa.unReserveInternal(reserve.node, reserve.ask)
171173
if err != nil {
172174
log.Logger().Warn("Removal of reservation failed while removing allocation ask",
173175
zap.String("appID", sa.ApplicationInfo.ApplicationID),
174176
zap.String("reservationKey", key),
175177
zap.Error(err))
178+
continue
176179
}
177180
// clean up the queue reservation
178-
sa.queue.unReserve(sa.ApplicationInfo.ApplicationID)
179-
toRelease++
181+
sa.queue.unReserve(sa.ApplicationInfo.ApplicationID, releases)
182+
toRelease += releases
180183
}
181184
if ask := sa.requests[allocKey]; ask != nil {
182185
deltaPendingResource = resources.MultiplyBy(ask.AllocatedResource, float64(ask.getPendingAskRepeat()))
@@ -325,39 +328,50 @@ func (sa *SchedulingApplication) reserve(node *SchedulingNode, ask *schedulingAl
325328

326329
// unReserve the application for this node and ask combination.
327330
// This first removes the reservation from the node.
328-
// The error is set if the reservation key cannot be generated on the app or node.
329-
// If the reservation does not exist it returns false, if the reservation is removed it returns true.
330-
func (sa *SchedulingApplication) unReserve(node *SchedulingNode, ask *schedulingAllocationAsk) error {
331+
// If the reservation does not exist it returns 0 for reservations removed, if the reservation is removed it returns 1.
332+
// The error is set if the reservation key cannot be removed from the app or node.
333+
func (sa *SchedulingApplication) unReserve(node *SchedulingNode, ask *schedulingAllocationAsk) (int, error) {
331334
sa.Lock()
332335
defer sa.Unlock()
333336
return sa.unReserveInternal(node, ask)
334337
}
335338

336339
// Unlocked version for unReserve that really does the work.
337340
// Must only be called while holding the application lock.
338-
func (sa *SchedulingApplication) unReserveInternal(node *SchedulingNode, ask *schedulingAllocationAsk) error {
341+
func (sa *SchedulingApplication) unReserveInternal(node *SchedulingNode, ask *schedulingAllocationAsk) (int, error) {
339342
resKey := reservationKey(node, nil, ask)
340343
if resKey == "" {
341344
log.Logger().Debug("unreserve reservation key create failed unexpectedly",
342345
zap.String("appID", sa.ApplicationInfo.ApplicationID),
343346
zap.Any("node", node),
344347
zap.Any("ask", ask))
345-
return fmt.Errorf("reservation key failed node or ask are nil for appID %s", sa.ApplicationInfo.ApplicationID)
348+
return 0, fmt.Errorf("reservation key failed node or ask are nil for appID %s", sa.ApplicationInfo.ApplicationID)
346349
}
347-
// find the reservation and then unReserve the node before removing from the app
350+
// unReserve the node before removing from the app
351+
var num int
352+
var err error
353+
if num, err = node.unReserve(sa, ask); err != nil {
354+
return 0, err
355+
}
356+
// if the unreserve worked on the node check the app
348357
if _, found := sa.reservations[resKey]; found {
349-
if err := node.unReserve(sa, ask); err != nil {
350-
return err
358+
// worked on the node means either found or not but no error, log difference here
359+
if num == 0 {
360+
log.Logger().Info("reservation not found while removing from node, app has reservation",
361+
zap.String("appID", sa.ApplicationInfo.ApplicationID),
362+
zap.String("nodeID", node.NodeID),
363+
zap.String("ask", ask.AskProto.AllocationKey))
351364
}
352365
delete(sa.reservations, resKey)
353-
return nil
366+
return 1, nil
354367
}
355368
// reservation was not found
356-
log.Logger().Debug("reservation not found while removing from app",
369+
log.Logger().Info("reservation not found while removing from app",
357370
zap.String("appID", sa.ApplicationInfo.ApplicationID),
358371
zap.String("nodeID", node.NodeID),
359-
zap.String("ask", ask.AskProto.AllocationKey))
360-
return nil
372+
zap.String("ask", ask.AskProto.AllocationKey),
373+
zap.Int("nodeReservationsRemoved", num))
374+
return 0, nil
361375
}
362376

363377
// Return the allocation reservations on any node.

pkg/scheduler/scheduling_application_test.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ func TestAppReservation(t *testing.T) {
142142
}
143143

144144
// unreserve unknown node/ask
145-
err = app.unReserve(nil, nil)
145+
var num int
146+
_, err = app.unReserve(nil, nil)
146147
if err == nil {
147148
t.Errorf("illegal reservation release but did not fail: error %v", err)
148149
}
@@ -162,23 +163,23 @@ func TestAppReservation(t *testing.T) {
162163
if err != nil {
163164
t.Errorf("reservation of 2nd node should not have failed: error %v", err)
164165
}
165-
err = app.unReserve(node2, ask2)
166+
_, err = app.unReserve(node2, ask2)
166167
if err != nil {
167168
t.Errorf("remove of reservation of 2nd node should not have failed: error %v", err)
168169
}
169170
// unreserve the same should fail
170-
err = app.unReserve(node2, ask2)
171+
_, err = app.unReserve(node2, ask2)
171172
if err != nil {
172173
t.Errorf("remove twice of reservation of 2nd node should have failed: error %v", err)
173174
}
174175

175-
// failure case: remove reservation from node
176-
err = node.unReserve(app, ask)
177-
assert.NilError(t, err, "un-reserve on node should not have failed: error")
178-
err = app.unReserve(node, ask)
179-
if err != nil {
180-
t.Errorf("node does not have reservation removal of app reservation should have failed: error %v", err)
181-
}
176+
// failure case: remove reservation from node, app still needs cleanup
177+
num, err = node.unReserve(app, ask)
178+
assert.NilError(t, err, "un-reserve on node should not have failed with error")
179+
assert.Equal(t, num, 1, "un-reserve on node should have removed reservation")
180+
num, err = app.unReserve(node, ask)
181+
assert.NilError(t, err, "app has reservation should not have failed")
182+
assert.Equal(t, num, 1, "un-reserve on app should have removed reservation from app")
182183
}
183184

184185
// test multiple reservations from one allocation
@@ -437,10 +438,11 @@ func TestRemoveReservedAllocAsk(t *testing.T) {
437438
if len(app.isAskReserved(allocKey)) != 1 || !node.isReserved() {
438439
t.Fatalf("app should have reservation for %v on node", allocKey)
439440
}
440-
err = node.unReserve(app, ask2)
441-
if err != nil {
442-
t.Errorf("unreserve on node should not have failed: error %v", err)
443-
}
441+
var num int
442+
num, err = node.unReserve(app, ask2)
443+
assert.NilError(t, err, "un-reserve on node should not have failed")
444+
assert.Equal(t, num, 1, "un-reserve on node should have removed reservation")
445+
444446
before = app.GetPendingResource().Clone()
445447
reservedAsks = app.removeAllocationAsk(allocKey)
446448
delta = resources.Sub(before, app.GetPendingResource())

pkg/scheduler/scheduling_node.go

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -338,9 +338,9 @@ func (sn *SchedulingNode) reserve(app *SchedulingApplication, ask *schedulingAll
338338
}
339339

340340
// unReserve the node for this application and ask combination
341-
// If the reservation does not exist it returns false, if the reservation is removed it returns true.
341+
// If the reservation does not exist it returns 0 for reservations removed, if the reservation is removed it returns 1.
342342
// The error is set if the reservation key cannot be generated.
343-
func (sn *SchedulingNode) unReserve(app *SchedulingApplication, ask *schedulingAllocationAsk) error {
343+
func (sn *SchedulingNode) unReserve(app *SchedulingApplication, ask *schedulingAllocationAsk) (int, error) {
344344
sn.Lock()
345345
defer sn.Unlock()
346346
resKey := reservationKey(nil, app, ask)
@@ -349,40 +349,42 @@ func (sn *SchedulingNode) unReserve(app *SchedulingApplication, ask *schedulingA
349349
zap.String("nodeID", sn.NodeID),
350350
zap.Any("app", app),
351351
zap.Any("ask", ask))
352-
return fmt.Errorf("reservation key failed app or ask are nil on nodeID %s", sn.NodeID)
352+
return 0, fmt.Errorf("reservation key failed app or ask are nil on nodeID %s", sn.NodeID)
353353
}
354354
if _, ok := sn.reservations[resKey]; ok {
355355
delete(sn.reservations, resKey)
356-
return nil
356+
return 1, nil
357357
}
358358
// reservation was not found
359359
log.Logger().Debug("reservation not found while removing from node",
360360
zap.String("nodeID", sn.NodeID),
361361
zap.String("appID", app.ApplicationInfo.ApplicationID),
362362
zap.String("ask", ask.AskProto.AllocationKey))
363-
return nil
363+
return 0, nil
364364
}
365365

366366
// Remove all reservation made on this node from the app.
367367
// This is an unlocked function, it does not use a copy of the map when calling unReserve. That call will via the app call
368368
// unReserve on the node which is locked and modifies the original map. However deleting an entry from a map while iterating
369369
// over the map is perfectly safe based on the Go Specs.
370370
// It must only be called when removing the node under a partition lock.
371-
// It returns a list of all apps that have been unreserved on the node regardless of the result of the app unReserve call.
372-
// If all unReserve calls work true will be returned, false in all other cases.
373-
func (sn *SchedulingNode) unReserveApps() ([]string, bool) {
374-
var allOK = true
371+
// It returns a list of all apps that have been checked on the node regardless of the result of the app unReserve call.
372+
// The corresponding integers show the number of reservations removed for each app entry
373+
func (sn *SchedulingNode) unReserveApps() ([]string, []int) {
375374
var appReserve []string
375+
var askRelease []int
376376
for key, res := range sn.reservations {
377-
appID, err := res.unReserve()
377+
appID := res.appID
378+
num, err := res.app.unReserveInternal(res.node, res.ask)
378379
if err != nil {
379380
log.Logger().Warn("Removal of reservation failed while removing node",
380381
zap.String("nodeID", sn.NodeID),
381382
zap.String("reservationKey", key),
382383
zap.Error(err))
383-
allOK = false
384384
}
385+
// pass back the removed asks for each app
385386
appReserve = append(appReserve, appID)
387+
askRelease = append(askRelease, num)
386388
}
387-
return appReserve, allOK
389+
return appReserve, askRelease
388390
}

pkg/scheduler/scheduling_node_test.go

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -334,18 +334,21 @@ func TestNodeReservation(t *testing.T) {
334334
}
335335

336336
// unreserve different app
337-
err = node.unReserve(nil, nil)
337+
_, err = node.unReserve(nil, nil)
338338
if err == nil {
339339
t.Errorf("illegal reservation release but did not fail: error %v", err)
340340
}
341341
appID = "app-2"
342342
ask2 := newAllocationAsk("alloc-2", appID, res)
343343
appInfo = cache.NewApplicationInfo(appID, "default", "root.unknown", security.UserGroup{}, nil)
344344
app2 := newSchedulingApplication(appInfo)
345-
err = node.unReserve(app2, ask2)
345+
var num int
346+
num, err = node.unReserve(app2, ask2)
346347
assert.NilError(t, err, "un-reserve different app should have failed without error")
347-
err = node.unReserve(app, ask)
348+
assert.Equal(t, num, 0, "un-reserve different app should have failed without releases")
349+
num, err = node.unReserve(app, ask)
348350
assert.NilError(t, err, "un-reserve should not have failed")
351+
assert.Equal(t, num, 1, "un-reserve app should have released ")
349352
}
350353

351354
func TestUnReserveApps(t *testing.T) {
@@ -356,9 +359,9 @@ func TestUnReserveApps(t *testing.T) {
356359
if node.isReserved() {
357360
t.Fatal("new node should not have reservations")
358361
}
359-
reservedKeys, ok := node.unReserveApps()
360-
if !ok || len(reservedKeys) != 0 {
361-
t.Fatal("new node should not fail remove all reservations")
362+
reservedKeys, releasedAsks := node.unReserveApps()
363+
if len(reservedKeys) != 0 || len(releasedAsks) != 0 {
364+
t.Fatalf("new node should not fail remove all reservations: asks released = %v, reservation keys = %v", releasedAsks, reservedKeys)
362365
}
363366

364367
// create some reservations and see it clean up via the app
@@ -379,18 +382,18 @@ func TestUnReserveApps(t *testing.T) {
379382
err = app.reserve(node, ask)
380383
assert.NilError(t, err, "reservation should not have failed")
381384
assert.Equal(t, 1, len(node.reservations), "node should have reservation")
382-
reservedKeys, ok = node.unReserveApps()
383-
if !ok || len(reservedKeys) != 1 {
385+
reservedKeys, releasedAsks = node.unReserveApps()
386+
if len(reservedKeys) != 1 || len(releasedAsks) != 1 {
384387
t.Fatal("node should have removed reservation")
385388
}
386389

387390
// reserve just the node
388391
err = node.reserve(app, ask)
389392
assert.NilError(t, err, "reservation should not have failed")
390393
assert.Equal(t, 1, len(node.reservations), "node should have reservation")
391-
reservedKeys, ok = node.unReserveApps()
392-
if !ok || len(reservedKeys) != 1 {
393-
t.Errorf("node should have removed reservation: status = %t, reservation keys = %v", ok, reservedKeys)
394+
reservedKeys, releasedAsks = node.unReserveApps()
395+
if len(reservedKeys) != 1 || len(releasedAsks) != 1 {
396+
t.Fatalf("node should have removed reservation: asks released = %v, reservation keys = %v", releasedAsks, reservedKeys)
394397
}
395398
}
396399

@@ -402,9 +405,9 @@ func TestIsReservedForApp(t *testing.T) {
402405
if node.isReserved() {
403406
t.Fatal("new node should not have reservations")
404407
}
405-
reservedKeys, ok := node.unReserveApps()
406-
if !ok || len(reservedKeys) != 0 {
407-
t.Fatalf("new node should not fail remove all reservations: status = %t, reservation keys = %v", ok, reservedKeys)
408+
reservedKeys, releasedAsks := node.unReserveApps()
409+
if len(reservedKeys) != 0 || len(releasedAsks) != 0 {
410+
t.Fatalf("new node should not fail remove all reservations: asks released = %v, reservation keys = %v", releasedAsks, reservedKeys)
408411
}
409412

410413
// check if we can allocate on a reserved node

pkg/scheduler/scheduling_partition.go

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -344,15 +344,10 @@ func (psc *partitionSchedulingContext) removeSchedulingNode(nodeID string) {
344344
// remove the node, this will also get the sync back between the two lists
345345
delete(psc.nodes, nodeID)
346346
// unreserve all the apps that were reserved on the node
347-
var reservedKeys []string
348-
reservedKeys, ok = node.unReserveApps()
349-
if !ok {
350-
log.Logger().Warn("Node removal did not remove all application reservations this can affect scheduling",
351-
zap.String("nodeID", nodeID))
352-
}
347+
reservedKeys, releasedAsks := node.unReserveApps()
353348
// update the partition reservations based on the node clean up
354-
for _, appID := range reservedKeys {
355-
psc.unReserveCount(appID, 1)
349+
for i, appID := range reservedKeys {
350+
psc.unReserveCount(appID, releasedAsks[i])
356351
}
357352
}
358353

@@ -379,12 +374,6 @@ func (psc *partitionSchedulingContext) tryAllocate() *schedulingAllocation {
379374
// Try process reservations for the partition
380375
// Lock free call this all locks are taken when needed in called functions
381376
func (psc *partitionSchedulingContext) tryReservedAllocate() *schedulingAllocation {
382-
psc.Lock()
383-
if len(psc.reservedApps) == 0 {
384-
psc.Unlock()
385-
return nil
386-
}
387-
psc.Unlock()
388377
// try allocating from the root down
389378
return psc.root.tryReservedAllocate(psc)
390379
}
@@ -553,21 +542,24 @@ func (psc *partitionSchedulingContext) unReserve(app *SchedulingApplication, nod
553542
return
554543
}
555544
// all ok, remove the reservation of the app, this will also unReserve the node
556-
if err := app.unReserve(node, ask); err != nil {
545+
var err error
546+
var num int
547+
if num, err = app.unReserve(node, ask); err != nil {
557548
log.Logger().Info("Failed to unreserve, error during allocate on the app",
558549
zap.Error(err))
559550
return
560551
}
561552
// remove the reservation of the queue
562-
app.queue.unReserve(appID)
553+
app.queue.unReserve(appID, num)
563554
// make sure we cannot go below 0
564-
psc.unReserveCount(appID, 1)
555+
psc.unReserveCount(appID, num)
565556

566557
log.Logger().Info("allocation ask is unreserved",
567558
zap.String("appID", ask.ApplicationID),
568559
zap.String("queue", ask.QueueName),
569560
zap.String("allocationKey", ask.AskProto.AllocationKey),
570-
zap.String("node", node.NodeID))
561+
zap.String("node", node.NodeID),
562+
zap.Int("reservationsRemoved", num))
571563
}
572564

573565
// Get the iterator for the sorted nodes list from the partition.

0 commit comments

Comments
 (0)