Skip to content

Commit 1f2ca50

Browse files
yaroslavborbatdiafour
authored andcommitted
refactor: improve gc (#1292)
GC will run every day at 00:00. GC will delete all VMOPs in completed state if their TTL has expired (24 hours). GC will delete VMOPs in completed state even if their TTL has not expired, provided there are more than 10 such VMOPs, keeping only the latest 10. --------- Signed-off-by: Yaroslav Borbat <[email protected]> Signed-off-by: Yaroslav Borbat <[email protected]> Co-authored-by: Ivan Mikheykin <[email protected]>
1 parent 32e294c commit 1f2ca50

File tree

11 files changed

+773
-195
lines changed

11 files changed

+773
-195
lines changed

images/virtualization-artifact/pkg/controller/gc/cron_source.go

Lines changed: 47 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -17,157 +17,90 @@ limitations under the License.
1717
package gc
1818

1919
import (
20-
"cmp"
2120
"context"
2221
"fmt"
23-
"slices"
2422
"time"
2523

2624
"github.com/robfig/cron/v3"
27-
"k8s.io/apimachinery/pkg/api/meta"
28-
"k8s.io/apimachinery/pkg/runtime"
2925
"k8s.io/apimachinery/pkg/types"
3026
"k8s.io/client-go/util/workqueue"
27+
"k8s.io/utils/clock"
3128
"sigs.k8s.io/controller-runtime/pkg/client"
3229
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3330
"sigs.k8s.io/controller-runtime/pkg/source"
3431

3532
"github.com/deckhouse/deckhouse/pkg/log"
36-
"github.com/deckhouse/virtualization-controller/pkg/common/object"
3733
"github.com/deckhouse/virtualization-controller/pkg/logger"
3834
)
3935

40-
const sourceName = "CronSource"
41-
42-
func NewCronSource(c client.Client,
43-
standardSpec string,
44-
objList client.ObjectList,
45-
option CronSourceOption,
46-
log *log.Logger,
47-
) *CronSource {
48-
return &CronSource{
49-
Client: c,
50-
standardSpec: standardSpec,
51-
objList: objList,
52-
option: option,
53-
log: log.With("WatchSource", sourceName),
54-
}
55-
}
56-
5736
var _ source.Source = &CronSource{}
5837

59-
type CronSource struct {
60-
client.Client
61-
standardSpec string
62-
objList client.ObjectList
63-
option CronSourceOption
64-
log *log.Logger
65-
}
38+
const sourceName = "CronSource"
6639

67-
type CronSourceOption struct {
68-
GetOlder func(objList client.ObjectList) client.ObjectList
40+
type SourceGCManager interface {
41+
ListForDelete(ctx context.Context, now time.Time) ([]client.Object, error)
6942
}
7043

71-
func NewDefaultCronSourceOption(objs client.ObjectList, ttl time.Duration, log *log.Logger) CronSourceOption {
72-
return CronSourceOption{
73-
GetOlder: DefaultGetOlder(objs, ttl, 10, log),
44+
func NewCronSource(scheduleSpec string, mgr SourceGCManager, log *log.Logger) (*CronSource, error) {
45+
schedule, err := cron.ParseStandard(scheduleSpec)
46+
if err != nil {
47+
return nil, fmt.Errorf("parsing standard spec %q: %w", scheduleSpec, err)
7448
}
75-
}
7649

77-
func DefaultGetOlder(objs client.ObjectList, ttl time.Duration, maxCount int, log *log.Logger) func(objList client.ObjectList) client.ObjectList {
78-
return func(objList client.ObjectList) client.ObjectList {
79-
var expiredItems []runtime.Object
80-
var notExpiredItems []runtime.Object
81-
82-
if err := meta.EachListItem(objList, func(o runtime.Object) error {
83-
obj, ok := o.(client.Object)
84-
if !ok {
85-
return nil
86-
}
87-
if object.GetAge(obj) > ttl {
88-
expiredItems = append(expiredItems, o)
89-
} else {
90-
notExpiredItems = append(notExpiredItems, o)
91-
}
92-
93-
return nil
94-
}); err != nil {
95-
log.Error("failed to populate list", logger.SlogErr(err))
96-
}
97-
98-
if maxCount != 0 && len(notExpiredItems) > maxCount {
99-
slices.SortFunc(notExpiredItems, func(a, b runtime.Object) int {
100-
aObj, _ := a.(client.Object)
101-
bObj, _ := b.(client.Object)
102-
103-
return cmp.Compare(object.GetAge(aObj), object.GetAge(bObj))
104-
})
105-
expiredItems = append(expiredItems, notExpiredItems[maxCount:]...)
106-
}
50+
return &CronSource{
51+
schedule: schedule,
52+
mgr: mgr,
53+
log: log.With("WatchSource", sourceName),
54+
clock: &clock.RealClock{},
55+
}, nil
56+
}
10757

108-
if err := meta.SetList(objs, expiredItems); err != nil {
109-
log.Error("failed to set list", logger.SlogErr(err))
110-
}
111-
return objs
112-
}
58+
type CronSource struct {
59+
schedule cron.Schedule
60+
mgr SourceGCManager
61+
log *log.Logger
62+
clock clock.Clock
11363
}
11464

11565
func (c *CronSource) Start(ctx context.Context, queue workqueue.TypedRateLimitingInterface[reconcile.Request]) error {
116-
schedule, err := cron.ParseStandard(c.standardSpec)
117-
if err != nil {
118-
return fmt.Errorf("parsing standard spec %q: %w", c.standardSpec, err)
119-
}
120-
work := func() {
121-
if err = meta.SetList(c.objList, nil); err != nil {
122-
c.log.Error("failed to reset resource list", logger.SlogErr(err))
123-
return
124-
}
125-
if err = c.List(ctx, c.objList); err != nil {
126-
c.log.Error("failed to listing resources", logger.SlogErr(err))
127-
return
128-
}
129-
if meta.LenList(c.objList) == 0 {
130-
c.log.Debug("no resources, skip")
131-
return
132-
}
133-
if c.option.GetOlder != nil {
134-
c.objList = c.option.GetOlder(c.objList)
135-
}
136-
if err = meta.EachListItem(c.objList, func(object runtime.Object) error {
137-
obj, ok := object.(client.Object)
138-
if !ok {
139-
c.log.Error(fmt.Sprintf("%s's type isn't metav1.Object", object.GetObjectKind().GroupVersionKind().String()))
140-
return nil
141-
}
142-
143-
queue.Add(reconcile.Request{
144-
NamespacedName: types.NamespacedName{
145-
Namespace: obj.GetNamespace(),
146-
Name: obj.GetName(),
147-
},
148-
})
149-
c.log.Debug(fmt.Sprintf("resource %s/%s enqueued", obj.GetNamespace(), obj.GetName()))
150-
return nil
151-
}); err != nil {
152-
c.log.Error("failed to enqueueing resources", logger.SlogErr(err))
153-
return
154-
}
155-
}
156-
ta := nextScheduleTimeDuration(schedule, time.Now())
66+
nextTime := nextScheduleTimeDuration(c.schedule, c.clock.Now())
15767
go func() {
15868
for {
15969
select {
16070
case <-ctx.Done():
16171
return
162-
case <-time.After(ta):
163-
work()
164-
ta = nextScheduleTimeDuration(schedule, time.Now())
72+
case <-c.clock.After(nextTime):
73+
c.addObjects(ctx, queue.Add)
74+
nextTime = nextScheduleTimeDuration(c.schedule, c.clock.Now())
16575
}
16676
}
16777
}()
16878
return nil
16979
}
17080

81+
func (c *CronSource) addObjects(ctx context.Context, addToQueue func(reconcile.Request)) {
82+
objs, err := c.mgr.ListForDelete(ctx, c.clock.Now())
83+
if err != nil {
84+
c.log.Error("Failed to get ObjectList for delete", logger.SlogErr(err))
85+
return
86+
}
87+
88+
if len(objs) == 0 {
89+
c.log.Debug("No resources, skip")
90+
return
91+
}
92+
93+
for _, obj := range objs {
94+
addToQueue(reconcile.Request{
95+
NamespacedName: types.NamespacedName{
96+
Namespace: obj.GetNamespace(),
97+
Name: obj.GetName(),
98+
},
99+
})
100+
c.log.Debug(fmt.Sprintf("Resource %s/%s enqueued", obj.GetNamespace(), obj.GetName()))
101+
}
102+
}
103+
171104
func nextScheduleTimeDuration(schedule cron.Schedule, now time.Time) time.Duration {
172105
return schedule.Next(now).Sub(now)
173106
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
Copyright 2025 Flant JSC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package gc
18+
19+
import (
20+
"context"
21+
"time"
22+
23+
. "github.com/onsi/ginkgo/v2"
24+
. "github.com/onsi/gomega"
25+
clock "k8s.io/utils/clock/testing"
26+
"sigs.k8s.io/controller-runtime/pkg/client"
27+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
28+
29+
dlog "github.com/deckhouse/deckhouse/pkg/log"
30+
"github.com/deckhouse/virtualization-controller/pkg/common/testutil"
31+
)
32+
33+
var _ = Describe("CronSource", func() {
34+
const (
35+
// Every day a 00:00
36+
scheduleSpec = "0 0 * * *"
37+
)
38+
39+
var (
40+
log *dlog.Logger
41+
baseCtx context.Context
42+
fakeClient client.Client
43+
mgr *fakeGCManager
44+
fakeClock *clock.FakeClock
45+
)
46+
47+
BeforeEach(func() {
48+
log = testutil.NewNoOpLogger()
49+
baseCtx = testutil.ToContext(context.Background(), log)
50+
51+
scheme := newScheme()
52+
fakeClient = fake.NewClientBuilder().WithScheme(scheme).Build()
53+
54+
mgr = newFakeGCManager(fakeClient, 24*time.Hour, 10)
55+
56+
t := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
57+
fakeClock = clock.NewFakeClock(t)
58+
})
59+
60+
newSource := func(scheduleSpec string) *CronSource {
61+
source, err := NewCronSource(scheduleSpec, mgr, log)
62+
Expect(err).NotTo(HaveOccurred())
63+
source.clock = fakeClock
64+
return source
65+
}
66+
67+
Context("with spawned objects", func() {
68+
var (
69+
ctx context.Context
70+
cancel context.CancelFunc
71+
source *CronSource
72+
queue *fakeQueue
73+
)
74+
75+
BeforeEach(func() {
76+
ctx, cancel = context.WithCancel(baseCtx)
77+
source = newSource(scheduleSpec)
78+
queue = newFakeQueue()
79+
})
80+
81+
AfterEach(func() {
82+
cancel()
83+
queue.ShutDown()
84+
})
85+
86+
It("should not enqueue objects because ttl is not expired", func() {
87+
spawnFakeObjects(5, 10, fakeObjectPhaseCompleted, fakeClient, fakeClock)
88+
spawnFakeObjects(10, 10, fakeObjectPhasePending, fakeClient, fakeClock)
89+
spawnFakeObjects(15, 10, fakeObjectPhaseRunning, fakeClient, fakeClock)
90+
91+
Expect(source.Start(ctx, queue)).To(Succeed())
92+
time.Sleep(1 * time.Second)
93+
94+
// Go to 2025-01-02 01:00.
95+
// CronSource should be started but not enqueued any objects because ttl is not expired.
96+
fakeClock.Step(13 * time.Hour)
97+
98+
Consistently(func() int {
99+
return len(queue.Requests())
100+
}).WithTimeout(10 * time.Second).Should(Equal(0))
101+
})
102+
103+
It("should enqueue 10 objects because ttl is not expired but objects completed and them more that maxCount", func() {
104+
spawnFakeObjects(10, 11, fakeObjectPhaseCompleted, fakeClient, fakeClock)
105+
spawnFakeObjects(10, 11, fakeObjectPhasePending, fakeClient, fakeClock)
106+
spawnFakeObjects(10, 11, fakeObjectPhaseRunning, fakeClient, fakeClock)
107+
108+
Expect(source.Start(ctx, queue)).To(Succeed())
109+
time.Sleep(1 * time.Second)
110+
111+
// Go to 2025-01-02 01:00.
112+
// CronSource should be started and enqueued 10 objects.
113+
fakeClock.Step(13 * time.Hour)
114+
115+
Eventually(func() int {
116+
return len(queue.Requests())
117+
}).WithTimeout(10 * time.Second).Should(Equal(10))
118+
})
119+
120+
It("should enqueue 100 objects in completed state", func() {
121+
spawnFakeObjects(10, 10, fakeObjectPhaseCompleted, fakeClient, fakeClock)
122+
spawnFakeObjects(10, 10, fakeObjectPhasePending, fakeClient, fakeClock)
123+
spawnFakeObjects(10, 10, fakeObjectPhaseRunning, fakeClient, fakeClock)
124+
125+
Expect(source.Start(ctx, queue)).To(Succeed())
126+
time.Sleep(1 * time.Second)
127+
128+
// Go to 2025-01-03 01:00.
129+
// CronSource should be started and enqueued 100 objects.
130+
fakeClock.Step(37 * time.Hour)
131+
132+
Eventually(func() int {
133+
return len(queue.Requests())
134+
}).WithTimeout(10 * time.Second).Should(Equal(100))
135+
})
136+
})
137+
})

0 commit comments

Comments
 (0)