-
Notifications
You must be signed in to change notification settings - Fork 743
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
Add retry support to objectTracker #1014
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ limitations under the License. | |
package readiness | ||
|
||
import ( | ||
"flag" | ||
"fmt" | ||
"sync" | ||
|
||
|
@@ -29,13 +30,16 @@ import ( | |
"k8s.io/apimachinery/pkg/types" | ||
) | ||
|
||
var readinessRetries = flag.Int("readiness-retries", 0, "The number of resource ingestion attempts allowed before the resource is disregarded. A value of -1 will retry indefinitely.") | ||
|
||
// Expectations tracks expectations for runtime.Objects. | ||
// A set of Expect() calls are made, demarcated by ExpectationsDone(). | ||
// Expectations are satisfied by calls to Observe(). | ||
// Once all expectations are satisfied, Satisfied() will begin returning true. | ||
type Expectations interface { | ||
Expect(o runtime.Object) | ||
CancelExpect(o runtime.Object) | ||
TryCancelExpect(o runtime.Object) | ||
ExpectationsDone() | ||
Observe(o runtime.Object) | ||
Satisfied() bool | ||
|
@@ -51,20 +55,28 @@ type objectTracker struct { | |
gvk schema.GroupVersionKind | ||
cancelled objSet // expectations that have been cancelled | ||
expect objSet // unresolved expectations | ||
tryCancelled objRetrySet // tracks TryCancelExpect calls, decrementing allotted retries for an object | ||
seen objSet // observations made before their expectations | ||
satisfied objSet // tracked to avoid re-adding satisfied expectations and to support unsatisfied() | ||
populated bool // all expectations have been provided | ||
allSatisfied bool // true once all expectations have been satisfied. Acts as a circuit-breaker. | ||
kindsSnapshot []schema.GroupVersionKind // Snapshot of kinds before freeing memory in Satisfied. | ||
tryCancelObj objDataFactory // Function that creates objData types used in tryCancelled | ||
} | ||
|
||
func newObjTracker(gvk schema.GroupVersionKind) *objectTracker { | ||
func newObjTracker(gvk schema.GroupVersionKind, fn objDataFactory) *objectTracker { | ||
if fn == nil { | ||
fn = objDataFromFlags | ||
} | ||
|
||
return &objectTracker{ | ||
gvk: gvk, | ||
cancelled: make(objSet), | ||
expect: make(objSet), | ||
seen: make(objSet), | ||
satisfied: make(objSet), | ||
gvk: gvk, | ||
cancelled: make(objSet), | ||
expect: make(objSet), | ||
tryCancelled: make(objRetrySet), | ||
seen: make(objSet), | ||
satisfied: make(objSet), | ||
tryCancelObj: fn, | ||
} | ||
} | ||
|
||
|
@@ -106,6 +118,14 @@ func (t *objectTracker) Expect(o runtime.Object) { | |
t.expect[k] = struct{}{} | ||
} | ||
|
||
func (t *objectTracker) cancelExpectNoLock(k objKey) { | ||
delete(t.expect, k) | ||
delete(t.seen, k) | ||
delete(t.satisfied, k) | ||
delete(t.tryCancelled, k) | ||
t.cancelled[k] = struct{}{} | ||
} | ||
|
||
// CancelExpect cancels an expectation and marks it so it | ||
// cannot be expected again going forward. | ||
func (t *objectTracker) CancelExpect(o runtime.Object) { | ||
|
@@ -123,10 +143,36 @@ func (t *objectTracker) CancelExpect(o runtime.Object) { | |
return | ||
} | ||
|
||
delete(t.expect, k) | ||
delete(t.seen, k) | ||
delete(t.satisfied, k) | ||
t.cancelled[k] = struct{}{} | ||
t.cancelExpectNoLock(k) | ||
} | ||
|
||
func (t *objectTracker) TryCancelExpect(o runtime.Object) { | ||
t.mu.Lock() | ||
defer t.mu.Unlock() | ||
|
||
// Respect circuit-breaker. | ||
if t.allSatisfied { | ||
return | ||
} | ||
|
||
k, err := objKeyFromObject(o) | ||
if err != nil { | ||
log.Error(err, "skipping") | ||
return | ||
} | ||
|
||
// Check if it's time to delete an expectation or just decrement its allotted retries | ||
obj, ok := t.tryCancelled[k] | ||
if !ok { | ||
// If the item isn't in the map, add it. This is the only place t.newObjData() should be called. | ||
obj = t.tryCancelObj() | ||
} | ||
shouldDel := obj.decrementRetries() | ||
t.tryCancelled[k] = obj // set the changed obj back to the map, as the value is not a pointer | ||
|
||
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. Why not decrement retries here and if 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 didn't want to duplicate this section:
Would it be better to duplicate it? I could also move this logic into 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. IMO error handling code is just kind of duplicative anyway, I don't sweat it too much. IMO the added code complexity here is more costly than duplicating trivial code. Personally, I'd have the interior function take an object key and duplicate the key generation on the caller function. I'd also duplicate the 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. The difference between copying those bits and copying the various deletes() is that the deletes are highly coordinated, where one missing delete could put the system in a bad state. In that situation, avoiding copying lowers the likelihood that a future change misses one copy, introducing a bug. For the get-key logic and the short-circuit logic, the risk of duplication is annoying a programmer and lower performance if a copy is mishandled, respectively. 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. That's great feedback. I like the part about trying to minimize complexity in the most crucial parts of the logic. DRYing up trivial logic is less valuable, as that's easier to grok/harder to break. 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. After moving all of this out of 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. +1, awesome! |
||
if shouldDel { | ||
t.cancelExpectNoLock(k) | ||
} | ||
} | ||
|
||
// ExpectationsDone tells the tracker to stop accepting new expectations. | ||
|
@@ -268,6 +314,7 @@ func (t *objectTracker) Satisfied() bool { | |
t.expect = nil | ||
t.satisfied = nil | ||
t.cancelled = nil | ||
t.tryCancelled = nil | ||
} | ||
return t.allSatisfied | ||
} | ||
|
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.
Can you please add this flag to the readme so people know how to use it?
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.
@ritazh I've added some information to the README. Could you take a look?
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.
Thanks @julianKatz!