Skip to content

Commit a981b0c

Browse files
committed
Send async notification in crcache when latest pkg revision is deleted
1 parent 3b83af2 commit a981b0c

4 files changed

Lines changed: 146 additions & 6 deletions

File tree

pkg/cache/crcache/repository.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,9 @@ func (r *cachedRepository) DeletePackageRevision(ctx context.Context, prToDelete
405405
}
406406
klog.Infof("PackageRevision %s deleted for real since no finalizers", prToDelete.KubeObjectName())
407407

408+
// Check if this is the latest revision before deletion
409+
isLatest := prToDelete.(*cachedPackageRevision).IsLatestRevision()
410+
408411
// Unwrap
409412
unwrapped := prToDelete.(*cachedPackageRevision).PackageRevision
410413
if err := r.repo.DeletePackageRevision(ctx, unwrapped); err != nil {
@@ -421,6 +424,12 @@ func (r *cachedRepository) DeletePackageRevision(ctx context.Context, prToDelete
421424
identifyLatestRevisions(ctx, r.cachedPackageRevisions)
422425
}
423426

427+
// Check if we need to send async notification for new latest revision
428+
if isLatest {
429+
klog.Infof("crcache: %+v: latest PackageRevision deleted. Sending notification.", prToDelete.Key().PkgKey)
430+
go r.sendLatestPkgUpdateNotification(prToDelete.Key().PkgKey)
431+
}
432+
424433
r.mutex.Unlock()
425434

426435
if _, err := r.metadataStore.Delete(ctx, namespacedName, true); err != nil {
@@ -436,6 +445,27 @@ func (r *cachedRepository) DeletePackageRevision(ctx context.Context, prToDelete
436445
return nil
437446
}
438447

448+
// sendLatestPkgUpdateNotification sends async notification when a new latest package revision is identified
449+
func (r *cachedRepository) sendLatestPkgUpdateNotification(pkgKey repository.PackageKey) {
450+
// Find the new latest revision for this package
451+
r.mutex.RLock()
452+
var newLatest repository.PackageRevision
453+
for _, pr := range r.cachedPackageRevisions {
454+
if pr.Key().PkgKey == pkgKey && pr.IsLatestRevision() {
455+
newLatest = pr
456+
break
457+
}
458+
}
459+
r.mutex.RUnlock()
460+
461+
if newLatest != nil {
462+
sent := r.repoPRChangeNotifier.NotifyPackageRevisionChange(watch.Modified, newLatest)
463+
klog.Infof("crcache: async notification sent %d for new latest PackageRevision %s/%s", sent, newLatest.KubeObjectNamespace(), newLatest.KubeObjectName())
464+
} else {
465+
klog.Infof("crcache: no new latest revision found for package %s after deletion. Notification not sent.", pkgKey.Package)
466+
}
467+
}
468+
439469
func (r *cachedRepository) ListPackages(ctx context.Context, filter repository.ListPackageFilter) ([]repository.Package, error) {
440470
packages, err := r.getPackages(ctx, filter, false)
441471
if err != nil {

pkg/cache/crcache/repository_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3535
"k8s.io/apimachinery/pkg/runtime"
3636
"k8s.io/apimachinery/pkg/types"
37+
"k8s.io/apimachinery/pkg/watch"
3738
)
3839

3940
func TestCachedRepoRefresh(t *testing.T) {
@@ -250,3 +251,111 @@ func TestHandleRunOnceAt(t *testing.T) {
250251
assert.NotNil(t, status, "Expected repository status to be updated")
251252
assert.Contains(t, []string{"Ready", "Error", "Reconciling"}, status.Conditions[0].Reason)
252253
}
254+
255+
func TestCRDeleteLatestRevision(t *testing.T) {
256+
mockRepo := mockrepo.NewMockRepository(t)
257+
mockMeta := mockmeta.NewMockMetadataStore(t)
258+
mockNotifier := mockcachetypes.NewMockRepoPRChangeNotifier(t)
259+
260+
repoSpec := configapi.Repository{
261+
ObjectMeta: metav1.ObjectMeta{
262+
Name: repoName,
263+
Namespace: namespace,
264+
},
265+
}
266+
267+
repoKey := repository.RepositoryKey{
268+
Namespace: namespace,
269+
Name: repoName,
270+
}
271+
272+
// Create two package revisions for the same package
273+
prKey1 := repository.PackageRevisionKey{
274+
PkgKey: repository.PackageKey{
275+
RepoKey: repoKey,
276+
Path: "",
277+
Package: "test-package",
278+
},
279+
WorkspaceName: "v1",
280+
Revision: 1,
281+
}
282+
283+
prKey2 := repository.PackageRevisionKey{
284+
PkgKey: repository.PackageKey{
285+
RepoKey: repoKey,
286+
Path: "",
287+
Package: "test-package",
288+
},
289+
WorkspaceName: "v2",
290+
Revision: 2,
291+
}
292+
293+
fpr1 := &fake.FakePackageRevision{
294+
PrKey: prKey1,
295+
PackageLifecycle: porchapi.PackageRevisionLifecyclePublished,
296+
}
297+
298+
fpr2 := &fake.FakePackageRevision{
299+
PrKey: prKey2,
300+
PackageLifecycle: porchapi.PackageRevisionLifecyclePublished,
301+
}
302+
303+
// Setup mocks
304+
mockRepo.On("Key").Return(repoKey).Maybe()
305+
mockRepo.EXPECT().DeletePackageRevision(mock.Anything, mock.Anything).Return(nil).Maybe()
306+
mockMeta.EXPECT().Delete(mock.Anything, mock.Anything, mock.Anything).Return(metav1.ObjectMeta{}, nil).Maybe()
307+
308+
// Expect exactly 2 notifications: one for deletion, one for async notification
309+
mockNotifier.EXPECT().NotifyPackageRevisionChange(watch.Deleted, mock.Anything).Return(1).Once()
310+
mockNotifier.EXPECT().NotifyPackageRevisionChange(watch.Modified, mock.Anything).Return(1).Once()
311+
312+
// Create repository manually to avoid sync manager
313+
cr := &cachedRepository{
314+
key: repoKey,
315+
repoSpec: &repoSpec,
316+
repo: mockRepo,
317+
metadataStore: mockMeta,
318+
repoPRChangeNotifier: mockNotifier,
319+
cachedPackages: make(map[repository.PackageKey]*cachedPackage), // Initialize to enable deletion
320+
}
321+
322+
// Initialize cache with two revisions
323+
cr.cachedPackageRevisions = make(map[repository.PackageRevisionKey]*cachedPackageRevision)
324+
cr.cachedPackageRevisions[prKey1] = &cachedPackageRevision{
325+
PackageRevision: fpr1,
326+
metadataStore: mockMeta,
327+
isLatestRevision: false,
328+
}
329+
cr.cachedPackageRevisions[prKey2] = &cachedPackageRevision{
330+
PackageRevision: fpr2,
331+
metadataStore: mockMeta,
332+
isLatestRevision: false,
333+
}
334+
335+
// Identify latest revisions (revision 2 should be latest)
336+
identifyLatestRevisions(context.TODO(), cr.cachedPackageRevisions)
337+
338+
// Verify revision 2 is marked as latest before deletion
339+
assert.True(t, cr.cachedPackageRevisions[prKey2].IsLatestRevision())
340+
assert.False(t, cr.cachedPackageRevisions[prKey1].IsLatestRevision())
341+
342+
// Delete the latest revision (revision 2)
343+
err := cr.DeletePackageRevision(context.TODO(), cr.cachedPackageRevisions[prKey2])
344+
assert.NoError(t, err)
345+
346+
// Give time for async notification to complete
347+
time.Sleep(100 * time.Millisecond)
348+
349+
// Now delete pkg1 - should only send 1 notification (deletion) since no more revisions remain
350+
mockNotifier.EXPECT().NotifyPackageRevisionChange(watch.Deleted, mock.Anything).Return(1).Once()
351+
// No async notification expected since no revisions remain
352+
353+
err = cr.DeletePackageRevision(context.TODO(), cr.cachedPackageRevisions[prKey1])
354+
assert.NoError(t, err)
355+
356+
// Give time for any potential async notification
357+
time.Sleep(100 * time.Millisecond)
358+
359+
// Verify all expected mock calls were made
360+
mockNotifier.AssertExpectations(t)
361+
}

pkg/cache/dbcache/dbpackage.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,16 +137,17 @@ func (p *dbPackage) DeletePackageRevision(ctx context.Context, old repository.Pa
137137

138138
if dbPR.IsLatestRevision() {
139139
klog.Infof("dbPackage %+v: latest PackageRevision deleted. Sending notification.", p.Key())
140-
updateCtx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
141-
defer cancel()
142-
go p.sendLatestPkgUpdateNotification(updateCtx)
140+
go p.sendLatestPkgUpdateNotification()
143141
}
144142

145143
return nil
146144
}
147145

148-
func (p *dbPackage) sendLatestPkgUpdateNotification(ctx context.Context) {
149-
_, span := tracer.Start(ctx, "dbPackage:sendLatestPkgUpdateNotification", trace.WithAttributes())
146+
// sendLatestPkgUpdateNotification sends async notification when a new latest package revision is identified
147+
func (p *dbPackage) sendLatestPkgUpdateNotification() {
148+
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
149+
defer cancel()
150+
_, span := tracer.Start(ctx, "dbPackage::sendLatestPkgUpdateNotification", trace.WithAttributes())
150151
defer span.End()
151152

152153
latestRevision, err := pkgRevReadLatestPRFromDB(ctx, p.Key())

pkg/cache/dbcache/dbpackagerevision_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ func (r *fakeRepoWithDeleteError) DeletePackageRevision(context.Context, reposit
312312
return errors.New("package not found")
313313
}
314314

315-
func (t *DbTestSuite) TestDBPackageRevisionDeleteLatest() {
315+
func (t *DbTestSuite) TestDBDeleteLatestRevision() {
316316
// Test that the async notification logic is triggered
317317
ctx := t.Context()
318318

0 commit comments

Comments
 (0)