-
Notifications
You must be signed in to change notification settings - Fork 38.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reduce contention in watchcache by not calling event handler under lock #76702
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -125,7 +125,7 @@ type watchCache struct { | |
|
||
// This handler is run at the end of every Add/Update/Delete method | ||
// and additionally gets the previous value of the object. | ||
onEvent func(*watchCacheEvent) | ||
eventHandler func(*watchCacheEvent) | ||
|
||
// for testing timeouts. | ||
clock clock.Clock | ||
|
@@ -137,6 +137,7 @@ type watchCache struct { | |
func newWatchCache( | ||
capacity int, | ||
keyFunc func(runtime.Object) (string, error), | ||
eventHandler func(*watchCacheEvent), | ||
getAttrsFunc func(runtime.Object) (labels.Set, fields.Set, error), | ||
versioner storage.Versioner) *watchCache { | ||
wc := &watchCache{ | ||
|
@@ -149,6 +150,7 @@ func newWatchCache( | |
store: cache.NewStore(storeElementKey), | ||
resourceVersion: 0, | ||
listResourceVersion: 0, | ||
eventHandler: eventHandler, | ||
clock: clock.RealClock{}, | ||
versioner: versioner, | ||
} | ||
|
@@ -204,6 +206,8 @@ func (w *watchCache) objectToVersionedRuntimeObject(obj interface{}) (runtime.Ob | |
return object, resourceVersion, nil | ||
} | ||
|
||
// processEvent is safe as long as there is at most one call to it in flight | ||
// at any point in time. | ||
func (w *watchCache) processEvent(event watch.Event, resourceVersion uint64, updateFunc func(*storeElement) error) error { | ||
key, err := w.keyFunc(event.Object) | ||
if err != nil { | ||
|
@@ -224,30 +228,41 @@ func (w *watchCache) processEvent(event watch.Event, resourceVersion uint64, upd | |
ResourceVersion: resourceVersion, | ||
} | ||
|
||
// TODO: We should consider moving this lock below after the watchCacheEvent | ||
// is created. In such situation, the only problematic scenario is Replace( | ||
// happening after getting object from store and before acquiring a lock. | ||
// Maybe introduce another lock for this purpose. | ||
w.Lock() | ||
defer w.Unlock() | ||
previous, exists, err := w.store.Get(elem) | ||
if err != nil { | ||
if err := func() error { | ||
// TODO: We should consider moving this lock below after the watchCacheEvent | ||
// is created. In such situation, the only problematic scenario is Replace( | ||
// happening after getting object from store and before acquiring a lock. | ||
// Maybe introduce another lock for this purpose. | ||
w.Lock() | ||
defer w.Unlock() | ||
|
||
previous, exists, err := w.store.Get(elem) | ||
if err != nil { | ||
return err | ||
} | ||
if exists { | ||
previousElem := previous.(*storeElement) | ||
watchCacheEvent.PrevObject = previousElem.Object | ||
watchCacheEvent.PrevObjLabels = previousElem.Labels | ||
watchCacheEvent.PrevObjFields = previousElem.Fields | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about we acquire the lock after watchCacheEvent is constructed (after PrevObjFields is assigned) ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is what I meant: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i don't understand how that helps - you still do Get() under lock, so this doesn't buy as almost anything for lock contention, and it add additional load. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can call Get() under read lock. Let me think more on this. |
||
} | ||
|
||
w.updateCache(watchCacheEvent) | ||
w.resourceVersion = resourceVersion | ||
defer w.cond.Broadcast() | ||
|
||
return updateFunc(elem) | ||
}(); err != nil { | ||
return err | ||
} | ||
if exists { | ||
previousElem := previous.(*storeElement) | ||
watchCacheEvent.PrevObject = previousElem.Object | ||
watchCacheEvent.PrevObjLabels = previousElem.Labels | ||
watchCacheEvent.PrevObjFields = previousElem.Fields | ||
} | ||
w.updateCache(watchCacheEvent) | ||
w.resourceVersion = resourceVersion | ||
|
||
if w.onEvent != nil { | ||
w.onEvent(watchCacheEvent) | ||
// Avoid calling event handler under lock. | ||
// This is safe as long as there is at most one call to processEvent in flight | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Include this info in the godoc for the function? It's an important part of this function's contract. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we use w.Lock() to protect w.cache and w.onEvent both, which should be fixed too.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Refactored to eliminate SetOnEvent method - we don't need to allow modifying this once watchCache is created. |
||
// at any point in time. | ||
if w.eventHandler != nil { | ||
w.eventHandler(watchCacheEvent) | ||
} | ||
w.cond.Broadcast() | ||
return updateFunc(elem) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
return nil | ||
} | ||
|
||
// Assumes that lock is already held for write. | ||
|
@@ -397,12 +412,6 @@ func (w *watchCache) SetOnReplace(onReplace func()) { | |
w.onReplace = onReplace | ||
} | ||
|
||
func (w *watchCache) SetOnEvent(onEvent func(*watchCacheEvent)) { | ||
w.Lock() | ||
defer w.Unlock() | ||
w.onEvent = onEvent | ||
} | ||
|
||
func (w *watchCache) GetAllEventsSinceThreadUnsafe(resourceVersion uint64) ([]*watchCacheEvent, error) { | ||
size := w.endIndex - w.startIndex | ||
var oldest uint64 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we need keep these code ? Have any reason to keep?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes - we need this code, because we create above time.NewTimer(0).
So in order to not have any element in the .C channel, we need to consume it here.