99 "time"
1010
1111 "github.com/pingcap/tiproxy/lib/util/errors"
12- "github.com/pingcap/tiproxy/lib/util/retry"
1312 "github.com/pingcap/tiproxy/lib/util/waitgroup"
1413 "github.com/pingcap/tiproxy/pkg/metrics"
1514 "github.com/pingcap/tiproxy/pkg/util/etcd"
@@ -22,7 +21,6 @@ import (
2221)
2322
2423const (
25- logInterval = 10
2624 ownerKeyPrefix = "/tiproxy/"
2725 ownerKeySuffix = "/owner"
2826)
@@ -40,23 +38,27 @@ type Election interface {
4038 ID () string
4139 // GetOwnerID gets the owner ID.
4240 GetOwnerID (ctx context.Context ) (string , error )
43- // Close stops compaining the owner .
41+ // Close resigns and but doesn't retire .
4442 Close ()
4543}
4644
4745type ElectionConfig struct {
48- Timeout time.Duration
49- RetryIntvl time.Duration
50- RetryCnt uint64
51- SessionTTL int
46+ Timeout time.Duration
47+ RetryIntvl time.Duration
48+ QueryIntvl time.Duration
49+ WaitBeforeRetire time.Duration
50+ RetryCnt uint64
51+ SessionTTL int
5252}
5353
5454func DefaultElectionConfig (sessionTTL int ) ElectionConfig {
5555 return ElectionConfig {
56- Timeout : 2 * time .Second ,
57- RetryIntvl : 500 * time .Millisecond ,
58- RetryCnt : 3 ,
59- SessionTTL : sessionTTL ,
56+ Timeout : 2 * time .Second ,
57+ RetryIntvl : 500 * time .Millisecond ,
58+ QueryIntvl : 1 * time .Second ,
59+ WaitBeforeRetire : 3 * time .Second ,
60+ RetryCnt : 3 ,
61+ SessionTTL : sessionTTL ,
6062 }
6163}
6264
@@ -75,6 +77,7 @@ type election struct {
7577 wg waitgroup.WaitGroup
7678 cancel context.CancelFunc
7779 member Member
80+ isOwner bool
7881}
7982
8083// NewElection creates an Election.
@@ -108,45 +111,20 @@ func (m *election) ID() string {
108111 return m .id
109112}
110113
111- func (m * election ) initSession (ctx context.Context ) (* concurrency.Session , error ) {
112- var session * concurrency.Session
113- // If the network breaks for sometime, the session will fail but it still needs to compaign after recovery.
114- // So retry it infinitely.
115- err := retry .RetryNotify (func () error {
116- var err error
117- // Do not use context.WithTimeout, otherwise the session will be cancelled after timeout, even if it is created successfully.
118- session , err = concurrency .NewSession (m .etcdCli , concurrency .WithTTL (m .cfg .SessionTTL ), concurrency .WithContext (ctx ))
119- return err
120- }, ctx , m .cfg .RetryIntvl , retry .InfiniteCnt ,
121- func (err error , duration time.Duration ) {
122- m .lg .Warn ("failed to init election session, retrying" , zap .Error (err ))
123- }, logInterval )
124- if err == nil {
125- m .lg .Info ("election session is initialized" )
126- } else {
127- m .lg .Error ("failed to init election session, quit" , zap .Error (err ))
128- }
129- return session , err
130- }
131-
132114func (m * election ) campaignLoop (ctx context.Context ) {
133- session , err := m . initSession ( ctx )
115+ session , err := concurrency . NewSession ( m . etcdCli , concurrency . WithTTL ( m . cfg . SessionTTL ), concurrency . WithContext ( ctx ) )
134116 if err != nil {
117+ m .lg .Error ("new session failed, break campaign loop" , zap .Error (errors .WithStack (err )))
135118 return
136119 }
137- isOwner := false
138- defer func () {
139- if isOwner {
140- m .onRetired ()
141- }
142- }()
143120 for {
144121 select {
145122 case <- session .Done ():
146123 m .lg .Info ("etcd session is done, creates a new one" )
147124 leaseID := session .Lease ()
148- if session , err = m .initSession (ctx ); err != nil {
149- m .lg .Error ("new session failed, break campaign loop" , zap .Error (err ))
125+ session , err = concurrency .NewSession (m .etcdCli , concurrency .WithTTL (m .cfg .SessionTTL ), concurrency .WithContext (ctx ))
126+ if err != nil {
127+ m .lg .Error ("new session failed, break campaign loop" , zap .Error (errors .WithStack (err )))
150128 m .revokeLease (leaseID )
151129 return
152130 }
@@ -166,17 +144,21 @@ func (m *election) campaignLoop(ctx context.Context) {
166144 continue
167145 }
168146
169- // Retire after the etcd server can be connected so that there will always be an owner.
170- // It's allowed if multiple members act as the owner but it's not allowed if no member acts as the owner.
171- // E.g. at least one member needs to bind the VIP.
172- if isOwner {
173- m .onRetired ()
174- isOwner = false
147+ var wg waitgroup.WaitGroup
148+ childCtx , cancel := context .WithCancel (ctx )
149+ if m .isOwner {
150+ // Check if another member becomes the new owner during campaign.
151+ wg .RunWithRecover (func () {
152+ m .waitRetire (childCtx )
153+ }, nil , m .lg )
175154 }
176155
177156 elec := concurrency .NewElection (session , m .key )
178- if err = elec .Campaign (ctx , m .id ); err != nil {
179- m .lg .Info ("failed to campaign" , zap .Error (err ))
157+ err = elec .Campaign (ctx , m .id )
158+ cancel ()
159+ wg .Wait ()
160+ if err != nil {
161+ m .lg .Info ("failed to campaign" , zap .Error (errors .WithStack (err )))
180162 continue
181163 }
182164
@@ -186,27 +168,61 @@ func (m *election) campaignLoop(ctx context.Context) {
186168 continue
187169 }
188170 if hack .String (kv .Value ) != m .id {
189- m .lg .Warn ("owner id mismatches" , zap .String ("owner" , hack .String (kv .Value )))
171+ // Campaign may finish without errors when the session is done.
172+ m .lg .Info ("owner id mismatches" , zap .String ("owner" , hack .String (kv .Value )))
173+ if m .isOwner {
174+ m .onRetired ()
175+ }
190176 continue
191177 }
192178
193- m .onElected ()
194- isOwner = true
179+ if ! m .isOwner {
180+ m .onElected ()
181+ } else {
182+ // It was the owner before the etcd failure and now is still the owner.
183+ m .lg .Info ("still the owner" )
184+ }
195185 m .watchOwner (ctx , session , hack .String (kv .Key ))
196186 }
197187}
198188
199189func (m * election ) onElected () {
190+ m .lg .Info ("elected as the owner" )
200191 m .member .OnElected ()
192+ m .isOwner = true
201193 metrics .OwnerGauge .WithLabelValues (m .trimedKey ).Set (1 )
202- m .lg .Info ("elected as the owner" )
203194}
204195
205196func (m * election ) onRetired () {
197+ m .lg .Info ("the owner retires" )
206198 m .member .OnRetired ()
199+ m .isOwner = false
207200 // Delete the metric so that it doesn't show on Grafana.
208201 metrics .OwnerGauge .MetricVec .DeletePartialMatch (map [string ]string {metrics .LblType : m .trimedKey })
209- m .lg .Info ("the owner retires" )
202+ }
203+
204+ // waitRetire retires after another member becomes the owner so that there will always be an owner.
205+ // It's allowed if multiple members act as the owner for some time but it's not allowed if no member acts as the owner.
206+ // E.g. at least one member needs to bind the VIP even if the etcd server leader is down.
207+ func (m * election ) waitRetire (ctx context.Context ) {
208+ ticker := time .NewTicker (m .cfg .QueryIntvl )
209+ defer ticker .Stop ()
210+ for ctx .Err () == nil {
211+ select {
212+ case <- ticker .C :
213+ id , err := m .GetOwnerID (ctx )
214+ if err != nil {
215+ continue
216+ }
217+ // Another member becomes the owner, retire.
218+ if id != m .id {
219+ m .onRetired ()
220+ return
221+ }
222+ case <- ctx .Done ():
223+ return
224+ }
225+ }
210226}
211227
212228// revokeLease revokes the session lease so that other members can compaign immediately.
@@ -271,8 +287,8 @@ func (m *election) watchOwner(ctx context.Context, session *concurrency.Session,
271287 }
272288}
273289
274- // Close is called before the instance is going to shutdown .
275- // It should hand over the owner to someone else .
290+ // Close is typically called before graceful shutdown. It resigns but doesn't retire or wait for the new owner .
291+ // The caller has to decide if it should retire after graceful wait .
276292func (m * election ) Close () {
277293 if m .cancel != nil {
278294 m .cancel ()
0 commit comments