Skip to content

Commit 59b2173

Browse files
committed
Do not let PostgreSQL to notify systemd (patroni#3590)
When systemd receives unexpected notifications it may terminate Patroni unit. Close patroni#3586
1 parent 7ef3cd4 commit 59b2173

10 files changed

Lines changed: 41 additions & 8 deletions

File tree

docs/releases.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,26 @@
33
Release notes
44
=============
55

6+
Version 4.1.2
7+
-------------
8+
9+
Released 2026-04-21
10+
11+
**Systemd support improvements**
12+
13+
- Add support for ``notify-reload`` systemd unit type (Ronan Dunklau)
14+
15+
Allows ``systemctl reload`` to wait until Patroni has actually processed the configuration reload by sending ``RELOADING=1`` and ``READY=1`` notifications to systemd.
16+
17+
- Send ``STOPPING=1`` notification to systemd on shutdown (Alexander Kukushkin)
18+
19+
Patroni now properly notifies systemd that it is shutting down, following the systemd notify protocol.
20+
21+
- Do not let PostgreSQL to notify systemd (Alexander Kukushkin)
22+
23+
Remove ``NotifyAccess=all`` from the example systemd unit file. Filter ``NOTIFY_SOCKET`` from the environment when starting PostgreSQL so it doesn't send ``READY=1`` or ``STOPPING=1`` to systemd. When taking over a PostgreSQL that was started before Patroni and already has ``NOTIFY_SOCKET``, re-assert ``READY=1`` during PostgreSQL shutdown to counteract its ``STOPPING=1``.
24+
25+
626
Version 4.1.1
727
-------------
828

extras/startup-scripts/patroni.service

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ After=syslog.target network.target
77

88
[Service]
99
Type=notify
10-
NotifyAccess=all
1110

1211
User=postgres
1312
Group=postgres

patroni/daemon.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ def shutdown(self) -> None:
169169
"""
170170
with self._sigterm_lock:
171171
self._received_sigterm = True
172+
notify_systemd("STOPPING=1")
172173
self._shutdown()
173174
self.logger.shutdown()
174175

patroni/postgresql/__init__.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from .. import global_config, psycopg
2020
from ..async_executor import CriticalTask
2121
from ..collections import CaseInsensitiveDict, CaseInsensitiveSet, EMPTY_DICT
22+
from ..daemon import notify_systemd
2223
from ..dcs import Cluster, Leader, Member, slot_name_from_member_name
2324
from ..exceptions import PostgresConnectionException
2425
from ..tags import Tags
@@ -899,10 +900,14 @@ def _do_stop(self, mode: str, block_callbacks: bool, checkpoint: bool,
899900
on_safepoint()
900901
return success, True
901902

902-
# We can skip safepoint detection if we don't have a callback
903+
# Wait for our connection to terminate to detect that PostgreSQL started shutting down.
904+
self._wait_for_connection_close(postmaster)
905+
# If the stopped PostgreSQL was started before Patroni (e.g. a takeover) it may have
906+
# had NOTIFY_SOCKET in its environment and sent STOPPING=1 to systemd on shutdown.
907+
# Re-assert READY=1 to counteract that when NotifyAccess=all is configured.
908+
notify_systemd("READY=1")
909+
903910
if on_safepoint:
904-
# Wait for our connection to terminate so we can be sure that no new connections are being initiated
905-
self._wait_for_connection_close(postmaster)
906911
postmaster.wait_for_user_backends_to_close(stop_timeout)
907912
on_safepoint()
908913

patroni/postgresql/postmaster.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,10 @@ def start(pgcommand: str, data_dir: str, conf: str, options: List[str]) -> Optio
225225
# In order to make everything portable we can't use fork&exec approach here, so we will call
226226
# ourselves and pass list of arguments which must be used to start postgres.
227227
# On Windows, in order to run a side-by-side assembly the specified env must include a valid SYSTEMROOT.
228-
env = {p: os.environ[p] for p in os.environ if not p.startswith(
229-
PATRONI_ENV_PREFIX) and not p.startswith(KUBERNETES_ENV_PREFIX)}
228+
# We also remove NOTIFY_SOCKET environment variable so that PostgreSQL doesn't send READY=1 or STOPPING=1
229+
# to systemd, because it is exclusively responsibility of Patroni to do it.
230+
env = {p: os.environ[p] for p in os.environ if p != 'NOTIFY_SOCKET'
231+
and not p.startswith(PATRONI_ENV_PREFIX) and not p.startswith(KUBERNETES_ENV_PREFIX)}
230232
if 'PG_MALLOC_ARENA_MAX' in env:
231233
env['MALLOC_ARENA_MAX'] = env.pop('PG_MALLOC_ARENA_MAX')
232234
try:

patroni/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
33
:var __version__: the current Patroni version.
44
"""
5-
__version__ = '4.1.1'
5+
__version__ = '4.1.2'

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import inspect
99
import logging
1010
import os
11+
import re
1112
import sys
1213

1314
from setuptools import Command, find_packages, setup
@@ -208,7 +209,7 @@ def main():
208209
license=LICENSE,
209210
license_files=('LICENSE',),
210211
keywords=KEYWORDS,
211-
long_description=read('README.rst').replace('**Important!**', '.. warning::\n'),
212+
long_description=re.sub(r'\n\n\.\. image:: docs/[^\n]*(?:\n [^\n]+)*', '', read('README.rst')),
212213
classifiers=CLASSIFIERS,
213214
packages=find_packages(exclude=['tests', 'tests.*']),
214215
package_data={MAIN_PACKAGE: [

tests/test_ha.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,7 @@ def test_bootstrap_release_initialize_key_on_failure(self):
658658
@patch('patroni.postgresql.mpp.citus.connect', psycopg_connect)
659659
@patch('patroni.postgresql.mpp.citus.quote_ident', Mock())
660660
@patch.object(Postgresql, 'connection', Mock(return_value=None))
661+
@patch.object(Postgresql, '_wait_for_connection_close', Mock())
661662
def test_bootstrap_release_initialize_key_on_watchdog_failure(self):
662663
self.ha.cluster = get_cluster_not_initialized_without_leader()
663664
self.e.initialize = true
@@ -671,6 +672,7 @@ def test_bootstrap_release_initialize_key_on_watchdog_failure(self):
671672
' watchdog activation failed'))
672673

673674
@patch('patroni.psycopg.connect', psycopg_connect)
675+
@patch.object(Postgresql, '_wait_for_connection_close', Mock())
674676
def test_reinitialize(self):
675677
self.assertIsNotNone(self.ha.reinitialize())
676678

@@ -1794,6 +1796,7 @@ def test_sysid_no_match_in_pause(self):
17941796
@patch('builtins.open', mock_open())
17951797
@patch.object(ConfigHandler, 'check_recovery_conf', Mock(return_value=(False, False)))
17961798
@patch.object(Postgresql, 'major_version', PropertyMock(return_value=130000))
1799+
@patch.object(Postgresql, '_wait_for_connection_close', Mock())
17971800
@patch.object(SlotsHandler, 'sync_replication_slots', Mock(return_value=['ls']))
17981801
def test_follow_copy(self):
17991802
self.ha.cluster.config.data['slots'] = {'ls': {'database': 'a', 'plugin': 'b'}}

tests/test_patroni.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ def test_apply_dynamic_configuration(self):
124124
@patch.object(AbstractEtcdClientWithFailover, '_get_machines_list', Mock(return_value=['http://remotehost:2379']))
125125
@patch.object(Thread, 'join', Mock())
126126
@patch.object(Postgresql, '_get_gucs', Mock(return_value={'foo': True, 'bar': True}))
127+
@patch.object(Postgresql, '_wait_for_connection_close', Mock())
127128
def test_patroni_patroni_main(self):
128129
with patch('subprocess.call', Mock(return_value=1)):
129130
with patch.object(Patroni, 'run', Mock(side_effect=SleepException)):

tests/test_postgresql.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,7 @@ def test_call_nowait(self):
490490
self.assertIsNone(self.p.call_nowait(CallbackAction.ON_START))
491491

492492
@patch.object(Postgresql, 'is_running', Mock(return_value=MockPostmaster()))
493+
@patch.object(Postgresql, '_wait_for_connection_close', Mock())
493494
def test_is_primary_exception(self):
494495
self.p.start()
495496
self.p.query = Mock(side_effect=psycopg.OperationalError("not supported"))

0 commit comments

Comments
 (0)