Start postgres not in recovery in some cases (#2726)

If we know for sure that a few moments ago postgres was still running as a primary and we still have the leader lock and can successfully update it, in this case we can safely start postgres back not in recovery. That will allow to avoid bumping timeline without a reason and hopefully improve reliability because it will address issues similar to #2720.

In addition to that remove `if self.state_handler.is_starting()` check from the `recover()` method. This branch could never be reached because the `starting` state is handled earlier in the `_run_cycle()`. Besides that remove redundant `self._crash_recovery_executed`.

P.S. now we do not cover cases when Patroni was killed along with Postgres.
Lets consider that we just started Patroni, there is no leader, and `pg_controldata` reports `Database cluster state` as `shut down`. It feels logical to use `Latest checkpoint location` and `Latest checkpoint's TimeLineID` to do a usual leader race and start directly as a primary, but it could be totally wrong. The thing is that we run `postgres --single` if standby wasn't shut down cleanly before executing `pg_rewind`. As a result `Database cluster state` transition from `in archive recovery` to `shut down`, but if such a node becomes a leader the timeline must be increased.
This commit is contained in:
Alexander Kukushkin
2023-07-12 09:42:34 +02:00
committed by GitHub
parent b8cff3515a
commit 6e96db173f
4 changed files with 71 additions and 19 deletions
+1 -6
View File
@@ -72,16 +72,11 @@ Feature: basic replication
Then table bar is present on postgres1 after 20 seconds Then table bar is present on postgres1 after 20 seconds
And Response on GET http://127.0.0.1:8010/config contains master_start_timeout after 10 seconds And Response on GET http://127.0.0.1:8010/config contains master_start_timeout after 10 seconds
Scenario: check immediate failover when master_start_timeout=0
Given I kill postmaster on postgres2
Then postgres1 is a leader after 10 seconds
And postgres1 role is the primary after 10 seconds
Scenario: check rejoin of the former primary with pg_rewind Scenario: check rejoin of the former primary with pg_rewind
Given I add the table splitbrain to postgres0 Given I add the table splitbrain to postgres0
And I start postgres0 And I start postgres0
Then postgres0 role is the secondary after 20 seconds Then postgres0 role is the secondary after 20 seconds
When I add the table buz to postgres1 When I add the table buz to postgres2
Then table buz is present on postgres0 after 20 seconds Then table buz is present on postgres0 after 20 seconds
Scenario: check graceful rejection when two nodes have the same name Scenario: check graceful rejection when two nodes have the same name
+24
View File
@@ -0,0 +1,24 @@
Feature: recovery
We want to check that crashed postgres is started back
Scenario: check that timeline is not incremented when primary is started after crash
Given I start postgres0
Then postgres0 is a leader after 10 seconds
And there is a non empty initialize key in DCS after 15 seconds
When I start postgres1
And I add the table foo to postgres0
Then table foo is present on postgres1 after 20 seconds
When I kill postmaster on postgres0
Then postgres0 role is the primary after 10 seconds
When I issue a GET request to http://127.0.0.1:8008/
Then I receive a response code 200
And I receive a response role master
And I receive a response timeline 1
Scenario: check immediate failover when master_start_timeout=0
Given I issue a PATCH request to http://127.0.0.1:8008/config with {"master_start_timeout": 0}
Then I receive a response code 200
And Response on GET http://127.0.0.1:8008/config contains master_start_timeout after 10 seconds
When I kill postmaster on postgres0
Then postgres1 is a leader after 10 seconds
And postgres1 role is the primary after 10 seconds
+36 -10
View File
@@ -149,7 +149,6 @@ class Ha(object):
self._leader_timeline = None self._leader_timeline = None
self.recovering = False self.recovering = False
self._async_response = CriticalTask() self._async_response = CriticalTask()
self._crash_recovery_executed = False
self._crash_recovery_started = 0 self._crash_recovery_started = 0
self._start_timeout = None self._start_timeout = None
self._async_executor = AsyncExecutor(self.state_handler.cancellable, self.wakeup) self._async_executor = AsyncExecutor(self.state_handler.cancellable, self.wakeup)
@@ -411,8 +410,7 @@ class Ha(object):
return result return result
def _handle_crash_recovery(self) -> Optional[str]: def _handle_crash_recovery(self) -> Optional[str]:
if not self._crash_recovery_executed and (self.cluster.is_unlocked() or self._rewind.can_rewind): if self._crash_recovery_started == 0 and (self.cluster.is_unlocked() or self._rewind.can_rewind):
self._crash_recovery_executed = True
self._crash_recovery_started = time.time() self._crash_recovery_started = time.time()
msg = 'doing crash recovery in a single user mode' msg = 'doing crash recovery in a single user mode'
return self._async_executor.try_run_async(msg, self._rewind.ensure_clean_shutdown) or msg return self._async_executor.try_run_async(msg, self._rewind.ensure_clean_shutdown) or msg
@@ -438,15 +436,29 @@ class Ha(object):
return self._async_executor.try_run_async(msg, self._do_reinitialize, args=(self.cluster,)) or msg return self._async_executor.try_run_async(msg, self._do_reinitialize, args=(self.cluster,)) or msg
def recover(self) -> str: def recover(self) -> str:
# Postgres is not running and we will restart in standby mode. Watchdog is not needed until we promote. """Handle the case when postgres isn't running.
self.watchdog.disable()
Depending on the state of Patroni, DCS cluster view, and pg_controldata the following could happen:
- if ``primary_start_timeout`` is 0 and this node owns the leader lock, the lock
will be voluntarily released if there are healthy replicas to take it over.
- if postgres was running as a ``primary`` and this node owns the leader lock, postgres is started as primary.
- crash recover in a single-user mode is executed in the following cases:
- postgres was running as ``primary`` wasn't ``shut down`` cleanly and there is no leader in DCS
- postgres was running as ``replica`` wasn't ``shut down in recovery`` (cleanly)
and we need to run ``pg_rewind`` to join back to the cluster.
- ``pg_rewind`` is executed if it is necessary, or optinally, the data directory could
be removed if it is allowed by configuration.
- after ``crash recovery`` and/or ``pg_rewind`` are executed, postgres is started in recovery.
:returns: action message, describing what was performed.
"""
if self.has_lock() and self.update_lock(): if self.has_lock() and self.update_lock():
timeout = self.global_config.primary_start_timeout timeout = self.global_config.primary_start_timeout
if timeout == 0: if timeout == 0:
# We are requested to prefer failing over to restarting primary. But see first if there # We are requested to prefer failing over to restarting primary. But see first if there
# is anyone to fail over to. # is anyone to fail over to.
if self.is_failover_possible(self.cluster.members): if self.is_failover_possible(self.cluster.members):
self.watchdog.disable()
logger.info("Primary crashed. Failing over.") logger.info("Primary crashed. Failing over.")
self.demote('immediate') self.demote('immediate')
return 'stopped PostgreSQL to fail over after a crash' return 'stopped PostgreSQL to fail over after a crash'
@@ -455,6 +467,23 @@ class Ha(object):
data = self.state_handler.controldata() data = self.state_handler.controldata()
logger.info('pg_controldata:\n%s\n', '\n'.join(' {0}: {1}'.format(k, v) for k, v in data.items())) logger.info('pg_controldata:\n%s\n', '\n'.join(' {0}: {1}'.format(k, v) for k, v in data.items()))
# timeout > 0 indicates that we still have the leader lock, and it was just updated
if timeout\
and data.get('Database cluster state') in ('in production', 'shutting down', 'shut down')\
and self.state_handler.state == 'crashed'\
and self.state_handler.role in ('primary', 'master')\
and not self.state_handler.config.recovery_conf_exists():
# We know 100% that we were running as a primary a few moments ago, therefore could just start postgres
msg = 'starting primary after failure'
if self._async_executor.try_run_async(msg, self.state_handler.start,
args=(timeout, self._async_executor.critical_task)) is None:
self.recovering = True
return msg
# Postgres is not running, and we will restart in standby mode. Watchdog is not needed until we promote.
self.watchdog.disable()
if data.get('Database cluster state') in ('in production', 'shutting down', 'in crash recovery'): if data.get('Database cluster state') in ('in production', 'shutting down', 'in crash recovery'):
msg = self._handle_crash_recovery() msg = self._handle_crash_recovery()
if msg: if msg:
@@ -965,9 +994,6 @@ class Ha(object):
if ret is not None: # continue if we just deleted the stale failover key as a leader if ret is not None: # continue if we just deleted the stale failover key as a leader
return ret return ret
if self.state_handler.is_starting(): # postgresql still starting up is unhealthy
return False
if self.state_handler.is_leader(): if self.state_handler.is_leader():
# in pause leader is the healthiest only when no initialize or sysid matches with initialize! # in pause leader is the healthiest only when no initialize or sysid matches with initialize!
return not self.is_paused() or not self.cluster.initialize\ return not self.is_paused() or not self.cluster.initialize\
@@ -1451,7 +1477,7 @@ class Ha(object):
if self.state_handler.role in ('master', 'primary'): if self.state_handler.role in ('master', 'primary'):
logger.info('Demoting primary during %s', self._async_executor.scheduled_action) logger.info('Demoting primary during %s', self._async_executor.scheduled_action)
if self._async_executor.scheduled_action == 'restart': if self._async_executor.scheduled_action in ('restart', 'starting primary after failure'):
# Restart needs a special interlocking cancel because postmaster may be just started in a # Restart needs a special interlocking cancel because postmaster may be just started in a
# background thread and has not even written a pid file yet. # background thread and has not even written a pid file yet.
with self._async_executor.critical_task as task: with self._async_executor.critical_task as task:
@@ -1616,7 +1642,7 @@ class Ha(object):
return msg return msg
# Reset some states after postgres successfully started up # Reset some states after postgres successfully started up
self._crash_recovery_executed = False self._crash_recovery_started = 0
if self._rewind.executed and not self._rewind.failed: if self._rewind.executed and not self._rewind.failed:
self._rewind.reset_state() self._rewind.reset_state()
+10 -3
View File
@@ -282,11 +282,20 @@ class TestHa(PostgresInit):
self.p.follow = false self.p.follow = false
self.p.is_running = false self.p.is_running = false
self.p.name = 'leader' self.p.name = 'leader'
self.p.set_role('primary') self.p.set_role('demoted')
self.p.controldata = lambda: {'Database cluster state': 'shut down', 'Database system identifier': SYSID} self.p.controldata = lambda: {'Database cluster state': 'shut down', 'Database system identifier': SYSID}
self.ha.cluster = get_cluster_initialized_with_leader() self.ha.cluster = get_cluster_initialized_with_leader()
self.assertEqual(self.ha.run_cycle(), 'starting as readonly because i had the session lock') self.assertEqual(self.ha.run_cycle(), 'starting as readonly because i had the session lock')
def test_start_primary_after_failure(self):
self.p.start = false
self.p.is_running = false
self.p.name = 'leader'
self.p.set_role('primary')
self.p.controldata = lambda: {'Database cluster state': 'in production', 'Database system identifier': SYSID}
self.ha.cluster = get_cluster_initialized_with_leader()
self.assertEqual(self.ha.run_cycle(), 'starting primary after failure')
@patch.object(Rewind, 'ensure_clean_shutdown', Mock()) @patch.object(Rewind, 'ensure_clean_shutdown', Mock())
def test_crash_recovery(self): def test_crash_recovery(self):
self.ha.has_lock = true self.ha.has_lock = true
@@ -837,8 +846,6 @@ class TestHa(PostgresInit):
self.ha.dcs._last_failsafe = None self.ha.dcs._last_failsafe = None
with patch.object(Watchdog, 'is_healthy', PropertyMock(return_value=False)): with patch.object(Watchdog, 'is_healthy', PropertyMock(return_value=False)):
self.assertFalse(self.ha.is_healthiest_node()) self.assertFalse(self.ha.is_healthiest_node())
with patch('patroni.postgresql.Postgresql.is_starting', return_value=True):
self.assertFalse(self.ha.is_healthiest_node())
self.ha.is_paused = true self.ha.is_paused = true
self.assertFalse(self.ha.is_healthiest_node()) self.assertFalse(self.ha.is_healthiest_node())