mirror of
https://github.com/outbackdingo/patroni.git
synced 2026-08-25 14:53:37 +00:00
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:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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())
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user