Skip to content
This repository was archived by the owner on Mar 13, 2022. It is now read-only.

Commit 9591616

Browse files
committed
Adding a test
1 parent e9ed4de commit 9591616

File tree

4 files changed

+263
-5
lines changed

4 files changed

+263
-5
lines changed

leaderelection/electionconfig.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,16 @@ def __init__(self, lock, lease_duration, renew_deadline, retry_period, onstarted
3030
sys.exit("lease_duration must be greater than renew_deadline")
3131

3232
if renew_deadline <= self.jitter_factor * retry_period:
33-
sys.exit("lease_duration must be greater than renew_deadline")
33+
sys.exit("renewDeadline must be greater than retry_period*jitter_factor")
3434

3535
if lease_duration < 1:
36-
sys.exit("lease_duration must be greater than zero")
36+
sys.exit("lease_duration must be greater than one")
3737

3838
if renew_deadline < 1:
39-
sys.exit("renew_deadline must be greater than zero")
39+
sys.exit("renew_deadline must be greater than one")
4040

4141
if retry_period < 1:
42-
sys.exit("retry_period must be greater than zero")
42+
sys.exit("retry_period must be greater than one")
4343

4444
self.lease_duration = lease_duration
4545
self.renew_deadline = renew_deadline

leaderelection/leaderelection.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,22 @@
1717
import time
1818
import json
1919
import threading
20+
from http import HTTPStatus
2021
from leaderelectionrecord import LeaderElectionRecord
2122
import logging
2223

2324
logging.basicConfig(level=logging.INFO)
2425

26+
"""
27+
This package implements leader election using an annotation in a Kubernetes object.
28+
The onstarted_leading function is run in a thread and when it returns, if it does
29+
it might not be safe to run it again in a process.
30+
31+
At first all candidates are considered followers. The one to create a lock or update
32+
an existing lock first becomes the leader and remains so until it keeps renewing its
33+
lease.
34+
"""
35+
2536

2637
class LeaderElection:
2738
def __init__(self, election_config):
@@ -102,7 +113,7 @@ def try_acquire_or_renew(self):
102113

103114
# A lock is not created with that name, try to create one
104115
if not lock_status:
105-
if json.loads(old_election_record.body)['code'] != 404:
116+
if json.loads(old_election_record.body)['code'] != HTTPStatus.NOT_FOUND:
106117
logging.info("Error retrieving resource lock {} as {}".format(self.election_config.lock.name,
107118
old_election_record.reason))
108119
return False

leaderelection/tests/__init__.py

Whitespace-only changes.
+247
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
# Copyright 2020 The Kubernetes Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
import leaderelection
17+
from leaderelectionrecord import LeaderElectionRecord
18+
from kubernetes.client.rest import ApiException
19+
import electionconfig
20+
import unittest
21+
import threading
22+
import json
23+
import pytest
24+
25+
class LeaderElectionTest(unittest.TestCase):
26+
def test_simple_leader_election(self):
27+
election_history = []
28+
leadership_history = []
29+
30+
def on_create():
31+
election_history.append("create record")
32+
leadership_history.append("get leadership")
33+
34+
def on_update():
35+
election_history.append("update record")
36+
37+
def on_change():
38+
election_history.append("change record")
39+
40+
mock_lock = MockResourceLock("mock", "mock_namespace", "mock", on_create, on_update, on_change, None)
41+
42+
def on_started_leading():
43+
leadership_history.append("start leading")
44+
45+
def on_stopped_leading():
46+
leadership_history.append("stop leading")
47+
48+
# Create config 4.5 4 3
49+
config = electionconfig.Config(lock=mock_lock, lease_duration=2.5,
50+
renew_deadline=2, retry_period=1.5, onstarted_leading=on_started_leading,
51+
onstopped_leading=on_stopped_leading)
52+
53+
# Enter leader election
54+
leaderelection.LeaderElection(config).run()
55+
56+
self.assert_history(election_history, ["create record", "update record", "update record", "update record"])
57+
self.assert_history(leadership_history, ["get leadership", "start leading", "stop leading"])
58+
59+
def test_leader_election(self):
60+
election_history = []
61+
leadership_history = []
62+
63+
def on_create_A():
64+
election_history.append("A creates record")
65+
leadership_history.append("A gets leadership")
66+
67+
def on_update_A():
68+
election_history.append("A updates record")
69+
70+
def on_change_A():
71+
election_history.append("A gets leadership")
72+
73+
mock_lock_A = MockResourceLock("mock", "mock_namespace", "MockA", on_create_A, on_update_A, on_change_A, None)
74+
mock_lock_A.renew_count_max = 3
75+
76+
def on_started_leading_A():
77+
leadership_history.append("A starts leading")
78+
79+
def on_stopped_leading_A():
80+
leadership_history.append("A stops leading")
81+
82+
config_A = electionconfig.Config(lock=mock_lock_A, lease_duration=2.5,
83+
renew_deadline=2, retry_period=1.5, onstarted_leading=on_started_leading_A,
84+
onstopped_leading=on_stopped_leading_A)
85+
86+
# Enter leader election
87+
leaderelection.LeaderElection(config_A).run()
88+
89+
def on_create_B():
90+
election_history.append("B creates record")
91+
leadership_history.append("B gets leadership")
92+
93+
def on_update_B():
94+
election_history.append("B updates record")
95+
96+
def on_change_B():
97+
leadership_history.append("B gets leadership")
98+
99+
mock_lock_B = MockResourceLock("mock", "mock_namespace", "MockB", on_create_B, on_update_B, on_change_B, None)
100+
mock_lock_B.renew_count_max = 4
101+
102+
def on_started_leading_B():
103+
leadership_history.append("B starts leading")
104+
105+
def on_stopped_leading_B():
106+
leadership_history.append("B stops leading")
107+
108+
config_B = electionconfig.Config(lock=mock_lock_B, lease_duration=2.5,
109+
renew_deadline=2, retry_period=1.5, onstarted_leading=on_started_leading_B,
110+
onstopped_leading=on_stopped_leading_B)
111+
112+
mock_lock_B.leader_record = mock_lock_A.leader_record
113+
leaderelection.LeaderElection(config_B).run()
114+
115+
self.assert_history(election_history,
116+
["A creates record",
117+
"A updates record",
118+
"A updates record",
119+
"B updates record",
120+
"B updates record",
121+
"B updates record",
122+
"B updates record"])
123+
self.assert_history(leadership_history,
124+
["A gets leadership",
125+
"A starts leading",
126+
"A stops leading",
127+
"B gets leadership",
128+
"B starts leading",
129+
"B stops leading"])
130+
131+
def test_Leader_election_with_renew_deadline(self):
132+
election_history = []
133+
leadership_history = []
134+
135+
def on_create():
136+
election_history.append("create record")
137+
leadership_history.append("get leadership")
138+
139+
def on_update():
140+
election_history.append("update record")
141+
142+
def on_change():
143+
election_history.append("change record")
144+
145+
def on_try_update():
146+
election_history.append("try update record")
147+
148+
mock_lock = MockResourceLock("mock", "mock_namespace", "mock", on_create, on_update, on_change, on_try_update)
149+
mock_lock.renew_count_max = 3
150+
151+
def on_started_leading():
152+
leadership_history.append("start leading")
153+
154+
def on_stopped_leading():
155+
leadership_history.append("stop leading")
156+
157+
# Create config
158+
config = electionconfig.Config(lock=mock_lock, lease_duration=2.5,
159+
renew_deadline=2, retry_period=1.5, onstarted_leading=on_started_leading,
160+
onstopped_leading=on_stopped_leading)
161+
162+
# Enter leader election
163+
leaderelection.LeaderElection(config).run()
164+
165+
self.assert_history(election_history,
166+
["create record",
167+
"try update record",
168+
"update record",
169+
"try update record",
170+
"update record",
171+
"try update record",
172+
"try update record"])
173+
174+
self.assert_history(leadership_history, ["get leadership", "start leading", "stop leading"])
175+
176+
def assert_history(self, history, expected):
177+
self.assertIsNotNone(expected)
178+
self.assertIsNotNone(history)
179+
self.assertEquals(len(expected), len(history))
180+
181+
for idx in range(len(history)):
182+
self.assertEquals(history[idx], expected[idx],
183+
msg="Not equal at index {}, expected {}, got {}".format(idx, expected[idx],
184+
history[idx]))
185+
186+
187+
class MockResourceLock:
188+
def __init__(self, name, namespace, identity, on_create=None, on_update=None, on_change=None, on_try_update=None):
189+
self.leader_record = None
190+
self.renew_count = 0
191+
self.renew_count_max = 4
192+
self.name = name
193+
self.namespace = namespace
194+
self.identity = str(identity)
195+
self.lock = threading.RLock()
196+
197+
self.on_create = on_create
198+
self.on_update = on_update
199+
self.on_change = on_change
200+
self.on_try_update = on_try_update
201+
202+
def get(self, name, namespace):
203+
self.lock.acquire()
204+
try:
205+
if self.leader_record:
206+
return True, self.leader_record
207+
ApiException.body = json.dumps({'code': 404})
208+
return False, ApiException
209+
finally:
210+
self.lock.release()
211+
212+
def create(self, name, namespace, election_record):
213+
self.lock.acquire()
214+
try:
215+
if self.leader_record is not None:
216+
return False
217+
self.leader_record = election_record
218+
self.on_create()
219+
self.renew_count += 1
220+
return True
221+
finally:
222+
self.lock.release()
223+
224+
def update(self, name, namespace, updated_record):
225+
self.lock.acquire()
226+
try:
227+
if self.on_try_update:
228+
self.on_try_update()
229+
if self.renew_count >= self.renew_count_max:
230+
return False
231+
232+
old_record = self.leader_record
233+
self.leader_record = updated_record
234+
235+
self.on_update()
236+
237+
if old_record.holder_identity != updated_record.holder_identity:
238+
self.on_change()
239+
240+
self.renew_count += 1
241+
return True
242+
finally:
243+
self.lock.release()
244+
245+
246+
if __name__ == '__main__':
247+
unittest.main()

0 commit comments

Comments
 (0)