diff --git a/docs/SETTINGS.rst b/docs/SETTINGS.rst index 009f1357..8e2a7607 100644 --- a/docs/SETTINGS.rst +++ b/docs/SETTINGS.rst @@ -131,7 +131,7 @@ PostgreSQL - **pg\_ctl\_timeout**: How long should pg_ctl wait when doing ``start``, ``stop`` or ``restart``. Default value is 60 seconds. - **use\_pg\_rewind**: try to use pg\_rewind on the former leader when it joins cluster as a replica. - **remove\_data\_directory\_on\_rewind\_failure**: If this option is enabled, Patroni will remove postgres data directory and recreate replica. Otherwise it will try to follow the new leader. Default value is **false**. -- **replica\_method**: for each create_replica_method other than basebackup, you would add a configuration section of the same name. At a minimum, this should include "command" with a full path to the actual script to be executed. Other configuration parameters will be passed along to the script in the form "parameter=value". +- **replica\_method**: for each create_replica_methods other than basebackup, you would add a configuration section of the same name. At a minimum, this should include "command" with a full path to the actual script to be executed. Other configuration parameters will be passed along to the script in the form "parameter=value". REST API -------- diff --git a/docs/replica_bootstrap.rst b/docs/replica_bootstrap.rst index f5359e7b..d5d79155 100644 --- a/docs/replica_bootstrap.rst +++ b/docs/replica_bootstrap.rst @@ -68,7 +68,7 @@ scripts to clone a new replica. Those are configured in the ``postgresql`` confi .. code:: YAML postgresql: - create_replica_method: + create_replica_methods: - wal_e - basebackup wal_e: @@ -80,7 +80,7 @@ scripts to clone a new replica. Those are configured in the ``postgresql`` confi max-rate: '100M' -The ``create_replica_method`` defines available replica creation methods and the order of executing them. Patroni will +The ``create_replica_methods`` defines available replica creation methods and the order of executing them. Patroni will stop on the first one that returns 0. Each method should define a separate section in the configuration file, listing the command to execute and any custom parameters that should be passed to that command. All parameters will be passed in a ``--name=value`` format. Besides user-defined parameters, Patroni supplies a couple of cluster-specific ones: @@ -99,8 +99,9 @@ A special ``no_master`` parameter, if defined, allows Patroni to call the replic running master or replicas. In that case, an empty string will be passed in a connection string. This is useful for restoring the formerly running cluster from the binary backup. -A ``basebackup`` method is a special case: it will be used if ``create_replica_method`` is empty, although it is possible -to list it explicitly among the ``create_replica_method`` methods. This method initializes a new replica with the +A ``basebackup`` method is a special case: it will be used if +``create_replica_methods`` is empty, although it is possible +to list it explicitly among the ``create_replica_methods`` methods. This method initializes a new replica with the ``pg_basebackup``, the base backup is taken from the master unless there are replicas with ``clonefrom`` tag, in which case one of such replicas will be used as the origin for pg_basebackup. It works without any configuration; however, it is possible to specify a ``basebackup`` configuration section. Same rules as with the other method configuration apply, diff --git a/patroni/postgresql.py b/patroni/postgresql.py index 7a9034aa..bc8add0a 100644 --- a/patroni/postgresql.py +++ b/patroni/postgresql.py @@ -180,6 +180,11 @@ class Postgresql(object): if self._replace_pg_hba(): self.reload() + @property + def _create_replica_methods(self): + return (self.config.get('create_replica_methods', []) or + self.config.get('create_replica_method', [])) + @property def _configuration_to_save(self): configuration = [os.path.basename(self._postgresql_conf)] @@ -638,7 +643,7 @@ class Postgresql(object): """ go through the replication methods to see if there are ones that does not require a working replication connection. """ - replica_methods = self.config.get('create_replica_method', []) + replica_methods = self._create_replica_methods return any(self.replica_method_can_work_without_replication_connection(method) for method in replica_methods) def create_replica(self, clone_member): @@ -653,7 +658,7 @@ class Postgresql(object): # get list of replica methods from config. # If there is no configuration key, or no value is specified, use basebackup - replica_methods = self.config.get('create_replica_method') or ['basebackup'] + replica_methods = self._create_replica_methods or ['basebackup'] if clone_member and clone_member.conn_url: r = clone_member.conn_kwargs(self._replication) @@ -1622,7 +1627,7 @@ $$""".format(name, ' '.join(options)), name, password, password) def basebackup(self, conn_url, env, options): # creates a replica data dir using pg_basebackup. - # this is the default, built-in create_replica_method + # this is the default, built-in create_replica_methods # tries twice, then returns failure (as 1) # uses "stream" as the xlog-method to avoid sync issues # supports additional user-supplied options, those are not validated diff --git a/tests/test_postgresql.py b/tests/test_postgresql.py index 7fab3d65..80e93f61 100644 --- a/tests/test_postgresql.py +++ b/tests/test_postgresql.py @@ -420,14 +420,14 @@ class TestPostgresql(unittest.TestCase): def test_create_replica(self, mock_cancellable_subprocess_call): self.p.delete_trigger_file = Mock(side_effect=OSError) - self.p.config['create_replica_method'] = ['wale', 'basebackup'] + self.p.config['create_replica_methods'] = ['wale', 'basebackup'] self.p.config['wale'] = {'command': 'foo'} mock_cancellable_subprocess_call.return_value = 0 self.assertEquals(self.p.create_replica(self.leader), 0) del self.p.config['wale'] self.assertEquals(self.p.create_replica(self.leader), 0) - self.p.config['create_replica_method'] = ['basebackup'] + self.p.config['create_replica_methods'] = ['basebackup'] self.p.config['basebackup'] = [{'max_rate': '100M'}, 'no-sync'] self.assertEquals(self.p.create_replica(self.leader), 0) @@ -448,7 +448,7 @@ class TestPostgresql(unittest.TestCase): self.p.config['basebackup'] = {"foo": "bar"} self.assertEquals(self.p.create_replica(self.leader), 0) - self.p.config['create_replica_method'] = ['wale', 'basebackup'] + self.p.config['create_replica_methods'] = ['wale', 'basebackup'] del self.p.config['basebackup'] mock_cancellable_subprocess_call.return_value = 1 self.assertEquals(self.p.create_replica(self.leader), 1) @@ -465,6 +465,31 @@ class TestPostgresql(unittest.TestCase): self.p.cancel() self.assertEquals(self.p.create_replica(self.leader), 1) + @patch('time.sleep', Mock()) + @patch.object(Postgresql, 'cancellable_subprocess_call') + @patch.object(Postgresql, 'remove_data_directory', Mock(return_value=True)) + def test_create_replica_old_format(self, mock_cancellable_subprocess_call): + """ The same test as before but with old 'create_replica_method' + to test backward compatibility + """ + self.p.delete_trigger_file = Mock(side_effect=OSError) + + self.p.config['create_replica_method'] = ['wale', 'basebackup'] + self.p.config['wale'] = {'command': 'foo'} + mock_cancellable_subprocess_call.return_value = 0 + self.assertEquals(self.p.create_replica(self.leader), 0) + del self.p.config['wale'] + self.assertEquals(self.p.create_replica(self.leader), 0) + + self.p.config['create_replica_method'] = ['basebackup'] + self.p.config['basebackup'] = [{'max_rate': '100M'}, 'no-sync'] + self.assertEquals(self.p.create_replica(self.leader), 0) + + self.p.config['create_replica_method'] = ['wale', 'basebackup'] + del self.p.config['basebackup'] + mock_cancellable_subprocess_call.return_value = 1 + self.assertEquals(self.p.create_replica(self.leader), 1) + def test_basebackup(self): self.p.cancel() self.p.basebackup(None, None, {'foo': 'bar'})