mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 12:36:39 +00:00
feat: upstream enterprise preview --------- Signed-off-by: Oliver Baehler <oliver@sudo-i.net> Co-authored-by: CorentinPtrl <pitrel.corentin@gmail.com>
439 lines
13 KiB
Go
439 lines
13 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package cache
|
|
|
|
import (
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
)
|
|
|
|
var _ = Describe("QuantityCache", func() {
|
|
var (
|
|
c *QuantityCache[string]
|
|
now time.Time
|
|
key string
|
|
limit resource.Quantity
|
|
used resource.Quantity
|
|
)
|
|
|
|
reservation := func(id string, qty string) Reservation {
|
|
return Reservation{
|
|
ID: id,
|
|
Usage: resource.MustParse(qty),
|
|
UID: types.UID("uid-" + id),
|
|
Group: "",
|
|
Version: "v1",
|
|
Kind: "Pod",
|
|
Namespace: "default",
|
|
Name: "pod-" + id,
|
|
}
|
|
}
|
|
|
|
BeforeEach(func() {
|
|
now = time.Date(2026, 4, 28, 12, 0, 0, 0, time.UTC)
|
|
c = NewQuantityCache[string]()
|
|
c.clock = func() time.Time { return now }
|
|
|
|
key = "quota-a"
|
|
limit = resource.MustParse("5")
|
|
used = resource.MustParse("0")
|
|
})
|
|
|
|
It("returns false for missing entries", func() {
|
|
entry, ok := c.Get(key)
|
|
|
|
Expect(ok).To(BeFalse())
|
|
Expect(entry.Reservations).To(BeNil())
|
|
Expect(entry.PendingDeletes).To(BeNil())
|
|
Expect(entry.Reserved.IsZero()).To(BeTrue())
|
|
})
|
|
|
|
It("creates a reservation when allowed", func() {
|
|
allowed, effectiveUsed, entry := c.UpsertReservation(key, reservation("a", "1"), used, limit)
|
|
|
|
Expect(allowed).To(BeTrue())
|
|
Expect(effectiveUsed.Cmp(resource.MustParse("1"))).To(Equal(0))
|
|
Expect(entry.Reserved.Cmp(resource.MustParse("1"))).To(Equal(0))
|
|
Expect(entry.Reservations).To(HaveLen(1))
|
|
Expect(entry.Reservations["a"].CreatedAt).To(Equal(now))
|
|
Expect(entry.Reservations["a"].UpdatedAt).To(Equal(now))
|
|
Expect(c.Len()).To(Equal(1))
|
|
})
|
|
|
|
It("rejects a reservation when it would exceed the limit", func() {
|
|
allowed, effectiveUsed, entry := c.UpsertReservation(
|
|
key,
|
|
reservation("a", "2"),
|
|
resource.MustParse("4"),
|
|
resource.MustParse("5"),
|
|
)
|
|
|
|
Expect(allowed).To(BeFalse())
|
|
Expect(effectiveUsed.Cmp(resource.MustParse("4"))).To(Equal(0))
|
|
Expect(entry.Reserved.IsZero()).To(BeTrue())
|
|
Expect(c.Len()).To(Equal(0))
|
|
})
|
|
|
|
It("keeps existing reservations unchanged when a new reservation is rejected", func() {
|
|
allowed, _, _ := c.UpsertReservation(key, reservation("a", "2"), used, limit)
|
|
Expect(allowed).To(BeTrue())
|
|
|
|
allowed, effectiveUsed, entry := c.UpsertReservation(
|
|
key,
|
|
reservation("b", "4"),
|
|
used,
|
|
limit,
|
|
)
|
|
|
|
Expect(allowed).To(BeFalse())
|
|
Expect(effectiveUsed.Cmp(resource.MustParse("2"))).To(Equal(0))
|
|
Expect(entry.Reserved.Cmp(resource.MustParse("2"))).To(Equal(0))
|
|
Expect(entry.Reservations).To(HaveLen(1))
|
|
Expect(entry.Reservations).To(HaveKey("a"))
|
|
|
|
stored, ok := c.Get(key)
|
|
Expect(ok).To(BeTrue())
|
|
Expect(stored.Reserved.Cmp(resource.MustParse("2"))).To(Equal(0))
|
|
Expect(stored.Reservations).To(HaveLen(1))
|
|
})
|
|
|
|
It("is idempotent for the same reservation identity and usage", func() {
|
|
allowed, _, first := c.UpsertReservation(key, reservation("a", "1"), used, limit)
|
|
Expect(allowed).To(BeTrue())
|
|
|
|
now = now.Add(time.Minute)
|
|
|
|
allowed, effectiveUsed, second := c.UpsertReservation(key, reservation("a", "1"), used, limit)
|
|
|
|
Expect(allowed).To(BeTrue())
|
|
Expect(effectiveUsed.Cmp(resource.MustParse("1"))).To(Equal(0))
|
|
Expect(second.Reserved.Cmp(resource.MustParse("1"))).To(Equal(0))
|
|
Expect(second.Reservations).To(HaveLen(1))
|
|
Expect(second.Reservations["a"].CreatedAt).To(Equal(first.Reservations["a"].CreatedAt))
|
|
Expect(second.Reservations["a"].UpdatedAt).To(Equal(first.Reservations["a"].UpdatedAt))
|
|
Expect(second.LastAccess).To(Equal(now))
|
|
})
|
|
|
|
It("updates an existing reservation when usage changes", func() {
|
|
allowed, _, first := c.UpsertReservation(key, reservation("a", "1"), used, limit)
|
|
Expect(allowed).To(BeTrue())
|
|
|
|
now = now.Add(time.Minute)
|
|
|
|
allowed, effectiveUsed, second := c.UpsertReservation(key, reservation("a", "3"), used, limit)
|
|
|
|
Expect(allowed).To(BeTrue())
|
|
Expect(effectiveUsed.Cmp(resource.MustParse("3"))).To(Equal(0))
|
|
Expect(second.Reserved.Cmp(resource.MustParse("3"))).To(Equal(0))
|
|
Expect(second.Reservations).To(HaveLen(1))
|
|
Expect(second.Reservations["a"].CreatedAt).To(Equal(first.Reservations["a"].CreatedAt))
|
|
Expect(second.Reservations["a"].UpdatedAt).To(Equal(now))
|
|
})
|
|
|
|
It("updates an existing reservation when object identity changes", func() {
|
|
res := reservation("a", "1")
|
|
|
|
allowed, _, first := c.UpsertReservation(key, res, used, limit)
|
|
Expect(allowed).To(BeTrue())
|
|
|
|
now = now.Add(time.Minute)
|
|
res.Name = "renamed-pod"
|
|
|
|
allowed, _, second := c.UpsertReservation(key, res, used, limit)
|
|
|
|
Expect(allowed).To(BeTrue())
|
|
Expect(second.Reservations["a"].Name).To(Equal("renamed-pod"))
|
|
Expect(second.Reservations["a"].CreatedAt).To(Equal(first.Reservations["a"].CreatedAt))
|
|
Expect(second.Reservations["a"].UpdatedAt).To(Equal(now))
|
|
})
|
|
|
|
It("returns defensive copies from Get", func() {
|
|
allowed, _, _ := c.UpsertReservation(key, reservation("a", "1"), used, limit)
|
|
Expect(allowed).To(BeTrue())
|
|
|
|
entry, ok := c.Get(key)
|
|
Expect(ok).To(BeTrue())
|
|
|
|
entry.Reserved = resource.MustParse("999")
|
|
entry.Reservations["a"] = reservation("mutated", "999")
|
|
entry.PendingDeletes = append(entry.PendingDeletes, PendingDeleteHint{UID: "fake"})
|
|
|
|
again, ok := c.Get(key)
|
|
Expect(ok).To(BeTrue())
|
|
Expect(again.Reserved.Cmp(resource.MustParse("1"))).To(Equal(0))
|
|
Expect(again.Reservations).To(HaveKey("a"))
|
|
Expect(again.Reservations).NotTo(HaveKey("mutated"))
|
|
Expect(again.PendingDeletes).To(BeEmpty())
|
|
})
|
|
|
|
It("returns defensive copies from Snapshot", func() {
|
|
allowed, _, _ := c.UpsertReservation(key, reservation("a", "1"), used, limit)
|
|
Expect(allowed).To(BeTrue())
|
|
|
|
snap := c.Snapshot()
|
|
snap[key] = QuantityEntry{
|
|
Reserved: resource.MustParse("999"),
|
|
}
|
|
|
|
entry, ok := c.Get(key)
|
|
Expect(ok).To(BeTrue())
|
|
Expect(entry.Reserved.Cmp(resource.MustParse("1"))).To(Equal(0))
|
|
})
|
|
|
|
It("deletes entries", func() {
|
|
allowed, _, _ := c.UpsertReservation(key, reservation("a", "1"), used, limit)
|
|
Expect(allowed).To(BeTrue())
|
|
|
|
Expect(c.Delete(key)).To(BeTrue())
|
|
Expect(c.Delete(key)).To(BeFalse())
|
|
Expect(c.Len()).To(Equal(0))
|
|
})
|
|
|
|
It("deletes reservations and removes empty entries", func() {
|
|
allowed, _, _ := c.UpsertReservation(key, reservation("a", "1"), used, limit)
|
|
Expect(allowed).To(BeTrue())
|
|
|
|
Expect(c.DeleteReservation(key, "a")).To(BeTrue())
|
|
Expect(c.Len()).To(Equal(0))
|
|
|
|
_, ok := c.Get(key)
|
|
Expect(ok).To(BeFalse())
|
|
})
|
|
|
|
It("deletes only the selected reservation and recomputes reserved", func() {
|
|
Expect(c.UpsertReservation(key, reservation("a", "1"), used, limit)).To(ReceiveAllowed())
|
|
Expect(c.UpsertReservation(key, reservation("b", "2"), used, limit)).To(ReceiveAllowed())
|
|
|
|
Expect(c.DeleteReservation(key, "a")).To(BeTrue())
|
|
|
|
entry, ok := c.Get(key)
|
|
Expect(ok).To(BeTrue())
|
|
Expect(entry.Reserved.Cmp(resource.MustParse("2"))).To(Equal(0))
|
|
Expect(entry.Reservations).To(HaveKey("b"))
|
|
Expect(entry.Reservations).NotTo(HaveKey("a"))
|
|
})
|
|
|
|
It("returns false when deleting a missing reservation", func() {
|
|
Expect(c.DeleteReservation(key, "missing")).To(BeFalse())
|
|
|
|
allowed, _, _ := c.UpsertReservation(key, reservation("a", "1"), used, limit)
|
|
Expect(allowed).To(BeTrue())
|
|
|
|
Expect(c.DeleteReservation(key, "missing")).To(BeFalse())
|
|
})
|
|
|
|
It("purges reservations for a key", func() {
|
|
Expect(c.UpsertReservation(key, reservation("a", "1"), used, limit)).To(ReceiveAllowed())
|
|
Expect(c.UpsertReservation(key, reservation("b", "2"), used, limit)).To(ReceiveAllowed())
|
|
Expect(c.UpsertReservation(key, reservation("c", "1"), used, limit)).To(ReceiveAllowed())
|
|
|
|
deleted := c.PurgeReservationsForKey(key, func(r Reservation) bool {
|
|
return r.ID == "a" || r.ID == "c"
|
|
})
|
|
|
|
Expect(deleted).To(Equal(2))
|
|
|
|
entry, ok := c.Get(key)
|
|
Expect(ok).To(BeTrue())
|
|
Expect(entry.Reserved.Cmp(resource.MustParse("2"))).To(Equal(0))
|
|
Expect(entry.Reservations).To(HaveLen(1))
|
|
Expect(entry.Reservations).To(HaveKey("b"))
|
|
})
|
|
|
|
It("purges all reservations and removes entry when no pending deletes remain", func() {
|
|
Expect(c.UpsertReservation(key, reservation("a", "1"), used, limit)).To(ReceiveAllowed())
|
|
|
|
deleted := c.PurgeReservationsForKey(key, func(r Reservation) bool {
|
|
return true
|
|
})
|
|
|
|
Expect(deleted).To(Equal(1))
|
|
Expect(c.Len()).To(Equal(0))
|
|
})
|
|
|
|
It("does not remove entry after purging all reservations if pending deletes remain", func() {
|
|
Expect(c.UpsertReservation(key, reservation("a", "1"), used, limit)).To(ReceiveAllowed())
|
|
c.AddPendingDelete(key, "uid-a")
|
|
|
|
deleted := c.PurgeReservationsForKey(key, func(r Reservation) bool {
|
|
return true
|
|
})
|
|
|
|
Expect(deleted).To(Equal(1))
|
|
|
|
entry, ok := c.Get(key)
|
|
Expect(ok).To(BeTrue())
|
|
Expect(entry.Reserved.IsZero()).To(BeTrue())
|
|
Expect(entry.PendingDeletes).To(HaveLen(1))
|
|
})
|
|
|
|
It("returns zero when purging a missing key or no reservation matches", func() {
|
|
Expect(c.PurgeReservationsForKey("missing", func(r Reservation) bool { return true })).To(Equal(0))
|
|
|
|
Expect(c.UpsertReservation(key, reservation("a", "1"), used, limit)).To(ReceiveAllowed())
|
|
|
|
Expect(c.PurgeReservationsForKey(key, func(r Reservation) bool { return false })).To(Equal(0))
|
|
})
|
|
|
|
It("adds pending deletes idempotently", func() {
|
|
c.AddPendingDelete(key, "uid-a")
|
|
c.AddPendingDelete(key, "uid-a")
|
|
|
|
Expect(c.HasPendingDeletes(key)).To(BeTrue())
|
|
Expect(c.PendingDeletes(key)).To(Equal([]types.UID{"uid-a"}))
|
|
|
|
entry, ok := c.Get(key)
|
|
Expect(ok).To(BeTrue())
|
|
Expect(entry.PendingDeletes[0].CreatedAt).To(Equal(now))
|
|
})
|
|
|
|
It("ignores empty pending delete UIDs", func() {
|
|
c.AddPendingDelete(key, "")
|
|
|
|
Expect(c.Len()).To(Equal(0))
|
|
Expect(c.PendingDeletes(key)).To(BeNil())
|
|
})
|
|
|
|
It("removes pending deletes and removes empty entry", func() {
|
|
c.AddPendingDelete(key, "uid-a")
|
|
|
|
Expect(c.RemovePendingDelete(key, "uid-a")).To(BeTrue())
|
|
Expect(c.RemovePendingDelete(key, "uid-a")).To(BeFalse())
|
|
Expect(c.Len()).To(Equal(0))
|
|
})
|
|
|
|
It("removes pending delete but keeps entry when reservations remain", func() {
|
|
Expect(c.UpsertReservation(key, reservation("a", "1"), used, limit)).To(ReceiveAllowed())
|
|
c.AddPendingDelete(key, "uid-a")
|
|
|
|
Expect(c.RemovePendingDelete(key, "uid-a")).To(BeTrue())
|
|
|
|
entry, ok := c.Get(key)
|
|
Expect(ok).To(BeTrue())
|
|
Expect(entry.Reserved.Cmp(resource.MustParse("1"))).To(Equal(0))
|
|
Expect(entry.PendingDeletes).To(BeEmpty())
|
|
})
|
|
|
|
It("returns nil pending deletes for missing keys", func() {
|
|
Expect(c.PendingDeletes("missing")).To(BeNil())
|
|
Expect(c.HasPendingDeletes("missing")).To(BeFalse())
|
|
})
|
|
|
|
It("clamps negative reserved totals to zero", func() {
|
|
Expect(c.UpsertReservation(key, reservation("a", "-5"), used, limit)).To(ReceiveAllowed())
|
|
|
|
entry, ok := c.Get(key)
|
|
Expect(ok).To(BeTrue())
|
|
Expect(entry.Reserved.IsZero()).To(BeTrue())
|
|
})
|
|
|
|
It("is safe under concurrent reservation upserts", func() {
|
|
const workers = 32
|
|
|
|
c.clock = time.Now
|
|
limit := resource.MustParse("100")
|
|
var allowedCount atomic.Int32
|
|
|
|
start := make(chan struct{})
|
|
var wg sync.WaitGroup
|
|
wg.Add(workers)
|
|
|
|
for i := 0; i < workers; i++ {
|
|
i := i
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
<-start
|
|
|
|
allowed, _, _ := c.UpsertReservation(
|
|
key,
|
|
reservation(string(rune('a'+i)), "1"),
|
|
resource.MustParse("0"),
|
|
limit,
|
|
)
|
|
if allowed {
|
|
allowedCount.Add(1)
|
|
}
|
|
}()
|
|
}
|
|
|
|
close(start)
|
|
wg.Wait()
|
|
|
|
Expect(allowedCount.Load()).To(Equal(int32(workers)))
|
|
|
|
entry, ok := c.Get(key)
|
|
Expect(ok).To(BeTrue())
|
|
Expect(entry.Reservations).To(HaveLen(workers))
|
|
Expect(entry.Reserved.Cmp(resource.MustParse("32"))).To(Equal(0))
|
|
})
|
|
|
|
It("enforces limit under concurrent reservation upserts", func() {
|
|
const workers = 16
|
|
|
|
c.clock = time.Now
|
|
limit := resource.MustParse("5")
|
|
var allowedCount atomic.Int32
|
|
|
|
start := make(chan struct{})
|
|
var wg sync.WaitGroup
|
|
wg.Add(workers)
|
|
|
|
for i := 0; i < workers; i++ {
|
|
i := i
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
|
|
<-start
|
|
|
|
allowed, _, _ := c.UpsertReservation(
|
|
key,
|
|
reservation(string(rune('a'+i)), "1"),
|
|
resource.MustParse("0"),
|
|
limit,
|
|
)
|
|
if allowed {
|
|
allowedCount.Add(1)
|
|
}
|
|
}()
|
|
}
|
|
|
|
close(start)
|
|
wg.Wait()
|
|
|
|
Expect(allowedCount.Load()).To(Equal(int32(5)))
|
|
|
|
entry, ok := c.Get(key)
|
|
Expect(ok).To(BeTrue())
|
|
Expect(entry.Reservations).To(HaveLen(5))
|
|
Expect(entry.Reserved.Cmp(resource.MustParse("5"))).To(Equal(0))
|
|
})
|
|
})
|
|
|
|
type quantityResult struct {
|
|
allowed bool
|
|
effectiveUsed resource.Quantity
|
|
entry QuantityEntry
|
|
}
|
|
|
|
func ReceiveAllowed() OmegaMatcher {
|
|
return WithTransform(func(in any) bool {
|
|
switch v := in.(type) {
|
|
case quantityResult:
|
|
return v.allowed
|
|
default:
|
|
return false
|
|
}
|
|
}, BeTrue())
|
|
}
|