Skip to content

Commit 4cf2c85

Browse files
Merge pull request #2670 from LALITH0110/leaderelection-stop-on-callback-exception
Stop leading when onstarted_leading raises
2 parents baa6d8c + c0b60e0 commit 4cf2c85

2 files changed

Lines changed: 46 additions & 3 deletions

File tree

kubernetes/base/leaderelection/leaderelection.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,24 @@ def run(self):
5555
logger.info("{} successfully acquired lease".format(self.election_config.lock.identity))
5656

5757
# Start leading and call OnStartedLeading()
58-
threading.Thread(target=self.election_config.onstarted_leading, daemon=True).start()
58+
leading_failed = threading.Event()
59+
threading.Thread(target=self.run_onstarted_leading, args=(leading_failed,), daemon=True).start()
5960

60-
self.renew_loop()
61+
self.renew_loop(leading_failed)
6162

6263
# Failed to update lease, run OnStoppedLeading callback
6364
self.election_config.onstopped_leading()
6465

66+
def run_onstarted_leading(self, leading_failed):
67+
# Run the callback in this thread, recording whether it raised. Without
68+
# this the exception is swallowed by the worker thread and the lease keeps
69+
# being renewed even though the work it protects is no longer running.
70+
try:
71+
self.election_config.onstarted_leading()
72+
except Exception:
73+
logger.exception("onstarted_leading raised an exception, stopping leading")
74+
leading_failed.set()
75+
6576
def acquire(self):
6677
# Follower
6778
logger.info("{} is a follower".format(self.election_config.lock.identity))
@@ -75,14 +86,19 @@ def acquire(self):
7586

7687
time.sleep(retry_period)
7788

78-
def renew_loop(self):
89+
def renew_loop(self, leading_failed=None):
7990
# Leader
8091
logger.info("Leader has entered renew loop and will try to update lease continuously")
8192

8293
retry_period = self.election_config.retry_period
8394
renew_deadline = self.election_config.renew_deadline * 1000
8495

8596
while True:
97+
# onstarted_leading raised, so stop renewing a lease that no longer
98+
# protects anything and let run() call onstopped_leading.
99+
if leading_failed is not None and leading_failed.is_set():
100+
return
101+
86102
timeout = int(time.time() * 1000) + renew_deadline
87103
succeeded = False
88104

kubernetes/base/leaderelection/leaderelection_test.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import unittest
2121
import threading
2222
import json
23+
import sys
2324
import time
2425
import pytest
2526
from unittest.mock import patch
@@ -222,6 +223,32 @@ def record_thread(*args, **kwargs):
222223
self.assertIn("daemon", captured)
223224
self.assertTrue(captured["daemon"])
224225

226+
"""Expected behavior: if onstarted_leading raises, the lease it protects is no
227+
longer backed by any running work, so the candidate must stop renewing it and
228+
run onstopped_leading. The lock below never refuses a renewal, so the renew
229+
loop can only end because the callback failed."""
230+
def test_stops_leading_when_onstarted_leading_raises(self):
231+
stopped = threading.Event()
232+
233+
mock_lock = MockResourceLock("mock", "mock_namespace", "mock", thread_lock,
234+
lambda: None, lambda: None, lambda: None, None)
235+
mock_lock.renew_count_max = sys.maxsize
236+
237+
def on_started_leading():
238+
raise RuntimeError("onstarted_leading failed")
239+
240+
config = electionconfig.Config(lock=mock_lock, lease_duration=2,
241+
renew_deadline=1.5, retry_period=1.1,
242+
onstarted_leading=on_started_leading,
243+
onstopped_leading=stopped.set)
244+
245+
# Run in a daemon thread so a regression times out instead of hanging.
246+
threading.Thread(target=leaderelection.LeaderElection(config).run,
247+
daemon=True).start()
248+
249+
self.assertTrue(stopped.wait(10),
250+
"onstopped_leading was not called after onstarted_leading raised")
251+
225252
def assert_history(self, history, expected):
226253
self.assertIsNotNone(expected)
227254
self.assertIsNotNone(history)

0 commit comments

Comments
 (0)