-
Notifications
You must be signed in to change notification settings - Fork 48
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 leader election retry #66
Conversation
* adds a test that ensures apiserver going away for 20 seconds would force a successfuly retry of leader election
f144193
to
e365dd7
Compare
pkg/leader/election.go
Outdated
} | ||
} | ||
log.Printf("Starting leader election process. RetryCount: %v", retryCount+1) | ||
leaderelection.RunOrDie(ctx, le.config) |
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.
Is the idea that RunOrDie eventually exits if it loses connection to the API Server?
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.
That's exactly what seems to end up happening. This Kong PR has more details: Kong/kubernetes-ingress-controller#578
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.
I double confirmed this by checking the logs and seeing how often it had to retry (re-run RunOrDie) when the apiserver is down for ~2 minutes
I verified it's working also when the connection is lost for longer duration:
|
This is needed to trigger the issue
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.
Cool way of testing this in e2e tests.
pkg/leader/election.go
Outdated
return | ||
default: | ||
if retryCount > 0 { | ||
backoff.Next(backoffID, backoff.Clock.Now()) |
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.
Neat, I didnt know about this library
pkg/leader/election.go
Outdated
backoff.Next(backoffID, backoff.Clock.Now()) | ||
delay := backoff.Get(backoffID) | ||
log.Printf("Leader election failed, retrying in %v. RetryCount: %v", delay, retryCount+1) | ||
<-time.After(delay) |
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.
time.After
does not respect context cancellation. I put together what I think would be a better way to go about this loop here as an example: https://github.com/substratusai/lingo/compare/example-leader-election-loop ... I played around with this implementation and it exits immediately on context cancellation while still accomplishing expo-backoff (go run ./hack/retry ... ctrl-c
). This is done by moving time.After
into the select cases.
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.
originally I had this in a select but in favor of simplifying I removed it since I thought the higher level select was good enough. I will move it back in, makes senses, thanks for validating this with a quick experiment!
tests/e2e/test.sh
Outdated
sleep 120 | ||
docker exec ${KIND_NODE} iptables -D INPUT -p tcp --dport 6443 -j DROP | ||
|
||
until kubectl get pods; do |
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.
What is until kubectl get pods; do
doing?
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.
so even after the iptable rule is removed it takes sometimes some time for kubernetes to recover. This until statement will wait until kubectl get pods start working again, meaning only if kubectl get pods returns exit code 0 it will continue.
tests/e2e/test.sh
Outdated
done | ||
|
||
# rerun kubectl logs because previous one got killed when apiserver was down | ||
kubectl logs --tail=500 -f deployment/lingo & |
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.
I think you might want --tail=-1
if you are trying to get at all logs since restart.
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.
I didn't want to get all the logs which may already have 1000+ entries and only the last 500. I tried and last 100 wasn't enough but last 500 was more than enough.
echo "Waiting for deployment to scale down back to 0 within 2 minutes" | ||
# Verify that leader election works by forcing a 120 second apiserver outage | ||
KIND_NODE=$(kind get nodes --name=substratus-test) | ||
docker exec ${KIND_NODE} iptables -I INPUT -p tcp --dport 6443 -j DROP |
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.
Is 6443
the port of the API Server running on kind?
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.
Yep and credit to chatGPT with coming up with the high-level idea to simply block the traffic.
k describe svc kubernetes
Name: kubernetes
Namespace: default
Labels: component=apiserver
provider=kubernetes
Annotations: <none>
Selector: <none>
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.96.0.1
IPs: 10.96.0.1
Port: https 443/TCP
TargetPort: 6443/TCP
Endpoints: 172.18.0.2:6443
Session Affinity: None
Events: <none>
Utilize select in waiting for backoff to handle scenario of context cancelled while waiting. Add returning of error so any errors are propogated in the log files
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.
LGTM!
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.
Good work 🏄
I added some minor comments
err := le.Start(ctx) | ||
if err != nil { | ||
setupLog.Error(err, "starting leader election") | ||
os.Exit(1) |
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.
When the context is canceled on a graceful shutdown (sigterm) then Start()
would return the "cancel" error that results in an Exit(1)
.
This error handling strategy is used in other places in this file as well. Instead, we could wrap the context in L110
ctx, cancel := context.WithCancel(ctrl.SetupSignalHandler())
and call cancel()
instead. WDYT?
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.
As the error comes from the context already, it may be simpler to do nothing and let the go routine complete
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.
I didn't fully understand how to change it. Could you elaborate? Are you saying instead of calling os.Exit() just call cancel()?
I didn't see any other example of using cancel on the context in main.go. I tried to follow the same pattern as the other start functions.
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.
The only error returned from Start()
is coming from the context, now. Thanks to your for loop. Calling cancel on the context again is not needed. I would not return an error in Start()
and let the Go routine complete. The main thread should handle the shutdown. I should have deleted my first comment. Sorry for the confusion.
To elaborate for a different use case where other errors can happen, this is an example:
ctx, cancel := context.WithCancel(ctrl.SetupSignalHandler())
go func() {
// in subroutine
if err:= doSomething(ctx); err!=nil{
// todo: log error
cancel()
}
}()
// main thread
if err:=doSomethingElse(ctx);err!=nil{
// todo: log error
os.Exit(1)
}
Please note that the error is not returned to the main thread.
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 for that example! I think I get it now. To me it feels cleaner to just directly exit instead of cancelling a context which then may in the future cause an exit? I favor being explicit and going to the exit path as soon as it makes sense.
pkg/deployments/scaler.go
Outdated
@@ -79,7 +79,7 @@ func (s *scaler) compareScales(current, desired int32) { | |||
s.desiredScale = desired | |||
} | |||
|
|||
if s.currentScale == -1 || s.desiredScale == -1 { | |||
if s.currentScale == -1 || s.desiredScale == -1 || desired == -1 { |
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.
good catch 👍
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.
It doesn't actually solve issue #67 . So I might revert this
pkg/leader/election.go
Outdated
@@ -35,11 +36,11 @@ func NewElection(clientset kubernetes.Interface, id, namespace string) *Election | |||
RetryPeriod: 2 * time.Second, | |||
Callbacks: leaderelection.LeaderCallbacks{ | |||
OnStartedLeading: func(ctx context.Context) { | |||
log.Println("Started leading") | |||
log.Printf("%v started leading", id) |
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.
nit: %v
does the job but it is more common to use %s
for string or %q
to have a quoted string
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.
I didn't know about %q that might actually be nice here.
68f7c53
to
610ba31
Compare
Any ideas why it's not scaling back to 0? https://github.com/substratusai/lingo/actions/runs/7795813038/job/21259370590?pr=66 |
64fecb4
to
7310411
Compare
For reference, see Kong gateway that had to implement the same thing: Kong/kubernetes-ingress-controller#578
Fixes #60