Skip to content

Commit 4300487

Browse files
committed
fix: serialize watch event dispatch per storage
1 parent dc07eba commit 4300487

3 files changed

Lines changed: 79 additions & 25 deletions

File tree

internal/aggregated/storage/template.go

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ type TemplateStorage struct {
6464
provider coder.ClientProvider
6565
tableConvertor rest.TableConvertor
6666
broadcaster *watch.Broadcaster
67+
watchEvents chan watch.Event
68+
watchEventsWG sync.WaitGroup
6769
destroyOnce sync.Once
6870
}
6971

@@ -73,11 +75,16 @@ func NewTemplateStorage(provider coder.ClientProvider) *TemplateStorage {
7375
panic("assertion failed: template client provider must not be nil")
7476
}
7577

76-
return &TemplateStorage{
78+
storage := &TemplateStorage{
7779
provider: provider,
7880
tableConvertor: rest.NewDefaultTableConvertor(aggregationv1alpha1.Resource("codertemplates")),
7981
broadcaster: watch.NewBroadcaster(watchBroadcasterQueueLen, watch.WaitIfChannelFull),
82+
watchEvents: make(chan watch.Event, watchBroadcasterQueueLen),
8083
}
84+
storage.watchEventsWG.Add(1)
85+
go storage.dispatchWatchEvents()
86+
87+
return storage
8188
}
8289

8390
// New returns an empty CoderTemplate object.
@@ -92,6 +99,12 @@ func (s *TemplateStorage) Destroy() {
9299
}
93100

94101
s.destroyOnce.Do(func() {
102+
if s.watchEvents == nil {
103+
panic("assertion failed: template watch event queue must not be nil")
104+
}
105+
close(s.watchEvents)
106+
s.watchEventsWG.Wait()
107+
95108
if s.broadcaster != nil {
96109
s.broadcaster.Shutdown()
97110
}
@@ -366,7 +379,7 @@ func (s *TemplateStorage) Create(
366379
return nil, fmt.Errorf("assertion failed: converted template must not be nil")
367380
}
368381

369-
broadcastEventAsync(s.broadcaster, watch.Added, result.DeepCopy())
382+
s.enqueueWatchEvent(watch.Added, result.DeepCopy())
370383

371384
return result, nil
372385
}
@@ -386,7 +399,7 @@ func (s *TemplateStorage) Create(
386399
return nil, fmt.Errorf("assertion failed: converted template must not be nil")
387400
}
388401

389-
broadcastEventAsync(s.broadcaster, watch.Added, result.DeepCopy())
402+
s.enqueueWatchEvent(watch.Added, result.DeepCopy())
390403

391404
return result, nil
392405
}
@@ -633,7 +646,7 @@ func (s *TemplateStorage) Update(
633646
return nil, false, fmt.Errorf("assertion failed: refreshed template must not be nil")
634647
}
635648

636-
broadcastEventAsync(s.broadcaster, watch.Modified, result.DeepCopy())
649+
s.enqueueWatchEvent(watch.Modified, result.DeepCopy())
637650

638651
return result, false, nil
639652
}
@@ -699,11 +712,33 @@ func (s *TemplateStorage) Delete(
699712
}
700713

701714
// Emit a Deleted event with the last-known template state.
702-
broadcastEventAsync(s.broadcaster, watch.Deleted, templateObj.DeepCopy())
715+
s.enqueueWatchEvent(watch.Deleted, templateObj.DeepCopy())
703716

704717
return &metav1.Status{Status: metav1.StatusSuccess}, true, nil
705718
}
706719

720+
func (s *TemplateStorage) dispatchWatchEvents() {
721+
defer s.watchEventsWG.Done()
722+
723+
for event := range s.watchEvents {
724+
_ = s.broadcaster.Action(event.Type, event.Object)
725+
}
726+
}
727+
728+
func (s *TemplateStorage) enqueueWatchEvent(action watch.EventType, obj runtime.Object) {
729+
if s == nil {
730+
panic("assertion failed: template storage must not be nil")
731+
}
732+
if s.watchEvents == nil {
733+
panic("assertion failed: template watch event queue must not be nil")
734+
}
735+
if obj == nil {
736+
panic("assertion failed: template watch event object must not be nil")
737+
}
738+
739+
s.watchEvents <- watch.Event{Type: action, Object: obj}
740+
}
741+
707742
// ConvertToTable converts a template object or list into kubectl table output.
708743
func (s *TemplateStorage) ConvertToTable(ctx context.Context, object, tableOptions runtime.Object) (*metav1.Table, error) {
709744
if s == nil {

internal/aggregated/storage/watch.go

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
88
"k8s.io/apimachinery/pkg/fields"
99
"k8s.io/apimachinery/pkg/labels"
10-
"k8s.io/apimachinery/pkg/runtime"
1110
"k8s.io/apimachinery/pkg/watch"
1211
)
1312

@@ -59,21 +58,6 @@ func validateUnsupportedWatchListOptions(opts *metainternalversion.ListOptions)
5958
return nil
6059
}
6160

