mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-01 07:47:56 +00:00
Add a utility for doing master election via etcd.
This commit is contained in:
parent
24b5d58268
commit
a17acd30ee
18
pkg/election/doc.go
Normal file
18
pkg/election/doc.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package election provides interfaces used for master election.
|
||||||
|
package election
|
176
pkg/election/etcd_master.go
Normal file
176
pkg/election/etcd_master.go
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package election
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
||||||
|
"github.com/coreos/go-etcd/etcd"
|
||||||
|
"github.com/golang/glog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewEtcdMasterElector returns an implementation of election.MasterElector backed by etcd
|
||||||
|
func NewEtcdMasterElector(h tools.EtcdGetSet) MasterElector {
|
||||||
|
return &etcdMasterElector{etcd: h}
|
||||||
|
}
|
||||||
|
|
||||||
|
type empty struct{}
|
||||||
|
|
||||||
|
// internal implementation struct
|
||||||
|
type etcdMasterElector struct {
|
||||||
|
etcd tools.EtcdGetSet
|
||||||
|
done chan empty
|
||||||
|
events chan watch.Event
|
||||||
|
}
|
||||||
|
|
||||||
|
// Elect implements the election.MasterElector interface
|
||||||
|
func (e *etcdMasterElector) Elect(path, id string) watch.Interface {
|
||||||
|
e.done = make(chan empty)
|
||||||
|
e.events = make(chan watch.Event)
|
||||||
|
go util.Forever(func() { e.run(path, id) }, time.Second*5)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *etcdMasterElector) run(path, id string) {
|
||||||
|
masters := make(chan string)
|
||||||
|
errors := make(chan error)
|
||||||
|
go e.master(path, id, 30, masters, errors, e.done)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case m := <-masters:
|
||||||
|
e.events <- watch.Event{
|
||||||
|
Type: watch.Modified,
|
||||||
|
Object: m,
|
||||||
|
}
|
||||||
|
case e := <-errors:
|
||||||
|
glog.Errorf("error in election: %v", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResultChan implements the watch.Interface interface
|
||||||
|
func (e *etcdMasterElector) ResultChan() <-chan watch.Event {
|
||||||
|
return e.events
|
||||||
|
}
|
||||||
|
|
||||||
|
// extendMaster attempts to extend ownership of a master lock for TTL seconds
|
||||||
|
// returns "", nil if extension failed
|
||||||
|
// returns id, nil if extension succeeded
|
||||||
|
// returns "", err if an error occurred
|
||||||
|
func (e *etcdMasterElector) extendMaster(path, id string, ttl uint64, res *etcd.Response) (string, error) {
|
||||||
|
// If it matches the passed in id, extend the lease by writing a new entry.
|
||||||
|
// Uses compare and swap, so that if we TTL out in the meantime, the write will fail.
|
||||||
|
// We don't handle the TTL delete w/o a write case here, it's handled in the next loop
|
||||||
|
// iteration.
|
||||||
|
_, err := e.etcd.CompareAndSwap(path, id, ttl, "", res.Node.ModifiedIndex)
|
||||||
|
if err != nil && !tools.IsEtcdTestFailed(err) {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err != nil && tools.IsEtcdTestFailed(err) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// becomeMaster attempts to become the master for this lock
|
||||||
|
// returns "", nil if the attempt failed
|
||||||
|
// returns id, nil if the attempt succeeded
|
||||||
|
// returns "", err if an error occurred
|
||||||
|
func (e *etcdMasterElector) becomeMaster(path, id string, ttl uint64) (string, error) {
|
||||||
|
_, err := e.etcd.Create(path, id, ttl)
|
||||||
|
if err != nil && !tools.IsEtcdNodeExist(err) {
|
||||||
|
// unexpected error
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err != nil && tools.IsEtcdNodeExist(err) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMaster performs one loop of master locking
|
||||||
|
// on success it returns <master>, nil
|
||||||
|
// on error it returns "", err
|
||||||
|
// in situations where you should try again due to concurrent state changes (e.g. another actor simultaneously acquiring the lock)
|
||||||
|
// it returns "", nil
|
||||||
|
func (e *etcdMasterElector) handleMaster(path, id string, ttl uint64) (string, error) {
|
||||||
|
res, err := e.etcd.Get(path, false, false)
|
||||||
|
|
||||||
|
// Unexpected error, bail out
|
||||||
|
if err != nil && !tools.IsEtcdNotFound(err) {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is no master, try to become the master.
|
||||||
|
if err != nil && tools.IsEtcdNotFound(err) {
|
||||||
|
return e.becomeMaster(path, id, ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should never happen.
|
||||||
|
if res.Node == nil {
|
||||||
|
return "", fmt.Errorf("unexpected response: %#v", res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're not the master, just return the current value
|
||||||
|
if res.Node.Value != id {
|
||||||
|
return res.Node.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are the master, try to extend out lease
|
||||||
|
return e.extendMaster(path, id, ttl, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Master provices a distributed master election lock, maintains lock until failure, or someone sends something in the done channel
|
||||||
|
// The basic algorithm is:
|
||||||
|
// while !done
|
||||||
|
// Get the current master
|
||||||
|
// If there is no current master
|
||||||
|
// Try to become the master
|
||||||
|
// Otherwise
|
||||||
|
// If we are the master, extend the lease
|
||||||
|
// If the master is different than the last time through the loop, report the master
|
||||||
|
// Sleep 80% of TTL
|
||||||
|
func (e *etcdMasterElector) master(path, id string, ttl uint64, masters chan<- string, errors chan<- error, done <-chan empty) {
|
||||||
|
lastMaster := ""
|
||||||
|
for {
|
||||||
|
master, err := e.handleMaster(path, id, ttl)
|
||||||
|
if err != nil {
|
||||||
|
errors <- err
|
||||||
|
} else if len(master) == 0 {
|
||||||
|
continue
|
||||||
|
} else if master != lastMaster {
|
||||||
|
lastMaster = master
|
||||||
|
masters <- master
|
||||||
|
}
|
||||||
|
// TODO: Add Watch here, skip the polling for faster reactions
|
||||||
|
// If done is closed, break out.
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
case <-time.After(time.Duration((ttl*8)/10) * time.Second):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResultChan implements the watch.Interface interface
|
||||||
|
func (e *etcdMasterElector) Stop() {
|
||||||
|
close(e.done)
|
||||||
|
}
|
98
pkg/election/etcd_master_test.go
Normal file
98
pkg/election/etcd_master_test.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package election
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
||||||
|
"github.com/coreos/go-etcd/etcd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEtcdMasterOther(t *testing.T) {
|
||||||
|
path := "foo"
|
||||||
|
etcd := tools.MakeFakeEtcdClient(t)
|
||||||
|
etcd.Set(path, "baz", 0)
|
||||||
|
master := NewEtcdMasterElector(etcd)
|
||||||
|
w := master.Elect(path, "bar")
|
||||||
|
result := <-w.ResultChan()
|
||||||
|
if result.Type != watch.Modified || result.Object.(string) != "baz" {
|
||||||
|
t.Errorf("unexpected event: %#v", result)
|
||||||
|
}
|
||||||
|
w.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEtcdMasterNoOther(t *testing.T) {
|
||||||
|
path := "foo"
|
||||||
|
e := tools.MakeFakeEtcdClient(t)
|
||||||
|
e.TestIndex = true
|
||||||
|
e.Data["foo"] = tools.EtcdResponseWithError{
|
||||||
|
R: &etcd.Response{
|
||||||
|
Node: nil,
|
||||||
|
},
|
||||||
|
E: &etcd.EtcdError{
|
||||||
|
ErrorCode: tools.EtcdErrorCodeNotFound,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
master := NewEtcdMasterElector(e)
|
||||||
|
w := master.Elect(path, "bar")
|
||||||
|
result := <-w.ResultChan()
|
||||||
|
if result.Type != watch.Modified || result.Object.(string) != "bar" {
|
||||||
|
t.Errorf("unexpected event: %#v", result)
|
||||||
|
}
|
||||||
|
w.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEtcdMasterNoOtherThenConflict(t *testing.T) {
|
||||||
|
path := "foo"
|
||||||
|
e := tools.MakeFakeEtcdClient(t)
|
||||||
|
e.TestIndex = true
|
||||||
|
// Ok, so we set up a chain of responses from etcd:
|
||||||
|
// 1) Nothing there
|
||||||
|
// 2) conflict (someone else wrote)
|
||||||
|
// 3) new value (the data they wrote)
|
||||||
|
empty := tools.EtcdResponseWithError{
|
||||||
|
R: &etcd.Response{
|
||||||
|
Node: nil,
|
||||||
|
},
|
||||||
|
E: &etcd.EtcdError{
|
||||||
|
ErrorCode: tools.EtcdErrorCodeNotFound,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
empty.N = &tools.EtcdResponseWithError{
|
||||||
|
R: &etcd.Response{},
|
||||||
|
E: &etcd.EtcdError{
|
||||||
|
ErrorCode: tools.EtcdErrorCodeNodeExist,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
empty.N.N = &tools.EtcdResponseWithError{
|
||||||
|
R: &etcd.Response{
|
||||||
|
Node: &etcd.Node{
|
||||||
|
Value: "baz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
e.Data["foo"] = empty
|
||||||
|
master := NewEtcdMasterElector(e)
|
||||||
|
w := master.Elect(path, "bar")
|
||||||
|
result := <-w.ResultChan()
|
||||||
|
if result.Type != watch.Modified || result.Object.(string) != "bar" {
|
||||||
|
t.Errorf("unexpected event: %#v", result)
|
||||||
|
}
|
||||||
|
w.Stop()
|
||||||
|
}
|
34
pkg/election/master.go
Normal file
34
pkg/election/master.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package election
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MasterElector is an interface for services that can elect masters.
|
||||||
|
// Important Note: MasterElectors are not inter-operable, all participants in the election need to be
|
||||||
|
// using the same underlying implementation of this interface for correct behavior.
|
||||||
|
type MasterElector interface {
|
||||||
|
// RequestMaster makes the caller represented by 'id' enter into a master election for the
|
||||||
|
// distributed lock defined by 'path'
|
||||||
|
// The returned watch.Interface provides a stream of Master objects which
|
||||||
|
// contain the current master.
|
||||||
|
// Calling Stop on the returned interface relinquishes ownership (if currently possesed)
|
||||||
|
// and removes the caller from the election
|
||||||
|
Elect(path, id string) watch.Interface
|
||||||
|
}
|
@ -91,16 +91,16 @@ func IsEtcdNotFound(err error) bool {
|
|||||||
return isEtcdErrorNum(err, EtcdErrorCodeNotFound)
|
return isEtcdErrorNum(err, EtcdErrorCodeNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsEtcdTestFailed returns true iff err is an etcd write conflict.
|
|
||||||
func IsEtcdTestFailed(err error) bool {
|
|
||||||
return isEtcdErrorNum(err, EtcdErrorCodeTestFailed)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsEtcdNodeExist returns true iff err is an etcd node aleady exist error.
|
// IsEtcdNodeExist returns true iff err is an etcd node aleady exist error.
|
||||||
func IsEtcdNodeExist(err error) bool {
|
func IsEtcdNodeExist(err error) bool {
|
||||||
return isEtcdErrorNum(err, EtcdErrorCodeNodeExist)
|
return isEtcdErrorNum(err, EtcdErrorCodeNodeExist)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsEtcdTestFailed returns true iff err is an etcd write conflict.
|
||||||
|
func IsEtcdTestFailed(err error) bool {
|
||||||
|
return isEtcdErrorNum(err, EtcdErrorCodeTestFailed)
|
||||||
|
}
|
||||||
|
|
||||||
// IsEtcdWatchStoppedByUser returns true iff err is a client triggered stop.
|
// IsEtcdWatchStoppedByUser returns true iff err is a client triggered stop.
|
||||||
func IsEtcdWatchStoppedByUser(err error) bool {
|
func IsEtcdWatchStoppedByUser(err error) bool {
|
||||||
return etcd.ErrWatchStoppedByUser == err
|
return etcd.ErrWatchStoppedByUser == err
|
||||||
|
@ -27,6 +27,8 @@ import (
|
|||||||
type EtcdResponseWithError struct {
|
type EtcdResponseWithError struct {
|
||||||
R *etcd.Response
|
R *etcd.Response
|
||||||
E error
|
E error
|
||||||
|
// if N is non-null, it will be assigned into the map after this response is used for an operation
|
||||||
|
N *EtcdResponseWithError
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestLogger is a type passed to Test functions to support formatted test logs.
|
// TestLogger is a type passed to Test functions to support formatted test logs.
|
||||||
@ -92,6 +94,15 @@ func (f *FakeEtcdClient) generateIndex() uint64 {
|
|||||||
return f.ChangeIndex
|
return f.ChangeIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Requires that f.Mutex be held.
|
||||||
|
func (f *FakeEtcdClient) updateResponse(key string) {
|
||||||
|
resp, found := f.Data[key]
|
||||||
|
if !found || resp.N == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.Data[key] = *resp.N
|
||||||
|
}
|
||||||
|
|
||||||
func (f *FakeEtcdClient) AddChild(key, data string, ttl uint64) (*etcd.Response, error) {
|
func (f *FakeEtcdClient) AddChild(key, data string, ttl uint64) (*etcd.Response, error) {
|
||||||
f.Mutex.Lock()
|
f.Mutex.Lock()
|
||||||
defer f.Mutex.Unlock()
|
defer f.Mutex.Unlock()
|
||||||
@ -103,6 +114,7 @@ func (f *FakeEtcdClient) AddChild(key, data string, ttl uint64) (*etcd.Response,
|
|||||||
func (f *FakeEtcdClient) Get(key string, sort, recursive bool) (*etcd.Response, error) {
|
func (f *FakeEtcdClient) Get(key string, sort, recursive bool) (*etcd.Response, error) {
|
||||||
f.Mutex.Lock()
|
f.Mutex.Lock()
|
||||||
defer f.Mutex.Unlock()
|
defer f.Mutex.Unlock()
|
||||||
|
defer f.updateResponse(key)
|
||||||
|
|
||||||
result := f.Data[key]
|
result := f.Data[key]
|
||||||
if result.R == nil {
|
if result.R == nil {
|
||||||
@ -161,6 +173,7 @@ func (f *FakeEtcdClient) setLocked(key, value string, ttl uint64) (*etcd.Respons
|
|||||||
func (f *FakeEtcdClient) Set(key, value string, ttl uint64) (*etcd.Response, error) {
|
func (f *FakeEtcdClient) Set(key, value string, ttl uint64) (*etcd.Response, error) {
|
||||||
f.Mutex.Lock()
|
f.Mutex.Lock()
|
||||||
defer f.Mutex.Unlock()
|
defer f.Mutex.Unlock()
|
||||||
|
defer f.updateResponse(key)
|
||||||
|
|
||||||
return f.setLocked(key, value, ttl)
|
return f.setLocked(key, value, ttl)
|
||||||
}
|
}
|
||||||
@ -182,6 +195,7 @@ func (f *FakeEtcdClient) CompareAndSwap(key, value string, ttl uint64, prevValue
|
|||||||
|
|
||||||
f.Mutex.Lock()
|
f.Mutex.Lock()
|
||||||
defer f.Mutex.Unlock()
|
defer f.Mutex.Unlock()
|
||||||
|
defer f.updateResponse(key)
|
||||||
|
|
||||||
if !f.nodeExists(key) {
|
if !f.nodeExists(key) {
|
||||||
f.t.Logf("c&s: node doesn't exist")
|
f.t.Logf("c&s: node doesn't exist")
|
||||||
@ -206,6 +220,7 @@ func (f *FakeEtcdClient) CompareAndSwap(key, value string, ttl uint64, prevValue
|
|||||||
func (f *FakeEtcdClient) Create(key, value string, ttl uint64) (*etcd.Response, error) {
|
func (f *FakeEtcdClient) Create(key, value string, ttl uint64) (*etcd.Response, error) {
|
||||||
f.Mutex.Lock()
|
f.Mutex.Lock()
|
||||||
defer f.Mutex.Unlock()
|
defer f.Mutex.Unlock()
|
||||||
|
defer f.updateResponse(key)
|
||||||
|
|
||||||
if f.nodeExists(key) {
|
if f.nodeExists(key) {
|
||||||
return nil, EtcdErrorNodeExist
|
return nil, EtcdErrorNodeExist
|
||||||
|
Loading…
Reference in New Issue
Block a user