Reshuffle acceptance tests

Move dynamic config tests from basic_replication to patroni_api
This commit is contained in:
Alexander Kukushkin
2016-05-30 11:30:41 +02:00
8 changed files with 68 additions and 66 deletions
+1 -1
View File
@@ -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
+1 -14
View File
@@ -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
+20 -4
View File
@@ -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
-18
View File
@@ -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)
+26 -11
View File
@@ -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
View File
@@ -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
+4 -4
View File
@@ -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
View File
@@ -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()