Switch to a streaming watcher (#1189)

Watch requests to K8s API either streaming the data or close connection by timeout. In any case it requires a second connection open, but opening a new connection every 10 seconds is more expensive for both, Patroni and K8s API.

Switching to the streaming model also brings other benefits: we can watch not only on leader object, but also on config and wake up Patroni main thread if the config was changed.
This commit is contained in:
Alexander Kukushkin
2019-10-07 15:16:35 +02:00
committed by GitHub
parent facee0186d
commit 86ee22efab
2 changed files with 91 additions and 24 deletions
+40 -6
View File
@@ -3,6 +3,8 @@ import unittest
from mock import Mock, patch
from patroni.dcs.kubernetes import Kubernetes, KubernetesError, k8s_client, k8s_watch, RetryFailedError
from threading import Thread
from . import SleepException
def mock_list_namespaced_config_map(self, *args, **kwargs):
@@ -27,11 +29,13 @@ def mock_list_namespaced_pod(self, *args, **kwargs):
@patch.object(k8s_client.CoreV1Api, 'patch_namespaced_config_map', Mock())
@patch.object(k8s_client.CoreV1Api, 'create_namespaced_config_map', Mock())
@patch.object(Thread, 'start', Mock())
class TestKubernetes(unittest.TestCase):
@patch('kubernetes.config.load_kube_config', Mock())
@patch.object(k8s_client.CoreV1Api, 'list_namespaced_config_map', mock_list_namespaced_config_map)
@patch.object(k8s_client.CoreV1Api, 'list_namespaced_pod', mock_list_namespaced_pod)
@patch.object(Thread, 'start', Mock())
def setUp(self):
self.k = Kubernetes({'ttl': 30, 'scope': 'test', 'name': 'p-0', 'retry_timeout': 10, 'labels': {'f': 'b'}})
self.k.get_cluster()
@@ -101,16 +105,14 @@ class TestKubernetes(unittest.TestCase):
'labels': {'f': 'b'}, 'use_endpoints': True, 'pod_ip': '10.0.0.0'})
self.assertFalse(k.delete_sync_state())
@patch.object(k8s_watch.Watch, 'stream', Mock(return_value=[{'raw_object': {'metadata': {}}}]))
def test_start_watch_stream(self):
self.assertIsNotNone(self.k.start_watch_stream())
def test_watch(self):
self.k.set_ttl(10)
self.k.watch(None, 0)
self.k.watch(None, 0)
with patch.object(k8s_watch.Watch, 'stream',
Mock(side_effect=[Exception, [], KeyboardInterrupt,
[{'raw_object': {'metadata': {'resourceVersion': '2'}}}]])):
self.assertFalse(self.k.watch('1', 2))
self.assertRaises(KeyboardInterrupt, self.k.watch, '1', 2)
self.assertTrue(self.k.watch('1', 2))
def test_set_history_value(self):
self.k.set_history_value('{}')
@@ -126,3 +128,35 @@ class TestKubernetes(unittest.TestCase):
self.assertIsNotNone(k.patch_or_create_config({'foo': 'bar'}))
self.assertIsNotNone(k.patch_or_create_config({'foo': 'bar'}))
k.touch_member({'state': 'running', 'role': 'replica'})
@patch('time.sleep', Mock(side_effect=SleepException))
@patch.object(Kubernetes, 'start_watch_stream')
class TestKubernetesWatcher(unittest.TestCase):
@patch('kubernetes.config.load_kube_config', Mock())
@patch.object(Thread, 'start', Mock())
def setUp(self):
self.k = Kubernetes({'ttl': 30, 'scope': 'test', 'name': 'p-0', 'retry_timeout': 10, 'labels': {'f': 'b'}})
def test_leader_update(self, mock_stream):
mock_stream.return_value = [
{'raw_object': {'type': 'MODIFIED',
'metadata': {'name': self.k.leader_path, 'annotations': {self.k._LEADER: 'foo'}}}},
{'raw_object': {'type': 'MODIFIED',
'metadata': {'name': self.k.leader_path, 'annotations': {self.k._LEADER: 'bar'}}}}
]
self.assertRaises(SleepException, self.k._watcher.run)
def test_config_update(self, mock_stream):
mock_stream.return_value = [
{'raw_object': {'type': 'MODIFIED',
'metadata': {'name': self.k.config_path, 'annotations': {self.k._CONFIG: 'foo'}}}},
{'raw_object': {'type': 'MODIFIED',
'metadata': {'name': self.k.config_path, 'annotations': {self.k._CONFIG: 'bar'}}}}
]
self.assertRaises(SleepException, self.k._watcher.run)
def test_run(self, mock_stream):
mock_stream.side_effect = Exception
self.assertRaises(SleepException, self.k._watcher.run)