62-
// broadcastEventAsync emits a watch event in a goroutine so mutation request
63-
// handlers are not blocked by slow watchers.
64-
func broadcastEventAsync(broadcaster *watch.Broadcaster, action watch.EventType, obj runtime.Object) {
65-
if broadcaster == nil {
66-
panic("assertion failed: watch broadcaster must not be nil")
67-
}
68-
if obj == nil {
69-
panic("assertion failed: watch event object must not be nil")
70-
}
71-
72-
go func() {
73-
_ = broadcaster.Action(action, obj)
74-
}()
75-
}
76-
7761
// filterForListOptions builds a watch.FilterFunc that applies namespace, label, and field selector filtering.
7862
// Returns nil if no filtering is needed.
7963
func filterForListOptions(requestNamespace string, opts *metainternalversion.ListOptions) (watch.FilterFunc, error) {

internal/aggregated/storage/workspace.go

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ type WorkspaceStorage struct {
3838
provider coder.ClientProvider
3939
tableConvertor rest.TableConvertor
4040
broadcaster *watch.Broadcaster
41+
watchEvents chan watch.Event
42+
watchEventsWG sync.WaitGroup
4143
destroyOnce sync.Once
4244
}
4345

@@ -47,11 +49,16 @@ func NewWorkspaceStorage(provider coder.ClientProvider) *WorkspaceStorage {
4749
panic("assertion failed: workspace client provider must not be nil")
4850
}
4951

50-
return &WorkspaceStorage{
52+
storage := &WorkspaceStorage{
5153
provider: provider,
5254
tableConvertor: rest.NewDefaultTableConvertor(aggregationv1alpha1.Resource("coderworkspaces")),
5355
broadcaster: watch.NewBroadcaster(watchBroadcasterQueueLen, watch.WaitIfChannelFull),
56+
watchEvents: make(chan watch.Event, watchBroadcasterQueueLen),
5457
}
58+
storage.watchEventsWG.Add(1)
59+
go storage.dispatchWatchEvents()
60+
61+
return storage
5562
}
5663

5764
// New returns an empty CoderWorkspace object.
@@ -66,6 +73,12 @@ func (s *WorkspaceStorage) Destroy() {
6673
}
6774

6875
s.destroyOnce.Do(func() {
76+
if s.watchEvents == nil {
77+
panic("assertion failed: workspace watch event queue must not be nil")
78+
}
79+
close(s.watchEvents)
80+
s.watchEventsWG.Wait()
81+
6982
if s.broadcaster != nil {
7083
s.broadcaster.Shutdown()
7184
}
@@ -368,7 +381,7 @@ func (s *WorkspaceStorage) Create(
368381
return nil, fmt.Errorf("assertion failed: converted workspace must not be nil")
369382
}
370383

371-
broadcastEventAsync(s.broadcaster, watch.Added, result.DeepCopy())
384+
s.enqueueWatchEvent(watch.Added, result.DeepCopy())
372385

373386
return result, nil
374387
}
@@ -518,7 +531,7 @@ func (s *WorkspaceStorage) Update(
518531
return nil, false, fmt.Errorf("assertion failed: converted workspace must not be nil")
519532
}
520533

521-
broadcastEventAsync(s.broadcaster, watch.Modified, result.DeepCopy())
534+
s.enqueueWatchEvent(watch.Modified, result.DeepCopy())
522535

523536
return result, false, nil
524537
}
@@ -593,13 +606,35 @@ func (s *WorkspaceStorage) Delete(
593606

594607
// Workspace deletion is asynchronous in Coder. Emit a Modified event
595608
// to signal that deletion was requested, rather than a Deleted event.
596-
broadcastEventAsync(s.broadcaster, watch.Modified, workspaceObj.DeepCopy())
609+
s.enqueueWatchEvent(watch.Modified, workspaceObj.DeepCopy())
597610

598611
// Deletion is asynchronous in Coder: we only enqueue a delete build transition here.
599612
// Report deleted=false so Kubernetes callers know the resource is not gone yet.
600613
return &metav1.Status{Status: metav1.StatusSuccess}, false, nil
601614
}
602615

616+
func (s *WorkspaceStorage) dispatchWatchEvents() {
617+
defer s.watchEventsWG.Done()
618+
619+
for event := range s.watchEvents {
620+
_ = s.broadcaster.Action(event.Type, event.Object)
621+
}
622+
}
623+
624+
func (s *WorkspaceStorage) enqueueWatchEvent(action watch.EventType, obj runtime.Object) {
625+
if s == nil {
626+
panic("assertion failed: workspace storage must not be nil")
627+
}
628+
if s.watchEvents == nil {
629+
panic("assertion failed: workspace watch event queue must not be nil")
630+
}
631+
if obj == nil {
632+
panic("assertion failed: workspace watch event object must not be nil")
633+
}
634+
635+
s.watchEvents <- watch.Event{Type: action, Object: obj}
636+
}
637+
603638
// ConvertToTable converts a workspace object or list into kubectl table output.
604639
func (s *WorkspaceStorage) ConvertToTable(ctx context.Context, object, tableOptions runtime.Object) (*metav1.Table, error) {
605640
if s == nil {

0 commit comments

Comments
 (0)