mirror of
https://github.com/outbackdingo/patroni.git
synced 2026-08-25 14:53:37 +00:00
Reshuffle acceptance tests
Move dynamic config tests from basic_replication to patroni_api
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@
|
||||
Patroni: A Template for PostgreSQL HA with ZooKeeper, etcd or Consul
|
||||
------------------------------------------------------------
|
||||
|
||||
Patroni was previously known as Governor.
|
||||
Patroni originates from Compose Governor and includes plenty of new features.
|
||||
|
||||
*There are many ways to run high availability with PostgreSQL. Here, we
|
||||
present a template for you to create your own customized, high-availability
|
||||
|
||||
@@ -8,22 +8,9 @@ Feature: basic replication
|
||||
When I add the table foo to postgres0
|
||||
Then table foo is present on postgres1 after 20 seconds
|
||||
|
||||
Scenario: check local configuration reload
|
||||
Given I issue an empty POST request to http://127.0.0.1:8008/reload
|
||||
Then I receive a response code 304
|
||||
When I add tag new_tag new_value to postgres0 config
|
||||
And I issue an empty POST request to http://127.0.0.1:8008/reload
|
||||
Then I receive a response code 202
|
||||
|
||||
Scenario: check dynamic configuration change via DCS
|
||||
Given I issue a PATCH request to http://127.0.0.1:8008/config with {"ttl": 20, "loop_wait": 5, "postgresql": {"parameters": {"max_connections": 101}}}
|
||||
Then Response on GET http://127.0.0.1:8008/patroni contains restart_pending after 11 seconds
|
||||
And Response on GET http://127.0.0.1:8009/patroni contains restart_pending after 11 seconds
|
||||
And Response on GET http://127.0.0.1:8008/patroni contains new_value after 1 seconds
|
||||
|
||||
Scenario: check the basic failover
|
||||
And I kill postgres0
|
||||
Then postgres1 role is the primary after 22 seconds
|
||||
Then postgres1 role is the primary after 32 seconds
|
||||
When I start postgres0
|
||||
Then postgres0 role is the secondary after 20 seconds
|
||||
When I add the table bar to postgres1
|
||||
|
||||
@@ -15,14 +15,31 @@ Scenario: check API requests on a stand-alone server
|
||||
And I receive a response text "I am the leader, can not reinitialize"
|
||||
When I issue a POST request to http://127.0.0.1:8008/failover with {"leader": "postgres0"}
|
||||
Then I receive a response code 500
|
||||
And I receive a response text "failover is not possible: cluster does not have members except leader"
|
||||
And I receive a response text failover is not possible: cluster does not have members except leader
|
||||
When I issue an empty POST request to http://127.0.0.1:8008/failover
|
||||
Then I receive a response code 400
|
||||
And I receive a response text "No values given for required parameters leader and candidate"
|
||||
|
||||
Scenario: check API requests for the primary-replica pair
|
||||
Given I start postgres1
|
||||
Scenario: check local configuration reload
|
||||
Given I issue an empty POST request to http://127.0.0.1:8008/reload
|
||||
Then I receive a response code 304
|
||||
When I add tag new_tag new_value to postgres0 config
|
||||
And I issue an empty POST request to http://127.0.0.1:8008/reload
|
||||
Then I receive a response code 202
|
||||
|
||||
Scenario: check dynamic configuration change via DCS
|
||||
Given I issue a PATCH request to http://127.0.0.1:8008/config with {"ttl": 20, "loop_wait": 1, "postgresql": {"parameters": {"max_connections": 101}}}
|
||||
And I start postgres1
|
||||
And replication works from postgres0 to postgres1 after 20 seconds
|
||||
When I issue a GET request to http://127.0.0.1:8008/config
|
||||
Then I receive a response code 200
|
||||
And I receive a response loop_wait 1
|
||||
When I issue a GET request to http://127.0.0.1:8008/patroni
|
||||
Then I receive a response code 200
|
||||
And I receive a response restart_pending True
|
||||
And I receive a response tags {'tag': 'new_value'}
|
||||
|
||||
Scenario: check API requests for the primary-replica pair
|
||||
When I issue a GET request to http://127.0.0.1:8009/replica
|
||||
Then I receive a response code 200
|
||||
And I receive a response state running
|
||||
@@ -50,4 +67,3 @@ Scenario: check the scheduled failover
|
||||
And postgres0 role is the primary after 5 seconds
|
||||
And postgres1 role is the secondary after 10 seconds
|
||||
And replication works from postgres0 to postgres1 after 25 seconds
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import psycopg2 as pg
|
||||
import requests
|
||||
|
||||
from behave import step, then
|
||||
from time import sleep, time
|
||||
@@ -53,20 +52,3 @@ def replication_works(context, master, replica, time_limit):
|
||||
When I add the table test_{0} to {1}
|
||||
Then table test_{0} is present on {2} after {3} seconds
|
||||
""".format(int(time()), master, replica, time_limit))
|
||||
|
||||
|
||||
@then('Response on GET {url} contains {value} after {timeout:d} seconds')
|
||||
def check_http_response(context, url, value, timeout):
|
||||
for _ in range(int(timeout)):
|
||||
r = requests.get(url)
|
||||
if value in r.content.decode('utf-8'):
|
||||
break
|
||||
sleep(1)
|
||||
else:
|
||||
assert False,\
|
||||
"Value {0} is not present in response after {1} seconds".format(value, timeout)
|
||||
|
||||
|
||||
@step('I add tag {tag:w} {value:w} to {pg_name:w} config')
|
||||
def add_tag_to_config(context, tag, value, pg_name):
|
||||
context.pctl.add_tag_to_config(pg_name, tag, value)
|
||||
|
||||
@@ -3,6 +3,7 @@ import parse
|
||||
import pytz
|
||||
import requests
|
||||
import time
|
||||
import yaml
|
||||
|
||||
from behave import register_type, step, then
|
||||
from datetime import datetime, timedelta
|
||||
@@ -35,6 +36,23 @@ def sleep_for_n_seconds(context, value):
|
||||
time.sleep(int(value))
|
||||
|
||||
|
||||
def _set_response(context, response):
|
||||
context.status_code = response.status_code
|
||||
data = response.content.decode('utf-8')
|
||||
ct = response.headers.get('content-type', '')
|
||||
if ct.startswith('application/json') or\
|
||||
ct.startswith('text/yaml') or\
|
||||
ct.startswith('text/x-yaml') or\
|
||||
ct.startswith('application/yaml') or\
|
||||
ct.startswith('application/x-yaml'):
|
||||
try:
|
||||
context.response = yaml.safe_load(data)
|
||||
except ValueError:
|
||||
context.response = data
|
||||
else:
|
||||
context.response = data
|
||||
|
||||
|
||||
@step('I issue a GET request to {url:url}')
|
||||
def do_get(context, url):
|
||||
try:
|
||||
@@ -43,11 +61,7 @@ def do_get(context, url):
|
||||
context.status_code = None
|
||||
context.response = None
|
||||
else:
|
||||
context.status_code = r.status_code
|
||||
try:
|
||||
context.response = r.json()
|
||||
except ValueError:
|
||||
context.response = r.content.decode('utf-8')
|
||||
_set_response(context, r)
|
||||
|
||||
|
||||
@step('I issue an empty POST request to {url:url}')
|
||||
@@ -67,11 +81,7 @@ def do_request(context, request_method, url, data):
|
||||
context.status_code = None
|
||||
context.response = None
|
||||
else:
|
||||
context.status_code = r.status_code
|
||||
try:
|
||||
context.response = r.json()
|
||||
except ValueError:
|
||||
context.response = r.content.decode('utf-8')
|
||||
_set_response(context, r)
|
||||
|
||||
|
||||
@then('I receive a response {component:w} {data}')
|
||||
@@ -83,7 +93,7 @@ def check_response(context, component, data):
|
||||
assert context.response == data.strip('"'), "response {0} does not contain {1}".format(context.response, data)
|
||||
else:
|
||||
assert component in context.response, "{0} is not part of the response".format(component)
|
||||
assert context.response[component] == data, "{0} does not contain {1}".format(component, data)
|
||||
assert str(context.response[component]) == str(data), "{0} does not contain {1}".format(component, data)
|
||||
|
||||
|
||||
@step('I issue a scheduled failover at {at_url:url} from {from_host:w} to {to_host:w} in {in_seconds:d} seconds')
|
||||
@@ -91,3 +101,8 @@ def scheduled_failover(context, at_url, from_host, to_host, in_seconds):
|
||||
context.execute_steps(u"""
|
||||
Given I issue a POST request to {0}/failover with {{"leader": "{1}", "candidate": "{2}", "scheduled_at": "{3}"}}
|
||||
""".format(at_url, from_host, to_host, datetime.now(pytz.utc) + timedelta(seconds=int(in_seconds))))
|
||||
|
||||
|
||||
@step('I add tag {tag:w} {value:w} to {pg_name:w} config')
|
||||
def add_tag_to_config(context, tag, value, pg_name):
|
||||
context.pctl.add_tag_to_config(pg_name, tag, value)
|
||||
|
||||
+6
-4
@@ -73,7 +73,7 @@ class Ha(object):
|
||||
logger.info('bootstrapped %s', msg)
|
||||
cluster = self.dcs.get_cluster()
|
||||
node_to_follow = self._get_node_to_follow(cluster)
|
||||
self.state_handler.follow(node_to_follow, True)
|
||||
self.state_handler.follow(node_to_follow, cluster.leader, True)
|
||||
else:
|
||||
logger.error('failed to bootstrap %s', msg)
|
||||
self.state_handler.remove_data_directory()
|
||||
@@ -138,7 +138,7 @@ class Ha(object):
|
||||
|
||||
if not self.state_handler.check_recovery_conf(node_to_follow) or recovery:
|
||||
self._async_executor.schedule('changing primary_conninfo and restarting')
|
||||
self._async_executor.run_async(self.state_handler.follow, (node_to_follow, recovery))
|
||||
self._async_executor.run_async(self.state_handler.follow, (node_to_follow, self.cluster.leader, recovery))
|
||||
return ret
|
||||
|
||||
def enforce_master_role(self, message, promote_message):
|
||||
@@ -280,9 +280,11 @@ class Ha(object):
|
||||
self.touch_member()
|
||||
self.dcs.reset_cluster()
|
||||
sleep(2) # Give a time to somebody to promote
|
||||
self.recover()
|
||||
cluster = self.dcs.get_cluster()
|
||||
node_to_follow = self._get_node_to_follow(cluster)
|
||||
self.state_handler.follow(node_to_follow, True)
|
||||
else:
|
||||
self.state_handler.follow(None)
|
||||
self.state_handler.follow(None, None)
|
||||
|
||||
def process_manual_failover_from_leader(self):
|
||||
failover = self.cluster.failover
|
||||
|
||||
@@ -651,8 +651,8 @@ class Postgresql(object):
|
||||
except OSError:
|
||||
logger.exception("Unable to list %s", status_dir)
|
||||
|
||||
def follow(self, leader, recovery=False):
|
||||
if self.check_recovery_conf(leader) and not recovery:
|
||||
def follow(self, member, leader, recovery=False):
|
||||
if self.check_recovery_conf(member) and not recovery:
|
||||
return True
|
||||
change_role = self.role == 'master'
|
||||
need_rewind = change_role and self.can_rewind
|
||||
@@ -677,14 +677,14 @@ class Postgresql(object):
|
||||
opts.update({'archive_mode': 'on', 'archive_command': 'false'})
|
||||
self.single_user_mode(options=opts)
|
||||
if self.rewind(leader):
|
||||
self.write_recovery_conf(leader)
|
||||
self.write_recovery_conf(member)
|
||||
ret = self.start()
|
||||
else:
|
||||
logger.error("unable to rewind the former master")
|
||||
self.remove_data_directory()
|
||||
ret = True
|
||||
else: # do not rewind until the leader becomes available
|
||||
self.write_recovery_conf(leader)
|
||||
self.write_recovery_conf(member)
|
||||
ret = self.restart()
|
||||
if change_role and ret:
|
||||
self.call_nowait(ACTION_ON_ROLE_CHANGE)
|
||||
|
||||
+10
-10
@@ -234,22 +234,22 @@ class TestPostgresql(unittest.TestCase):
|
||||
@patch('patroni.postgresql.Postgresql.write_pgpass', MagicMock(return_value=dict()))
|
||||
@patch('subprocess.check_output', Mock(return_value=0, side_effect=pg_controldata_string))
|
||||
def test_follow(self, mock_pg_rewind):
|
||||
self.p.follow(None)
|
||||
self.p.follow(self.leader)
|
||||
self.p.follow(Leader(-1, 28, self.other))
|
||||
self.p.follow(None, None)
|
||||
self.p.follow(self.leader, self.leader)
|
||||
self.p.follow(Leader(-1, 28, self.other), self.leader)
|
||||
self.p.rewind = mock_pg_rewind
|
||||
self.p.follow(self.leader)
|
||||
self.p.follow(self.leader, self.leader)
|
||||
with mock.patch('os.path.islink', MagicMock(return_value=True)):
|
||||
with mock.patch('patroni.postgresql.Postgresql.can_rewind', new_callable=PropertyMock(return_value=True)):
|
||||
with mock.patch('os.unlink', MagicMock(return_value=True)):
|
||||
self.p.follow(self.leader, recovery=True)
|
||||
self.p.follow(self.leader, self.leader, recovery=True)
|
||||
with mock.patch('patroni.postgresql.Postgresql.can_rewind', new_callable=PropertyMock(return_value=True)):
|
||||
self.p.rewind.return_value = True
|
||||
self.p.follow(self.leader, recovery=True)
|
||||
self.p.follow(self.leader, self.leader, recovery=True)
|
||||
self.p.rewind.return_value = False
|
||||
self.p.follow(self.leader, recovery=True)
|
||||
self.p.follow(self.leader, self.leader, recovery=True)
|
||||
with mock.patch('patroni.postgresql.Postgresql.check_recovery_conf', MagicMock(return_value=True)):
|
||||
self.assertTrue(self.p.follow(None))
|
||||
self.assertTrue(self.p.follow(None, None))
|
||||
|
||||
@patch('subprocess.check_output', Mock(return_value=0, side_effect=pg_controldata_string))
|
||||
def test_can_rewind(self):
|
||||
@@ -425,7 +425,7 @@ class TestPostgresql(unittest.TestCase):
|
||||
def test_cleanup_archive_status(self, mock_file, mock_link, mock_remove, mock_unlink):
|
||||
ap = os.path.join(self.data_dir, 'pg_xlog', 'archive_status/')
|
||||
self.p.cleanup_archive_status()
|
||||
mock_remove.assert_has_calls([mock.call(ap+'a'), mock.call(ap+'b'), mock.call(ap+'c')])
|
||||
mock_remove.assert_has_calls([mock.call(ap + 'a'), mock.call(ap + 'b'), mock.call(ap + 'c')])
|
||||
mock_unlink.assert_not_called()
|
||||
|
||||
mock_remove.reset_mock()
|
||||
@@ -433,7 +433,7 @@ class TestPostgresql(unittest.TestCase):
|
||||
mock_file.return_value = False
|
||||
mock_link.return_value = True
|
||||
self.p.cleanup_archive_status()
|
||||
mock_unlink.assert_has_calls([mock.call(ap+'a'), mock.call(ap+'b'), mock.call(ap+'c')])
|
||||
mock_unlink.assert_has_calls([mock.call(ap + 'a'), mock.call(ap + 'b'), mock.call(ap + 'c')])
|
||||
mock_remove.assert_not_called()
|
||||
|
||||
mock_unlink.reset_mock()
|
||||
|
||||
Reference in New Issue
Block a user