Skip to content

Commit 0c654a7

Browse files
committed
Merge remote-tracking branch 'zalando/master' into multisite
2 parents a981600 + 9225336 commit 0c654a7

File tree

7 files changed

+34
-18
lines changed

7 files changed

+34
-18
lines changed

.github/workflows/install_deps.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
def install_requirements(what):
1212
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'pip'])
13-
s = subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'wheel', 'setuptools'])
13+
s = subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'wheel', 'setuptools', 'distlib'])
1414
if s != 0:
1515
return s
1616

@@ -25,11 +25,11 @@ def install_requirements(what):
2525
requirements += ['coverage']
2626
# try to split tests between psycopg2 and psycopg3
2727
requirements += ['psycopg[binary]'] if sys.version_info >= (3, 8, 0) and\
28-
(sys.platform != 'darwin' or what == 'etcd3') else ['psycopg2-binary==2.9.9'
28+
(sys.platform != 'darwin' or what == 'etcd3') else ['psycopg2-binary==2.9.9'
2929
if sys.platform == 'darwin' else 'psycopg2-binary']
3030

31-
from pip._vendor.distlib.markers import evaluator, DEFAULT_CONTEXT
32-
from pip._vendor.distlib.util import parse_requirement
31+
from distlib.markers import evaluator, DEFAULT_CONTEXT
32+
from distlib.util import parse_requirement
3333

3434
for r in read('requirements.txt').split('\n'):
3535
r = parse_requirement(r)

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## This Dockerfile is meant to aid in the building and debugging patroni whilst developing on your local machine
22
## It has all the necessary components to play/debug with a single node appliance, running etcd
3-
ARG PG_MAJOR=16
3+
ARG PG_MAJOR=17
44
ARG COMPRESS=false
55
ARG PGHOME=/home/postgres
66
ARG PGDATA=$PGHOME/data

Dockerfile.citus

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## This Dockerfile is meant to aid in the building and debugging patroni whilst developing on your local machine
22
## It has all the necessary components to play/debug with a single node appliance, running etcd
3-
ARG PG_MAJOR=16
3+
ARG PG_MAJOR=17
44
ARG COMPRESS=false
55
ARG PGHOME=/home/postgres
66
ARG PGDATA=$PGHOME/data
@@ -40,7 +40,7 @@ RUN set -ex \
4040
echo "deb [signed-by=/etc/apt/trusted.gpg.d/citusdata_community.gpg] https://packagecloud.io/citusdata/community/debian/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/citusdata_community.list \
4141
&& curl -sL https://packagecloud.io/citusdata/community/gpgkey | gpg --dearmor > /etc/apt/trusted.gpg.d/citusdata_community.gpg \
4242
&& apt-get update -y \
43-
&& apt-get -y install postgresql-$PG_MAJOR-citus-12.1; \
43+
&& apt-get -y install postgresql-$PG_MAJOR-citus-13.1; \
4444
fi \
4545
\
4646
# Cleanup all locales but en_US.UTF-8

features/steps/basic_replication.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def initiate_load(context, pg_name):
102102
assert False, "Error loading test data on {0}: {1}".format(pg_name, e)
103103

104104

105-
@then('Table {table_name:w} is present on {pg_name:name} after {max_replication_delay:d} seconds')
105+
@then('table {table_name:w} is present on {pg_name:name} after {max_replication_delay:d} seconds')
106106
def table_is_present_on(context, table_name, pg_name, max_replication_delay):
107107
max_replication_delay *= context.timeout_multiplier
108108
for _ in range(int(max_replication_delay)):
@@ -124,7 +124,7 @@ def check_role(context, pg_name, pg_role, max_promotion_timeout):
124124
@step('replication works from {primary:name} to {replica:name} after {time_limit:d} seconds')
125125
@then('replication works from {primary:name} to {replica:name} after {time_limit:d} seconds')
126126
def replication_works(context, primary, replica, time_limit):
127-
context.execute_steps(u"""
127+
context.execute_steps("""
128128
When I add the table test_{0} to {1}
129129
Then table test_{0} is present on {2} after {3} seconds
130130
""".format(str(time()).replace('.', '_').replace(',', '_'), primary, replica, time_limit))

features/steps/cascading_replication.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def start_patroni_with_a_name_value_tag(context, name, tag_name, tag_value):
99
return context.pctl.start(name, custom_config={'tags': {tag_name: tag_value}})
1010

1111

12-
@then('There is a {label} with "{content}" in {name:name} data directory')
12+
@then('there is a {label} with "{content}" in {name:name} data directory')
1313
def check_label(context, label, content, name):
1414
value = (context.pctl.read_label(name, label) or '').replace('\n', '\\n')
1515
assert content in value, "\"{0}\" in {1} doesn't contain {2}".format(value, label, content)

patroni/dcs/etcd3.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from ..collections import EMPTY_DICT
2222
from ..exceptions import DCSError, PatroniException
2323
from ..postgresql.mpp import AbstractMPP
24-
from ..utils import deep_compare, enable_keepalive, iter_response_objects, RetryFailedError, USER_AGENT
24+
from ..utils import deep_compare, enable_keepalive, iter_response_objects, parse_bool, RetryFailedError, USER_AGENT
2525
from . import catch_return_false_exception, Cluster, ClusterConfig, \
2626
Failover, Leader, Member, Status, SyncState, TimelineHistory
2727
from .etcd import AbstractEtcd, AbstractEtcdClientWithFailover, catch_etcd_errors, \
@@ -66,6 +66,10 @@ class Etcd3Exception(etcd.EtcdException):
6666
pass
6767

6868

69+
class Etcd3WatchCanceled(Etcd3Exception):
70+
pass
71+
72+
6973
class Etcd3ClientError(Etcd3Exception):
7074

7175
def __init__(self, code: Optional[int] = None, error: Optional[str] = None, status: Optional[int] = None) -> None:
@@ -356,7 +360,6 @@ def handle_auth_errors(self: 'Etcd3Client', func: Callable[..., Any], *args: Any
356360
exc = e
357361
self._reauthenticate = True
358362
if retry:
359-
logger.error('retry = %s', retry)
360363
retry.ensure_deadline(0.5, exc)
361364
elif reauthenticated:
362365
raise exc
@@ -508,6 +511,8 @@ def _process_message(self, message: Dict[str, Any]) -> None:
508511
if 'error' in message:
509512
raise _raise_for_data(message)
510513
result = message.get('result', EMPTY_DICT)
514+
if parse_bool(result.get('canceled')):
515+
raise Etcd3WatchCanceled('Watch canceled')
511516
header = result.get('header', EMPTY_DICT)
512517
self._check_cluster_raft_term(header.get('cluster_id'), header.get('raft_term'))
513518
events: List[Dict[str, Any]] = result.get('events', [])
@@ -555,8 +560,10 @@ def _build_cache(self) -> None:
555560

556561
try:
557562
self._do_watch(result['header']['revision'])
563+
except Etcd3WatchCanceled:
564+
logger.info('Watch request canceled')
558565
except Exception as e:
559-
# Following exceptions are expected on Windows because the /watch request is done with `read_timeout`
566+
# Following exceptions are expected on Windows because the /watch request is done with `read_timeout`
560567
if not (os.name == 'nt' and isinstance(e, (ReadTimeoutError, ProtocolError))):
561568
logger.error('watchprefix failed: %r', e)
562569
finally:
@@ -576,16 +583,21 @@ def run(self) -> None:
576583
time.sleep(1)
577584

578585
def kill_stream(self) -> None:
579-
sock = None
586+
conn_sock: Any = None
580587
with self._response_lock:
581588
if isinstance(self._response, urllib3.response.HTTPResponse):
582589
try:
583-
sock = self._response.connection.sock if self._response.connection else None
590+
conn_sock = self._response.connection.sock if self._response.connection else None
584591
except Exception:
585-
sock = None
592+
conn_sock = None
586593
else:
587594
self._response = False
588-
if sock:
595+
if conn_sock:
596+
# python-etcd forces usage of pyopenssl if the last one is available.
597+
# In this case HTTPConnection.socket is not inherited from socket.socket, but urllib3 uses custom
598+
# class `WrappedSocket`, which shutdown() method could be incompatible with socket.shutdown().
599+
# Therefore we use WrappedSocket.socket, which points to original `socket` object.
600+
sock: socket.socket = conn_sock.socket if conn_sock.__class__.__name__ == 'WrappedSocket' else conn_sock
589601
try:
590602
sock.shutdown(socket.SHUT_RDWR)
591603
sock.close()

tests/test_etcd3.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,12 @@ class TestKVCache(BaseTestEtcd3):
9696

9797
@patch.object(urllib3.PoolManager, 'urlopen', mock_urlopen)
9898
@patch.object(Etcd3Client, 'watchprefix', Mock(return_value=urllib3.response.HTTPResponse()))
99+
@patch.object(urllib3.response.HTTPResponse, 'read_chunked',
100+
Mock(return_value=[b'{"result":{"canceled":true}}']))
99101
def test__build_cache(self):
100-
self.kv_cache._build_cache()
102+
with patch('patroni.dcs.etcd3.logger') as mock_logger:
103+
self.kv_cache._build_cache()
104+
mock_logger.info.assert_called_once_with('Watch request canceled')
101105

102106
def test__do_watch(self):
103107
self.client.watchprefix = Mock(return_value=False)

0 commit comments

Comments
 (0)