-
Notifications
You must be signed in to change notification settings - Fork 1.1k
choose switchover candidate based on lag and role #1700
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
Changes from 1 commit
92b2e9d
87d50d2
d784c96
4f87238
b47b826
3418be5
eede7cf
1b0d51a
f29c2f8
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 |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| package cluster | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "fmt" | ||
| "io/ioutil" | ||
| "net/http" | ||
| "testing" | ||
|
|
||
| "github.com/golang/mock/gomock" | ||
| "github.com/zalando/postgres-operator/mocks" | ||
| acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" | ||
| "github.com/zalando/postgres-operator/pkg/spec" | ||
| "github.com/zalando/postgres-operator/pkg/util/k8sutil" | ||
| "github.com/zalando/postgres-operator/pkg/util/patroni" | ||
| ) | ||
|
|
||
| func TestGetSwitchoverCandidate(t *testing.T) { | ||
| testName := "test getting right switchover candidate" | ||
| namespace := "default" | ||
|
|
||
| ctrl := gomock.NewController(t) | ||
| defer ctrl.Finish() | ||
|
|
||
| var cluster = New(Config{}, k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger, eventRecorder) | ||
|
|
||
| // simulate different member scenarios | ||
| tests := []struct { | ||
| subtest string | ||
| clusterJson string | ||
| expectedCandidate spec.NamespacedName | ||
| expectedError error | ||
| }{ | ||
| { | ||
| subtest: "choose sync_standby over replica", | ||
| clusterJson: `{"members": [{"name": "acid-test-cluster-0", "role": "leader", "state": "running", "api_url": "http://192.168.100.1:8008/patroni", "host": "192.168.100.1", "port": 5432, "timeline": 1}, {"name": "acid-test-cluster-1", "role": "sync_standby", "state": "running", "api_url": "http://192.168.100.2:8008/patroni", "host": "192.168.100.2", "port": 5432, "timeline": 1, "lag": 0}, {"name": "acid-test-cluster-2", "role": "replica", "state": "running", "api_url": "http://192.168.100.3:8008/patroni", "host": "192.168.100.3", "port": 5432, "timeline": 1, "lag": 0}]}`, | ||
| expectedCandidate: spec.NamespacedName{Namespace: namespace, Name: "acid-test-cluster-1"}, | ||
| expectedError: nil, | ||
| }, | ||
| { | ||
| subtest: "choose replica without a lag", | ||
| clusterJson: `{"members": [{"name": "acid-test-cluster-0", "role": "leader", "state": "running", "api_url": "http://192.168.100.1:8008/patroni", "host": "192.168.100.1", "port": 5432, "timeline": 1}, {"name": "acid-test-cluster-1", "role": "replica", "state": "running", "api_url": "http://192.168.100.2:8008/patroni", "host": "192.168.100.2", "port": 5432, "timeline": 1, "lag": 5}, {"name": "acid-test-cluster-2", "role": "replica", "state": "running", "api_url": "http://192.168.100.3:8008/patroni", "host": "192.168.100.3", "port": 5432, "timeline": 1, "lag": 0}]}`, | ||
| expectedCandidate: spec.NamespacedName{Namespace: namespace, Name: "acid-test-cluster-2"}, | ||
| expectedError: nil, | ||
| }, | ||
| { | ||
| subtest: "no suitable replica available", | ||
| clusterJson: `{"members": [{"name": "acid-test-cluster-0", "role": "leader", "state": "running", "api_url": "http://192.168.100.1:8008/patroni", "host": "192.168.100.1", "port": 5432, "timeline": 1}, {"name": "acid-test-cluster-1", "role": "replica", "state": "running", "api_url": "http://192.168.100.2:8008/patroni", "host": "192.168.100.2", "port": 5432, "timeline": 1, "lag": 5}]}`, | ||
| expectedCandidate: spec.NamespacedName{}, | ||
| expectedError: fmt.Errorf("no replica suitable for switchover: acid-test-cluster-1 lags behind by 5 MB"), | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| // mocking cluster members | ||
| r := ioutil.NopCloser(bytes.NewReader([]byte(tt.clusterJson))) | ||
|
|
||
| response := http.Response{ | ||
| StatusCode: 200, | ||
| Body: r, | ||
| } | ||
|
|
||
| mockClient := mocks.NewMockHTTPClient(ctrl) | ||
| mockClient.EXPECT().Get(gomock.Any()).Return(&response, nil).AnyTimes() | ||
|
|
||
| p := patroni.New(patroniLogger, mockClient) | ||
| cluster.patroni = p | ||
| mockMasterPod := newMockPod("192.168.100.1") | ||
| mockMasterPod.Namespace = namespace | ||
|
|
||
| candidate, err := cluster.getSwitchoverCandidate(mockMasterPod) | ||
| if err != nil && err.Error() != tt.expectedError.Error() { | ||
| t.Errorf("%s - %s: unexpected error, %v", testName, tt.subtest, err) | ||
| } | ||
|
|
||
| if candidate != tt.expectedCandidate { | ||
| t.Errorf("%s - %s: unexpect switchover candidate, got %s, expected %s", testName, tt.subtest, candidate, tt.expectedCandidate) | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,11 +14,11 @@ import ( | |
| type PostgresRole string | ||
|
|
||
| const ( | ||
| // Master role | ||
| Master PostgresRole = "master" | ||
|
|
||
| // Replica role | ||
| Replica PostgresRole = "replica" | ||
| Master PostgresRole = "master" | ||
| Replica PostgresRole = "replica" | ||
| Leader PostgresRole = "leader" | ||
| StandbyLeader PostgresRole = "standby_leader" | ||
| SyncStandby PostgresRole = "sync_standby" | ||
|
Member
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 feel there is a comment missing here:
While it is still possible to figure it out from the code, a short comment about why these are not completely independent states is worthy.
Member
Author
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.
|
||
| ) | ||
|
|
||
| // PodEventType represents the type of a pod-related event | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ import ( | |
| const ( | ||
| failoverPath = "/failover" | ||
| configPath = "/config" | ||
| clusterPath = "/cluster" | ||
| statusPath = "/patroni" | ||
| restartPath = "/restart" | ||
| apiPort = 8008 | ||
|
|
@@ -29,6 +30,7 @@ const ( | |
|
|
||
| // Interface describe patroni methods | ||
| type Interface interface { | ||
| GetClusterMembers(master *v1.Pod) ([]ClusterMember, error) | ||
| Switchover(master *v1.Pod, candidate string) error | ||
| SetPostgresParameters(server *v1.Pod, options map[string]string) error | ||
| GetMemberData(server *v1.Pod) (MemberData, error) | ||
|
|
@@ -175,6 +177,20 @@ func (p *Patroni) SetConfig(server *v1.Pod, config map[string]interface{}) error | |
| return p.httpPostOrPatch(http.MethodPatch, apiURLString+configPath, buf) | ||
| } | ||
|
|
||
| // ClusterMembers array of cluster members from Patroni API | ||
| type ClusterMembers struct { | ||
| Members []ClusterMember `json:"members"` | ||
| } | ||
|
|
||
| // ClusterMember cluster member data from Patroni API | ||
| type ClusterMember struct { | ||
| Name string `json:"name"` | ||
| Role string `json:"role"` | ||
| State string `json:"state"` | ||
| Timeline int `json:"timeline"` | ||
| LagInMB int `json:"lag"` | ||
|
||
| } | ||
|
|
||
| // MemberDataPatroni child element | ||
| type MemberDataPatroni struct { | ||
| Version string `json:"version"` | ||
|
|
@@ -246,6 +262,27 @@ func (p *Patroni) Restart(server *v1.Pod) error { | |
| return nil | ||
| } | ||
|
|
||
| // GetClusterMembers read cluster data from patroni API | ||
| func (p *Patroni) GetClusterMembers(server *v1.Pod) ([]ClusterMember, error) { | ||
|
|
||
| apiURLString, err := apiURL(server) | ||
| if err != nil { | ||
| return []ClusterMember{}, err | ||
| } | ||
| body, err := p.httpGet(apiURLString + clusterPath) | ||
| if err != nil { | ||
| return []ClusterMember{}, err | ||
| } | ||
|
|
||
| data := ClusterMembers{} | ||
| err = json.Unmarshal([]byte(body), &data) | ||
| if err != nil { | ||
| return []ClusterMember{}, err | ||
| } | ||
|
|
||
| return data.Members, nil | ||
| } | ||
|
|
||
| // GetMemberData read member data from patroni API | ||
| func (p *Patroni) GetMemberData(server *v1.Pod) (MemberData, error) { | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.