mirror of
https://github.com/outbackdingo/patroni.git
synced 2026-08-26 15:40:21 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a9befc1a2 | ||
|
|
8f22fd255e | ||
|
|
6938c21ff7 | ||
|
|
32934b205f | ||
|
|
1ba9e68b4b | ||
|
|
deb9cc6b73 | ||
|
|
a3c772dfc9 | ||
|
|
9977850b56 | ||
|
|
7543e64000 | ||
|
|
5ed4d33f7d | ||
|
|
c6943dc415 | ||
|
|
36011e936a | ||
|
|
a316105412 | ||
|
|
92c4f9fbb5 | ||
|
|
1c5d9f5653 | ||
|
|
66cf21767d | ||
|
|
b573bd4c9d | ||
|
|
33600976b1 | ||
|
|
e9ba775959 | ||
|
|
cf427e8b0b | ||
|
|
7531d41587 | ||
|
|
5dbfc9401b | ||
|
|
ce79152088 | ||
|
|
6920b3af0e | ||
|
|
0d87270897 | ||
|
|
1a31ea6e20 | ||
|
|
8de904e556 | ||
|
|
c97ad83396 | ||
|
|
0bb12473fb | ||
|
|
302757b71a | ||
|
|
3d932e1e73 | ||
|
|
38aef484e8 | ||
|
|
34b2a77294 | ||
|
|
6caa2fa99c | ||
|
|
b4eab48971 | ||
|
|
2bc25a32e4 | ||
|
|
7db7dfd3c5 | ||
|
|
3938bb9a16 | ||
|
|
26ae38960a | ||
|
|
836e527e6d | ||
|
|
e73f2044c8 | ||
|
|
39f5de2e77 | ||
|
|
46e20edbc2 | ||
|
|
578dc39291 | ||
|
|
9d1609e0eb | ||
|
|
fb0fcc859a | ||
|
|
a903438a5a | ||
|
|
19f75b407e | ||
|
|
3f00b7a6c7 | ||
|
|
4ce0f99cfb | ||
|
|
efba02f52e | ||
|
|
e1faa38e90 | ||
|
|
177101a1cc | ||
|
|
7dcb9b9840 | ||
|
|
e8a8bfe42f | ||
|
|
72be036c99 | ||
|
|
969d7ec4ab | ||
|
|
75ff8b3256 | ||
|
|
4853b3b430 | ||
|
|
ba970d8c63 | ||
|
|
ff278705d6 | ||
|
|
74c0acf36d | ||
|
|
58ee52b401 | ||
|
|
877acf2a55 | ||
|
|
8e46086335 | ||
|
|
e91e6b5484 | ||
|
|
6b685036d0 | ||
|
|
78a46b9ebc | ||
|
|
d7e172c20a | ||
|
|
87cb7481ae | ||
|
|
bfa9b0ca4b | ||
|
|
416a0f7c8b | ||
|
|
94a592d275 | ||
|
|
74a72e4f78 | ||
|
|
4c951a2937 | ||
|
|
b3ae8652c9 | ||
|
|
66f98c80e8 | ||
|
|
57ed40f66c | ||
|
|
d5d6a51e2c | ||
|
|
2f800173a5 | ||
|
|
db82a83eb4 | ||
|
|
3ecdf01b50 | ||
|
|
c9322df095 | ||
|
|
b470ade20e | ||
|
|
835d93951d | ||
|
|
8cdb0c25d9 | ||
|
|
6d65aa311a | ||
|
|
8c5ab4c07d | ||
|
|
31cf951b69 | ||
|
|
7659ccd50b | ||
|
|
a03dba04e3 | ||
|
|
93eb4edbe6 | ||
|
|
c931da1eb3 | ||
|
|
fc5a8ed01c | ||
|
|
0fa41502f1 | ||
|
|
384705ad97 | ||
|
|
56dba93c55 | ||
|
|
b458bd992a | ||
|
|
5eb431b719 | ||
|
|
ab9faf9471 | ||
|
|
cd3f52b029 | ||
|
|
4456e267eb | ||
|
|
c6339234c6 | ||
|
|
b1d442e7a4 | ||
|
|
b8b5518e8c | ||
|
|
a5796a03f1 | ||
|
|
fbbd32a537 | ||
|
|
c687838074 | ||
|
|
622d41c83c | ||
|
|
f00826d6e6 | ||
|
|
a4a6dc0299 | ||
|
|
3c07410695 | ||
|
|
7067d744c6 | ||
|
|
6b7ec49282 | ||
|
|
d4fd782038 | ||
|
|
6e1f9f7a6e | ||
|
|
a5d095e316 | ||
|
|
2a003a36bb | ||
|
|
af03c619ec | ||
|
|
14a44e14ba | ||
|
|
dc7ba3fe15 | ||
|
|
1ed207cbf0 | ||
|
|
b6c5a12017 | ||
|
|
1b7b8e60fb | ||
|
|
0a91948a49 | ||
|
|
0a6c09e252 | ||
|
|
ff31f45226 | ||
|
|
ff99d29e6d | ||
|
|
03bb9125cb | ||
|
|
634b44ee05 | ||
|
|
290d05c642 | ||
|
|
9d231aeecd | ||
|
|
48fbf64ea9 | ||
|
|
3fd7c98d2b | ||
|
|
d7454f7bcd | ||
|
|
ceb2965ab8 | ||
|
|
ae53260030 | ||
|
|
9b237b332e | ||
|
|
b09af642e6 | ||
|
|
014777b20a | ||
|
|
a8cfd46801 | ||
|
|
fd3e3ca472 | ||
|
|
e131065d74 | ||
|
|
bdd02324b4 | ||
|
|
7adfc0dbe7 | ||
|
|
f6943a859d | ||
|
|
e532f9dc38 | ||
|
|
688c85389c | ||
|
|
5c4ee30dae | ||
|
|
266cdc4810 | ||
|
|
2ac1efea54 | ||
|
|
5d8c2fb559 | ||
|
|
4e5b2ee249 | ||
|
|
3390ee9dea | ||
|
|
71ccf91e36 | ||
|
|
8acefefc42 | ||
|
|
dd548c4964 | ||
|
|
bcfd8438a5 | ||
|
|
5c3e1a693e | ||
|
|
206ee91b07 | ||
|
|
c1ee99d81d | ||
|
|
f0719d148c | ||
|
|
efdedc7049 | ||
|
|
bbddca6a76 | ||
|
|
a4e0a2220d | ||
|
|
0e6a2ff3a9 | ||
|
|
6976939f09 | ||
|
|
ef5f320602 | ||
|
|
47cadc9f63 | ||
|
|
5a77cbb087 | ||
|
|
92f4aa2ef9 | ||
|
|
7c3ce78231 | ||
|
|
76e19ecfe2 | ||
|
|
9afaf6eb51 | ||
|
|
36e3dfbe41 | ||
|
|
bb804074f7 | ||
|
|
ed9d4750f9 | ||
|
|
193c73f6b8 | ||
|
|
91327f943c | ||
|
|
ac6f6ae1c2 | ||
|
|
70b0991e6a | ||
|
|
5dab735534 | ||
|
|
ecf158bce3 | ||
|
|
1870dcd8f9 | ||
|
|
7370f70f13 | ||
|
|
1b96ae9c0a | ||
|
|
3ffd598a1c | ||
|
|
552e8643d9 | ||
|
|
269b04be5d | ||
|
|
8adddb3467 | ||
|
|
d72f7cb259 | ||
|
|
c3dce46830 | ||
|
|
ce10e5fccc | ||
|
|
bb90feb393 | ||
|
|
3d527f5728 | ||
|
|
6c06f5cc96 | ||
|
|
f5ee67fa1c | ||
|
|
65030c56ee | ||
|
|
d471f1156d | ||
|
|
6d98944e73 | ||
|
|
6cfd90401e | ||
|
|
ce187bec38 | ||
|
|
c5fffb3c97 | ||
|
|
cb5f34b721 | ||
|
|
260ab36f2e | ||
|
|
fc67ba73f0 | ||
|
|
60d8bc3a70 | ||
|
|
e513f7f127 | ||
|
|
aa3ebe0af8 | ||
|
|
c96e35c807 | ||
|
|
88b35252c3 | ||
|
|
d93db20baa | ||
|
|
42976df86f | ||
|
|
6f4c2fe132 | ||
|
|
588df5da05 | ||
|
|
fb367cd73e | ||
|
|
535dc631ec | ||
|
|
9b8c40a6e1 | ||
|
|
e19a8730ea | ||
|
|
28a604983b | ||
|
|
efacc6c16b | ||
|
|
9283ebda64 | ||
|
|
a329a9d320 | ||
|
|
f77073c8e1 | ||
|
|
aaac6f6fb0 | ||
|
|
27915984b4 | ||
|
|
220cacd95f | ||
|
|
a3b3e1bc1c | ||
|
|
c855b0bff9 | ||
|
|
4c1c804cfd | ||
|
|
48514db84b | ||
|
|
2bd821a768 | ||
|
|
fe16c3610e | ||
|
|
bc15813de0 | ||
|
|
18d9cb1124 | ||
|
|
66bdb1ae12 | ||
|
|
28b9d3d2d9 | ||
|
|
25ceb68257 | ||
|
|
5a504e67c1 | ||
|
|
75dbe4ff96 | ||
|
|
71863cedcb | ||
|
|
728abfcc37 | ||
|
|
238b8db91e | ||
|
|
b31a4d55c9 | ||
|
|
3c24c33e59 | ||
|
|
83a060fc15 | ||
|
|
a2ceff1517 | ||
|
|
19f20ec2eb | ||
|
|
30f0f132e8 | ||
|
|
941e883dde | ||
|
|
89a162e000 | ||
|
|
0ab5b49757 | ||
|
|
80a03a4892 | ||
|
|
d2603402ea | ||
|
|
6b7f914da7 | ||
|
|
03107e6d8b | ||
|
|
77dba39585 | ||
|
|
89d794facc | ||
|
|
3333e78500 | ||
|
|
0ab4bc9d27 | ||
|
|
13cfe0af36 | ||
|
|
7319d12026 | ||
|
|
2ec9834c60 | ||
|
|
2be64e5131 | ||
|
|
93be10a655 | ||
|
|
366829e379 | ||
|
|
899cad1c0f | ||
|
|
a4ac4963d1 | ||
|
|
704d36815a | ||
|
|
4138d0b830 | ||
|
|
b7ea511511 | ||
|
|
badf1da183 | ||
|
|
6a75b1591b | ||
|
|
82d2ef4878 | ||
|
|
b83f1c0f44 | ||
|
|
9209a5a133 | ||
|
|
3734ecc851 | ||
|
|
713244975c | ||
|
|
efaba9f183 | ||
|
|
f24db395c6 | ||
|
|
9dd177e5c9 | ||
|
|
eb100fd586 | ||
|
|
a74985f41d | ||
|
|
da9aaf6cdf | ||
|
|
84aac437c1 | ||
|
|
48e3d31e1d | ||
|
|
01d07f86cd | ||
|
|
b6fc4bc393 | ||
|
|
018a2f4dd9 | ||
|
|
b7caf3b7f2 | ||
|
|
ec61aede85 | ||
|
|
e4703d4f74 | ||
|
|
a26e46cf76 | ||
|
|
94bfea1a81 | ||
|
|
01976ec10b | ||
|
|
8f3ed00886 | ||
|
|
7e89583ec7 | ||
|
|
2735c937fd | ||
|
|
384a2a4d8f | ||
|
|
238aba3956 | ||
|
|
ae2bbd28ae | ||
|
|
0e19e3e98e | ||
|
|
06db296612 | ||
|
|
817f39ad6d | ||
|
|
c5a4befdc4 | ||
|
|
e860cac348 | ||
|
|
48164774c2 | ||
|
|
0a8fb0860e | ||
|
|
ffd1ad97d2 | ||
|
|
84c574e1ec | ||
|
|
cb9998ade6 | ||
|
|
4830e36e2b | ||
|
|
0c5bf3c4cd | ||
|
|
480b8dbf95 | ||
|
|
a4d29eb99e | ||
|
|
d46ca88e6b | ||
|
|
665f49b320 | ||
|
|
47854d77e8 | ||
|
|
e4fe239a9d | ||
|
|
6e96db173f | ||
|
|
b8cff3515a | ||
|
|
412c51ddf1 | ||
|
|
4725f12f9a | ||
|
|
3c1b274ab7 | ||
|
|
35c97fa402 | ||
|
|
1c36112b44 | ||
|
|
768d563fba | ||
|
|
4b023bc9ad | ||
|
|
c4f8e72765 | ||
|
|
0eea239f6b | ||
|
|
e72d3ba79e | ||
|
|
ed02826103 | ||
|
|
74d78dbba2 | ||
|
|
6f91f4f4e2 | ||
|
|
43e2290fdf | ||
|
|
a00ffcb1a6 | ||
|
|
9b01041175 | ||
|
|
2354f8f004 | ||
|
|
bd951ccdef | ||
|
|
e9f9e1cfad | ||
|
|
4e52d4bb2e | ||
|
|
0cf2083161 | ||
|
|
4b960477bb | ||
|
|
21e92fd166 | ||
|
|
af318b2473 | ||
|
|
f3c80d5706 | ||
|
|
df18885f20 | ||
|
|
d11328020d | ||
|
|
37fffa618f | ||
|
|
101ea10e98 | ||
|
|
2158f4a87b | ||
|
|
af8e5f0d0f | ||
|
|
1c7bf2f59e | ||
|
|
822b6ec711 | ||
|
|
73797e8572 | ||
|
|
b4afc6830b | ||
|
|
d1fdb45179 | ||
|
|
6c8a3b0d25 | ||
|
|
0ead20f6a4 | ||
|
|
2f5bcbd877 | ||
|
|
db71ba3955 | ||
|
|
e0a4a0c6a6 | ||
|
|
506b5bec48 | ||
|
|
44e58a1ba1 | ||
|
|
66a0e44371 | ||
|
|
fdcf8b1997 | ||
|
|
ab9fea7d6b | ||
|
|
7941c86775 | ||
|
|
13164daf28 | ||
|
|
76b3b99de2 | ||
|
|
1ac9b11f33 | ||
|
|
4e1b9937b9 | ||
|
|
90ed581d87 | ||
|
|
bebe6754fc | ||
|
|
4d35f85b87 | ||
|
|
54b6d8186f | ||
|
|
ff6d728f07 | ||
|
|
4f458baa0e | ||
|
|
7c0a565985 | ||
|
|
782ebda77e | ||
|
|
24af774adb | ||
|
|
8a5d6ec74d | ||
|
|
6ffc73946a | ||
|
|
e30d96a468 | ||
|
|
2c7b547a29 | ||
|
|
3fe2a7868a | ||
|
|
6f357a4e17 | ||
|
|
c549ea7d5c | ||
|
|
1003af6d20 | ||
|
|
786f7eba97 | ||
|
|
f42bab5081 | ||
|
|
39875f448c | ||
|
|
a1095e385c | ||
|
|
84353b88c9 | ||
|
|
60723f5fa4 | ||
|
|
a8b90f0cd6 | ||
|
|
918674e7bb | ||
|
|
ddac8683e6 | ||
|
|
36c17e944b | ||
|
|
c1bfb0e6d6 | ||
|
|
373affe707 | ||
|
|
95ba8b9e59 | ||
|
|
60a7e5a514 | ||
|
|
eefa15b390 | ||
|
|
8622fcea3d | ||
|
|
2afcaa9d83 | ||
|
|
09d0d78b74 | ||
|
|
89595babdf | ||
|
|
dff5537954 | ||
|
|
c985974ece | ||
|
|
388bb40b71 |
@@ -1,48 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Environment**
|
|
||||||
- Patroni version:
|
|
||||||
- PostgreSQL version:
|
|
||||||
- DCS (and its version):
|
|
||||||
|
|
||||||
**Patroni configuration file**
|
|
||||||
```
|
|
||||||
Please copy&paste your Patroni configuration file here
|
|
||||||
```
|
|
||||||
|
|
||||||
**patronictl show-config**
|
|
||||||
```
|
|
||||||
Please copy&paste the output of "patronictl show-config" command here
|
|
||||||
```
|
|
||||||
|
|
||||||
**Have you checked Patroni logs?**
|
|
||||||
Please provide a snippet of Patroni log files here
|
|
||||||
|
|
||||||
**Have you checked PostgreSQL logs?**
|
|
||||||
Please provide a snippet here
|
|
||||||
|
|
||||||
**Have you tried to use GitHub issue search?**
|
|
||||||
Maybe there is already a similar issue solved.
|
|
||||||
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Create a report to help us improve
|
||||||
|
labels:
|
||||||
|
- bug
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
If you have a question please post it on channel [#patroni](https://postgresteam.slack.com/archives/C9XPYG92A) in the [PostgreSQL Slack](https://pgtreats.info/slack-invite).
|
||||||
|
Before reporting a bug please make sure to **reproduce it with the latest Patroni version**!
|
||||||
|
Please fill the form below and provide as much information as possible.
|
||||||
|
Not doing so may result in your bug not being addressed in a timely manner.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: What happened?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: repro
|
||||||
|
attributes:
|
||||||
|
label: How can we reproduce it (as minimally and precisely as possible)?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: What did you expect to happen?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: environment
|
||||||
|
attributes:
|
||||||
|
label: Patroni/PostgreSQL/DCS version
|
||||||
|
value: |
|
||||||
|
- Patroni version:
|
||||||
|
- PostgreSQL version:
|
||||||
|
- DCS (and its version):
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: patroniConfig
|
||||||
|
attributes:
|
||||||
|
label: Patroni configuration file
|
||||||
|
description: Please copy and paste Patroni configuration file here. This will be automatically formatted into code, so no need for backticks.
|
||||||
|
render: yaml
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: globalConfig
|
||||||
|
attributes:
|
||||||
|
label: patronictl show-config
|
||||||
|
description: Please copy and paste `patronictl show-config` output here. This will be automatically formatted into code, so no need for backticks.
|
||||||
|
render: yaml
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: patroniLogs
|
||||||
|
attributes:
|
||||||
|
label: Patroni log files
|
||||||
|
description: Please copy and paste any relevant Patroni log output. This will be automatically formatted into code, so no need for backticks.
|
||||||
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: postgresLogs
|
||||||
|
attributes:
|
||||||
|
label: PostgreSQL log files
|
||||||
|
description: Please copy and paste any relevant PostgreSQL log output. This will be automatically formatted into code, so no need for backticks.
|
||||||
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: issueSearch
|
||||||
|
attributes:
|
||||||
|
label: Have you tried to use GitHub issue search?
|
||||||
|
description: Maybe there is already a similar issue solved.
|
||||||
|
options:
|
||||||
|
- label: 'Yes'
|
||||||
|
required: true
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional
|
||||||
|
attributes:
|
||||||
|
label: Anything else we need to know?
|
||||||
|
description: Add any other context about the problem here.
|
||||||
@@ -1 +1,5 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Question
|
||||||
|
url: https://pgtreats.info/slack-invite
|
||||||
|
about: "Please ask questions on channel #patroni in the PostgreSQL Slack"
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ import zipfile
|
|||||||
|
|
||||||
|
|
||||||
def install_requirements(what):
|
def install_requirements(what):
|
||||||
|
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'pip'])
|
||||||
|
s = subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'wheel', 'setuptools'])
|
||||||
|
if s != 0:
|
||||||
|
return s
|
||||||
|
|
||||||
old_path = sys.path[:]
|
old_path = sys.path[:]
|
||||||
w = os.path.join(os.getcwd(), os.path.dirname(inspect.getfile(inspect.currentframe())))
|
w = os.path.join(os.getcwd(), os.path.dirname(inspect.getfile(inspect.currentframe())))
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(w)))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(w)))
|
||||||
@@ -16,23 +21,25 @@ def install_requirements(what):
|
|||||||
from setup import EXTRAS_REQUIRE, read
|
from setup import EXTRAS_REQUIRE, read
|
||||||
finally:
|
finally:
|
||||||
sys.path = old_path
|
sys.path = old_path
|
||||||
requirements = ['mock>=2.0.0', 'flake8', 'pytest', 'pytest-cov'] if what == 'all' else ['behave']
|
requirements = ['flake8', 'pytest', 'pytest-cov'] if what == 'all' else ['behave']
|
||||||
requirements += ['coverage']
|
requirements += ['coverage']
|
||||||
# try to split tests between psycopg2 and psycopg3
|
# try to split tests between psycopg2 and psycopg3
|
||||||
requirements += ['psycopg[binary]'] if sys.version_info > (3, 7, 0) and\
|
requirements += ['psycopg[binary]'] if sys.version_info >= (3, 8, 0) and\
|
||||||
(sys.platform != 'darwin' or what == 'etcd3') else ['psycopg2-binary']
|
(sys.platform != 'darwin' or what == 'etcd3') else ['psycopg2-binary==2.9.9'
|
||||||
for r in read('requirements.txt').split('\n'):
|
if sys.platform == 'darwin' else 'psycopg2-binary']
|
||||||
r = r.strip()
|
|
||||||
if r != '':
|
|
||||||
extras = {e for e, v in EXTRAS_REQUIRE.items() if v and any(r.startswith(x) for x in v)}
|
|
||||||
if not extras or what == 'all' or what in extras:
|
|
||||||
requirements.append(r)
|
|
||||||
|
|
||||||
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'pip'])
|
from pip._vendor.distlib.markers import evaluator, DEFAULT_CONTEXT
|
||||||
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'wheel'])
|
from pip._vendor.distlib.util import parse_requirement
|
||||||
r = subprocess.call([sys.executable, '-m', 'pip', 'install'] + requirements)
|
|
||||||
s = subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'setuptools'])
|
for r in read('requirements.txt').split('\n'):
|
||||||
return s | r
|
r = parse_requirement(r)
|
||||||
|
if not r or r.marker and not evaluator.evaluate(r.marker, DEFAULT_CONTEXT):
|
||||||
|
continue
|
||||||
|
extras = {e for e, v in EXTRAS_REQUIRE.items() if v and any(r.requirement.startswith(x) for x in v)}
|
||||||
|
if not extras or what == 'all' or what in extras:
|
||||||
|
requirements.append(r.requirement)
|
||||||
|
|
||||||
|
return subprocess.call([sys.executable, '-m', 'pip', 'install'] + requirements)
|
||||||
|
|
||||||
|
|
||||||
def install_packages(what):
|
def install_packages(what):
|
||||||
@@ -45,8 +52,8 @@ def install_packages(what):
|
|||||||
packages['exhibitor'] = packages['zookeeper']
|
packages['exhibitor'] = packages['zookeeper']
|
||||||
packages = packages.get(what, [])
|
packages = packages.get(what, [])
|
||||||
ver = versions.get(what)
|
ver = versions.get(what)
|
||||||
if float(ver) >= 15:
|
if 15 <= float(ver) < 18:
|
||||||
packages += ['postgresql-{0}-citus-11.2'.format(ver)]
|
packages += ['postgresql-{0}-citus-13.0'.format(ver)]
|
||||||
subprocess.call(['sudo', 'apt-get', 'update', '-y'])
|
subprocess.call(['sudo', 'apt-get', 'update', '-y'])
|
||||||
return subprocess.call(['sudo', 'apt-get', 'install', '-y', 'postgresql-' + ver, 'expect-dev'] + packages)
|
return subprocess.call(['sudo', 'apt-get', 'install', '-y', 'postgresql-' + ver, 'expect-dev'] + packages)
|
||||||
|
|
||||||
@@ -85,8 +92,8 @@ def unzip_all(archive):
|
|||||||
|
|
||||||
|
|
||||||
def chmod_755(name):
|
def chmod_755(name):
|
||||||
os.chmod(name, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR |
|
os.chmod(name, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR
|
||||||
stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
|
| stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
|
||||||
|
|
||||||
|
|
||||||
def unpack(archive, name):
|
def unpack(archive, name):
|
||||||
@@ -110,7 +117,7 @@ def install_etcd():
|
|||||||
|
|
||||||
|
|
||||||
def install_postgres():
|
def install_postgres():
|
||||||
version = os.environ.get('PGVERSION', '15.1-1')
|
version = os.environ.get('PGVERSION', '16.1-1')
|
||||||
platform = {'darwin': 'osx', 'win32': 'windows-x64', 'cygwin': 'windows-x64'}[sys.platform]
|
platform = {'darwin': 'osx', 'win32': 'windows-x64', 'cygwin': 'windows-x64'}[sys.platform]
|
||||||
if platform == 'osx':
|
if platform == 'osx':
|
||||||
return subprocess.call(['brew', 'install', 'expect', 'postgresql@{0}'.format(version.split('.')[0])])
|
return subprocess.call(['brew', 'install', 'expect', 'postgresql@{0}'.format(version.split('.')[0])])
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
versions = {'etcd': '9.6', 'etcd3': '14', 'consul': '13', 'exhibitor': '12', 'raft': '11', 'kubernetes': '15'}
|
versions = {'etcd': '9.6', 'etcd3': '16', 'consul': '17', 'exhibitor': '12', 'raft': '14', 'kubernetes': '15'}
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ jobs:
|
|||||||
build-n-publish:
|
build-n-publish:
|
||||||
name: Build and publish Patroni distributions to PyPI and TestPyPI
|
name: Build and publish Patroni distributions to PyPI and TestPyPI
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.11
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: python .github/workflows/install_deps.py
|
run: python .github/workflows/install_deps.py
|
||||||
@@ -24,18 +26,18 @@ jobs:
|
|||||||
- name: Run tests and flake8
|
- name: Run tests and flake8
|
||||||
run: python .github/workflows/run_tests.py
|
run: python .github/workflows/run_tests.py
|
||||||
|
|
||||||
|
- name: Install Python packaging build frontend
|
||||||
|
run: python -m pip install build
|
||||||
|
|
||||||
- name: Build a binary wheel and a source tarball
|
- name: Build a binary wheel and a source tarball
|
||||||
run: python setup.py sdist bdist_wheel
|
run: python -m build
|
||||||
|
|
||||||
- name: Publish distribution to Test PyPI
|
- name: Publish distribution to Test PyPI
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
uses: pypa/gh-action-pypi-publish@v1.5.1
|
uses: pypa/gh-action-pypi-publish@v1.9.0
|
||||||
with:
|
with:
|
||||||
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
|
|
||||||
repository_url: https://test.pypi.org/legacy/
|
repository_url: https://test.pypi.org/legacy/
|
||||||
|
|
||||||
- name: Publish distribution to PyPI
|
- name: Publish distribution to PyPI
|
||||||
if: github.event_name == 'release'
|
if: github.event_name == 'release'
|
||||||
uses: pypa/gh-action-pypi-publish@v1.5.1
|
uses: pypa/gh-action-pypi-publish@v1.9.0
|
||||||
with:
|
|
||||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ def main():
|
|||||||
|
|
||||||
version = versions.get(what)
|
version = versions.get(what)
|
||||||
path = '/usr/lib/postgresql/{0}/bin:.'.format(version)
|
path = '/usr/lib/postgresql/{0}/bin:.'.format(version)
|
||||||
unbuffer = ['timeout', '900', 'unbuffer']
|
unbuffer = ['timeout', '1200', 'unbuffer']
|
||||||
else:
|
else:
|
||||||
if sys.platform == 'darwin':
|
if sys.platform == 'darwin':
|
||||||
version = os.environ.get('PGVERSION', '15.1-1')
|
version = os.environ.get('PGVERSION', '16.1-1')
|
||||||
path = '/usr/local/opt/postgresql@{0}/bin:.'.format(version.split('.')[0])
|
path = '/opt/homebrew/opt/postgresql@{0}/bin:.'.format(version.split('.')[0])
|
||||||
unbuffer = ['unbuffer']
|
unbuffer = ['unbuffer']
|
||||||
else:
|
else:
|
||||||
path = os.path.abspath(os.path.join('pgsql', 'bin'))
|
path = os.path.abspath(os.path.join('pgsql', 'bin'))
|
||||||
|
|||||||
+117
-17
@@ -5,6 +5,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- 'REL_[0-9]+_[0-9]+'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
|
CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
|
||||||
@@ -12,26 +13,29 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
unit:
|
unit:
|
||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ fromJson('{"ubuntu":"ubuntu-22.04","windows":"windows-latest","macos":"macos-latest"}')[matrix.os] }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu, windows, macos]
|
os: [ubuntu, windows, macos]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python 3.7
|
- name: Set up Python 3.7
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.7
|
python-version: 3.7
|
||||||
|
if: matrix.os != 'macos'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: python .github/workflows/install_deps.py
|
run: python .github/workflows/install_deps.py
|
||||||
|
if: matrix.os != 'macos'
|
||||||
- name: Run tests and flake8
|
- name: Run tests and flake8
|
||||||
run: python .github/workflows/run_tests.py
|
run: python .github/workflows/run_tests.py
|
||||||
|
if: matrix.os != 'macos'
|
||||||
|
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.8
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.8
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -40,7 +44,7 @@ jobs:
|
|||||||
run: python .github/workflows/run_tests.py
|
run: python .github/workflows/run_tests.py
|
||||||
|
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.9
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.9
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -49,7 +53,7 @@ jobs:
|
|||||||
run: python .github/workflows/run_tests.py
|
run: python .github/workflows/run_tests.py
|
||||||
|
|
||||||
- name: Set up Python 3.10
|
- name: Set up Python 3.10
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.10'
|
python-version: '3.10'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -58,7 +62,7 @@ jobs:
|
|||||||
run: python .github/workflows/run_tests.py
|
run: python .github/workflows/run_tests.py
|
||||||
|
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -66,6 +70,27 @@ jobs:
|
|||||||
- name: Run tests and flake8
|
- name: Run tests and flake8
|
||||||
run: python .github/workflows/run_tests.py
|
run: python .github/workflows/run_tests.py
|
||||||
|
|
||||||
|
- name: Set up Python 3.12
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: 3.12
|
||||||
|
- name: Install dependencies
|
||||||
|
run: python .github/workflows/install_deps.py
|
||||||
|
- name: Run tests and flake8
|
||||||
|
run: python .github/workflows/run_tests.py
|
||||||
|
|
||||||
|
- name: Set up Python 3.13
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: 3.13
|
||||||
|
if: matrix.os != 'macos'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: python .github/workflows/install_deps.py
|
||||||
|
if: matrix.os != 'macos'
|
||||||
|
- name: Run tests and flake8
|
||||||
|
run: python .github/workflows/run_tests.py
|
||||||
|
if: matrix.os != 'macos'
|
||||||
|
|
||||||
- name: Combine coverage
|
- name: Combine coverage
|
||||||
run: python .github/workflows/run_tests.py combine
|
run: python .github/workflows/run_tests.py combine
|
||||||
|
|
||||||
@@ -80,16 +105,16 @@ jobs:
|
|||||||
run: python -m coveralls --service=github
|
run: python -m coveralls --service=github
|
||||||
|
|
||||||
behave:
|
behave:
|
||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ fromJson('{"ubuntu":"ubuntu-22.04","windows":"windows-latest","macos":"macos-latest"}')[matrix.os] }}
|
||||||
env:
|
env:
|
||||||
DCS: ${{ matrix.dcs }}
|
DCS: ${{ matrix.dcs }}
|
||||||
ETCDVERSION: 3.4.23
|
ETCDVERSION: 3.4.23
|
||||||
PGVERSION: 15.1-1 # for windows and macos
|
PGVERSION: 16.1-1 # for windows and macos
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu]
|
os: [ubuntu]
|
||||||
python-version: [3.7, '3.10']
|
python-version: [3.7, 3.13]
|
||||||
dcs: [etcd, etcd3, consul, exhibitor, kubernetes, raft]
|
dcs: [etcd, etcd3, consul, exhibitor, kubernetes, raft]
|
||||||
include:
|
include:
|
||||||
- os: macos
|
- os: macos
|
||||||
@@ -103,9 +128,9 @@ jobs:
|
|||||||
dcs: etcd3
|
dcs: etcd3
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- uses: nolar/setup-k3d-k3s@v1
|
- uses: nolar/setup-k3d-k3s@v1
|
||||||
@@ -116,20 +141,20 @@ jobs:
|
|||||||
sudo apt-get install -y wget ca-certificates gnupg debian-archive-keyring apt-transport-https
|
sudo apt-get install -y wget ca-certificates gnupg debian-archive-keyring apt-transport-https
|
||||||
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
||||||
sudo sh -c 'wget -qO - https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor > /etc/apt/trusted.gpg.d/apt.postgresql.org.gpg'
|
sudo sh -c 'wget -qO - https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor > /etc/apt/trusted.gpg.d/apt.postgresql.org.gpg'
|
||||||
sudo sh -c 'echo "deb [signed-by=/etc/apt/trusted.gpg.d/citusdata_community.gpg] https://repos.citusdata.com/community/ubuntu/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/citusdata_community.list'
|
sudo sh -c 'echo "deb [signed-by=/etc/apt/trusted.gpg.d/citusdata_community.gpg] https://packagecloud.io/citusdata/community/ubuntu/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/citusdata_community.list'
|
||||||
sudo sh -c 'wget -qO - https://repos.citusdata.com/community/gpgkey | gpg --dearmor > /etc/apt/trusted.gpg.d/citusdata_community.gpg'
|
sudo sh -c 'wget -qO - https://packagecloud.io/citusdata/community/gpgkey | gpg --dearmor > /etc/apt/trusted.gpg.d/citusdata_community.gpg'
|
||||||
if: matrix.os == 'ubuntu'
|
if: matrix.os == 'ubuntu'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: python .github/workflows/install_deps.py
|
run: python .github/workflows/install_deps.py
|
||||||
- name: Run behave tests
|
- name: Run behave tests
|
||||||
run: python .github/workflows/run_tests.py
|
run: python .github/workflows/run_tests.py
|
||||||
- name: Upload logs if behave failed
|
- name: Upload logs if behave failed
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: behave-${{ matrix.os }}-${{ matrix.dcs }}-${{ matrix.python-version }}-logs
|
name: behave-${{ matrix.os }}-${{ matrix.dcs }}-${{ matrix.python-version }}-logs
|
||||||
path: |
|
path: |
|
||||||
features/output/*_failed/*postgres?.*
|
features/output/*_failed/*postgres-?.*
|
||||||
features/output/*.log
|
features/output/*.log
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 5
|
retention-days: 5
|
||||||
@@ -144,7 +169,7 @@ jobs:
|
|||||||
needs: unit
|
needs: unit
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v5
|
||||||
- run: python -m pip install coveralls
|
- run: python -m pip install coveralls
|
||||||
- run: python -m coveralls --service=github --finish
|
- run: python -m coveralls --service=github --finish
|
||||||
env:
|
env:
|
||||||
@@ -157,3 +182,78 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- run: bash <(curl -Ls https://coverage.codacy.com/get.sh) final
|
- run: bash <(curl -Ls https://coverage.codacy.com/get.sh) final
|
||||||
if: ${{ env.SECRETS_AVAILABLE == 'true' }}
|
if: ${{ env.SECRETS_AVAILABLE == 'true' }}
|
||||||
|
|
||||||
|
pyright:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python 3.13
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: 3.13
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: python -m pip install -r requirements.txt psycopg2-binary psycopg
|
||||||
|
|
||||||
|
- uses: jakebailey/pyright-action@v2
|
||||||
|
with:
|
||||||
|
version: 1.1.394
|
||||||
|
|
||||||
|
ydiff:
|
||||||
|
name: Test compatibility with the latest version of ydiff
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python 3.13
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: 3.13
|
||||||
|
- name: Install dependencies
|
||||||
|
run: python .github/workflows/install_deps.py
|
||||||
|
- name: Update ydiff
|
||||||
|
run: python -m pip install -U ydiff
|
||||||
|
- name: Run tests
|
||||||
|
run: python -m pytest tests/test_ctl.py -v
|
||||||
|
|
||||||
|
docs:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python 3.11
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: 3.11
|
||||||
|
cache: pip
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install tox
|
||||||
|
|
||||||
|
- name: Install package dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt update \
|
||||||
|
&& sudo apt install -y \
|
||||||
|
latexmk texlive-latex-extra tex-gyre \
|
||||||
|
--no-install-recommends
|
||||||
|
|
||||||
|
- name: Generate documentation
|
||||||
|
run: tox -m docs
|
||||||
|
|
||||||
|
isort:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python 3.12
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: 3.12
|
||||||
|
cache: pip
|
||||||
|
|
||||||
|
- name: isort
|
||||||
|
uses: isort/isort-action@master
|
||||||
|
with:
|
||||||
|
requirementsFiles: "requirements.txt requirements.dev.txt requirements.docs.txt"
|
||||||
|
sort-paths: "patroni tests features setup.py"
|
||||||
|
|||||||
+15
-2
@@ -27,14 +27,15 @@ lib64
|
|||||||
pip-log.txt
|
pip-log.txt
|
||||||
|
|
||||||
# Unit test / coverage reports
|
# Unit test / coverage reports
|
||||||
.coverage
|
.coverage*
|
||||||
.tox
|
.tox
|
||||||
nosetests.xml
|
nosetests.xml
|
||||||
coverage.xml
|
coverage.xml
|
||||||
htmlcov
|
htmlcov
|
||||||
junit.xml
|
junit.xml
|
||||||
features/output
|
features/output*
|
||||||
dummy
|
dummy
|
||||||
|
result.json
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
*.mo
|
*.mo
|
||||||
@@ -48,12 +49,24 @@ pgpass
|
|||||||
scm-source.json
|
scm-source.json
|
||||||
|
|
||||||
# Sphinx-generated documentation
|
# Sphinx-generated documentation
|
||||||
|
docs/_build/
|
||||||
docs/build/
|
docs/build/
|
||||||
docs/source/_static/
|
docs/source/_static/
|
||||||
docs/source/_templates/
|
docs/source/_templates/
|
||||||
|
docs/modules/
|
||||||
|
docs/pdf/
|
||||||
|
|
||||||
# Pycharm IDE
|
# Pycharm IDE
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
#VSCode IDE
|
#VSCode IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
# Virtual environment
|
||||||
|
venv*/
|
||||||
|
|
||||||
|
# Default test data directory
|
||||||
|
data/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
**/.DS_Store
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# .readthedocs.yaml
|
||||||
|
# Read the Docs configuration file
|
||||||
|
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||||
|
|
||||||
|
# Required
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
# Set the version of Python and other tools you might need
|
||||||
|
build:
|
||||||
|
os: ubuntu-22.04
|
||||||
|
tools:
|
||||||
|
python: "3.11"
|
||||||
|
|
||||||
|
# Build documentation in the docs/ directory with Sphinx
|
||||||
|
sphinx:
|
||||||
|
configuration: docs/conf.py
|
||||||
|
|
||||||
|
formats:
|
||||||
|
- epub
|
||||||
|
- pdf
|
||||||
|
- htmlzip
|
||||||
|
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- requirements: requirements.docs.txt
|
||||||
|
- requirements: requirements.txt
|
||||||
+28
-13
@@ -1,6 +1,6 @@
|
|||||||
## This Dockerfile is meant to aid in the building and debugging patroni whilst developing on your local machine
|
## This Dockerfile is meant to aid in the building and debugging patroni whilst developing on your local machine
|
||||||
## It has all the necessary components to play/debug with a single node appliance, running etcd
|
## It has all the necessary components to play/debug with a single node appliance, running etcd
|
||||||
ARG PG_MAJOR=15
|
ARG PG_MAJOR=16
|
||||||
ARG COMPRESS=false
|
ARG COMPRESS=false
|
||||||
ARG PGHOME=/home/postgres
|
ARG PGHOME=/home/postgres
|
||||||
ARG PGDATA=$PGHOME/data
|
ARG PGDATA=$PGHOME/data
|
||||||
@@ -25,8 +25,7 @@ RUN set -ex \
|
|||||||
| grep -Ev '^python3-(sphinx|etcd|consul|kazoo|kubernetes)' \
|
| grep -Ev '^python3-(sphinx|etcd|consul|kazoo|kubernetes)' \
|
||||||
| xargs apt-get install -y vim curl less jq locales haproxy sudo \
|
| xargs apt-get install -y vim curl less jq locales haproxy sudo \
|
||||||
python3-etcd python3-kazoo python3-pip busybox \
|
python3-etcd python3-kazoo python3-pip busybox \
|
||||||
net-tools iputils-ping --fix-missing \
|
net-tools iputils-ping dumb-init --fix-missing \
|
||||||
&& pip3 install dumb-init \
|
|
||||||
\
|
\
|
||||||
# Cleanup all locales but en_US.UTF-8
|
# Cleanup all locales but en_US.UTF-8
|
||||||
&& find /usr/share/i18n/charmaps/ -type f ! -name UTF-8.gz -delete \
|
&& find /usr/share/i18n/charmaps/ -type f ! -name UTF-8.gz -delete \
|
||||||
@@ -53,14 +52,26 @@ RUN set -ex \
|
|||||||
&& curl -sL "https://github.com/coreos/etcd/releases/download/v$ETCDVERSION/etcd-v$ETCDVERSION-linux-$(dpkg --print-architecture).tar.gz" \
|
&& curl -sL "https://github.com/coreos/etcd/releases/download/v$ETCDVERSION/etcd-v$ETCDVERSION-linux-$(dpkg --print-architecture).tar.gz" \
|
||||||
| tar xz -C /usr/local/bin --strip=1 --wildcards --no-anchored etcd etcdctl \
|
| tar xz -C /usr/local/bin --strip=1 --wildcards --no-anchored etcd etcdctl \
|
||||||
\
|
\
|
||||||
# Download confd
|
&& if [ $(dpkg --print-architecture) = 'arm64' ]; then \
|
||||||
&& curl -sL "https://github.com/kelseyhightower/confd/releases/download/v$CONFDVERSION/confd-$CONFDVERSION-linux-$(dpkg --print-architecture)" \
|
# Build confd
|
||||||
> /usr/local/bin/confd && chmod +x /usr/local/bin/confd \
|
apt-get install -y git make \
|
||||||
|
&& curl -sL https://go.dev/dl/go1.20.4.linux-arm64.tar.gz | tar xz -C /usr/local go \
|
||||||
|
&& export GOROOT=/usr/local/go && export PATH=$PATH:$GOROOT/bin \
|
||||||
|
&& git clone --recurse-submodules https://github.com/kelseyhightower/confd.git \
|
||||||
|
&& make -C confd \
|
||||||
|
&& cp confd/bin/confd /usr/local/bin/confd \
|
||||||
|
&& rm -rf /confd /usr/local/go; \
|
||||||
|
else \
|
||||||
|
# Download confd
|
||||||
|
curl -sL "https://github.com/kelseyhightower/confd/releases/download/v$CONFDVERSION/confd-$CONFDVERSION-linux-$(dpkg --print-architecture)" \
|
||||||
|
> /usr/local/bin/confd && chmod +x /usr/local/bin/confd; \
|
||||||
|
fi \
|
||||||
\
|
\
|
||||||
# Clean up all useless packages and some files
|
# Clean up all useless packages and some files
|
||||||
&& apt-get purge -y --allow-remove-essential python3-pip gzip bzip2 util-linux e2fsprogs \
|
&& apt-get purge -y --allow-remove-essential python3-pip gzip bzip2 util-linux e2fsprogs \
|
||||||
libmagic1 bsdmainutils login ncurses-bin libmagic-mgc e2fslibs bsdutils \
|
libmagic1 bsdmainutils login ncurses-bin libmagic-mgc e2fslibs bsdutils \
|
||||||
exim4-config gnupg-agent dirmngr libpython2.7-stdlib libpython2.7-minimal \
|
exim4-config gnupg-agent dirmngr \
|
||||||
|
git make \
|
||||||
&& apt-get autoremove -y \
|
&& apt-get autoremove -y \
|
||||||
&& apt-get clean -y \
|
&& apt-get clean -y \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
@@ -83,9 +94,9 @@ RUN set -ex \
|
|||||||
/usr/share/locale/??_?? \
|
/usr/share/locale/??_?? \
|
||||||
/usr/share/postgresql/*/man \
|
/usr/share/postgresql/*/man \
|
||||||
/usr/share/postgresql-common/pg_wrapper \
|
/usr/share/postgresql-common/pg_wrapper \
|
||||||
/usr/share/vim/vim80/doc \
|
/usr/share/vim/vim*/doc \
|
||||||
/usr/share/vim/vim80/lang \
|
/usr/share/vim/vim*/lang \
|
||||||
/usr/share/vim/vim80/tutor \
|
/usr/share/vim/vim*/tutor \
|
||||||
# /var/lib/dpkg/info/* \
|
# /var/lib/dpkg/info/* \
|
||||||
&& find /usr/bin -xtype l -delete \
|
&& find /usr/bin -xtype l -delete \
|
||||||
&& find /var/log -type f -exec truncate --size 0 {} \; \
|
&& find /var/log -type f -exec truncate --size 0 {} \; \
|
||||||
@@ -114,6 +125,8 @@ RUN if [ "$COMPRESS" = "true" ]; then \
|
|||||||
&& /bin/busybox sh -c "(find $save_dirs -not -type d && cat /exclude /exclude && echo exclude) | sort | uniq -u | xargs /bin/busybox rm" \
|
&& /bin/busybox sh -c "(find $save_dirs -not -type d && cat /exclude /exclude && echo exclude) | sort | uniq -u | xargs /bin/busybox rm" \
|
||||||
&& /bin/busybox --install -s \
|
&& /bin/busybox --install -s \
|
||||||
&& /bin/busybox sh -c "find $save_dirs -type d -depth -exec rmdir -p {} \; 2> /dev/null"; \
|
&& /bin/busybox sh -c "find $save_dirs -type d -depth -exec rmdir -p {} \; 2> /dev/null"; \
|
||||||
|
else \
|
||||||
|
/bin/busybox --install -s; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
@@ -132,6 +145,7 @@ ARG PGBIN=/usr/lib/postgresql/$PG_MAJOR/bin
|
|||||||
|
|
||||||
ENV LC_ALL=$LC_ALL LANG=$LANG EDITOR=/usr/bin/editor
|
ENV LC_ALL=$LC_ALL LANG=$LANG EDITOR=/usr/bin/editor
|
||||||
ENV PGDATA=$PGDATA PATH=$PATH:$PGBIN
|
ENV PGDATA=$PGDATA PATH=$PATH:$PGBIN
|
||||||
|
ENV ETCDCTL_API=3
|
||||||
|
|
||||||
COPY patroni /patroni/
|
COPY patroni /patroni/
|
||||||
COPY extras/confd/conf.d/haproxy.toml /etc/confd/conf.d/
|
COPY extras/confd/conf.d/haproxy.toml /etc/confd/conf.d/
|
||||||
@@ -143,14 +157,15 @@ WORKDIR $PGHOME
|
|||||||
|
|
||||||
RUN sed -i 's/env python/&3/' /patroni*.py \
|
RUN sed -i 's/env python/&3/' /patroni*.py \
|
||||||
# "fix" patroni configs
|
# "fix" patroni configs
|
||||||
&& sed -i 's/^\( connect_address:\| - host\)/#&/' postgres?.yml \
|
|
||||||
&& sed -i 's/^ listen: 127.0.0.1/ listen: 0.0.0.0/' postgres?.yml \
|
&& sed -i 's/^ listen: 127.0.0.1/ listen: 0.0.0.0/' postgres?.yml \
|
||||||
&& sed -i "s|^\( data_dir: \).*|\1$PGDATA|" postgres?.yml \
|
&& sed -i "s|^\( data_dir: \).*|\1$PGDATA|" postgres?.yml \
|
||||||
&& sed -i "s|^#\( bin_dir: \).*|\1$PGBIN|" postgres?.yml \
|
&& sed -i "s|^#\( bin_dir: \).*|\1$PGBIN|" postgres?.yml \
|
||||||
&& sed -i 's/^ - encoding: UTF8/ - locale: en_US.UTF-8\n&/' postgres?.yml \
|
&& sed -i 's/^ - encoding: UTF8/ - locale: en_US.UTF-8\n&/' postgres?.yml \
|
||||||
&& sed -i 's/^\(scope\|name\|etcd\| host\| authentication\| pg_hba\| parameters\):/#&/' postgres?.yml \
|
&& sed -i 's/^\(scope\|name\|etcd\| host\| authentication\| connect_address\| parameters\):/#&/' postgres?.yml \
|
||||||
&& sed -i 's/^ \(replication\|superuser\|rewind\|unix_socket_directories\|\(\( \)\{0,1\}\(username\|password\)\)\):/#&/' postgres?.yml \
|
&& sed -i 's/^ \(replication\|superuser\|rewind\|unix_socket_directories\|\(\( \)\{0,1\}\(username\|password\)\)\):/#&/' postgres?.yml \
|
||||||
&& sed -i 's/^ parameters:/ pg_hba:\n - local all all trust\n - host replication all all md5\n - host all all all md5\n&\n max_connections: 100/' postgres?.yml \
|
&& sed -i 's/^ parameters:/&\n max_connections: 100/' postgres?.yml \
|
||||||
|
&& sed -i 's/^ pg_hba:/&\n - local all all trust/' postgres?.yml \
|
||||||
|
&& sed -i 's/^\(.*\) \(.*\) md5/\1 all md5/' postgres?.yml \
|
||||||
&& if [ "$COMPRESS" = "true" ]; then chmod u+s /usr/bin/sudo; fi \
|
&& if [ "$COMPRESS" = "true" ]; then chmod u+s /usr/bin/sudo; fi \
|
||||||
&& chmod +s /bin/ping \
|
&& chmod +s /bin/ping \
|
||||||
&& chown -R postgres:postgres "$PGHOME" /run /etc/haproxy
|
&& chown -R postgres:postgres "$PGHOME" /run /etc/haproxy
|
||||||
|
|||||||
+46
-18
@@ -1,13 +1,13 @@
|
|||||||
## This Dockerfile is meant to aid in the building and debugging patroni whilst developing on your local machine
|
## This Dockerfile is meant to aid in the building and debugging patroni whilst developing on your local machine
|
||||||
## It has all the necessary components to play/debug with a single node appliance, running etcd
|
## It has all the necessary components to play/debug with a single node appliance, running etcd
|
||||||
ARG PG_MAJOR=15
|
ARG PG_MAJOR=16
|
||||||
ARG COMPRESS=false
|
ARG COMPRESS=false
|
||||||
ARG PGHOME=/home/postgres
|
ARG PGHOME=/home/postgres
|
||||||
ARG PGDATA=$PGHOME/data
|
ARG PGDATA=$PGHOME/data
|
||||||
ARG LC_ALL=C.UTF-8
|
ARG LC_ALL=C.UTF-8
|
||||||
ARG LANG=C.UTF-8
|
ARG LANG=C.UTF-8
|
||||||
|
|
||||||
FROM postgres:$PG_MAJOR as builder
|
FROM postgres:$PG_MAJOR AS builder
|
||||||
|
|
||||||
ARG PGHOME
|
ARG PGHOME
|
||||||
ARG PGDATA
|
ARG PGDATA
|
||||||
@@ -20,15 +20,28 @@ RUN set -ex \
|
|||||||
&& export DEBIAN_FRONTEND=noninteractive \
|
&& export DEBIAN_FRONTEND=noninteractive \
|
||||||
&& echo 'APT::Install-Recommends "0";\nAPT::Install-Suggests "0";' > /etc/apt/apt.conf.d/01norecommend \
|
&& echo 'APT::Install-Recommends "0";\nAPT::Install-Suggests "0";' > /etc/apt/apt.conf.d/01norecommend \
|
||||||
&& apt-get update -y \
|
&& apt-get update -y \
|
||||||
# postgres:10 is based on debian, which has the patroni package. We will install all required dependencies
|
# postgres:PG_MAJOR is based on debian, which has the patroni package. We will install all required dependencies
|
||||||
&& apt-cache depends patroni | sed -n -e 's/.*Depends: \(python3-.\+\)$/\1/p' \
|
&& apt-cache depends patroni | sed -n -e 's/.*Depends: \(python3-.\+\)$/\1/p' \
|
||||||
| grep -Ev '^python3-(sphinx|etcd|consul|kazoo|kubernetes)' \
|
| grep -Ev '^python3-(sphinx|etcd|consul|kazoo|kubernetes)' \
|
||||||
| xargs apt-get install -y vim curl less jq locales haproxy sudo \
|
| xargs apt-get install -y vim curl less jq locales haproxy sudo \
|
||||||
python3-etcd python3-kazoo python3-pip busybox \
|
python3-etcd python3-kazoo python3-pip busybox \
|
||||||
net-tools iputils-ping --fix-missing \
|
net-tools iputils-ping lsb-release dumb-init --fix-missing \
|
||||||
&& curl https://install.citusdata.com/community/deb.sh | bash \
|
&& if [ $(dpkg --print-architecture) = 'arm64' ]; then \
|
||||||
&& apt-get -y install postgresql-$PG_MAJOR-citus-11.2 \
|
apt-get install -y postgresql-server-dev-$PG_MAJOR \
|
||||||
&& pip3 install dumb-init \
|
git gcc make autoconf \
|
||||||
|
libc6-dev flex libcurl4-gnutls-dev \
|
||||||
|
libicu-dev libkrb5-dev liblz4-dev \
|
||||||
|
libpam0g-dev libreadline-dev libselinux1-dev\
|
||||||
|
libssl-dev libxslt1-dev libzstd-dev uuid-dev \
|
||||||
|
&& git clone -b "main" https://github.com/citusdata/citus.git \
|
||||||
|
&& MAKEFLAGS="-j $(grep -c ^processor /proc/cpuinfo)" \
|
||||||
|
&& cd citus && ./configure && make install && cd ../ && rm -rf /citus; \
|
||||||
|
else \
|
||||||
|
echo "deb [signed-by=/etc/apt/trusted.gpg.d/citusdata_community.gpg] https://packagecloud.io/citusdata/community/debian/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/citusdata_community.list \
|
||||||
|
&& curl -sL https://packagecloud.io/citusdata/community/gpgkey | gpg --dearmor > /etc/apt/trusted.gpg.d/citusdata_community.gpg \
|
||||||
|
&& apt-get update -y \
|
||||||
|
&& apt-get -y install postgresql-$PG_MAJOR-citus-12.1; \
|
||||||
|
fi \
|
||||||
\
|
\
|
||||||
# Cleanup all locales but en_US.UTF-8
|
# Cleanup all locales but en_US.UTF-8
|
||||||
&& find /usr/share/i18n/charmaps/ -type f ! -name UTF-8.gz -delete \
|
&& find /usr/share/i18n/charmaps/ -type f ! -name UTF-8.gz -delete \
|
||||||
@@ -55,16 +68,29 @@ RUN set -ex \
|
|||||||
&& curl -sL https://github.com/coreos/etcd/releases/download/v${ETCDVERSION}/etcd-v${ETCDVERSION}-linux-$(dpkg --print-architecture).tar.gz \
|
&& curl -sL https://github.com/coreos/etcd/releases/download/v${ETCDVERSION}/etcd-v${ETCDVERSION}-linux-$(dpkg --print-architecture).tar.gz \
|
||||||
| tar xz -C /usr/local/bin --strip=1 --wildcards --no-anchored etcd etcdctl \
|
| tar xz -C /usr/local/bin --strip=1 --wildcards --no-anchored etcd etcdctl \
|
||||||
\
|
\
|
||||||
# Download confd
|
&& if [ $(dpkg --print-architecture) = 'arm64' ]; then \
|
||||||
&& curl -sL https://github.com/kelseyhightower/confd/releases/download/v${CONFDVERSION}/confd-${CONFDVERSION}-linux-$(dpkg --print-architecture) \
|
# Build confd
|
||||||
> /usr/local/bin/confd && chmod +x /usr/local/bin/confd \
|
curl -sL https://go.dev/dl/go1.20.4.linux-arm64.tar.gz | tar xz -C /usr/local go \
|
||||||
|
&& export GOROOT=/usr/local/go && export PATH=$PATH:$GOROOT/bin \
|
||||||
|
&& git clone --recurse-submodules https://github.com/kelseyhightower/confd.git \
|
||||||
|
&& make -C confd \
|
||||||
|
&& cp confd/bin/confd /usr/local/bin/confd \
|
||||||
|
&& rm -rf /confd /usr/local/go; \
|
||||||
|
else \
|
||||||
|
# Download confd
|
||||||
|
curl -sL "https://github.com/kelseyhightower/confd/releases/download/v$CONFDVERSION/confd-$CONFDVERSION-linux-$(dpkg --print-architecture)" \
|
||||||
|
> /usr/local/bin/confd && chmod +x /usr/local/bin/confd; \
|
||||||
|
fi \
|
||||||
# Prepare client cert for HAProxy
|
# Prepare client cert for HAProxy
|
||||||
&& cat /etc/ssl/private/ssl-cert-snakeoil.key /etc/ssl/certs/ssl-cert-snakeoil.pem > /etc/ssl/private/ssl-cert-snakeoil.crt \
|
&& cat /etc/ssl/private/ssl-cert-snakeoil.key /etc/ssl/certs/ssl-cert-snakeoil.pem > /etc/ssl/private/ssl-cert-snakeoil.crt \
|
||||||
\
|
\
|
||||||
# Clean up all useless packages and some files
|
# Clean up all useless packages and some files
|
||||||
&& apt-get purge -y --allow-remove-essential python3-pip gzip bzip2 util-linux e2fsprogs \
|
&& apt-get purge -y --allow-remove-essential python3-pip gzip bzip2 util-linux e2fsprogs \
|
||||||
libmagic1 bsdmainutils login ncurses-bin libmagic-mgc e2fslibs bsdutils \
|
libmagic1 bsdmainutils login ncurses-bin libmagic-mgc e2fslibs bsdutils \
|
||||||
exim4-config gnupg-agent dirmngr libpython2.7-stdlib libpython2.7-minimal \
|
exim4-config gnupg-agent dirmngr \
|
||||||
|
postgresql-server-dev-$PG_MAJOR git gcc make autoconf \
|
||||||
|
libc6-dev flex libicu-dev libkrb5-dev liblz4-dev \
|
||||||
|
libpam0g-dev libreadline-dev libselinux1-dev libssl-dev libxslt1-dev libzstd-dev uuid-dev \
|
||||||
&& apt-get autoremove -y \
|
&& apt-get autoremove -y \
|
||||||
&& apt-get clean -y \
|
&& apt-get clean -y \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
@@ -87,9 +113,9 @@ RUN set -ex \
|
|||||||
/usr/share/locale/??_?? \
|
/usr/share/locale/??_?? \
|
||||||
/usr/share/postgresql/*/man \
|
/usr/share/postgresql/*/man \
|
||||||
/usr/share/postgresql-common/pg_wrapper \
|
/usr/share/postgresql-common/pg_wrapper \
|
||||||
/usr/share/vim/vim80/doc \
|
/usr/share/vim/vim*/doc \
|
||||||
/usr/share/vim/vim80/lang \
|
/usr/share/vim/vim*/lang \
|
||||||
/usr/share/vim/vim80/tutor \
|
/usr/share/vim/vim*/tutor \
|
||||||
# /var/lib/dpkg/info/* \
|
# /var/lib/dpkg/info/* \
|
||||||
&& find /usr/bin -xtype l -delete \
|
&& find /usr/bin -xtype l -delete \
|
||||||
&& find /var/log -type f -exec truncate --size 0 {} \; \
|
&& find /var/log -type f -exec truncate --size 0 {} \; \
|
||||||
@@ -138,6 +164,7 @@ ARG PGBIN=/usr/lib/postgresql/$PG_MAJOR/bin
|
|||||||
|
|
||||||
ENV LC_ALL=$LC_ALL LANG=$LANG EDITOR=/usr/bin/editor
|
ENV LC_ALL=$LC_ALL LANG=$LANG EDITOR=/usr/bin/editor
|
||||||
ENV PGDATA=$PGDATA PATH=$PATH:$PGBIN
|
ENV PGDATA=$PGDATA PATH=$PATH:$PGBIN
|
||||||
|
ENV ETCDCTL_API=3
|
||||||
|
|
||||||
COPY patroni /patroni/
|
COPY patroni /patroni/
|
||||||
COPY extras/confd/conf.d/haproxy.toml /etc/confd/conf.d/
|
COPY extras/confd/conf.d/haproxy.toml /etc/confd/conf.d/
|
||||||
@@ -149,16 +176,17 @@ WORKDIR $PGHOME
|
|||||||
|
|
||||||
RUN sed -i 's/env python/&3/' /patroni*.py \
|
RUN sed -i 's/env python/&3/' /patroni*.py \
|
||||||
# "fix" patroni configs
|
# "fix" patroni configs
|
||||||
&& sed -i 's/^\( connect_address:\| - host\)/#&/' postgres?.yml \
|
|
||||||
&& sed -i 's/^ listen: 127.0.0.1/ listen: 0.0.0.0/' postgres?.yml \
|
&& sed -i 's/^ listen: 127.0.0.1/ listen: 0.0.0.0/' postgres?.yml \
|
||||||
&& sed -i "s|^\( data_dir: \).*|\1$PGDATA|" postgres?.yml \
|
&& sed -i "s|^\( data_dir: \).*|\1$PGDATA|" postgres?.yml \
|
||||||
&& sed -i "s|^#\( bin_dir: \).*|\1$PGBIN|" postgres?.yml \
|
&& sed -i "s|^#\( bin_dir: \).*|\1$PGBIN|" postgres?.yml \
|
||||||
&& sed -i 's/^ - encoding: UTF8/ - locale: en_US.UTF-8\n&/' postgres?.yml \
|
&& sed -i 's/^ - encoding: UTF8/ - locale: en_US.UTF-8\n&/' postgres?.yml \
|
||||||
&& sed -i 's/^scope:/log:\n loggers:\n patroni.postgresql.citus: DEBUG\n#&/' postgres?.yml \
|
&& sed -i 's/^scope:/log:\n loggers:\n patroni.postgresql.mpp.citus: DEBUG\n#&/' postgres?.yml \
|
||||||
&& sed -i 's/^\(name\|etcd\| host\| authentication\| pg_hba\| parameters\):/#&/' postgres?.yml \
|
&& sed -i 's/^\(name\|etcd\| host\| authentication\| connect_address\| parameters\):/#&/' postgres?.yml \
|
||||||
&& sed -i 's/^ \(replication\|superuser\|rewind\|unix_socket_directories\|\(\( \)\{0,1\}\(username\|password\)\)\):/#&/' postgres?.yml \
|
&& sed -i 's/^ \(replication\|superuser\|rewind\|unix_socket_directories\|\(\( \)\{0,1\}\(username\|password\)\)\):/#&/' postgres?.yml \
|
||||||
&& sed -i 's/^postgresql:/&\n basebackup:\n checkpoint: fast/' postgres?.yml \
|
&& sed -i 's/^postgresql:/&\n basebackup:\n checkpoint: fast/' postgres?.yml \
|
||||||
&& sed -i 's|^ parameters:| pg_hba:\n - local all all trust\n - hostssl replication all all md5 clientcert=verify-ca\n - hostssl all all all md5 clientcert=verify-ca\n&\n max_connections: 100\n shared_buffers: 16MB\n ssl: "on"\n ssl_ca_file: /etc/ssl/certs/ssl-cert-snakeoil.pem\n ssl_cert_file: /etc/ssl/certs/ssl-cert-snakeoil.pem\n ssl_key_file: /etc/ssl/private/ssl-cert-snakeoil.key\n citus.node_conninfo: "sslrootcert=/etc/ssl/certs/ssl-cert-snakeoil.pem sslkey=/etc/ssl/private/ssl-cert-snakeoil.key sslcert=/etc/ssl/certs/ssl-cert-snakeoil.pem sslmode=verify-ca"|' postgres?.yml \
|
&& sed -i 's|^ parameters:|&\n max_connections: 100\n shared_buffers: 16MB\n ssl: "on"\n ssl_ca_file: /etc/ssl/certs/ssl-cert-snakeoil.pem\n ssl_cert_file: /etc/ssl/certs/ssl-cert-snakeoil.pem\n ssl_key_file: /etc/ssl/private/ssl-cert-snakeoil.key\n citus.node_conninfo: "sslrootcert=/etc/ssl/certs/ssl-cert-snakeoil.pem sslkey=/etc/ssl/private/ssl-cert-snakeoil.key sslcert=/etc/ssl/certs/ssl-cert-snakeoil.pem sslmode=verify-ca"|' postgres?.yml \
|
||||||
|
&& sed -i 's/^ pg_hba:/&\n - local all all trust/' postgres?.yml \
|
||||||
|
&& sed -i 's/^\(.*\) \(.*\) \(.*\) \(.*\) \(.*\) md5.*$/\1 hostssl \3 \4 all md5 clientcert=verify-ca/' postgres?.yml \
|
||||||
&& sed -i 's/^#\(ctl\| certfile\| keyfile\)/\1/' postgres?.yml \
|
&& sed -i 's/^#\(ctl\| certfile\| keyfile\)/\1/' postgres?.yml \
|
||||||
&& sed -i 's|^# cafile: .*$| verify_client: required\n cafile: /etc/ssl/certs/ssl-cert-snakeoil.pem|' postgres?.yml \
|
&& sed -i 's|^# cafile: .*$| verify_client: required\n cafile: /etc/ssl/certs/ssl-cert-snakeoil.pem|' postgres?.yml \
|
||||||
&& sed -i 's|^# cacert: .*$| cacert: /etc/ssl/certs/ssl-cert-snakeoil.pem|' postgres?.yml \
|
&& sed -i 's|^# cacert: .*$| cacert: /etc/ssl/certs/ssl-cert-snakeoil.pem|' postgres?.yml \
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2015 Compose, Zalando SE
|
Copyright (c) 2025 Compose, Zalando SE, Patroni Contributors
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
+33
-41
@@ -8,15 +8,15 @@ You can find a version of this documentation that is searchable and also easier
|
|||||||
|
|
||||||
There are many ways to run high availability with PostgreSQL; for a list, see the `PostgreSQL Documentation <https://wiki.postgresql.org/wiki/Replication,_Clustering,_and_Connection_Pooling>`__.
|
There are many ways to run high availability with PostgreSQL; for a list, see the `PostgreSQL Documentation <https://wiki.postgresql.org/wiki/Replication,_Clustering,_and_Connection_Pooling>`__.
|
||||||
|
|
||||||
Patroni is a template for you to create your own customized, high-availability solution using Python and - for maximum accessibility - a distributed configuration store like `ZooKeeper <https://zookeeper.apache.org/>`__, `etcd <https://github.com/coreos/etcd>`__, `Consul <https://github.com/hashicorp/consul>`__ or `Kubernetes <https://kubernetes.io>`__. Database engineers, DBAs, DevOps engineers, and SREs who are looking to quickly deploy HA PostgreSQL in the datacenter-or anywhere else-will hopefully find it useful.
|
Patroni is a template for high availability (HA) PostgreSQL solutions using Python. For maximum accessibility, Patroni supports a variety of distributed configuration stores like `ZooKeeper <https://zookeeper.apache.org/>`__, `etcd <https://github.com/coreos/etcd>`__, `Consul <https://github.com/hashicorp/consul>`__ or `Kubernetes <https://kubernetes.io>`__. Database engineers, DBAs, DevOps engineers, and SREs who are looking to quickly deploy HA PostgreSQL in datacenters - or anywhere else - will hopefully find it useful.
|
||||||
|
|
||||||
We call Patroni a "template" because it is far from being a one-size-fits-all or plug-and-play replication system. It will have its own caveats. Use wisely.
|
We call Patroni a "template" because it is far from being a one-size-fits-all or plug-and-play replication system. It will have its own caveats. Use wisely.
|
||||||
|
|
||||||
Currently supported PostgreSQL versions: 9.3 to 15.
|
Currently supported PostgreSQL versions: 9.3 to 17.
|
||||||
|
|
||||||
**Note to Citus users**: Starting from 3.0 Patroni nicely integrates with the `Citus <https://github.com/citusdata/citus>`__ database extension to Postgres. Please check the `Citus support page <https://github.com/zalando/patroni/blob/master/docs/citus.rst>`__ in the Patroni documentation for more info about how to use Patroni high availability together with a Citus distributed cluster.
|
**Note to Citus users**: Starting from 3.0 Patroni nicely integrates with the `Citus <https://github.com/citusdata/citus>`__ database extension to Postgres. Please check the `Citus support page <https://github.com/patroni/patroni/blob/master/docs/citus.rst>`__ in the Patroni documentation for more info about how to use Patroni high availability together with a Citus distributed cluster.
|
||||||
|
|
||||||
**Note to Kubernetes users**: Patroni can run natively on top of Kubernetes. Take a look at the `Kubernetes <https://github.com/zalando/patroni/blob/master/docs/kubernetes.rst>`__ chapter of the Patroni documentation.
|
**Note to Kubernetes users**: Patroni can run natively on top of Kubernetes. Take a look at the `Kubernetes <https://github.com/patroni/patroni/blob/master/docs/kubernetes.rst>`__ chapter of the Patroni documentation.
|
||||||
|
|
||||||
.. contents::
|
.. contents::
|
||||||
:local:
|
:local:
|
||||||
@@ -27,29 +27,27 @@ Currently supported PostgreSQL versions: 9.3 to 15.
|
|||||||
How Patroni Works
|
How Patroni Works
|
||||||
=================
|
=================
|
||||||
|
|
||||||
Patroni originated as a fork of `Governor <https://github.com/compose/governor>`__, the project from Compose. It includes plenty of new features.
|
Patroni (formerly known as Zalando's Patroni) originated as a fork of `Governor <https://github.com/compose/governor>`__, the project from Compose. It includes plenty of new features.
|
||||||
|
|
||||||
For an example of a Docker-based deployment with Patroni, see `Spilo <https://github.com/zalando/spilo>`__, currently in use at Zalando.
|
|
||||||
|
|
||||||
For additional background info, see:
|
For additional background info, see:
|
||||||
|
|
||||||
* `Elephants on Automatic: HA Clustered PostgreSQL with Helm <https://www.youtube.com/watch?v=CftcVhFMGSY>`_, talk by Josh Berkus and Oleksii Kliukin at KubeCon Berlin 2017
|
* `Elephants on Automatic: HA Clustered PostgreSQL with Helm <https://www.youtube.com/watch?v=CftcVhFMGSY>`_, talk by Josh Berkus and Oleksii Kliukin at KubeCon Berlin 2017
|
||||||
* `PostgreSQL HA with Kubernetes and Patroni <https://www.youtube.com/watch?v=iruaCgeG7qs>`__, talk by Josh Berkus at KubeCon 2016 (video)
|
* `PostgreSQL HA with Kubernetes and Patroni <https://www.youtube.com/watch?v=iruaCgeG7qs>`__, talk by Josh Berkus at KubeCon 2016 (video)
|
||||||
* `Feb. 2016 Zalando Tech blog post <https://tech.zalando.de/blog/zalandos-patroni-a-template-for-high-availability-postgresql/>`__
|
* `Feb. 2016 Zalando Tech blog post <https://engineering.zalando.com/posts/2016/02/zalandos-patroni-a-template-for-high-availability-postgresql.html>`__
|
||||||
|
|
||||||
==================
|
==================
|
||||||
Development Status
|
Development Status
|
||||||
==================
|
==================
|
||||||
|
|
||||||
Patroni is in active development and accepts contributions. See our `Contributing <https://github.com/zalando/patroni/blob/master/docs/CONTRIBUTING.rst>`__ section below for more details.
|
Patroni is in active development and accepts contributions. See our `Contributing <https://github.com/patroni/patroni/blob/master/docs/contributing_guidelines.rst>`__ section below for more details.
|
||||||
|
|
||||||
We report new releases information `here <https://github.com/zalando/patroni/releases>`__.
|
We report new releases information `here <https://github.com/patroni/patroni/releases>`__.
|
||||||
|
|
||||||
=========
|
=========
|
||||||
Community
|
Community
|
||||||
=========
|
=========
|
||||||
|
|
||||||
There are two places to connect with the Patroni community: `on github <https://github.com/zalando/patroni>`__, via Issues and PRs, and on channel `#patroni <https://postgresteam.slack.com/archives/C9XPYG92A>`__ in the `PostgreSQL Slack <https://postgresteam.slack.com/>`__. If you're using Patroni, or just interested, please join us.
|
There are two places to connect with the Patroni community: `on github <https://github.com/patroni/patroni>`__, via Issues and PRs, and on channel `#patroni <https://postgresteam.slack.com/archives/C9XPYG92A>`__ in the `PostgreSQL Slack <https://pgtreats.info/slack-invite>`__. If you're using Patroni, or just interested, please join us.
|
||||||
|
|
||||||
===================================
|
===================================
|
||||||
Technical Requirements/Installation
|
Technical Requirements/Installation
|
||||||
@@ -74,27 +72,11 @@ There are a few options available:
|
|||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
sudo apt-get install python-psycopg2 # install python2 psycopg2 module on Debian/Ubuntu
|
sudo apt-get install python3-psycopg2 # install psycopg2 module on Debian/Ubuntu
|
||||||
sudo apt-get install python3-psycopg2 # install python3 psycopg2 module on Debian/Ubuntu
|
sudo yum install python3-psycopg2 # install psycopg2 on RedHat/Fedora/CentOS
|
||||||
sudo yum install python-psycopg2 # install python2 psycopg2 on RedHat/Fedora/CentOS
|
|
||||||
|
|
||||||
2. Install psycopg2 from the binary package
|
2. Specify one of `psycopg`, `psycopg2`, or `psycopg2-binary` in the list of dependencies when installing Patroni with pip (see below).
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
pip install psycopg2-binary
|
|
||||||
|
|
||||||
3. Install psycopg2 from source
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
pip install psycopg2>=2.5.4
|
|
||||||
|
|
||||||
4. Use psycopg 3.0 instead of psycopg2
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
pip install psycopg[binary]
|
|
||||||
|
|
||||||
**General installation for pip**
|
**General installation for pip**
|
||||||
|
|
||||||
@@ -109,7 +91,7 @@ where dependencies can be either empty, or consist of one or more of the followi
|
|||||||
etcd or etcd3
|
etcd or etcd3
|
||||||
`python-etcd` module in order to use Etcd as DCS
|
`python-etcd` module in order to use Etcd as DCS
|
||||||
consul
|
consul
|
||||||
`python-consul` module in order to use Consul as DCS
|
`py-consul` module in order to use Consul as DCS
|
||||||
zookeeper
|
zookeeper
|
||||||
`kazoo` module in order to use Zookeeper as DCS
|
`kazoo` module in order to use Zookeeper as DCS
|
||||||
exhibitor
|
exhibitor
|
||||||
@@ -119,13 +101,23 @@ kubernetes
|
|||||||
raft
|
raft
|
||||||
`pysyncobj` module in order to use python Raft implementation as DCS
|
`pysyncobj` module in order to use python Raft implementation as DCS
|
||||||
aws
|
aws
|
||||||
`boto` in order to use AWS callbacks
|
`boto3` in order to use AWS callbacks
|
||||||
|
systemd
|
||||||
|
`systemd-python` in order to use sd_notify integration
|
||||||
|
all
|
||||||
|
all of the above (except psycopg family)
|
||||||
|
psycopg3
|
||||||
|
`psycopg[binary]>=3.0.0` module
|
||||||
|
psycopg2
|
||||||
|
`psycopg2>=2.5.4` module
|
||||||
|
psycopg2-binary
|
||||||
|
`psycopg2-binary` module
|
||||||
|
|
||||||
For example, the command in order to install Patroni together with dependencies for Etcd as a DCS and AWS callbacks is:
|
For example, the command in order to install Patroni together with psycopg3, dependencies for Etcd as a DCS, and AWS callbacks is:
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
pip install patroni[etcd,aws]
|
pip install patroni[psycopg3,etcd3,aws]
|
||||||
|
|
||||||
Note that external tools to call in the replica creation or custom bootstrap scripts (i.e. WAL-E) should be installed independently of Patroni.
|
Note that external tools to call in the replica creation or custom bootstrap scripts (i.e. WAL-E) should be installed independently of Patroni.
|
||||||
|
|
||||||
@@ -159,19 +151,19 @@ run:
|
|||||||
YAML Configuration
|
YAML Configuration
|
||||||
==================
|
==================
|
||||||
|
|
||||||
Go `here <https://github.com/zalando/patroni/blob/master/docs/SETTINGS.rst>`__ for comprehensive information about settings for etcd, consul, and ZooKeeper. And for an example, see `postgres0.yml <https://github.com/zalando/patroni/blob/master/postgres0.yml>`__.
|
Go `here <https://github.com/patroni/patroni/blob/master/docs/dynamic_configuration.rst>`__ for comprehensive information about settings for etcd, consul, and ZooKeeper. And for an example, see `postgres0.yml <https://github.com/patroni/patroni/blob/master/postgres0.yml>`__.
|
||||||
|
|
||||||
=========================
|
=========================
|
||||||
Environment Configuration
|
Environment Configuration
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
Go `here <https://github.com/zalando/patroni/blob/master/docs/ENVIRONMENT.rst>`__ for comprehensive information about configuring(overriding) settings via environment variables.
|
Go `here <https://github.com/patroni/patroni/blob/master/docs/ENVIRONMENT.rst>`__ for comprehensive information about configuring(overriding) settings via environment variables.
|
||||||
|
|
||||||
===================
|
===================
|
||||||
Replication Choices
|
Replication Choices
|
||||||
===================
|
===================
|
||||||
|
|
||||||
Patroni uses Postgres' streaming replication, which is asynchronous by default. Patroni's asynchronous replication configuration allows for ``maximum_lag_on_failover`` settings. This setting ensures failover will not occur if a follower is more than a certain number of bytes behind the leader. This setting should be increased or decreased based on business requirements. It's also possible to use synchronous replication for better durability guarantees. See `replication modes documentation <https://github.com/zalando/patroni/blob/master/docs/replication_modes.rst>`__ for details.
|
Patroni uses Postgres' streaming replication, which is asynchronous by default. Patroni's asynchronous replication configuration allows for ``maximum_lag_on_failover`` settings. This setting ensures failover will not occur if a follower is more than a certain number of bytes behind the leader. This setting should be increased or decreased based on business requirements. It's also possible to use synchronous replication for better durability guarantees. See `replication modes documentation <https://github.com/patroni/patroni/blob/master/docs/replication_modes.rst>`__ for details.
|
||||||
|
|
||||||
======================================
|
======================================
|
||||||
Applications Should Not Use Superusers
|
Applications Should Not Use Superusers
|
||||||
@@ -179,7 +171,7 @@ Applications Should Not Use Superusers
|
|||||||
|
|
||||||
When connecting from an application, always use a non-superuser. Patroni requires access to the database to function properly. By using a superuser from an application, you can potentially use the entire connection pool, including the connections reserved for superusers, with the ``superuser_reserved_connections`` setting. If Patroni cannot access the Primary because the connection pool is full, behavior will be undesirable.
|
When connecting from an application, always use a non-superuser. Patroni requires access to the database to function properly. By using a superuser from an application, you can potentially use the entire connection pool, including the connections reserved for superusers, with the ``superuser_reserved_connections`` setting. If Patroni cannot access the Primary because the connection pool is full, behavior will be undesirable.
|
||||||
|
|
||||||
.. |Tests Status| image:: https://github.com/zalando/patroni/actions/workflows/tests.yaml/badge.svg
|
.. |Tests Status| image:: https://github.com/patroni/patroni/actions/workflows/tests.yaml/badge.svg
|
||||||
:target: https://github.com/zalando/patroni/actions/workflows/tests.yaml?query=branch%3Amaster
|
:target: https://github.com/patroni/patroni/actions/workflows/tests.yaml?query=branch%3Amaster
|
||||||
.. |Coverage Status| image:: https://coveralls.io/repos/zalando/patroni/badge.svg?branch=master
|
.. |Coverage Status| image:: https://coveralls.io/repos/patroni/patroni/badge.svg?branch=master
|
||||||
:target: https://coveralls.io/github/zalando/patroni?branch=master
|
:target: https://coveralls.io/github/patroni/patroni?branch=master
|
||||||
|
|||||||
+37
-15
@@ -9,40 +9,49 @@
|
|||||||
# $ docker-compose -f docker-compose-citus.yml up -d
|
# $ docker-compose -f docker-compose-citus.yml up -d
|
||||||
# You can read more about it in the:
|
# You can read more about it in the:
|
||||||
# https://github.com/zalando/patroni/blob/master/docker/README.md#citus-cluster
|
# https://github.com/zalando/patroni/blob/master/docker/README.md#citus-cluster
|
||||||
version: "2"
|
version: "3"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
demo:
|
demo:
|
||||||
|
|
||||||
services:
|
services:
|
||||||
etcd1: &etcd
|
etcd1: &etcd
|
||||||
image: patroni-citus
|
image: harbor.optimcloud.com/library/optim/patroni-citus:latest
|
||||||
networks: [ demo ]
|
networks: [ demo ]
|
||||||
|
ports:
|
||||||
|
- 2379
|
||||||
|
- 2380
|
||||||
environment:
|
environment:
|
||||||
ETCDCTL_API: 3
|
|
||||||
ETCD_LISTEN_PEER_URLS: http://0.0.0.0:2380
|
ETCD_LISTEN_PEER_URLS: http://0.0.0.0:2380
|
||||||
ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379
|
ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379
|
||||||
ETCD_INITIAL_CLUSTER: etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
|
ETCD_INITIAL_CLUSTER: etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
|
||||||
ETCD_INITIAL_CLUSTER_STATE: new
|
ETCD_INITIAL_CLUSTER_STATE: new
|
||||||
ETCD_INITIAL_CLUSTER_TOKEN: tutorial
|
ETCD_INITIAL_CLUSTER_TOKEN: tutorial
|
||||||
|
ETCD_UNSUPPORTED_ARCH: arm64
|
||||||
container_name: demo-etcd1
|
container_name: demo-etcd1
|
||||||
hostname: etcd1
|
hostname: etcd1
|
||||||
command: etcd -name etcd1 -initial-advertise-peer-urls http://etcd1:2380
|
command: etcd --name etcd1 --initial-advertise-peer-urls http://etcd1:2380
|
||||||
|
|
||||||
etcd2:
|
etcd2:
|
||||||
<<: *etcd
|
<<: *etcd
|
||||||
container_name: demo-etcd2
|
container_name: demo-etcd2
|
||||||
|
ports:
|
||||||
|
- 2379
|
||||||
|
- 2380
|
||||||
hostname: etcd2
|
hostname: etcd2
|
||||||
command: etcd -name etcd2 -initial-advertise-peer-urls http://etcd2:2380
|
command: etcd --name etcd2 --initial-advertise-peer-urls http://etcd2:2380
|
||||||
|
|
||||||
etcd3:
|
etcd3:
|
||||||
<<: *etcd
|
<<: *etcd
|
||||||
container_name: demo-etcd3
|
container_name: demo-etcd3
|
||||||
|
ports:
|
||||||
|
- 2379
|
||||||
|
- 2380
|
||||||
hostname: etcd3
|
hostname: etcd3
|
||||||
command: etcd -name etcd3 -initial-advertise-peer-urls http://etcd3:2380
|
command: etcd --name etcd3 --initial-advertise-peer-urls http://etcd3:2380
|
||||||
|
|
||||||
haproxy:
|
haproxy:
|
||||||
image: patroni-citus
|
image: harbor.optimcloud.com/library/optim/patroni-citus:latest
|
||||||
networks: [ demo ]
|
networks: [ demo ]
|
||||||
env_file: docker/patroni.env
|
env_file: docker/patroni.env
|
||||||
hostname: haproxy
|
hostname: haproxy
|
||||||
@@ -52,7 +61,6 @@ services:
|
|||||||
- "5001:5001" # Load-balancing across workers primaries
|
- "5001:5001" # Load-balancing across workers primaries
|
||||||
command: haproxy
|
command: haproxy
|
||||||
environment: &haproxy_env
|
environment: &haproxy_env
|
||||||
ETCDCTL_API: 3
|
|
||||||
ETCDCTL_ENDPOINTS: http://etcd1:2379,http://etcd2:2379,http://etcd3:2379
|
ETCDCTL_ENDPOINTS: http://etcd1:2379,http://etcd2:2379,http://etcd3:2379
|
||||||
PATRONI_ETCD3_HOSTS: "'etcd1:2379','etcd2:2379','etcd3:2379'"
|
PATRONI_ETCD3_HOSTS: "'etcd1:2379','etcd2:2379','etcd3:2379'"
|
||||||
PATRONI_SCOPE: demo
|
PATRONI_SCOPE: demo
|
||||||
@@ -64,8 +72,10 @@ services:
|
|||||||
PGSSLROOTCERT: /etc/ssl/certs/ssl-cert-snakeoil.pem
|
PGSSLROOTCERT: /etc/ssl/certs/ssl-cert-snakeoil.pem
|
||||||
|
|
||||||
coord1:
|
coord1:
|
||||||
image: patroni-citus
|
image: harbor.optimcloud.com/library/optim/patroni-citus:latest
|
||||||
networks: [ demo ]
|
networks: [ demo ]
|
||||||
|
ports:
|
||||||
|
- "5432"
|
||||||
env_file: docker/patroni.env
|
env_file: docker/patroni.env
|
||||||
hostname: coord1
|
hostname: coord1
|
||||||
container_name: demo-coord1
|
container_name: demo-coord1
|
||||||
@@ -75,8 +85,10 @@ services:
|
|||||||
PATRONI_CITUS_GROUP: 0
|
PATRONI_CITUS_GROUP: 0
|
||||||
|
|
||||||
coord2:
|
coord2:
|
||||||
image: patroni-citus
|
image: harbor.optimcloud.com/library/optim/patroni-citus:latest
|
||||||
networks: [ demo ]
|
networks: [ demo ]
|
||||||
|
ports:
|
||||||
|
- "5432"
|
||||||
env_file: docker/patroni.env
|
env_file: docker/patroni.env
|
||||||
hostname: coord2
|
hostname: coord2
|
||||||
container_name: demo-coord2
|
container_name: demo-coord2
|
||||||
@@ -85,8 +97,10 @@ services:
|
|||||||
PATRONI_NAME: coord2
|
PATRONI_NAME: coord2
|
||||||
|
|
||||||
coord3:
|
coord3:
|
||||||
image: patroni-citus
|
image: harbor.optimcloud.com/library/optim/patroni-citus:latest
|
||||||
networks: [ demo ]
|
networks: [ demo ]
|
||||||
|
ports:
|
||||||
|
- "5432"
|
||||||
env_file: docker/patroni.env
|
env_file: docker/patroni.env
|
||||||
hostname: coord3
|
hostname: coord3
|
||||||
container_name: demo-coord3
|
container_name: demo-coord3
|
||||||
@@ -96,8 +110,10 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
work1-1:
|
work1-1:
|
||||||
image: patroni-citus
|
image: harbor.optimcloud.com/library/optim/patroni-citus:latest
|
||||||
networks: [ demo ]
|
networks: [ demo ]
|
||||||
|
ports:
|
||||||
|
- "5432"
|
||||||
env_file: docker/patroni.env
|
env_file: docker/patroni.env
|
||||||
hostname: work1-1
|
hostname: work1-1
|
||||||
container_name: demo-work1-1
|
container_name: demo-work1-1
|
||||||
@@ -107,8 +123,10 @@ services:
|
|||||||
PATRONI_CITUS_GROUP: 1
|
PATRONI_CITUS_GROUP: 1
|
||||||
|
|
||||||
work1-2:
|
work1-2:
|
||||||
image: patroni-citus
|
image: harbor.optimcloud.com/library/optim/patroni-citus:latest
|
||||||
networks: [ demo ]
|
networks: [ demo ]
|
||||||
|
ports:
|
||||||
|
- "5432"
|
||||||
env_file: docker/patroni.env
|
env_file: docker/patroni.env
|
||||||
hostname: work1-2
|
hostname: work1-2
|
||||||
container_name: demo-work1-2
|
container_name: demo-work1-2
|
||||||
@@ -118,8 +136,10 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
work2-1:
|
work2-1:
|
||||||
image: patroni-citus
|
image: harbor.optimcloud.com/library/optim/patroni-citus:latest
|
||||||
networks: [ demo ]
|
networks: [ demo ]
|
||||||
|
ports:
|
||||||
|
- "5432"
|
||||||
env_file: docker/patroni.env
|
env_file: docker/patroni.env
|
||||||
hostname: work2-1
|
hostname: work2-1
|
||||||
container_name: demo-work2-1
|
container_name: demo-work2-1
|
||||||
@@ -129,8 +149,10 @@ services:
|
|||||||
PATRONI_CITUS_GROUP: 2
|
PATRONI_CITUS_GROUP: 2
|
||||||
|
|
||||||
work2-2:
|
work2-2:
|
||||||
image: patroni-citus
|
image: harbor.optimcloud.com/library/optim/patroni-citus:latest
|
||||||
networks: [ demo ]
|
networks: [ demo ]
|
||||||
|
ports:
|
||||||
|
- "5432"
|
||||||
env_file: docker/patroni.env
|
env_file: docker/patroni.env
|
||||||
hostname: work2-2
|
hostname: work2-2
|
||||||
container_name: demo-work2-2
|
container_name: demo-work2-2
|
||||||
|
|||||||
+10
-9
@@ -6,7 +6,7 @@
|
|||||||
# The cluster could be started as:
|
# The cluster could be started as:
|
||||||
# $ docker-compose up -d
|
# $ docker-compose up -d
|
||||||
# You can read more about it in the:
|
# You can read more about it in the:
|
||||||
# https://github.com/zalando/patroni/blob/master/docker/README.md
|
# https://github.com/patroni/patroni/blob/master/docker/README.md
|
||||||
version: "2"
|
version: "2"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
@@ -14,7 +14,7 @@ networks:
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
etcd1: &etcd
|
etcd1: &etcd
|
||||||
image: patroni
|
image: ${PATRONI_TEST_IMAGE:-patroni}
|
||||||
networks: [ demo ]
|
networks: [ demo ]
|
||||||
environment:
|
environment:
|
||||||
ETCD_LISTEN_PEER_URLS: http://0.0.0.0:2380
|
ETCD_LISTEN_PEER_URLS: http://0.0.0.0:2380
|
||||||
@@ -22,24 +22,25 @@ services:
|
|||||||
ETCD_INITIAL_CLUSTER: etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
|
ETCD_INITIAL_CLUSTER: etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
|
||||||
ETCD_INITIAL_CLUSTER_STATE: new
|
ETCD_INITIAL_CLUSTER_STATE: new
|
||||||
ETCD_INITIAL_CLUSTER_TOKEN: tutorial
|
ETCD_INITIAL_CLUSTER_TOKEN: tutorial
|
||||||
|
ETCD_UNSUPPORTED_ARCH: arm64
|
||||||
container_name: demo-etcd1
|
container_name: demo-etcd1
|
||||||
hostname: etcd1
|
hostname: etcd1
|
||||||
command: etcd -name etcd1 -initial-advertise-peer-urls http://etcd1:2380
|
command: etcd --name etcd1 --initial-advertise-peer-urls http://etcd1:2380
|
||||||
|
|
||||||
etcd2:
|
etcd2:
|
||||||
<<: *etcd
|
<<: *etcd
|
||||||
container_name: demo-etcd2
|
container_name: demo-etcd2
|
||||||
hostname: etcd2
|
hostname: etcd2
|
||||||
command: etcd -name etcd2 -initial-advertise-peer-urls http://etcd2:2380
|
command: etcd --name etcd2 --initial-advertise-peer-urls http://etcd2:2380
|
||||||
|
|
||||||
etcd3:
|
etcd3:
|
||||||
<<: *etcd
|
<<: *etcd
|
||||||
container_name: demo-etcd3
|
container_name: demo-etcd3
|
||||||
hostname: etcd3
|
hostname: etcd3
|
||||||
command: etcd -name etcd3 -initial-advertise-peer-urls http://etcd3:2380
|
command: etcd --name etcd3 --initial-advertise-peer-urls http://etcd3:2380
|
||||||
|
|
||||||
haproxy:
|
haproxy:
|
||||||
image: patroni
|
image: ${PATRONI_TEST_IMAGE:-patroni}
|
||||||
networks: [ demo ]
|
networks: [ demo ]
|
||||||
env_file: docker/patroni.env
|
env_file: docker/patroni.env
|
||||||
hostname: haproxy
|
hostname: haproxy
|
||||||
@@ -54,7 +55,7 @@ services:
|
|||||||
PATRONI_SCOPE: demo
|
PATRONI_SCOPE: demo
|
||||||
|
|
||||||
patroni1:
|
patroni1:
|
||||||
image: patroni
|
image: ${PATRONI_TEST_IMAGE:-patroni}
|
||||||
networks: [ demo ]
|
networks: [ demo ]
|
||||||
env_file: docker/patroni.env
|
env_file: docker/patroni.env
|
||||||
hostname: patroni1
|
hostname: patroni1
|
||||||
@@ -64,7 +65,7 @@ services:
|
|||||||
PATRONI_NAME: patroni1
|
PATRONI_NAME: patroni1
|
||||||
|
|
||||||
patroni2:
|
patroni2:
|
||||||
image: patroni
|
image: ${PATRONI_TEST_IMAGE:-patroni}
|
||||||
networks: [ demo ]
|
networks: [ demo ]
|
||||||
env_file: docker/patroni.env
|
env_file: docker/patroni.env
|
||||||
hostname: patroni2
|
hostname: patroni2
|
||||||
@@ -74,7 +75,7 @@ services:
|
|||||||
PATRONI_NAME: patroni2
|
PATRONI_NAME: patroni2
|
||||||
|
|
||||||
patroni3:
|
patroni3:
|
||||||
image: patroni
|
image: ${PATRONI_TEST_IMAGE:-patroni}
|
||||||
networks: [ demo ]
|
networks: [ demo ]
|
||||||
env_file: docker/patroni.env
|
env_file: docker/patroni.env
|
||||||
hostname: patroni3
|
hostname: patroni3
|
||||||
|
|||||||
+195
-173
@@ -19,102 +19,96 @@ The haproxy listens on ports 5000 (connects to the primary) and 5001 (does load-
|
|||||||
|
|
||||||
Example session:
|
Example session:
|
||||||
|
|
||||||
$ docker-compose up -d
|
$ docker compose up -d
|
||||||
Creating demo-haproxy ...
|
✔ Network patroni_demo Created
|
||||||
Creating demo-patroni2 ...
|
✔ Container demo-etcd1 Started
|
||||||
Creating demo-patroni1 ...
|
✔ Container demo-haproxy Started
|
||||||
Creating demo-patroni3 ...
|
✔ Container demo-patroni1 Started
|
||||||
Creating demo-etcd2 ...
|
✔ Container demo-patroni2 Started
|
||||||
Creating demo-etcd1 ...
|
✔ Container demo-patroni3 Started
|
||||||
Creating demo-etcd3 ...
|
✔ Container demo-etcd2 Started
|
||||||
Creating demo-haproxy
|
✔ Container demo-etcd3 Started
|
||||||
Creating demo-patroni2
|
|
||||||
Creating demo-patroni1
|
|
||||||
Creating demo-patroni3
|
|
||||||
Creating demo-etcd1
|
|
||||||
Creating demo-etcd2
|
|
||||||
Creating demo-etcd2 ... done
|
|
||||||
|
|
||||||
$ docker ps
|
$ docker ps
|
||||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||||
5b7a90b4cfbf patroni "/bin/sh /entrypoint…" 29 seconds ago Up 27 seconds demo-etcd2
|
a37bcec56726 patroni "/bin/sh /entrypoint…" 15 minutes ago Up 15 minutes demo-etcd3
|
||||||
e30eea5222f2 patroni "/bin/sh /entrypoint…" 29 seconds ago Up 27 seconds demo-etcd1
|
034ab73868a8 patroni "/bin/sh /entrypoint…" 15 minutes ago Up 15 minutes demo-patroni2
|
||||||
83bcf3cb208f patroni "/bin/sh /entrypoint…" 29 seconds ago Up 27 seconds demo-etcd3
|
03837736f710 patroni "/bin/sh /entrypoint…" 15 minutes ago Up 15 minutes demo-patroni3
|
||||||
922532c56e7d patroni "/bin/sh /entrypoint…" 29 seconds ago Up 28 seconds demo-patroni3
|
22815c3d85b3 patroni "/bin/sh /entrypoint…" 15 minutes ago Up 15 minutes demo-etcd2
|
||||||
14f875e445f3 patroni "/bin/sh /entrypoint…" 29 seconds ago Up 28 seconds demo-patroni2
|
814b4304d132 patroni "/bin/sh /entrypoint…" 15 minutes ago Up 15 minutes 0.0.0.0:5000-5001->5000-5001/tcp, :::5000-5001->5000-5001/tcp demo-haproxy
|
||||||
110d1073b383 patroni "/bin/sh /entrypoint…" 29 seconds ago Up 28 seconds demo-patroni1
|
6375b0ba2d0a patroni "/bin/sh /entrypoint…" 15 minutes ago Up 15 minutes demo-patroni1
|
||||||
5af5e6e36028 patroni "/bin/sh /entrypoint…" 29 seconds ago Up 28 seconds 0.0.0.0:5000-5001->5000-5001/tcp demo-haproxy
|
aef8bf3ee91f patroni "/bin/sh /entrypoint…" 15 minutes ago Up 15 minutes demo-etcd1
|
||||||
|
|
||||||
$ docker logs demo-patroni1
|
$ docker logs demo-patroni1
|
||||||
2019-02-20 08:19:32,714 INFO: Failed to import patroni.dcs.consul
|
2024-08-26 09:04:33,547 INFO: Selected new etcd server http://172.29.0.3:2379
|
||||||
2019-02-20 08:19:32,737 INFO: Selected new etcd server http://etcd3:2379
|
2024-08-26 09:04:33,605 INFO: Lock owner: None; I am patroni1
|
||||||
2019-02-20 08:19:35,140 INFO: Lock owner: None; I am patroni1
|
2024-08-26 09:04:33,693 INFO: trying to bootstrap a new cluster
|
||||||
2019-02-20 08:19:35,174 INFO: trying to bootstrap a new cluster
|
|
||||||
...
|
...
|
||||||
2019-02-20 08:19:39,310 INFO: postmaster pid=37
|
2024-08-26 09:04:34.920 UTC [43] LOG: starting PostgreSQL 16.4 (Debian 16.4-1.pgdg120+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit
|
||||||
2019-02-20 08:19:39.314 UTC [37] LOG: listening on IPv4 address "0.0.0.0", port 5432
|
2024-08-26 09:04:34.921 UTC [43] LOG: listening on IPv4 address "0.0.0.0", port 5432
|
||||||
2019-02-20 08:19:39.321 UTC [37] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
|
2024-08-26 09:04:34,922 INFO: postmaster pid=43
|
||||||
2019-02-20 08:19:39.353 UTC [39] LOG: database system was shut down at 2019-02-20 08:19:36 UTC
|
2024-08-26 09:04:34.922 UTC [43] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
|
||||||
2019-02-20 08:19:39.354 UTC [40] FATAL: the database system is starting up
|
2024-08-26 09:04:34.925 UTC [47] LOG: database system was shut down at 2024-08-26 09:04:34 UTC
|
||||||
localhost:5432 - rejecting connections
|
2024-08-26 09:04:34.928 UTC [43] LOG: database system is ready to accept connections
|
||||||
2019-02-20 08:19:39.369 UTC [37] LOG: database system is ready to accept connections
|
|
||||||
localhost:5432 - accepting connections
|
localhost:5432 - accepting connections
|
||||||
2019-02-20 08:19:39,383 INFO: establishing a new patroni connection to the postgres cluster
|
localhost:5432 - accepting connections
|
||||||
2019-02-20 08:19:39,408 INFO: running post_bootstrap
|
2024-08-26 09:04:34,938 INFO: establishing a new patroni heartbeat connection to postgres
|
||||||
2019-02-20 08:19:39,432 WARNING: Could not activate Linux watchdog device: "Can't open watchdog device: [Errno 2] No such file or directory: '/dev/watchdog'"
|
2024-08-26 09:04:34,992 INFO: running post_bootstrap
|
||||||
2019-02-20 08:19:39,515 INFO: initialized a new cluster
|
2024-08-26 09:04:35,004 WARNING: User creation via "bootstrap.users" will be removed in v4.0.0
|
||||||
2019-02-20 08:19:49,424 INFO: Lock owner: patroni1; I am patroni1
|
2024-08-26 09:04:35,189 INFO: initialized a new cluster
|
||||||
2019-02-20 08:19:49,447 INFO: Lock owner: patroni1; I am patroni1
|
2024-08-26 09:04:35,328 INFO: no action. I am (patroni1), the leader with the lock
|
||||||
2019-02-20 08:19:49,480 INFO: no action. i am the leader with the lock
|
2024-08-26 09:04:43,824 INFO: establishing a new patroni restapi connection to postgres
|
||||||
2019-02-20 08:19:59,422 INFO: Lock owner: patroni1; I am patroni1
|
2024-08-26 09:04:45,322 INFO: no action. I am (patroni1), the leader with the lock
|
||||||
|
2024-08-26 09:04:55,320 INFO: no action. I am (patroni1), the leader with the lock
|
||||||
|
...
|
||||||
|
|
||||||
$ docker exec -ti demo-patroni1 bash
|
$ docker exec -ti demo-patroni1 bash
|
||||||
postgres@patroni1:~$ patronictl list
|
postgres@patroni1:~$ patronictl list
|
||||||
+---------+----------+------------+--------+---------+----+-----------+
|
+ Cluster: demo (7303838734793224214) --------+----+-----------+
|
||||||
| Cluster | Member | Host | Role | State | TL | Lag in MB |
|
| Member | Host | Role | State | TL | Lag in MB |
|
||||||
+---------+----------+------------+--------+---------+----+-----------+
|
+----------+------------+---------+-----------+----+-----------+
|
||||||
| demo | patroni1 | 172.22.0.3 | Leader | running | 1 | 0 |
|
| patroni1 | 172.29.0.2 | Leader | running | 1 | |
|
||||||
| demo | patroni2 | 172.22.0.7 | | running | 1 | 0 |
|
| patroni2 | 172.29.0.6 | Replica | streaming | 1 | 0 |
|
||||||
| demo | patroni3 | 172.22.0.4 | | running | 1 | 0 |
|
| patroni3 | 172.29.0.5 | Replica | streaming | 1 | 0 |
|
||||||
+---------+----------+------------+--------+---------+----+-----------+
|
+----------+------------+---------+-----------+----+-----------+
|
||||||
|
|
||||||
postgres@patroni1:~$ etcdctl ls --recursive --sort -p /service/demo
|
postgres@patroni1:~$ etcdctl get --keys-only --prefix /service/demo
|
||||||
/service/demo/config
|
/service/demo/config
|
||||||
/service/demo/initialize
|
/service/demo/initialize
|
||||||
/service/demo/leader
|
/service/demo/leader
|
||||||
/service/demo/members/
|
|
||||||
/service/demo/members/patroni1
|
/service/demo/members/patroni1
|
||||||
/service/demo/members/patroni2
|
/service/demo/members/patroni2
|
||||||
/service/demo/members/patroni3
|
/service/demo/members/patroni3
|
||||||
/service/demo/optime/
|
/service/demo/status
|
||||||
/service/demo/optime/leader
|
|
||||||
|
|
||||||
postgres@patroni1:~$ etcdctl member list
|
postgres@patroni1:~$ etcdctl member list
|
||||||
1bab629f01fa9065: name=etcd3 peerURLs=http://etcd3:2380 clientURLs=http://etcd3:2379 isLeader=false
|
2bf3e2ceda5d5960, started, etcd2, http://etcd2:2380, http://172.29.0.3:2379
|
||||||
8ecb6af518d241cc: name=etcd2 peerURLs=http://etcd2:2380 clientURLs=http://etcd2:2379 isLeader=true
|
55b3264e129c7005, started, etcd3, http://etcd3:2380, http://172.29.0.7:2379
|
||||||
b2e169fcb8a34028: name=etcd1 peerURLs=http://etcd1:2380 clientURLs=http://etcd1:2379 isLeader=false
|
acce7233f8ec127e, started, etcd1, http://etcd1:2380, http://172.29.0.8:2379
|
||||||
|
|
||||||
|
|
||||||
postgres@patroni1:~$ exit
|
postgres@patroni1:~$ exit
|
||||||
|
|
||||||
$ docker exec -ti demo-haproxy bash
|
$ docker exec -ti demo-haproxy bash
|
||||||
postgres@haproxy:~$ psql -h localhost -p 5000 -U postgres -W
|
postgres@haproxy:~$ psql -h localhost -p 5000 -U postgres -W
|
||||||
Password: postgres
|
Password: postgres
|
||||||
psql (11.2 (Ubuntu 11.2-1.pgdg18.04+1), server 10.7 (Debian 10.7-1.pgdg90+1))
|
psql (16.4 (Debian 16.4-1.pgdg120+1))
|
||||||
Type "help" for help.
|
Type "help" for help.
|
||||||
|
|
||||||
localhost/postgres=# select pg_is_in_recovery();
|
postgres=# SELECT pg_is_in_recovery();
|
||||||
pg_is_in_recovery
|
pg_is_in_recovery
|
||||||
───────────────────
|
───────────────────
|
||||||
f
|
f
|
||||||
(1 row)
|
(1 row)
|
||||||
|
|
||||||
localhost/postgres=# \q
|
postgres=# \q
|
||||||
|
|
||||||
$postgres@haproxy:~ psql -h localhost -p 5001 -U postgres -W
|
postgres@haproxy:~$ psql -h localhost -p 5001 -U postgres -W
|
||||||
Password: postgres
|
Password: postgres
|
||||||
psql (11.2 (Ubuntu 11.2-1.pgdg18.04+1), server 10.7 (Debian 10.7-1.pgdg90+1))
|
psql (16.4 (Debian 16.4-1.pgdg120+1))
|
||||||
Type "help" for help.
|
Type "help" for help.
|
||||||
|
|
||||||
localhost/postgres=# select pg_is_in_recovery();
|
postgres=# SELECT pg_is_in_recovery();
|
||||||
pg_is_in_recovery
|
pg_is_in_recovery
|
||||||
───────────────────
|
───────────────────
|
||||||
t
|
t
|
||||||
@@ -128,80 +122,99 @@ The haproxy listens on ports 5000 (connects to the coordinator primary) and 5001
|
|||||||
Example session:
|
Example session:
|
||||||
|
|
||||||
$ docker-compose -f docker-compose-citus.yml up -d
|
$ docker-compose -f docker-compose-citus.yml up -d
|
||||||
Creating demo-work2-1 ... done
|
✔ Network patroni_demo Created
|
||||||
Creating demo-work1-1 ... done
|
✔ Container demo-coord2 Started
|
||||||
Creating demo-etcd2 ... done
|
✔ Container demo-work2-2 Started
|
||||||
Creating demo-etcd1 ... done
|
✔ Container demo-etcd1 Started
|
||||||
Creating demo-coord3 ... done
|
✔ Container demo-haproxy Started
|
||||||
Creating demo-etcd3 ... done
|
✔ Container demo-work1-1 Started
|
||||||
Creating demo-coord1 ... done
|
✔ Container demo-work2-1 Started
|
||||||
Creating demo-haproxy ... done
|
✔ Container demo-work1-2 Started
|
||||||
Creating demo-work2-2 ... done
|
✔ Container demo-coord1 Started
|
||||||
Creating demo-coord2 ... done
|
✔ Container demo-etcd3 Started
|
||||||
Creating demo-work1-2 ... done
|
✔ Container demo-coord3 Started
|
||||||
|
✔ Container demo-etcd2 Started
|
||||||
|
|
||||||
|
|
||||||
$ docker ps
|
$ docker ps
|
||||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||||
852d8885a612 patroni-citus "/bin/sh /entrypoint…" 6 seconds ago Up 3 seconds demo-coord3
|
79c95492fac9 patroni-citus "/bin/sh /entrypoint…" 11 minutes ago Up 11 minutes demo-etcd3
|
||||||
cdd692f947ab patroni-citus "/bin/sh /entrypoint…" 6 seconds ago Up 3 seconds demo-work1-2
|
77eb82d0f0c1 patroni-citus "/bin/sh /entrypoint…" 11 minutes ago Up 11 minutes demo-work2-1
|
||||||
9f4e340b36da patroni-citus "/bin/sh /entrypoint…" 6 seconds ago Up 3 seconds demo-etcd3
|
03dacd7267ef patroni-citus "/bin/sh /entrypoint…" 11 minutes ago Up 11 minutes demo-etcd1
|
||||||
d69c129a960a patroni-citus "/bin/sh /entrypoint…" 6 seconds ago Up 4 seconds demo-etcd1
|
db9206c66f85 patroni-citus "/bin/sh /entrypoint…" 11 minutes ago Up 11 minutes demo-etcd2
|
||||||
c5849689b8cd patroni-citus "/bin/sh /entrypoint…" 6 seconds ago Up 4 seconds demo-coord1
|
9a0fef7b7dd4 patroni-citus "/bin/sh /entrypoint…" 11 minutes ago Up 11 minutes demo-work1-2
|
||||||
c9d72bd6217d patroni-citus "/bin/sh /entrypoint…" 6 seconds ago Up 3 seconds demo-work2-1
|
f06b031d99dc patroni-citus "/bin/sh /entrypoint…" 11 minutes ago Up 11 minutes demo-work2-2
|
||||||
24b1b43efa05 patroni-citus "/bin/sh /entrypoint…" 6 seconds ago Up 4 seconds demo-coord2
|
f7c58545f314 patroni-citus "/bin/sh /entrypoint…" 11 minutes ago Up 11 minutes demo-coord2
|
||||||
cb0cc2b4ca0a patroni-citus "/bin/sh /entrypoint…" 6 seconds ago Up 3 seconds demo-work2-2
|
383f9e7e188a patroni-citus "/bin/sh /entrypoint…" 11 minutes ago Up 11 minutes demo-work1-1
|
||||||
9796c6b8aad5 patroni-citus "/bin/sh /entrypoint…" 6 seconds ago Up 5 seconds demo-work1-1
|
f02e96dcc9d6 patroni-citus "/bin/sh /entrypoint…" 11 minutes ago Up 11 minutes demo-coord3
|
||||||
8baccd74dcae patroni-citus "/bin/sh /entrypoint…" 6 seconds ago Up 4 seconds demo-etcd2
|
6945834b7056 patroni-citus "/bin/sh /entrypoint…" 11 minutes ago Up 11 minutes demo-coord1
|
||||||
353ec62a0187 patroni-citus "/bin/sh /entrypoint…" 6 seconds ago Up 4 seconds 0.0.0.0:5000-5001->5000-5001/tcp demo-haproxy
|
b96ca42f785d patroni-citus "/bin/sh /entrypoint…" 11 minutes ago Up 11 minutes 0.0.0.0:5000-5001->5000-5001/tcp, :::5000-5001->5000-5001/tcp demo-haproxy
|
||||||
|
|
||||||
|
|
||||||
$ docker logs demo-coord1
|
$ docker logs demo-coord1
|
||||||
2023-01-05 15:09:31,295 INFO: Selected new etcd server http://172.27.0.4:2379
|
2024-08-26 08:21:05,323 INFO: Selected new etcd server http://172.19.0.5:2379
|
||||||
2023-01-05 15:09:31,388 INFO: Lock owner: None; I am coord1
|
2024-08-26 08:21:05,339 INFO: No PostgreSQL configuration items changed, nothing to reload.
|
||||||
2023-01-05 15:09:31,501 INFO: trying to bootstrap a new cluster
|
2024-08-26 08:21:05,388 INFO: Lock owner: None; I am coord1
|
||||||
|
2024-08-26 08:21:05,480 INFO: trying to bootstrap a new cluster
|
||||||
...
|
...
|
||||||
2023-01-05 15:09:45,096 INFO: postmaster pid=39
|
2024-08-26 08:21:17,115 INFO: postmaster pid=35
|
||||||
localhost:5432 - no response
|
localhost:5432 - no response
|
||||||
2023-01-05 15:09:45.137 UTC [39] LOG: starting PostgreSQL 15.1 (Debian 15.1-1.pgdg110+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit
|
2024-08-26 08:21:17.127 UTC [35] LOG: starting PostgreSQL 16.4 (Debian 16.4-1.pgdg120+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit
|
||||||
2023-01-05 15:09:45.137 UTC [39] LOG: listening on IPv4 address "0.0.0.0", port 5432
|
2024-08-26 08:21:17.127 UTC [35] LOG: listening on IPv4 address "0.0.0.0", port 5432
|
||||||
2023-01-05 15:09:45.152 UTC [39] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
|
2024-08-26 08:21:17.141 UTC [35] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
|
||||||
2023-01-05 15:09:45.177 UTC [43] LOG: database system was shut down at 2023-01-05 15:09:32 UTC
|
2024-08-26 08:21:17.155 UTC [39] LOG: database system was shut down at 2024-08-26 08:21:05 UTC
|
||||||
2023-01-05 15:09:45.193 UTC [39] LOG: database system is ready to accept connections
|
2024-08-26 08:21:17.182 UTC [35] LOG: database system is ready to accept connections
|
||||||
|
2024-08-26 08:21:17,683 INFO: establishing a new patroni heartbeat connection to postgres
|
||||||
|
2024-08-26 08:21:17,704 INFO: establishing a new patroni restapi connection to postgres
|
||||||
localhost:5432 - accepting connections
|
localhost:5432 - accepting connections
|
||||||
localhost:5432 - accepting connections
|
localhost:5432 - accepting connections
|
||||||
2023-01-05 15:09:46,139 INFO: establishing a new patroni connection to the postgres cluster
|
2024-08-26 08:21:18,202 INFO: running post_bootstrap
|
||||||
2023-01-05 15:09:46,208 INFO: running post_bootstrap
|
2024-08-26 08:21:19.048 UTC [53] LOG: starting maintenance daemon on database 16385 user 10
|
||||||
2023-01-05 15:09:47.209 UTC [55] LOG: starting maintenance daemon on database 16386 user 10
|
2024-08-26 08:21:19.048 UTC [53] CONTEXT: Citus maintenance daemon for database 16385 user 10
|
||||||
2023-01-05 15:09:47.209 UTC [55] CONTEXT: Citus maintenance daemon for database 16386 user 10
|
2024-08-26 08:21:19,058 DEBUG: Could not activate Linux watchdog device: Can't open watchdog device: [Errno 2] No such file or directory: '/dev/watchdog'
|
||||||
2023-01-05 15:09:47,215 WARNING: Could not activate Linux watchdog device: "Can't open watchdog device: [Errno 2] No such file or directory: '/dev/watchdog'"
|
2024-08-26 08:21:19.250 UTC [37] LOG: checkpoint starting: immediate force wait
|
||||||
2023-01-05 15:09:47.446 UTC [41] LOG: checkpoint starting: immediate force wait
|
2024-08-26 08:21:19,275 INFO: initialized a new cluster
|
||||||
2023-01-05 15:09:47,466 INFO: initialized a new cluster
|
2024-08-26 08:21:22.946 UTC [37] LOG: checkpoint starting: immediate force wait
|
||||||
2023-01-05 15:09:47,594 DEBUG: query(SELECT nodeid, groupid, nodename, nodeport, noderole FROM pg_catalog.pg_dist_node WHERE noderole = 'primary', ())
|
2024-08-26 08:21:29,059 INFO: Lock owner: coord1; I am coord1
|
||||||
2023-01-05 15:09:47,594 INFO: establishing a new patroni connection to the postgres cluster
|
2024-08-26 08:21:29,205 INFO: Enabled synchronous replication
|
||||||
2023-01-05 15:09:47,467 INFO: Lock owner: coord1; I am coord1
|
2024-08-26 08:21:29,206 DEBUG: query(SELECT groupid, nodename, nodeport, noderole, nodeid FROM pg_catalog.pg_dist_node, ())
|
||||||
2023-01-05 15:09:47,613 DEBUG: query(SELECT pg_catalog.citus_set_coordinator_host(%s, %s, 'primary', 'default'), ('172.27.0.6', 5432))
|
2024-08-26 08:21:29,206 INFO: establishing a new patroni citus connection to postgres
|
||||||
2023-01-05 15:09:47,924 INFO: no action. I am (coord1), the leader with the lock
|
2024-08-26 08:21:29,206 DEBUG: Adding the new task: PgDistTask({PgDistNode(nodeid=None,host=172.19.0.8,port=5432,role=primary)})
|
||||||
2023-01-05 15:09:51.282 UTC [41] LOG: checkpoint complete: wrote 1086 buffers (53.0%); 0 WAL file(s) added, 0 removed, 0 recycled; write=0.029 s, sync=3.746 s, total=3.837 s; sync files=280, longest=0.028 s, average=0.014 s; distance=8965 kB, estimate=8965 kB
|
2024-08-26 08:21:29,206 DEBUG: Adding the new task: PgDistTask({PgDistNode(nodeid=None,host=172.19.0.2,port=5432,role=primary)})
|
||||||
2023-01-05 15:09:51.283 UTC [41] LOG: checkpoint starting: immediate force wait
|
2024-08-26 08:21:29,206 DEBUG: Adding the new task: PgDistTask({PgDistNode(nodeid=None,host=172.19.0.9,port=5432,role=primary)})
|
||||||
2023-01-05 15:09:51.495 UTC [41] LOG: checkpoint complete: wrote 18 buffers (0.9%); 0 WAL file(s) added, 0 removed, 0 recycled; write=0.044 s, sync=0.091 s, total=0.212 s; sync files=15, longest=0.015 s, average=0.007 s; distance=67 kB, estimate=8076 kB
|
2024-08-26 08:21:29,219 DEBUG: query(SELECT pg_catalog.citus_add_node(%s, %s, %s, %s, 'default'), ('172.19.0.2', 5432, 1, 'primary'))
|
||||||
2023-01-05 15:09:57,467 INFO: Lock owner: coord1; I am coord1
|
2024-08-26 08:21:29,256 DEBUG: query(SELECT pg_catalog.citus_add_node(%s, %s, %s, %s, 'default'), ('172.19.0.9', 5432, 2, 'primary'))
|
||||||
2023-01-05 15:09:57,569 INFO: Assigning synchronous standby status to ['coord3']
|
2024-08-26 08:21:29,474 INFO: no action. I am (coord1), the leader with the lock
|
||||||
|
2024-08-26 08:21:39,060 INFO: Lock owner: coord1; I am coord1
|
||||||
|
2024-08-26 08:21:39,159 DEBUG: Adding the new task: PgDistTask({PgDistNode(nodeid=None,host=172.19.0.8,port=5432,role=primary), PgDistNode(nodeid=None,host=172.19.0.11,port=5432,role=secondary), PgDistNode(nodeid=None,host=172.19.0.7,port=5432,role=secondary)})
|
||||||
|
2024-08-26 08:21:39,159 DEBUG: Adding the new task: PgDistTask({PgDistNode(nodeid=None,host=172.19.0.2,port=5432,role=primary), PgDistNode(nodeid=None,host=172.19.0.12,port=5432,role=secondary)})
|
||||||
|
2024-08-26 08:21:39,159 DEBUG: Adding the new task: PgDistTask({PgDistNode(nodeid=None,host=172.19.0.6,port=5432,role=secondary), PgDistNode(nodeid=None,host=172.19.0.9,port=5432,role=primary)})
|
||||||
|
2024-08-26 08:21:39,160 DEBUG: query(BEGIN, ())
|
||||||
|
2024-08-26 08:21:39,160 DEBUG: query(SELECT pg_catalog.citus_add_node(%s, %s, %s, %s, 'default'), ('172.19.0.11', 5432, 0, 'secondary'))
|
||||||
|
2024-08-26 08:21:39,164 DEBUG: query(SELECT pg_catalog.citus_add_node(%s, %s, %s, %s, 'default'), ('172.19.0.7', 5432, 0, 'secondary'))
|
||||||
|
2024-08-26 08:21:39,166 DEBUG: query(COMMIT, ())
|
||||||
|
2024-08-26 08:21:39,176 DEBUG: query(SELECT pg_catalog.citus_add_node(%s, %s, %s, %s, 'default'), ('172.19.0.12', 5432, 1, 'secondary'))
|
||||||
|
2024-08-26 08:21:39,191 DEBUG: query(SELECT pg_catalog.citus_add_node(%s, %s, %s, %s, 'default'), ('172.19.0.6', 5432, 2, 'secondary'))
|
||||||
|
2024-08-26 08:21:39,211 INFO: no action. I am (coord1), the leader with the lock
|
||||||
|
2024-08-26 08:21:49,060 INFO: Lock owner: coord1; I am coord1
|
||||||
|
2024-08-26 08:21:49,166 INFO: Setting synchronous replication to 1 of 2 (coord2, coord3)
|
||||||
server signaled
|
server signaled
|
||||||
2023-01-05 15:09:57.574 UTC [39] LOG: received SIGHUP, reloading configuration files
|
2024-08-26 08:21:49.170 UTC [35] LOG: received SIGHUP, reloading configuration files
|
||||||
2023-01-05 15:09:57.580 UTC [39] LOG: parameter "synchronous_standby_names" changed to "coord3"
|
2024-08-26 08:21:49.171 UTC [35] LOG: parameter "synchronous_standby_names" changed to "ANY 1 (coord2,coord3)"
|
||||||
2023-01-05 15:09:59,637 INFO: Synchronous standby status assigned to ['coord3']
|
2024-08-26 08:21:49.377 UTC [68] LOG: standby "coord2" is now a candidate for quorum synchronous standby
|
||||||
2023-01-05 15:09:59,638 DEBUG: query(SELECT pg_catalog.citus_add_node(%s, %s, %s, 'primary', 'default'), ('172.27.0.2', 5432, 1))
|
2024-08-26 08:21:49.377 UTC [68] STATEMENT: START_REPLICATION SLOT "coord2" 0/3000000 TIMELINE 1
|
||||||
2023-01-05 15:09:59.690 UTC [67] LOG: standby "coord3" is now a synchronous standby with priority 1
|
2024-08-26 08:21:49.377 UTC [69] LOG: standby "coord3" is now a candidate for quorum synchronous standby
|
||||||
2023-01-05 15:09:59.690 UTC [67] STATEMENT: START_REPLICATION SLOT "coord3" 0/3000000 TIMELINE 1
|
2024-08-26 08:21:49.377 UTC [69] STATEMENT: START_REPLICATION SLOT "coord3" 0/4000000 TIMELINE 1
|
||||||
2023-01-05 15:09:59,694 INFO: no action. I am (coord1), the leader with the lock
|
2024-08-26 08:21:50,278 INFO: Setting leader to coord1, quorum to 1 of 2 (coord2, coord3)
|
||||||
2023-01-05 15:09:59,704 DEBUG: query(SELECT pg_catalog.citus_add_node(%s, %s, %s, 'primary', 'default'), ('172.27.0.8', 5432, 2))
|
2024-08-26 08:21:50,390 INFO: no action. I am (coord1), the leader with the lock
|
||||||
2023-01-05 15:10:07,625 INFO: no action. I am (coord1), the leader with the lock
|
2024-08-26 08:21:59,159 INFO: no action. I am (coord1), the leader with the lock
|
||||||
2023-01-05 15:10:17,579 INFO: no action. I am (coord1), the leader with the lock
|
...
|
||||||
|
|
||||||
$ docker exec -ti demo-haproxy bash
|
$ docker exec -ti demo-haproxy bash
|
||||||
postgres@haproxy:~$ etcdctl member list
|
postgres@haproxy:~$ etcdctl member list
|
||||||
1bab629f01fa9065, started, etcd3, http://etcd3:2380, http://172.27.0.10:2379
|
2b28411e74c0c281, started, etcd3, http://etcd3:2380, http://172.30.0.4:2379
|
||||||
8ecb6af518d241cc, started, etcd2, http://etcd2:2380, http://172.27.0.4:2379
|
6c70137d27cfa6c1, started, etcd2, http://etcd2:2380, http://172.30.0.5:2379
|
||||||
b2e169fcb8a34028, started, etcd1, http://etcd1:2380, http://172.27.0.7:2379
|
a28f9a70ebf21304, started, etcd1, http://etcd1:2380, http://172.30.0.6:2379
|
||||||
|
|
||||||
postgres@haproxy:~$ etcdctl get --keys-only --prefix /service/demo
|
postgres@haproxy:~$ etcdctl get --keys-only --prefix /service/demo
|
||||||
/service/demo/0/config
|
/service/demo/0/config
|
||||||
@@ -229,7 +242,7 @@ Example session:
|
|||||||
|
|
||||||
postgres@haproxy:~$ psql -h localhost -p 5000 -U postgres -d citus
|
postgres@haproxy:~$ psql -h localhost -p 5000 -U postgres -d citus
|
||||||
Password for user postgres: postgres
|
Password for user postgres: postgres
|
||||||
psql (15.1 (Debian 15.1-1.pgdg110+1))
|
psql (16.4 (Debian 16.4-1.pgdg120+1))
|
||||||
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
|
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
|
||||||
Type "help" for help.
|
Type "help" for help.
|
||||||
|
|
||||||
@@ -240,67 +253,76 @@ Example session:
|
|||||||
(1 row)
|
(1 row)
|
||||||
|
|
||||||
citus=# table pg_dist_node;
|
citus=# table pg_dist_node;
|
||||||
nodeid | groupid | nodename | nodeport | noderack | hasmetadata | isactive | noderole | nodecluster | metadatasynced | shouldhaveshards
|
nodeid | groupid | nodename | nodeport | noderack | hasmetadata | isactive | noderole | nodecluster | metadatasynced | shouldhaveshards
|
||||||
--------+---------+------------+----------+----------+-------------+----------+----------+-------------+----------------+------------------
|
--------+---------+-------------+----------+----------+-------------+----------+-----------+-------------+----------------+------------------
|
||||||
1 | 0 | 172.27.0.6 | 5432 | default | t | t | primary | default | t | f
|
1 | 0 | 172.19.0.8 | 5432 | default | t | t | primary | default | t | f
|
||||||
2 | 1 | 172.27.0.2 | 5432 | default | t | t | primary | default | t | t
|
2 | 1 | 172.19.0.2 | 5432 | default | t | t | primary | default | t | t
|
||||||
3 | 2 | 172.27.0.8 | 5432 | default | t | t | primary | default | t | t
|
3 | 2 | 172.19.0.9 | 5432 | default | t | t | primary | default | t | t
|
||||||
(3 rows)
|
4 | 0 | 172.19.0.11 | 5432 | default | t | t | secondary | default | t | f
|
||||||
|
5 | 0 | 172.19.0.7 | 5432 | default | t | t | secondary | default | t | f
|
||||||
|
6 | 1 | 172.19.0.12 | 5432 | default | f | t | secondary | default | f | t
|
||||||
|
7 | 2 | 172.19.0.6 | 5432 | default | f | t | secondary | default | f | t
|
||||||
|
(7 rows)
|
||||||
|
|
||||||
citus=# \q
|
citus=# \q
|
||||||
|
|
||||||
postgres@haproxy:~$ patronictl list
|
postgres@haproxy:~$ patronictl list
|
||||||
+ Citus cluster: demo ----------+--------------+---------+----+-----------+
|
+ Citus cluster: demo ----------+----------------+-----------+----+-----------+
|
||||||
| Group | Member | Host | Role | State | TL | Lag in MB |
|
| Group | Member | Host | Role | State | TL | Lag in MB |
|
||||||
+-------+---------+-------------+--------------+---------+----+-----------+
|
+-------+---------+-------------+----------------+-----------+----+-----------+
|
||||||
| 0 | coord1 | 172.27.0.6 | Leader | running | 1 | |
|
| 0 | coord1 | 172.19.0.8 | Leader | running | 1 | |
|
||||||
| 0 | coord2 | 172.27.0.5 | Replica | running | 1 | 0 |
|
| 0 | coord2 | 172.19.0.7 | Quorum Standby | streaming | 1 | 0 |
|
||||||
| 0 | coord3 | 172.27.0.9 | Sync Standby | running | 1 | 0 |
|
| 0 | coord3 | 172.19.0.11 | Quorum Standby | streaming | 1 | 0 |
|
||||||
| 1 | work1-1 | 172.27.0.2 | Leader | running | 1 | |
|
| 1 | work1-1 | 172.19.0.12 | Quorum Standby | streaming | 1 | 0 |
|
||||||
| 1 | work1-2 | 172.27.0.12 | Sync Standby | running | 1 | 0 |
|
| 1 | work1-2 | 172.19.0.2 | Leader | running | 1 | |
|
||||||
| 2 | work2-1 | 172.27.0.11 | Sync Standby | running | 1 | 0 |
|
| 2 | work2-1 | 172.19.0.6 | Quorum Standby | streaming | 1 | 0 |
|
||||||
| 2 | work2-2 | 172.27.0.8 | Leader | running | 1 | |
|
| 2 | work2-2 | 172.19.0.9 | Leader | running | 1 | |
|
||||||
+-------+---------+-------------+--------------+---------+----+-----------+
|
+-------+---------+-------------+----------------+-----------+----+-----------+
|
||||||
|
|
||||||
|
|
||||||
postgres@haproxy:~$ patronictl switchover --group 2 --force
|
postgres@haproxy:~$ patronictl switchover --group 2 --force
|
||||||
Current cluster topology
|
Current cluster topology
|
||||||
+ Citus cluster: demo (group: 2, 7185185529556963355) +-----------+
|
+ Citus cluster: demo (group: 2, 7407360296219029527) ---+-----------+
|
||||||
| Member | Host | Role | State | TL | Lag in MB |
|
| Member | Host | Role | State | TL | Lag in MB |
|
||||||
+---------+-------------+--------------+---------+----+-----------+
|
+---------+------------+----------------+-----------+----+-----------+
|
||||||
| work2-1 | 172.27.0.11 | Sync Standby | running | 1 | 0 |
|
| work2-1 | 172.19.0.6 | Quorum Standby | streaming | 1 | 0 |
|
||||||
| work2-2 | 172.27.0.8 | Leader | running | 1 | |
|
| work2-2 | 172.19.0.9 | Leader | running | 1 | |
|
||||||
+---------+-------------+--------------+---------+----+-----------+
|
+---------+------------+----------------+-----------+----+-----------+
|
||||||
2023-01-05 15:29:29.54204 Successfully switched over to "work2-1"
|
2024-08-26 08:31:45.92277 Successfully switched over to "work2-1"
|
||||||
+ Citus cluster: demo (group: 2, 7185185529556963355) -------+
|
+ Citus cluster: demo (group: 2, 7407360296219029527) ------+
|
||||||
| Member | Host | Role | State | TL | Lag in MB |
|
| Member | Host | Role | State | TL | Lag in MB |
|
||||||
+---------+-------------+---------+---------+----+-----------+
|
+---------+------------+---------+---------+----+-----------+
|
||||||
| work2-1 | 172.27.0.11 | Leader | running | 1 | |
|
| work2-1 | 172.19.0.6 | Leader | running | 1 | |
|
||||||
| work2-2 | 172.27.0.8 | Replica | stopped | | unknown |
|
| work2-2 | 172.19.0.9 | Replica | stopped | | unknown |
|
||||||
+---------+-------------+---------+---------+----+-----------+
|
+---------+------------+---------+---------+----+-----------+
|
||||||
|
|
||||||
postgres@haproxy:~$ patronictl list
|
postgres@haproxy:~$ patronictl list
|
||||||
+ Citus cluster: demo ----------+--------------+---------+----+-----------+
|
+ Citus cluster: demo ----------+----------------+-----------+----+-----------+
|
||||||
| Group | Member | Host | Role | State | TL | Lag in MB |
|
| Group | Member | Host | Role | State | TL | Lag in MB |
|
||||||
+-------+---------+-------------+--------------+---------+----+-----------+
|
+-------+---------+-------------+----------------+-----------+----+-----------+
|
||||||
| 0 | coord1 | 172.27.0.6 | Leader | running | 1 | |
|
| 0 | coord1 | 172.19.0.8 | Leader | running | 1 | |
|
||||||
| 0 | coord2 | 172.27.0.5 | Replica | running | 1 | 0 |
|
| 0 | coord2 | 172.19.0.7 | Quorum Standby | streaming | 1 | 0 |
|
||||||
| 0 | coord3 | 172.27.0.9 | Sync Standby | running | 1 | 0 |
|
| 0 | coord3 | 172.19.0.11 | Quorum Standby | streaming | 1 | 0 |
|
||||||
| 1 | work1-1 | 172.27.0.2 | Leader | running | 1 | |
|
| 1 | work1-1 | 172.19.0.12 | Quorum Standby | streaming | 1 | 0 |
|
||||||
| 1 | work1-2 | 172.27.0.12 | Sync Standby | running | 1 | 0 |
|
| 1 | work1-2 | 172.19.0.2 | Leader | running | 1 | |
|
||||||
| 2 | work2-1 | 172.27.0.11 | Leader | running | 2 | |
|
| 2 | work2-1 | 172.19.0.6 | Leader | running | 2 | |
|
||||||
| 2 | work2-2 | 172.27.0.8 | Sync Standby | running | 2 | 0 |
|
| 2 | work2-2 | 172.19.0.9 | Quorum Standby | streaming | 2 | 0 |
|
||||||
+-------+---------+-------------+--------------+---------+----+-----------+
|
+-------+---------+-------------+----------------+-----------+----+-----------+
|
||||||
|
|
||||||
postgres@haproxy:~$ psql -h localhost -p 5000 -U postgres -d citus
|
postgres@haproxy:~$ psql -h localhost -p 5000 -U postgres -d citus
|
||||||
Password for user postgres: postgres
|
Password for user postgres: postgres
|
||||||
psql (15.1 (Debian 15.1-1.pgdg110+1))
|
psql (16.4 (Debian 16.4-1.pgdg120+1))
|
||||||
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
|
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
|
||||||
Type "help" for help.
|
Type "help" for help.
|
||||||
|
|
||||||
citus=# table pg_dist_node;
|
citus=# table pg_dist_node;
|
||||||
nodeid | groupid | nodename | nodeport | noderack | hasmetadata | isactive | noderole | nodecluster | metadatasynced | shouldhaveshards
|
nodeid | groupid | nodename | nodeport | noderack | hasmetadata | isactive | noderole | nodecluster | metadatasynced | shouldhaveshards
|
||||||
--------+---------+-------------+----------+----------+-------------+----------+----------+-------------+----------------+------------------
|
--------+---------+-------------+----------+----------+-------------+----------+-----------+-------------+----------------+------------------
|
||||||
1 | 0 | 172.27.0.6 | 5432 | default | t | t | primary | default | t | f
|
1 | 0 | 172.19.0.8 | 5432 | default | t | t | primary | default | t | f
|
||||||
3 | 2 | 172.27.0.11 | 5432 | default | t | t | primary | default | t | t
|
4 | 0 | 172.19.0.11 | 5432 | default | t | t | secondary | default | t | f
|
||||||
2 | 1 | 172.27.0.2 | 5432 | default | t | t | primary | default | t | t
|
5 | 0 | 172.19.0.7 | 5432 | default | t | t | secondary | default | t | f
|
||||||
(3 rows)
|
6 | 1 | 172.19.0.12 | 5432 | default | f | t | secondary | default | f | t
|
||||||
|
3 | 2 | 172.19.0.6 | 5432 | default | t | t | primary | default | t | t
|
||||||
|
2 | 1 | 172.19.0.2 | 5432 | default | t | t | primary | default | t | t
|
||||||
|
8 | 2 | 172.19.0.9 | 5432 | default | f | t | secondary | default | f | t
|
||||||
|
(7 rows)
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ readonly PATRONI_NAMESPACE="${PATRONI_NAMESPACE%/}"
|
|||||||
DOCKER_IP=$(hostname --ip-address)
|
DOCKER_IP=$(hostname --ip-address)
|
||||||
readonly DOCKER_IP
|
readonly DOCKER_IP
|
||||||
|
|
||||||
|
export DUMB_INIT_SETSID=0
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
haproxy)
|
haproxy)
|
||||||
haproxy -f /etc/haproxy/haproxy.cfg -p /var/run/haproxy.pid -D
|
haproxy -f /etc/haproxy/haproxy.cfg -p /var/run/haproxy.pid -D
|
||||||
@@ -36,7 +38,7 @@ EOT
|
|||||||
exec dumb-init "$@"
|
exec dumb-init "$@"
|
||||||
;;
|
;;
|
||||||
etcd)
|
etcd)
|
||||||
exec "$@" -advertise-client-urls "http://$DOCKER_IP:2379"
|
exec "$@" --auto-compaction-retention=1 -advertise-client-urls "http://$DOCKER_IP:2379"
|
||||||
;;
|
;;
|
||||||
zookeeper)
|
zookeeper)
|
||||||
exec /usr/share/zookeeper/bin/zkServer.sh start-foreground
|
exec /usr/share/zookeeper/bin/zkServer.sh start-foreground
|
||||||
@@ -72,4 +74,4 @@ export PATRONI_SUPERUSER_SSLKEY="${PATRONI_SUPERUSER_SSLKEY:-$PGSSLKEY}"
|
|||||||
export PATRONI_SUPERUSER_SSLCERT="${PATRONI_SUPERUSER_SSLCERT:-$PGSSLCERT}"
|
export PATRONI_SUPERUSER_SSLCERT="${PATRONI_SUPERUSER_SSLCERT:-$PGSSLCERT}"
|
||||||
export PATRONI_SUPERUSER_SSLROOTCERT="${PATRONI_SUPERUSER_SSLROOTCERT:-$PGSSLROOTCERT}"
|
export PATRONI_SUPERUSER_SSLROOTCERT="${PATRONI_SUPERUSER_SSLROOTCERT:-$PGSSLROOTCERT}"
|
||||||
|
|
||||||
exec python3 /patroni.py postgres0.yml
|
exec dumb-init python3 /patroni.py postgres0.yml
|
||||||
|
|||||||
+7
-57
@@ -1,62 +1,12 @@
|
|||||||
.. _contributing:
|
.. _contributing:
|
||||||
|
|
||||||
Contributing guidelines
|
Contributing
|
||||||
=======================
|
============
|
||||||
|
|
||||||
Wanna contribute to Patroni? Yay - here is how!
|
Resources and information for developers can be found in the pages below.
|
||||||
|
|
||||||
Chatting
|
.. toctree::
|
||||||
--------
|
:maxdepth: 2
|
||||||
|
|
||||||
Just want to chat with other Patroni users? Looking for interactive troubleshooting help? Join us on channel `#patroni <https://postgresteam.slack.com/archives/C9XPYG92A>`__ in the `PostgreSQL Slack <https://postgresteam.slack.com/>`__.
|
contributing_guidelines
|
||||||
|
Patroni API docs<modules/modules>
|
||||||
Running tests
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Requirements for running behave tests:
|
|
||||||
|
|
||||||
1. PostgreSQL packages need to be installed.
|
|
||||||
2. PostgreSQL binaries must be available in your `PATH`. You may need to add them to the path with something like `PATH=/usr/lib/postgresql/11/bin:$PATH python -m behave`.
|
|
||||||
3. If you'd like to test with external DCSs (e.g., Etcd, Consul, and Zookeeper) you'll need the packages installed and respective services running and accepting unencrypted/unprotected connections on localhost and default port. In the case of Etcd or Consul, the behave test suite could start them up if binaries are available in the `PATH`.
|
|
||||||
|
|
||||||
Install dependencies:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
# You may want to use Virtualenv or specify pip3.
|
|
||||||
pip install -r requirements.txt
|
|
||||||
pip install -r requirements.dev.txt
|
|
||||||
|
|
||||||
After you have all dependencies installed, you can run the various test suites:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
# You may want to use Virtualenv or specify python3.
|
|
||||||
|
|
||||||
# Run flake8 to check syntax and formatting:
|
|
||||||
python setup.py flake8
|
|
||||||
|
|
||||||
# Run the pytest suite in tests/:
|
|
||||||
python setup.py test
|
|
||||||
|
|
||||||
# Run the behave (https://behave.readthedocs.io/en/latest/) test suite in features/;
|
|
||||||
# modify DCS as desired (raft has no dependencies so is the easiest to start with):
|
|
||||||
DCS=raft python -m behave
|
|
||||||
|
|
||||||
Reporting issues
|
|
||||||
----------------
|
|
||||||
|
|
||||||
If you have a question about patroni or have a problem using it, please read the :ref:`README <readme>` before filing an issue.
|
|
||||||
Also double check with the current issues on our `Issues Tracker <https://github.com/zalando/patroni/issues>`__.
|
|
||||||
|
|
||||||
Contributing a pull request
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
1) Submit a comment to the relevant issue or create a new issue describing your proposed change.
|
|
||||||
2) Do a fork, develop and test your code changes.
|
|
||||||
3) Include documentation
|
|
||||||
4) Submit a pull request.
|
|
||||||
|
|
||||||
You'll get feedback about your pull request as soon as possible.
|
|
||||||
|
|
||||||
Happy Patroni hacking ;-)
|
|
||||||
|
|||||||
+55
-24
@@ -14,24 +14,28 @@ Global/Universal
|
|||||||
|
|
||||||
Log
|
Log
|
||||||
---
|
---
|
||||||
|
- **PATRONI\_LOG\_TYPE**: sets the format of logs. Can be either **plain** or **json**. To use **json** format, you must have the :ref:`jsonlogger <extras>` installed. The default value is **plain**.
|
||||||
- **PATRONI\_LOG\_LEVEL**: sets the general logging level. Default value is **INFO** (see `the docs for Python logging <https://docs.python.org/3.6/library/logging.html#levels>`_)
|
- **PATRONI\_LOG\_LEVEL**: sets the general logging level. Default value is **INFO** (see `the docs for Python logging <https://docs.python.org/3.6/library/logging.html#levels>`_)
|
||||||
- **PATRONI\_LOG\_TRACEBACK\_LEVEL**: sets the level where tracebacks will be visible. Default value is **ERROR**. Set it to **DEBUG** if you want to see tracebacks only if you enable **PATRONI\_LOG\_LEVEL=DEBUG**.
|
- **PATRONI\_LOG\_TRACEBACK\_LEVEL**: sets the level where tracebacks will be visible. Default value is **ERROR**. Set it to **DEBUG** if you want to see tracebacks only if you enable **PATRONI\_LOG\_LEVEL=DEBUG**.
|
||||||
- **PATRONI\_LOG\_FORMAT**: sets the log formatting string. Default value is **%(asctime)s %(levelname)s: %(message)s** (see `the LogRecord attributes <https://docs.python.org/3.6/library/logging.html#logrecord-attributes>`_)
|
- **PATRONI\_LOG\_FORMAT**: sets the log formatting string. If the log type is **plain**, the log format should be a string.
|
||||||
|
Refer to `the LogRecord attributes <https://docs.python.org/3.6/library/logging.html#logrecord-attributes>`_ for
|
||||||
|
available attributes. If the log type is **json**, the log format can be a list in addition to a string. Each list
|
||||||
|
item should correspond to LogRecord attributes. Be cautious that only the field name is required, and the **%(**
|
||||||
|
and **)** should be omitted. If you wish to print a log field with a different key name, use a dictionary where
|
||||||
|
the dictionary key is the log field, and the value is the name of the field you want to be printed in the log.
|
||||||
|
Default value is **%(asctime)s %(levelname)s: %(message)s**
|
||||||
- **PATRONI\_LOG\_DATEFORMAT**: sets the datetime formatting string. (see the `formatTime() documentation <https://docs.python.org/3.6/library/logging.html#logging.Formatter.formatTime>`_)
|
- **PATRONI\_LOG\_DATEFORMAT**: sets the datetime formatting string. (see the `formatTime() documentation <https://docs.python.org/3.6/library/logging.html#logging.Formatter.formatTime>`_)
|
||||||
|
- **PATRONI\_LOG\_STATIC\_FIELDS**: add additional fields to the log. This option is only available when the log type is set to **json**. Example ``PATRONI_LOG_STATIC_FIELDS="{app: patroni}"``
|
||||||
- **PATRONI\_LOG\_MAX\_QUEUE\_SIZE**: Patroni is using two-step logging. Log records are written into the in-memory queue and there is a separate thread which pulls them from the queue and writes to stderr or file. The maximum size of the internal queue is limited by default by **1000** records, which is enough to keep logs for the past 1h20m.
|
- **PATRONI\_LOG\_MAX\_QUEUE\_SIZE**: Patroni is using two-step logging. Log records are written into the in-memory queue and there is a separate thread which pulls them from the queue and writes to stderr or file. The maximum size of the internal queue is limited by default by **1000** records, which is enough to keep logs for the past 1h20m.
|
||||||
- **PATRONI\_LOG\_DIR**: Directory to write application logs to. The directory must exist and be writable by the user executing Patroni. If you set this env variable, the application will retain 4 25MB logs by default. You can tune those retention values with `PATRONI_LOG_FILE_NUM` and `PATRONI_LOG_FILE_SIZE` (see below).
|
- **PATRONI\_LOG\_DIR**: Directory to write application logs to. The directory must exist and be writable by the user executing Patroni. If you set this env variable, the application will retain 4 25MB logs by default. You can tune those retention values with `PATRONI_LOG_FILE_NUM` and `PATRONI_LOG_FILE_SIZE` (see below).
|
||||||
|
- **PATRONI\_LOG\_MODE**: Permissions for log files (for example, ``0644``). If not specified, permissions will be set based on the current umask value.
|
||||||
- **PATRONI\_LOG\_FILE\_NUM**: The number of application logs to retain.
|
- **PATRONI\_LOG\_FILE\_NUM**: The number of application logs to retain.
|
||||||
- **PATRONI\_LOG\_FILE\_SIZE**: Size of patroni.log file (in bytes) that triggers a log rolling.
|
- **PATRONI\_LOG\_FILE\_SIZE**: Size of patroni.log file (in bytes) that triggers a log rolling.
|
||||||
- **PATRONI\_LOG\_LOGGERS**: Redefine logging level per python module. Example ``PATRONI_LOG_LOGGERS="{patroni.postmaster: WARNING, urllib3: DEBUG}"``
|
- **PATRONI\_LOG\_LOGGERS**: Redefine logging level per python module. Example ``PATRONI_LOG_LOGGERS="{patroni.postmaster: WARNING, urllib3: DEBUG}"``
|
||||||
|
- **PATRONI\_LOG\_DEDUPLICATE\_HEARTBEAT\_LOGS**: If set to ``true``, successive heartbeat logs that are identical shall not be output. Default value is ``false``.
|
||||||
|
|
||||||
Bootstrap configuration
|
.. warning::
|
||||||
-----------------------
|
The time the HA loop executes at can be very valuable information in diagnosing failovers due to resource exhaustion and similar problems. When ``PATRONI_LOG_DEDUPLICATE_HEARTBEAT_LOGS`` is set to ``true`` there will be no log generated for the HA loop execution (unless the leader changes) and hence this potentially useful information will not be available from the logs.
|
||||||
It is possible to create new database users right after the successful initialization of a new cluster. This process is defined by the following variables:
|
|
||||||
|
|
||||||
- **PATRONI\_<username>\_PASSWORD='<password>'**
|
|
||||||
- **PATRONI\_<username>\_OPTIONS='list,of,options'**
|
|
||||||
|
|
||||||
Example: defining ``PATRONI_admin_PASSWORD=strongpasswd`` and ``PATRONI_admin_OPTIONS='createrole,createdb'`` will cause creation of the user **admin** with the password **strongpasswd** that is allowed to create other users and databases.
|
|
||||||
|
|
||||||
Citus
|
Citus
|
||||||
-----
|
-----
|
||||||
@@ -55,9 +59,9 @@ Consul
|
|||||||
- **PATRONI\_CONSUL\_CONSISTENCY**: (optional) Select consul consistency mode. Possible values are ``default``, ``consistent``, or ``stale`` (more details in `consul API reference <https://www.consul.io/api/features/consistency.html/>`__)
|
- **PATRONI\_CONSUL\_CONSISTENCY**: (optional) Select consul consistency mode. Possible values are ``default``, ``consistent``, or ``stale`` (more details in `consul API reference <https://www.consul.io/api/features/consistency.html/>`__)
|
||||||
- **PATRONI\_CONSUL\_CHECKS**: (optional) list of Consul health checks used for the session. By default an empty list is used.
|
- **PATRONI\_CONSUL\_CHECKS**: (optional) list of Consul health checks used for the session. By default an empty list is used.
|
||||||
- **PATRONI\_CONSUL\_REGISTER\_SERVICE**: (optional) whether or not to register a service with the name defined by the scope parameter and the tag master, primary, replica, or standby-leader depending on the node's role. Defaults to **false**
|
- **PATRONI\_CONSUL\_REGISTER\_SERVICE**: (optional) whether or not to register a service with the name defined by the scope parameter and the tag master, primary, replica, or standby-leader depending on the node's role. Defaults to **false**
|
||||||
- **PATRONI\_CONSUL\_SERVICE\_TAGS**: (optional) additional static tags to add to the Consul service apart from the role (``master``/``primary``/``replica``/``standby-leader``). By default an empty list is used.
|
- **PATRONI\_CONSUL\_SERVICE\_TAGS**: (optional) additional static tags to add to the Consul service apart from the role (``primary``/``replica``/``standby-leader``). By default an empty list is used.
|
||||||
- **PATRONI\_CONSUL\_SERVICE\_CHECK\_INTERVAL**: (optional) how often to perform health check against registered url
|
- **PATRONI\_CONSUL\_SERVICE\_CHECK\_INTERVAL**: (optional) how often to perform health check against registered url
|
||||||
- **PATRONI\_CONSUL\_SERVICE\_CHECK\_TLS\_SERVER\_NAME**: (optional) overide SNI host when connecting via TLS, see also `consul agent check API reference <https://www.consul.io/api-docs/agent/check#tlsservername>`__.
|
- **PATRONI\_CONSUL\_SERVICE\_CHECK\_TLS\_SERVER\_NAME**: (optional) override SNI host when connecting via TLS, see also `consul agent check API reference <https://www.consul.io/api-docs/agent/check#tlsservername>`__.
|
||||||
|
|
||||||
Etcd
|
Etcd
|
||||||
----
|
----
|
||||||
@@ -81,7 +85,7 @@ Etcdv3
|
|||||||
Environment names for Etcdv3 are similar as for Etcd, you just need to use ``ETCD3`` instead of ``ETCD`` in the variable name. Example: ``PATRONI_ETCD3_HOST``, ``PATRONI_ETCD3_CACERT``, and so on.
|
Environment names for Etcdv3 are similar as for Etcd, you just need to use ``ETCD3`` instead of ``ETCD`` in the variable name. Example: ``PATRONI_ETCD3_HOST``, ``PATRONI_ETCD3_CACERT``, and so on.
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
Keys created with protocol version 2 are not visible with protocol version 3 and the other way around, therefore it is not possible to switch from Etcd to Etcdv3 just by updating Patroni configuration.
|
Keys created with protocol version 2 are not visible with protocol version 3 and the other way around, therefore it is not possible to switch from Etcd to Etcdv3 just by updating Patroni configuration. In addition, Patroni uses Etcd's gRPC-gateway (proxy) to communicate with the V3 API, which means that TLS common name authentication is not possible.
|
||||||
|
|
||||||
|
|
||||||
ZooKeeper
|
ZooKeeper
|
||||||
@@ -94,6 +98,7 @@ ZooKeeper
|
|||||||
- **PATRONI\_ZOOKEEPER\_KEY\_PASSWORD**: (optional) The client key password.
|
- **PATRONI\_ZOOKEEPER\_KEY\_PASSWORD**: (optional) The client key password.
|
||||||
- **PATRONI\_ZOOKEEPER\_VERIFY**: (optional) Whether to verify certificate or not. Defaults to ``true``.
|
- **PATRONI\_ZOOKEEPER\_VERIFY**: (optional) Whether to verify certificate or not. Defaults to ``true``.
|
||||||
- **PATRONI\_ZOOKEEPER\_SET\_ACLS**: (optional) If set, configure Kazoo to apply a default ACL to each ZNode that it creates. ACLs will assume 'x509' schema and should be specified as a dictionary with the principal as the key and one or more permissions as a list in the value. Permissions may be one of ``CREATE``, ``READ``, ``WRITE``, ``DELETE`` or ``ADMIN``. For example, ``set_acls: {CN=principal1: [CREATE, READ], CN=principal2: [ALL]}``.
|
- **PATRONI\_ZOOKEEPER\_SET\_ACLS**: (optional) If set, configure Kazoo to apply a default ACL to each ZNode that it creates. ACLs will assume 'x509' schema and should be specified as a dictionary with the principal as the key and one or more permissions as a list in the value. Permissions may be one of ``CREATE``, ``READ``, ``WRITE``, ``DELETE`` or ``ADMIN``. For example, ``set_acls: {CN=principal1: [CREATE, READ], CN=principal2: [ALL]}``.
|
||||||
|
- **PATRONI\_ZOOKEEPER\_AUTH\_DATA**: (optional) Authentication credentials to use for the connection. Should be a dictionary in the form that `scheme` is the key and `credential` is the value. Defaults to empty dictionary.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
It is required to install ``kazoo>=2.6.0`` to support SSL.
|
It is required to install ``kazoo>=2.6.0`` to support SSL.
|
||||||
@@ -112,11 +117,17 @@ Kubernetes
|
|||||||
- **PATRONI\_KUBERNETES\_NAMESPACE**: (optional) Kubernetes namespace where the Patroni pod is running. Default value is `default`.
|
- **PATRONI\_KUBERNETES\_NAMESPACE**: (optional) Kubernetes namespace where the Patroni pod is running. Default value is `default`.
|
||||||
- **PATRONI\_KUBERNETES\_LABELS**: Labels in format ``{label1: value1, label2: value2}``. These labels will be used to find existing objects (Pods and either Endpoints or ConfigMaps) associated with the current cluster. Also Patroni will set them on every object (Endpoint or ConfigMap) it creates.
|
- **PATRONI\_KUBERNETES\_LABELS**: Labels in format ``{label1: value1, label2: value2}``. These labels will be used to find existing objects (Pods and either Endpoints or ConfigMaps) associated with the current cluster. Also Patroni will set them on every object (Endpoint or ConfigMap) it creates.
|
||||||
- **PATRONI\_KUBERNETES\_SCOPE\_LABEL**: (optional) name of the label containing cluster name. Default value is `cluster-name`.
|
- **PATRONI\_KUBERNETES\_SCOPE\_LABEL**: (optional) name of the label containing cluster name. Default value is `cluster-name`.
|
||||||
- **PATRONI\_KUBERNETES\_ROLE\_LABEL**: (optional) name of the label containing Postgres role (`master` or `replica`). Patroni will set this label on the pod it is running in. Default value is `role`.
|
- **PATRONI\_KUBERNETES\_BOOTSTRAP\_LABELS**: (optional) Labels in format ``{label1: value1, label2: value2}``. These labels will be assigned to a Patroni pod when its state is either ``initializing new cluster``, ``running custom bootstrap script``, ``starting after custom bootstrap`` or ``creating replica``.
|
||||||
|
- **PATRONI\_KUBERNETES\_ROLE\_LABEL**: (optional) name of the label containing role (`primary`, `replica` or other custom value). Patroni will set this label on the pod it runs in. Default value is ``role``.
|
||||||
|
- **PATRONI\_KUBERNETES\_LEADER\_LABEL\_VALUE**: (optional) value of the pod label when Postgres role is `primary`. Default value is `primary`.
|
||||||
|
- **PATRONI\_KUBERNETES\_FOLLOWER\_LABEL\_VALUE**: (optional) value of the pod label when Postgres role is `replica`. Default value is `replica`.
|
||||||
|
- **PATRONI\_KUBERNETES\_STANDBY\_LEADER\_LABEL\_VALUE**: (optional) value of the pod label when Postgres role is ``standby_leader``. Default value is ``primary``.
|
||||||
|
- **PATRONI\_KUBERNETES\_TMP\_ROLE\_LABEL**: (optional) name of the temporary label containing role (`primary` or `replica`). Value of this label will always use the default of corresponding role. Set only when necessary.
|
||||||
- **PATRONI\_KUBERNETES\_USE\_ENDPOINTS**: (optional) if set to true, Patroni will use Endpoints instead of ConfigMaps to run leader elections and keep cluster state.
|
- **PATRONI\_KUBERNETES\_USE\_ENDPOINTS**: (optional) if set to true, Patroni will use Endpoints instead of ConfigMaps to run leader elections and keep cluster state.
|
||||||
- **PATRONI\_KUBERNETES\_POD\_IP**: (optional) IP address of the pod Patroni is running in. This value is required when `PATRONI_KUBERNETES_USE_ENDPOINTS` is enabled and is used to populate the leader endpoint subsets when the pod's PostgreSQL is promoted.
|
- **PATRONI\_KUBERNETES\_POD\_IP**: (optional) IP address of the pod Patroni is running in. This value is required when `PATRONI_KUBERNETES_USE_ENDPOINTS` is enabled and is used to populate the leader endpoint subsets when the pod's PostgreSQL is promoted.
|
||||||
- **PATRONI\_KUBERNETES\_PORTS**: (optional) if the Service object has the name for the port, the same name must appear in the Endpoint object, otherwise service won't work. For example, if your service is defined as ``{Kind: Service, spec: {ports: [{name: postgresql, port: 5432, targetPort: 5432}]}}``, then you have to set ``PATRONI_KUBERNETES_PORTS='[{"name": "postgresql", "port": 5432}]'`` and Patroni will use it for updating subsets of the leader Endpoint. This parameter is used only if `PATRONI_KUBERNETES_USE_ENDPOINTS` is set.
|
- **PATRONI\_KUBERNETES\_PORTS**: (optional) if the Service object has the name for the port, the same name must appear in the Endpoint object, otherwise service won't work. For example, if your service is defined as ``{Kind: Service, spec: {ports: [{name: postgresql, port: 5432, targetPort: 5432}]}}``, then you have to set ``PATRONI_KUBERNETES_PORTS='[{"name": "postgresql", "port": 5432}]'`` and Patroni will use it for updating subsets of the leader Endpoint. This parameter is used only if `PATRONI_KUBERNETES_USE_ENDPOINTS` is set.
|
||||||
- **PATRONI\_KUBERNETES\_CACERT**: (optional) Specifies the file with the CA_BUNDLE file with certificates of trusted CAs to use while verifying Kubernetes API SSL certs. If not provided, patroni will use the value provided by the ServiceAccount secret.
|
- **PATRONI\_KUBERNETES\_CACERT**: (optional) Specifies the file with the CA_BUNDLE file with certificates of trusted CAs to use while verifying Kubernetes API SSL certs. If not provided, patroni will use the value provided by the ServiceAccount secret.
|
||||||
|
- **PATRONI\_RETRIABLE\_HTTP\_CODES**: (optional) list of HTTP status codes from K8s API to retry on. By default Patroni is retrying on ``500``, ``503``, and ``504``, or if K8s API response has ``retry-after`` HTTP header.
|
||||||
|
|
||||||
Raft (deprecated)
|
Raft (deprecated)
|
||||||
-----------------
|
-----------------
|
||||||
@@ -134,7 +145,14 @@ PostgreSQL
|
|||||||
- **PATRONI\_POSTGRESQL\_PROXY\_ADDRESS**: IP address + port through which a connection pool (e.g. pgbouncer) running next to Postgres is accessible. The value is written to the member key in DCS as ``proxy_url`` and could be used/useful for service discovery.
|
- **PATRONI\_POSTGRESQL\_PROXY\_ADDRESS**: IP address + port through which a connection pool (e.g. pgbouncer) running next to Postgres is accessible. The value is written to the member key in DCS as ``proxy_url`` and could be used/useful for service discovery.
|
||||||
- **PATRONI\_POSTGRESQL\_DATA\_DIR**: The location of the Postgres data directory, either existing or to be initialized by Patroni.
|
- **PATRONI\_POSTGRESQL\_DATA\_DIR**: The location of the Postgres data directory, either existing or to be initialized by Patroni.
|
||||||
- **PATRONI\_POSTGRESQL\_CONFIG\_DIR**: The location of the Postgres configuration directory, defaults to the data directory. Must be writable by Patroni.
|
- **PATRONI\_POSTGRESQL\_CONFIG\_DIR**: The location of the Postgres configuration directory, defaults to the data directory. Must be writable by Patroni.
|
||||||
- **PATRONI\_POSTGRESQL\_BIN_DIR**: Path to PostgreSQL binaries. (pg_ctl, pg_rewind, pg_basebackup, postgres) The default value is an empty string meaning that PATH environment variable will be used to find the executables.
|
- **PATRONI\_POSTGRESQL\_BIN_DIR**: Path to PostgreSQL binaries. (pg_ctl, initdb, pg_controldata, pg_basebackup, postgres, pg_isready, pg_rewind) The default value is an empty string meaning that PATH environment variable will be used to find the executables.
|
||||||
|
- **PATRONI\_POSTGRESQL\_BIN\_PG\_CTL**: (optional) Custom name for ``pg_ctl`` binary.
|
||||||
|
- **PATRONI\_POSTGRESQL\_BIN\_INITDB**: (optional) Custom name for ``initdb`` binary.
|
||||||
|
- **PATRONI\_POSTGRESQL\_BIN\_PG\_CONTROLDATA**: (optional) Custom name for ``pg_controldata`` binary.
|
||||||
|
- **PATRONI\_POSTGRESQL\_BIN\_PG\_BASEBACKUP**: (optional) Custom name for ``pg_basebackup`` binary.
|
||||||
|
- **PATRONI\_POSTGRESQL\_BIN\_POSTGRES**: (optional) Custom name for ``postgres`` binary.
|
||||||
|
- **PATRONI\_POSTGRESQL\_BIN\_IS\_READY**: (optional) Custom name for ``pg_isready`` binary.
|
||||||
|
- **PATRONI\_POSTGRESQL\_BIN\_PG\_REWIND**: (optional) Custom name for ``pg_rewind`` binary.
|
||||||
- **PATRONI\_POSTGRESQL\_PGPASS**: path to the `.pgpass <https://www.postgresql.org/docs/current/static/libpq-pgpass.html>`__ password file. Patroni creates this file before executing pg\_basebackup and under some other circumstances. The location must be writable by Patroni.
|
- **PATRONI\_POSTGRESQL\_PGPASS**: path to the `.pgpass <https://www.postgresql.org/docs/current/static/libpq-pgpass.html>`__ password file. Patroni creates this file before executing pg\_basebackup and under some other circumstances. The location must be writable by Patroni.
|
||||||
- **PATRONI\_REPLICATION\_USERNAME**: replication username; the user will be created during initialization. Replicas will use this user to access the replication source via streaming replication
|
- **PATRONI\_REPLICATION\_USERNAME**: replication username; the user will be created during initialization. Replicas will use this user to access the replication source via streaming replication
|
||||||
- **PATRONI\_REPLICATION\_PASSWORD**: replication password; the user will be created during initialization.
|
- **PATRONI\_REPLICATION\_PASSWORD**: replication password; the user will be created during initialization.
|
||||||
@@ -142,9 +160,10 @@ PostgreSQL
|
|||||||
- **PATRONI\_REPLICATION\_SSLKEY**: (optional) maps to the `sslkey <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLKEY>`__ connection parameter, which specifies the location of the secret key used with the client's certificate.
|
- **PATRONI\_REPLICATION\_SSLKEY**: (optional) maps to the `sslkey <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLKEY>`__ connection parameter, which specifies the location of the secret key used with the client's certificate.
|
||||||
- **PATRONI\_REPLICATION\_SSLPASSWORD**: (optional) maps to the `sslpassword <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLPASSWORD>`__ connection parameter, which specifies the password for the secret key specified in ``PATRONI_REPLICATION_SSLKEY``.
|
- **PATRONI\_REPLICATION\_SSLPASSWORD**: (optional) maps to the `sslpassword <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLPASSWORD>`__ connection parameter, which specifies the password for the secret key specified in ``PATRONI_REPLICATION_SSLKEY``.
|
||||||
- **PATRONI\_REPLICATION\_SSLCERT**: (optional) maps to the `sslcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCERT>`__ connection parameter, which specifies the location of the client certificate.
|
- **PATRONI\_REPLICATION\_SSLCERT**: (optional) maps to the `sslcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCERT>`__ connection parameter, which specifies the location of the client certificate.
|
||||||
- **PATRONI\_REPLICATION\_SSLROOTCERT**: (optional) maps to the `sslrootcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLROOTCERT>`__ connection parameter, which specifies the location of a file containing one ore more certificate authorities (CA) certificates that the client will use to verify a server's certificate.
|
- **PATRONI\_REPLICATION\_SSLROOTCERT**: (optional) maps to the `sslrootcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLROOTCERT>`__ connection parameter, which specifies the location of a file containing one or more certificate authorities (CA) certificates that the client will use to verify a server's certificate.
|
||||||
- **PATRONI\_REPLICATION\_SSLCRL**: (optional) maps to the `sslcrl <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRL>`__ connection parameter, which specifies the location of a file containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
- **PATRONI\_REPLICATION\_SSLCRL**: (optional) maps to the `sslcrl <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRL>`__ connection parameter, which specifies the location of a file containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
||||||
- **PATRONI\_REPLICATION\_SSLCRLDIR**: (optional) maps to the `sslcrldir <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRLDIR>`__ connection parameter, which specifies the location of a directory with files containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
- **PATRONI\_REPLICATION\_SSLCRLDIR**: (optional) maps to the `sslcrldir <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRLDIR>`__ connection parameter, which specifies the location of a directory with files containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
||||||
|
- **PATRONI\_REPLICATION\_SSLNEGOTIATION**: (optional) maps to the `sslnegotiation <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLNEGOTIATION>`__ connection parameter, which controls how SSL encryption is negotiated with the server, if SSL is used.
|
||||||
- **PATRONI\_REPLICATION\_GSSENCMODE**: (optional) maps to the `gssencmode <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-GSSENCMODE>`__ connection parameter, which determines whether or with what priority a secure GSS TCP/IP connection will be negotiated with the server
|
- **PATRONI\_REPLICATION\_GSSENCMODE**: (optional) maps to the `gssencmode <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-GSSENCMODE>`__ connection parameter, which determines whether or with what priority a secure GSS TCP/IP connection will be negotiated with the server
|
||||||
- **PATRONI\_REPLICATION\_CHANNEL\_BINDING**: (optional) maps to the `channel_binding <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-CHANNEL-BINDING>`__ connection parameter, which controls the client's use of channel binding.
|
- **PATRONI\_REPLICATION\_CHANNEL\_BINDING**: (optional) maps to the `channel_binding <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-CHANNEL-BINDING>`__ connection parameter, which controls the client's use of channel binding.
|
||||||
- **PATRONI\_SUPERUSER\_USERNAME**: name for the superuser, set during initialization (initdb) and later used by Patroni to connect to the postgres. Also this user is used by pg_rewind.
|
- **PATRONI\_SUPERUSER\_USERNAME**: name for the superuser, set during initialization (initdb) and later used by Patroni to connect to the postgres. Also this user is used by pg_rewind.
|
||||||
@@ -153,20 +172,22 @@ PostgreSQL
|
|||||||
- **PATRONI\_SUPERUSER\_SSLKEY**: (optional) maps to the `sslkey <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLKEY>`__ connection parameter, which specifies the location of the secret key used with the client's certificate.
|
- **PATRONI\_SUPERUSER\_SSLKEY**: (optional) maps to the `sslkey <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLKEY>`__ connection parameter, which specifies the location of the secret key used with the client's certificate.
|
||||||
- **PATRONI\_SUPERUSER\_SSLPASSWORD**: (optional) maps to the `sslpassword <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLPASSWORD>`__ connection parameter, which specifies the password for the secret key specified in ``PATRONI_SUPERUSER_SSLKEY``.
|
- **PATRONI\_SUPERUSER\_SSLPASSWORD**: (optional) maps to the `sslpassword <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLPASSWORD>`__ connection parameter, which specifies the password for the secret key specified in ``PATRONI_SUPERUSER_SSLKEY``.
|
||||||
- **PATRONI\_SUPERUSER\_SSLCERT**: (optional) maps to the `sslcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCERT>`__ connection parameter, which specifies the location of the client certificate.
|
- **PATRONI\_SUPERUSER\_SSLCERT**: (optional) maps to the `sslcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCERT>`__ connection parameter, which specifies the location of the client certificate.
|
||||||
- **PATRONI\_SUPERUSER\_SSLROOTCERT**: (optional) maps to the `sslrootcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLROOTCERT>`__ connection parameter, which specifies the location of a file containing one ore more certificate authorities (CA) certificates that the client will use to verify a server's certificate.
|
- **PATRONI\_SUPERUSER\_SSLROOTCERT**: (optional) maps to the `sslrootcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLROOTCERT>`__ connection parameter, which specifies the location of a file containing one or more certificate authorities (CA) certificates that the client will use to verify a server's certificate.
|
||||||
- **PATRONI\_SUPERUSER\_SSLCRL**: (optional) maps to the `sslcrl <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRL>`__ connection parameter, which specifies the location of a file containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
- **PATRONI\_SUPERUSER\_SSLCRL**: (optional) maps to the `sslcrl <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRL>`__ connection parameter, which specifies the location of a file containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
||||||
- **PATRONI\_SUPERUSER\_SSLCRLDIR**: (optional) maps to the `sslcrldir <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRLDIR>`__ connection parameter, which specifies the location of a directory with files containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
- **PATRONI\_SUPERUSER\_SSLCRLDIR**: (optional) maps to the `sslcrldir <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRLDIR>`__ connection parameter, which specifies the location of a directory with files containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
||||||
|
- **PATRONI\_SUPERUSER\_SSLNEGOTIATION**: (optional) maps to the `sslnegotiation <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLNEGOTIATION>`__ connection parameter, which controls how SSL encryption is negotiated with the server, if SSL is used.
|
||||||
- **PATRONI\_SUPERUSER\_GSSENCMODE**: (optional) maps to the `gssencmode <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-GSSENCMODE>`__ connection parameter, which determines whether or with what priority a secure GSS TCP/IP connection will be negotiated with the server
|
- **PATRONI\_SUPERUSER\_GSSENCMODE**: (optional) maps to the `gssencmode <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-GSSENCMODE>`__ connection parameter, which determines whether or with what priority a secure GSS TCP/IP connection will be negotiated with the server
|
||||||
- **PATRONI\_SUPERUSER\_CHANNEL\_BINDING**: (optional) maps to the `channel_binding <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-CHANNEL-BINDING>`__ connection parameter, which controls the client's use of channel binding.
|
- **PATRONI\_SUPERUSER\_CHANNEL\_BINDING**: (optional) maps to the `channel_binding <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-CHANNEL-BINDING>`__ connection parameter, which controls the client's use of channel binding.
|
||||||
- **PATRONI\_REWIND\_USERNAME**: name for the user for ``pg_rewind``; the user will be created during initialization of postgres 11+ and all necessary `permissions <https://www.postgresql.org/docs/11/app-pgrewind.html#id-1.9.5.8.8>`__ will be granted.
|
- **PATRONI\_REWIND\_USERNAME**: (optional) name for the user for ``pg_rewind``; the user will be created during initialization of postgres 11+ and all necessary `permissions <https://www.postgresql.org/docs/11/app-pgrewind.html#id-1.9.5.8.8>`__ will be granted.
|
||||||
- **PATRONI\_REWIND\_PASSWORD**: password for the user for ``pg_rewind``; the user will be created during initialization.
|
- **PATRONI\_REWIND\_PASSWORD**: (optional) password for the user for ``pg_rewind``; the user will be created during initialization.
|
||||||
- **PATRONI\_REWIND\_SSLMODE**: (optional) maps to the `sslmode <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLMODE>`__ connection parameter, which allows a client to specify the type of TLS negotiation mode with the server. For more information on how each mode works, please visit the `PostgreSQL documentation <https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS>`__. The default mode is ``prefer``.
|
- **PATRONI\_REWIND\_SSLMODE**: (optional) maps to the `sslmode <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLMODE>`__ connection parameter, which allows a client to specify the type of TLS negotiation mode with the server. For more information on how each mode works, please visit the `PostgreSQL documentation <https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS>`__. The default mode is ``prefer``.
|
||||||
- **PATRONI\_REWIND\_SSLKEY**: (optional) maps to the `sslkey <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLKEY>`__ connection parameter, which specifies the location of the secret key used with the client's certificate.
|
- **PATRONI\_REWIND\_SSLKEY**: (optional) maps to the `sslkey <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLKEY>`__ connection parameter, which specifies the location of the secret key used with the client's certificate.
|
||||||
- **PATRONI\_REWIND\_SSLPASSWORD**: (optional) maps to the `sslpassword <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLPASSWORD>`__ connection parameter, which specifies the password for the secret key specified in ``PATRONI_REWIND_SSLKEY``.
|
- **PATRONI\_REWIND\_SSLPASSWORD**: (optional) maps to the `sslpassword <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLPASSWORD>`__ connection parameter, which specifies the password for the secret key specified in ``PATRONI_REWIND_SSLKEY``.
|
||||||
- **PATRONI\_REWIND\_SSLCERT**: (optional) maps to the `sslcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCERT>`__ connection parameter, which specifies the location of the client certificate.
|
- **PATRONI\_REWIND\_SSLCERT**: (optional) maps to the `sslcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCERT>`__ connection parameter, which specifies the location of the client certificate.
|
||||||
- **PATRONI\_REWIND\_SSLROOTCERT**: (optional) maps to the `sslrootcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLROOTCERT>`__ connection parameter, which specifies the location of a file containing one ore more certificate authorities (CA) certificates that the client will use to verify a server's certificate.
|
- **PATRONI\_REWIND\_SSLROOTCERT**: (optional) maps to the `sslrootcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLROOTCERT>`__ connection parameter, which specifies the location of a file containing one or more certificate authorities (CA) certificates that the client will use to verify a server's certificate.
|
||||||
- **PATRONI\_REWIND\_SSLCRL**: (optional) maps to the `sslcrl <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRL>`__ connection parameter, which specifies the location of a file containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
- **PATRONI\_REWIND\_SSLCRL**: (optional) maps to the `sslcrl <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRL>`__ connection parameter, which specifies the location of a file containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
||||||
- **PATRONI\_REWIND\_SSLCRLDIR**: (optional) maps to the `sslcrldir <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRLDIR>`__ connection parameter, which specifies the location of a directory with files containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
- **PATRONI\_REWIND\_SSLCRLDIR**: (optional) maps to the `sslcrldir <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRLDIR>`__ connection parameter, which specifies the location of a directory with files containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
||||||
|
- **PATRONI\_REWIND\_SSLNEGOTIATION**: (optional) maps to the `sslnegotiation <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLNEGOTIATION>`__ connection parameter, which controls how SSL encryption is negotiated with the server, if SSL is used.
|
||||||
- **PATRONI\_REWIND\_GSSENCMODE**: (optional) maps to the `gssencmode <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-GSSENCMODE>`__ connection parameter, which determines whether or with what priority a secure GSS TCP/IP connection will be negotiated with the server
|
- **PATRONI\_REWIND\_GSSENCMODE**: (optional) maps to the `gssencmode <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-GSSENCMODE>`__ connection parameter, which determines whether or with what priority a secure GSS TCP/IP connection will be negotiated with the server
|
||||||
- **PATRONI\_REWIND\_CHANNEL\_BINDING**: (optional) maps to the `channel_binding <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-CHANNEL-BINDING>`__ connection parameter, which controls the client's use of channel binding.
|
- **PATRONI\_REWIND\_CHANNEL\_BINDING**: (optional) maps to the `channel_binding <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-CHANNEL-BINDING>`__ connection parameter, which controls the client's use of channel binding.
|
||||||
|
|
||||||
@@ -186,11 +207,21 @@ REST API
|
|||||||
- **PATRONI\_RESTAPI\_ALLOWLIST\_INCLUDE\_MEMBERS**: (optional): If set to ``true`` it allows accessing unsafe REST API endpoints from other cluster members registered in DCS (IP address or hostname is taken from the members ``api_url``). Be careful, it might happen that OS will use a different IP for outgoing connections.
|
- **PATRONI\_RESTAPI\_ALLOWLIST\_INCLUDE\_MEMBERS**: (optional): If set to ``true`` it allows accessing unsafe REST API endpoints from other cluster members registered in DCS (IP address or hostname is taken from the members ``api_url``). Be careful, it might happen that OS will use a different IP for outgoing connections.
|
||||||
- **PATRONI\_RESTAPI\_HTTP\_EXTRA\_HEADERS**: (optional) HTTP headers let the REST API server pass additional information with an HTTP response.
|
- **PATRONI\_RESTAPI\_HTTP\_EXTRA\_HEADERS**: (optional) HTTP headers let the REST API server pass additional information with an HTTP response.
|
||||||
- **PATRONI\_RESTAPI\_HTTPS\_EXTRA\_HEADERS**: (optional) HTTPS headers let the REST API server pass additional information with an HTTP response when TLS is enabled. This will also pass additional information set in ``http_extra_headers``.
|
- **PATRONI\_RESTAPI\_HTTPS\_EXTRA\_HEADERS**: (optional) HTTPS headers let the REST API server pass additional information with an HTTP response when TLS is enabled. This will also pass additional information set in ``http_extra_headers``.
|
||||||
|
- **PATRONI\_RESTAPI\_REQUEST\_QUEUE\_SIZE**: (optional): Sets request queue size for TCP socket used by Patroni REST API. Once the queue is full, further requests get a "Connection denied" error. The default value is 5.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
- The ``PATRONI_RESTAPI_CONNECT_ADDRESS`` must be accessible from all nodes of a given Patroni cluster. Internally Patroni is using it during the leader race to find nodes with minimal replication lag.
|
||||||
|
- If you enabled client certificates validation (``PATRONI_RESTAPI_VERIFY_CLIENT`` is set to ``required``), you also **must** provide **valid client certificates** in the ``PATRONI_CTL_CERTFILE``, ``PATRONI_CTL_KEYFILE``, ``PATRONI_CTL_KEYFILE_PASSWORD``. If not provided, Patroni will not work correctly.
|
||||||
|
|
||||||
|
|
||||||
CTL
|
CTL
|
||||||
---
|
---
|
||||||
- **PATRONICTL\_CONFIG\_FILE**: location of the configuration file.
|
- **PATRONICTL\_CONFIG\_FILE**: (optional) location of the configuration file.
|
||||||
- **PATRONI\_CTL\_INSECURE**: Allow connections to REST API without verifying SSL certs.
|
- **PATRONI\_CTL\_USERNAME**: (optional) Basic-auth username for accessing protected REST API endpoints. If not provided :ref:`patronictl` will use the value provided for REST API "username" parameter.
|
||||||
- **PATRONI\_CTL\_CACERT**: Specifies the file with the CA_BUNDLE file or directory with certificates of trusted CAs to use while verifying REST API SSL certs. If not provided patronictl will use the value provided for REST API "cafile" parameter.
|
- **PATRONI\_CTL\_PASSWORD**: (optional) Basic-auth password for accessing protected REST API endpoints. If not provided :ref:`patronictl` will use the value provided for REST API "password" parameter.
|
||||||
- **PATRONI\_CTL\_CERTFILE**: Specifies the file with the client certificate in the PEM format. If not provided patronictl will use the value provided for REST API "certfile" parameter.
|
- **PATRONI\_CTL\_INSECURE**: (optional) Allow connections to REST API without verifying SSL certs.
|
||||||
- **PATRONI\_CTL\_KEYFILE**: Specifies the file with the client secret key in the PEM format. If not provided patronictl will use the value provided for REST API "keyfile" parameter.
|
- **PATRONI\_CTL\_CACERT**: (optional) Specifies the file with the CA_BUNDLE file or directory with certificates of trusted CAs to use while verifying REST API SSL certs. If not provided :ref:`patronictl` will use the value provided for REST API "cafile" parameter.
|
||||||
|
- **PATRONI\_CTL\_CERTFILE**: (optional) Specifies the file with the client certificate in the PEM format.
|
||||||
|
- **PATRONI\_CTL\_KEYFILE**: (optional) Specifies the file with the client secret key in the PEM format.
|
||||||
|
- **PATRONI\_CTL\_KEYFILE\_PASSWORD**: (optional) Specifies a password for decrypting the client keyfile.
|
||||||
|
|||||||
+6
-88
@@ -4,14 +4,12 @@
|
|||||||
Introduction
|
Introduction
|
||||||
============
|
============
|
||||||
|
|
||||||
Patroni originated as a fork of `Governor <https://github.com/compose/governor>`__, the project from Compose. It includes plenty of new features.
|
Patroni is a template for high availability (HA) PostgreSQL solutions using Python. Patroni originated as a fork of `Governor <https://github.com/compose/governor>`__, the project from Compose. It includes plenty of new features.
|
||||||
|
|
||||||
For an example of a Docker-based deployment with Patroni, see `Spilo <https://github.com/zalando/spilo>`__, currently in use at Zalando.
|
|
||||||
|
|
||||||
For additional background info, see:
|
For additional background info, see:
|
||||||
|
|
||||||
* `PostgreSQL HA with Kubernetes and Patroni <https://www.youtube.com/watch?v=iruaCgeG7qs>`__, talk by Josh Berkus at KubeCon 2016 (video)
|
* `PostgreSQL HA with Kubernetes and Patroni <https://www.youtube.com/watch?v=iruaCgeG7qs>`__, talk by Josh Berkus at KubeCon 2016 (video)
|
||||||
* `Feb. 2016 Zalando Tech blog post <https://tech.zalando.de/blog/zalandos-patroni-a-template-for-high-availability-postgresql/>`__
|
* `Feb. 2016 Zalando Tech blog post <https://engineering.zalando.com/posts/2016/02/zalandos-patroni-a-template-for-high-availability-postgresql.html>`__
|
||||||
|
|
||||||
|
|
||||||
Development Status
|
Development Status
|
||||||
@@ -25,83 +23,7 @@ We report new releases information :ref:`here <releases>`.
|
|||||||
Technical Requirements/Installation
|
Technical Requirements/Installation
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
**Pre-requirements for Mac OS**
|
Go :ref:`here <installation>` for guidance on installing and upgrading Patroni on various platforms.
|
||||||
|
|
||||||
To install requirements on a Mac, run the following:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
brew install postgresql etcd haproxy libyaml python
|
|
||||||
|
|
||||||
.. _psycopg2_install_options:
|
|
||||||
|
|
||||||
**Psycopg**
|
|
||||||
|
|
||||||
Starting from `psycopg2-2.8 <http://initd.org/psycopg/articles/2019/04/04/psycopg-28-released/>`__ the binary version of psycopg2 will no longer be installed by default. Installing it from the source code requires C compiler and postgres+python dev packages.
|
|
||||||
Since in the python world it is not possible to specify dependency as ``psycopg2 OR psycopg2-binary`` you will have to decide how to install it.
|
|
||||||
|
|
||||||
There are a few options available:
|
|
||||||
|
|
||||||
1. Use the package manager from your distro
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
sudo apt-get install python-psycopg2 # install python2 psycopg2 module on Debian/Ubuntu
|
|
||||||
sudo apt-get install python3-psycopg2 # install python3 psycopg2 module on Debian/Ubuntu
|
|
||||||
sudo yum install python-psycopg2 # install python2 psycopg2 on RedHat/Fedora/CentOS
|
|
||||||
|
|
||||||
2. Install psycopg2 from the binary package
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
pip install psycopg2-binary
|
|
||||||
|
|
||||||
3. Install psycopg2 from source
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
pip install psycopg2>=2.5.4
|
|
||||||
|
|
||||||
4. Use psycopg 3.0 instead of psycopg2
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
pip install psycopg[binary]>=3.0.0
|
|
||||||
|
|
||||||
**General installation for pip**
|
|
||||||
|
|
||||||
Patroni can be installed with pip:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
pip install patroni[dependencies]
|
|
||||||
|
|
||||||
where dependencies can be either empty, or consist of one or more of the following:
|
|
||||||
|
|
||||||
etcd or etcd3
|
|
||||||
`python-etcd` module in order to use Etcd as Distributed Configuration Store (DCS)
|
|
||||||
consul
|
|
||||||
`python-consul` module in order to use Consul as DCS
|
|
||||||
zookeeper
|
|
||||||
`kazoo` module in order to use Zookeeper as DCS
|
|
||||||
exhibitor
|
|
||||||
`kazoo` module in order to use Exhibitor as DCS (same dependencies as for Zookeeper)
|
|
||||||
kubernetes
|
|
||||||
`kubernetes` module in order to use Kubernetes as DCS in Patroni
|
|
||||||
raft
|
|
||||||
`pysyncobj` module in order to use python Raft implementation as DCS
|
|
||||||
aws
|
|
||||||
`boto` in order to use AWS callbacks
|
|
||||||
|
|
||||||
For example, the command in order to install Patroni together with dependencies for Etcd as a DCS and AWS callbacks is:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
pip install patroni[etcd,aws]
|
|
||||||
|
|
||||||
Note that external tools to call in the replica creation or custom bootstrap scripts (i.e. WAL-E) should be installed
|
|
||||||
independently of Patroni.
|
|
||||||
|
|
||||||
|
|
||||||
.. _running_configuring:
|
.. _running_configuring:
|
||||||
|
|
||||||
@@ -115,7 +37,7 @@ perfectly fine. You can add more standby nodes later.
|
|||||||
Running and Configuring
|
Running and Configuring
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
The following section assumes Patroni repository as being cloned from https://github.com/zalando/patroni. Namely, you
|
The following section assumes Patroni repository as being cloned from https://github.com/patroni/patroni. Namely, you
|
||||||
will need example configuration files `postgres0.yml` and `postgres1.yml`. If you installed Patroni with pip, you can
|
will need example configuration files `postgres0.yml` and `postgres1.yml`. If you installed Patroni with pip, you can
|
||||||
obtain those files from the git repository and replace `./patroni.py` below with `patroni` command.
|
obtain those files from the git repository and replace `./patroni.py` below with `patroni` command.
|
||||||
|
|
||||||
@@ -145,7 +67,7 @@ run:
|
|||||||
YAML Configuration
|
YAML Configuration
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
Go :ref:`here <settings>` for comprehensive information about settings for etcd, consul, and ZooKeeper. And for an example, see `postgres0.yml <https://github.com/zalando/patroni/blob/master/postgres0.yml>`__.
|
Go :ref:`here <yaml_configuration>` for comprehensive information about settings for etcd, consul, and ZooKeeper. And for an example, see `postgres0.yml <https://github.com/patroni/patroni/blob/master/postgres0.yml>`__.
|
||||||
|
|
||||||
|
|
||||||
Environment Configuration
|
Environment Configuration
|
||||||
@@ -165,10 +87,6 @@ Applications Should Not Use Superusers
|
|||||||
|
|
||||||
When connecting from an application, always use a non-superuser. Patroni requires access to the database to function properly. By using a superuser from an application, you can potentially use the entire connection pool, including the connections reserved for superusers, with the ``superuser_reserved_connections`` setting. If Patroni cannot access the Primary because the connection pool is full, behavior will be undesirable.
|
When connecting from an application, always use a non-superuser. Patroni requires access to the database to function properly. By using a superuser from an application, you can potentially use the entire connection pool, including the connections reserved for superusers, with the ``superuser_reserved_connections`` setting. If Patroni cannot access the Primary because the connection pool is full, behavior will be undesirable.
|
||||||
|
|
||||||
.. |Build Status| image:: https://travis-ci.org/zalando/patroni.svg?branch=master
|
|
||||||
:target: https://travis-ci.org/zalando/patroni
|
|
||||||
.. |Coverage Status| image:: https://coveralls.io/repos/zalando/patroni/badge.svg?branch=master
|
|
||||||
:target: https://coveralls.io/r/zalando/patroni?branch=master
|
|
||||||
|
|
||||||
Testing Your HA Solution
|
Testing Your HA Solution
|
||||||
--------------------------------------
|
--------------------------------------
|
||||||
@@ -179,7 +97,7 @@ That said, here are some pieces of your infrastructure you should be sure to tes
|
|||||||
* Network (the network in front of your system as well as the NICs [physical or virtual] themselves)
|
* Network (the network in front of your system as well as the NICs [physical or virtual] themselves)
|
||||||
* Disk IO
|
* Disk IO
|
||||||
* file limits (nofile in Linux)
|
* file limits (nofile in Linux)
|
||||||
* RAM. Even if you have oomkiller turned off as suggested, the unavailability of RAM could cause issues.
|
* RAM. Even if you have oomkiller turned off, the unavailability of RAM could cause issues.
|
||||||
* CPU
|
* CPU
|
||||||
* Virtualization Contention (overcommitting the hypervisor)
|
* Virtualization Contention (overcommitting the hypervisor)
|
||||||
* Any cgroup limitation (likely to be related to the above)
|
* Any cgroup limitation (likely to be related to the above)
|
||||||
|
|||||||
@@ -1,409 +0,0 @@
|
|||||||
.. _settings:
|
|
||||||
|
|
||||||
===========================
|
|
||||||
YAML Configuration Settings
|
|
||||||
===========================
|
|
||||||
|
|
||||||
.. _dynamic_configuration_settings:
|
|
||||||
|
|
||||||
Dynamic configuration settings
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
Dynamic configuration is stored in the DCS (Distributed Configuration Store) and applied on all cluster nodes. Some parameters, like **loop_wait**, **ttl**, **postgresql.parameters.max_connections**, **postgresql.parameters.max_worker_processes** and so on could be set only in the dynamic configuration. Some other parameters like **postgresql.listen**, **postgresql.data_dir** could be set only locally, i.e. in the Patroni config file or via :ref:`configuration <environment>` variable. In most cases the local configuration will override the dynamic configuration. In order to change the dynamic configuration you can use either ``patronictl edit-config`` tool or Patroni :ref:`REST API <rest_api>`.
|
|
||||||
|
|
||||||
- **loop\_wait**: the number of seconds the loop will sleep. Default value: 10
|
|
||||||
- **ttl**: the TTL to acquire the leader lock (in seconds). Think of it as the length of time before initiation of the automatic failover process. Default value: 30
|
|
||||||
- **retry\_timeout**: timeout for DCS and PostgreSQL operation retries (in seconds). DCS or network issues shorter than this will not cause Patroni to demote the leader. Default value: 10
|
|
||||||
- **maximum\_lag\_on\_failover**: the maximum bytes a follower may lag to be able to participate in leader election.
|
|
||||||
- **maximum\_lag\_on\_syncnode**: the maximum bytes a synchronous follower may lag before it is considered as an unhealthy candidate and swapped by healthy asynchronous follower. Patroni utilize the max replica lsn if there is more than one follower, otherwise it will use leader's current wal lsn. Default is -1, Patroni will not take action to swap synchronous unhealthy follower when the value is set to 0 or below. Please set the value high enough so Patroni won't swap synchrounous follower fequently during high transaction volume.
|
|
||||||
- **max\_timelines\_history**: maximum number of timeline history items kept in DCS. Default value: 0. When set to 0, it keeps the full history in DCS.
|
|
||||||
- **primary\_start\_timeout**: the amount of time a primary is allowed to recover from failures before failover is triggered (in seconds). Default is 300 seconds. When set to 0 failover is done immediately after a crash is detected if possible. When using asynchronous replication a failover can cause lost transactions. Worst case failover time for primary failure is: loop\_wait + primary\_start\_timeout + loop\_wait, unless primary\_start\_timeout is zero, in which case it's just loop\_wait. Set the value according to your durability/availability tradeoff.
|
|
||||||
- **primary\_stop\_timeout**: The number of seconds Patroni is allowed to wait when stopping Postgres and effective only when synchronous_mode is enabled. When set to > 0 and the synchronous_mode is enabled, Patroni sends SIGKILL to the postmaster if the stop operation is running for more than the value set by primary\_stop\_timeout. Set the value according to your durability/availability tradeoff. If the parameter is not set or set <= 0, primary\_stop\_timeout does not apply.
|
|
||||||
- **synchronous\_mode**: turns on synchronous replication mode. In this mode a replica will be chosen as synchronous and only the latest leader and synchronous replica are able to participate in leader election. Synchronous mode makes sure that successfully committed transactions will not be lost at failover, at the cost of losing availability for writes when Patroni cannot ensure transaction durability. See :ref:`replication modes documentation <replication_modes>` for details.
|
|
||||||
- **synchronous\_mode\_strict**: prevents disabling synchronous replication if no synchronous replicas are available, blocking all client writes to the primary. See :ref:`replication modes documentation <replication_modes>` for details.
|
|
||||||
- **failsafe\_mode**: Enables :ref:`DCS Failsafe Mode <dcs_failsafe_mode>`. Defaults to `false`.
|
|
||||||
- **postgresql**:
|
|
||||||
- **use\_pg\_rewind**: whether or not to use pg_rewind. Defaults to `false`.
|
|
||||||
- **use\_slots**: whether or not to use replication slots. Defaults to `true` on PostgreSQL 9.4+.
|
|
||||||
- **recovery\_conf**: additional configuration settings written to recovery.conf when configuring follower. There is no recovery.conf anymore in PostgreSQL 12, but you may continue using this section, because Patroni handles it transparently.
|
|
||||||
- **parameters**: list of configuration settings for Postgres.
|
|
||||||
- **standby\_cluster**: if this section is defined, we want to bootstrap a standby cluster.
|
|
||||||
- **host**: an address of remote node
|
|
||||||
- **port**: a port of remote node
|
|
||||||
- **primary\_slot\_name**: which slot on the remote node to use for replication. This parameter is optional, the default value is derived from the instance name (see function `slot_name_from_member_name`).
|
|
||||||
- **create\_replica\_methods**: an ordered list of methods that can be used to bootstrap standby leader from the remote primary, can be different from the list defined in :ref:`postgresql_settings`
|
|
||||||
- **restore\_command**: command to restore WAL records from the remote primary to nodes in a standby cluster, can be different from the list defined in :ref:`postgresql_settings`
|
|
||||||
- **archive\_cleanup\_command**: cleanup command for standby leader
|
|
||||||
- **recovery\_min\_apply\_delay**: how long to wait before actually apply WAL records on a standby leader
|
|
||||||
- **slots**: define permanent replication slots. These slots will be preserved during switchover/failover. The logical slots are copied from the primary to a standby with restart, and after that their position advanced every **loop_wait** seconds (if necessary). Copying logical slot files performed via ``libpq`` connection and using either rewind or superuser credentials (see **postgresql.authentication** section). There is always a chance that the logical slot position on the replica is a bit behind the former primary, therefore application should be prepared that some messages could be received the second time after the failover. The easiest way of doing so - tracking ``confirmed_flush_lsn``. Enabling permanent logical replication slots requires **postgresql.use_slots** to be set and will also automatically enable the ``hot_standby_feedback``. Since the failover of logical replication slots is unsafe on PostgreSQL 9.6 and older and PostgreSQL version 10 is missing some important functions, the feature only works with PostgreSQL 11+.
|
|
||||||
- **my_slot_name**: the name of replication slot. If the permanent slot name matches with the name of the current primary it will not be created. Everything else is the responsibility of the operator to make sure that there are no clashes in names between replication slots automatically created by Patroni for members and permanent replication slots.
|
|
||||||
- **type**: slot type. Could be ``physical`` or ``logical``. If the slot is logical, you have to additionally define ``database`` and ``plugin``.
|
|
||||||
- **database**: the database name where logical slots should be created.
|
|
||||||
- **plugin**: the plugin name for the logical slot.
|
|
||||||
- **ignore_slots**: list of sets of replication slot properties for which Patroni should ignore matching slots. This configuration/feature/etc. is useful when some replication slots are managed outside of Patroni. Any subset of matching properties will cause a slot to be ignored.
|
|
||||||
- **name**: the name of the replication slot.
|
|
||||||
- **type**: slot type. Can be ``physical`` or ``logical``. If the slot is logical, you may additionally define ``database`` and/or ``plugin``.
|
|
||||||
- **database**: the database name (when matching a ``logical`` slot).
|
|
||||||
- **plugin**: the logical decoding plugin (when matching a ``logical`` slot).
|
|
||||||
|
|
||||||
Note: **slots** is a hashmap while **ignore_slots** is an array. For example:
|
|
||||||
|
|
||||||
.. code:: YAML
|
|
||||||
|
|
||||||
slots:
|
|
||||||
permanent_logical_slot_name:
|
|
||||||
type: logical
|
|
||||||
database: my_db
|
|
||||||
plugin: test_decoding
|
|
||||||
permanent_physical_slot_name:
|
|
||||||
type: physical
|
|
||||||
...
|
|
||||||
ignore_slots:
|
|
||||||
- name: ignored_logical_slot_name
|
|
||||||
type: logical
|
|
||||||
database: my_db
|
|
||||||
plugin: test_decoding
|
|
||||||
- name: ignored_physical_slot_name
|
|
||||||
type: physical
|
|
||||||
...
|
|
||||||
|
|
||||||
Global/Universal
|
|
||||||
----------------
|
|
||||||
- **name**: the name of the host. Must be unique for the cluster.
|
|
||||||
- **namespace**: path within the configuration store where Patroni will keep information about the cluster. Default value: "/service"
|
|
||||||
- **scope**: cluster name
|
|
||||||
|
|
||||||
Log
|
|
||||||
---
|
|
||||||
- **level**: sets the general logging level. Default value is **INFO** (see `the docs for Python logging <https://docs.python.org/3.6/library/logging.html#levels>`_)
|
|
||||||
- **traceback\_level**: sets the level where tracebacks will be visible. Default value is **ERROR**. Set it to **DEBUG** if you want to see tracebacks only if you enable **log.level=DEBUG**.
|
|
||||||
- **format**: sets the log formatting string. Default value is **%(asctime)s %(levelname)s: %(message)s** (see `the LogRecord attributes <https://docs.python.org/3.6/library/logging.html#logrecord-attributes>`_)
|
|
||||||
- **dateformat**: sets the datetime formatting string. (see the `formatTime() documentation <https://docs.python.org/3.6/library/logging.html#logging.Formatter.formatTime>`_)
|
|
||||||
- **max\_queue\_size**: Patroni is using two-step logging. Log records are written into the in-memory queue and there is a separate thread which pulls them from the queue and writes to stderr or file. The maximum size of the internal queue is limited by default by **1000** records, which is enough to keep logs for the past 1h20m.
|
|
||||||
- **dir**: Directory to write application logs to. The directory must exist and be writable by the user executing Patroni. If you set this value, the application will retain 4 25MB logs by default. You can tune those retention values with `file_num` and `file_size` (see below).
|
|
||||||
- **file\_num**: The number of application logs to retain.
|
|
||||||
- **file\_size**: Size of patroni.log file (in bytes) that triggers a log rolling.
|
|
||||||
- **loggers**: This section allows redefining logging level per python module
|
|
||||||
- **patroni.postmaster: WARNING**
|
|
||||||
- **urllib3: DEBUG**
|
|
||||||
|
|
||||||
.. _bootstrap_settings:
|
|
||||||
|
|
||||||
Bootstrap configuration
|
|
||||||
-----------------------
|
|
||||||
- **bootstrap**:
|
|
||||||
- **dcs**: This section will be written into `/<namespace>/<scope>/config` of the given configuration store after initializing of new cluster. The global dynamic configuration for the cluster. Under the ``bootstrap.dcs`` you can put any of the parameters described in the :ref:`Dynamic Configuration settings <dynamic_configuration_settings>` and after Patroni initialized (bootstrapped) the new cluster, it will write this section into `/<namespace>/<scope>/config` of the configuration store. All later changes of ``bootstrap.dcs`` will not take any effect! If you want to change them please use either ``patronictl edit-config`` or Patroni :ref:`REST API <rest_api>`.
|
|
||||||
- **method**: custom script to use for bootstrapping this cluster.
|
|
||||||
See :ref:`custom bootstrap methods documentation <custom_bootstrap>` for details.
|
|
||||||
When ``initdb`` is specified revert to the default ``initdb`` command. ``initdb`` is also triggered when no ``method``
|
|
||||||
parameter is present in the configuration file.
|
|
||||||
- **initdb**: List options to be passed on to initdb.
|
|
||||||
- **- data-checksums**: Must be enabled when pg_rewind is needed on 9.3.
|
|
||||||
- **- encoding: UTF8**: default encoding for new databases.
|
|
||||||
- **- locale: UTF8**: default locale for new databases.
|
|
||||||
- **pg\_hba**: list of lines that you should add to pg\_hba.conf.
|
|
||||||
- **- host all all 0.0.0.0/0 md5**.
|
|
||||||
- **- host replication replicator 127.0.0.1/32 md5**: A line like this is required for replication.
|
|
||||||
- **users**: Some additional users which need to be created after initializing new cluster
|
|
||||||
- **admin**: the name of user
|
|
||||||
- **password: zalando**:
|
|
||||||
- **options**: list of options for CREATE USER statement
|
|
||||||
- **- createrole**
|
|
||||||
- **- createdb**
|
|
||||||
- **post\_bootstrap** or **post\_init**: An additional script that will be executed after initializing the cluster. The script receives a connection string URL (with the cluster superuser as a user name). The PGPASSFILE variable is set to the location of pgpass file.
|
|
||||||
|
|
||||||
.. _citus_settings:
|
|
||||||
|
|
||||||
Citus
|
|
||||||
-----
|
|
||||||
Enables integration Patroni with `Citus <https://docs.citusdata.com>`__. If configured, Patroni will take care of registering Citus worker nodes on the coordinator. You can find more information about Citus support :ref:`here <citus>`.
|
|
||||||
|
|
||||||
- **group**: the Citus group id, integer. Use ``0`` for coordinator and ``1``, ``2``, etc... for workers
|
|
||||||
- **database**: the database where ``citus`` extension should be created. Must be the same on the coordinator and all workers. Currently only one database is supported.
|
|
||||||
|
|
||||||
.. _consul_settings:
|
|
||||||
|
|
||||||
Consul
|
|
||||||
------
|
|
||||||
Most of the parameters are optional, but you have to specify one of the **host** or **url**
|
|
||||||
|
|
||||||
- **host**: the host:port for the Consul local agent.
|
|
||||||
- **url**: url for the Consul local agent, in format: http(s)://host:port.
|
|
||||||
- **port**: (optional) Consul port.
|
|
||||||
- **scheme**: (optional) **http** or **https**, defaults to **http**.
|
|
||||||
- **token**: (optional) ACL token.
|
|
||||||
- **verify**: (optional) whether to verify the SSL certificate for HTTPS requests.
|
|
||||||
- **cacert**: (optional) The ca certificate. If present it will enable validation.
|
|
||||||
- **cert**: (optional) file with the client certificate.
|
|
||||||
- **key**: (optional) file with the client key. Can be empty if the key is part of **cert**.
|
|
||||||
- **dc**: (optional) Datacenter to communicate with. By default the datacenter of the host is used.
|
|
||||||
- **consistency**: (optional) Select consul consistency mode. Possible values are ``default``, ``consistent``, or ``stale`` (more details in `consul API reference <https://www.consul.io/api/features/consistency.html/>`__)
|
|
||||||
- **checks**: (optional) list of Consul health checks used for the session. By default an empty list is used.
|
|
||||||
- **register\_service**: (optional) whether or not to register a service with the name defined by the scope parameter and the tag master, primary, replica, or standby-leader depending on the node's role. Defaults to **false**.
|
|
||||||
- **service\_tags**: (optional) additional static tags to add to the Consul service apart from the role (``master``/``primary``/``replica``/``standby-leader``). By default an empty list is used.
|
|
||||||
- **service\_check\_interval**: (optional) how often to perform health check against registered url. Defaults to '5s'.
|
|
||||||
- **service\_check\_tls\_server\_name**: (optional) overide SNI host when connecting via TLS, see also `consul agent check API reference <https://www.consul.io/api-docs/agent/check#tlsservername>`__.
|
|
||||||
|
|
||||||
The ``token`` needs to have the following ACL permissions:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
service_prefix "${scope}" {
|
|
||||||
policy = "write"
|
|
||||||
}
|
|
||||||
key_prefix "${namespace}/${scope}" {
|
|
||||||
policy = "write"
|
|
||||||
}
|
|
||||||
session_prefix "" {
|
|
||||||
policy = "write"
|
|
||||||
}
|
|
||||||
|
|
||||||
Etcd
|
|
||||||
----
|
|
||||||
Most of the parameters are optional, but you have to specify one of the **host**, **hosts**, **url**, **proxy** or **srv**
|
|
||||||
|
|
||||||
- **host**: the host:port for the etcd endpoint.
|
|
||||||
- **hosts**: list of etcd endpoint in format host1:port1,host2:port2,etc... Could be a comma separated string or an actual yaml list.
|
|
||||||
- **use\_proxies**: If this parameter is set to true, Patroni will consider **hosts** as a list of proxies and will not perform a topology discovery of etcd cluster.
|
|
||||||
- **url**: url for the etcd.
|
|
||||||
- **proxy**: proxy url for the etcd. If you are connecting to the etcd using proxy, use this parameter instead of **url**.
|
|
||||||
- **srv**: Domain to search the SRV record(s) for cluster autodiscovery. Patroni will try to query these SRV service names for specified domain (in that order until first success): ``_etcd-client-ssl``, ``_etcd-client``, ``_etcd-ssl``, ``_etcd``, ``_etcd-server-ssl``, ``_etcd-server``. If SRV records for ``_etcd-server-ssl`` or ``_etcd-server`` are retrieved then ETCD peer protocol is used do query ETCD for available members. Otherwise hosts from SRV records will be used.
|
|
||||||
- **srv\_suffix**: Configures a suffix to the SRV name that is queried during discovery. Use this flag to differentiate between multiple etcd clusters under the same domain. Works only with conjunction with **srv**. For example, if ``srv_suffix: foo`` and ``srv: example.org`` are set, the following DNS SRV query is made:``_etcd-client-ssl-foo._tcp.example.com`` (and so on for every possible ETCD SRV service name).
|
|
||||||
- **protocol**: (optional) http or https, if not specified http is used. If the **url** or **proxy** is specified - will take protocol from them.
|
|
||||||
- **username**: (optional) username for etcd authentication.
|
|
||||||
- **password**: (optional) password for etcd authentication.
|
|
||||||
- **cacert**: (optional) The ca certificate. If present it will enable validation.
|
|
||||||
- **cert**: (optional) file with the client certificate.
|
|
||||||
- **key**: (optional) file with the client key. Can be empty if the key is part of **cert**.
|
|
||||||
|
|
||||||
Etcdv3
|
|
||||||
------
|
|
||||||
If you want that Patroni works with Etcd cluster via protocol version 3, you need to use the ``etcd3`` section in the Patroni configuration file. All configuration parameters are the same as for ``etcd``.
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
Keys created with protocol version 2 are not visible with protocol version 3 and the other way around, therefore it is not possible to switch from ``etcd`` to ``etcd3`` just by updating Patroni config file.
|
|
||||||
|
|
||||||
|
|
||||||
ZooKeeper
|
|
||||||
----------
|
|
||||||
- **hosts**: List of ZooKeeper cluster members in format: ['host1:port1', 'host2:port2', 'etc...'].
|
|
||||||
- **use_ssl**: (optional) Whether SSL is used or not. Defaults to ``false``. If set to ``false``, all SSL specific parameters are ignored.
|
|
||||||
- **cacert**: (optional) The CA certificate. If present it will enable validation.
|
|
||||||
- **cert**: (optional) File with the client certificate.
|
|
||||||
- **key**: (optional) File with the client key.
|
|
||||||
- **key_password**: (optional) The client key password.
|
|
||||||
- **verify**: (optional) Whether to verify certificate or not. Defaults to ``true``.
|
|
||||||
- **set_acls**: (optional) If set, configure Kazoo to apply a default ACL to each ZNode that it creates. ACLs will assume 'x509' schema and should be specified as a dictionary with the principal as the key and one or more permissions as a list in the value. Permissions may be one of ``CREATE``, ``READ``, ``WRITE``, ``DELETE`` or ``ADMIN``. For example, ``set_acls: {CN=principal1: [CREATE, READ], CN=principal2: [ALL]}``.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
It is required to install ``kazoo>=2.6.0`` to support SSL.
|
|
||||||
|
|
||||||
|
|
||||||
Exhibitor
|
|
||||||
---------
|
|
||||||
- **hosts**: initial list of Exhibitor (ZooKeeper) nodes in format: 'host1,host2,etc...'. This list updates automatically whenever the Exhibitor (ZooKeeper) cluster topology changes.
|
|
||||||
- **poll\_interval**: how often the list of ZooKeeper and Exhibitor nodes should be updated from Exhibitor.
|
|
||||||
- **port**: Exhibitor port.
|
|
||||||
|
|
||||||
.. _kubernetes_settings:
|
|
||||||
|
|
||||||
Kubernetes
|
|
||||||
----------
|
|
||||||
- **bypass\_api\_service**: (optional) When communicating with the Kubernetes API, Patroni is usually relying on the `kubernetes` service, the address of which is exposed in the pods via the `KUBERNETES_SERVICE_HOST` environment variable. If `bypass_api_service` is set to ``true``, Patroni will resolve the list of API nodes behind the service and connect directly to them.
|
|
||||||
- **namespace**: (optional) Kubernetes namespace where Patroni pod is running. Default value is `default`.
|
|
||||||
- **labels**: Labels in format ``{label1: value1, label2: value2}``. These labels will be used to find existing objects (Pods and either Endpoints or ConfigMaps) associated with the current cluster. Also Patroni will set them on every object (Endpoint or ConfigMap) it creates.
|
|
||||||
- **scope\_label**: (optional) name of the label containing cluster name. Default value is `cluster-name`.
|
|
||||||
- **role\_label**: (optional) name of the label containing role (master or replica). Patroni will set this label on the pod it runs in. Default value is ``role``.
|
|
||||||
- **use\_endpoints**: (optional) if set to true, Patroni will use Endpoints instead of ConfigMaps to run leader elections and keep cluster state.
|
|
||||||
- **pod\_ip**: (optional) IP address of the pod Patroni is running in. This value is required when `use_endpoints` is enabled and is used to populate the leader endpoint subsets when the pod's PostgreSQL is promoted.
|
|
||||||
- **ports**: (optional) if the Service object has the name for the port, the same name must appear in the Endpoint object, otherwise service won't work. For example, if your service is defined as ``{Kind: Service, spec: {ports: [{name: postgresql, port: 5432, targetPort: 5432}]}}``, then you have to set ``kubernetes.ports: [{"name": "postgresql", "port": 5432}]`` and Patroni will use it for updating subsets of the leader Endpoint. This parameter is used only if `kubernetes.use_endpoints` is set.
|
|
||||||
- **cacert**: (optional) Specifies the file with the CA_BUNDLE file with certificates of trusted CAs to use while verifying Kubernetes API SSL certs. If not provided, patroni will use the value provided by the ServiceAccount secret.
|
|
||||||
|
|
||||||
|
|
||||||
.. _raft_settings:
|
|
||||||
|
|
||||||
Raft (deprecated)
|
|
||||||
-----------------
|
|
||||||
- **self\_addr**: ``ip:port`` to listen on for Raft connections. The ``self_addr`` must be accessible from other nodes of the cluster. If not set, the node will not participate in consensus.
|
|
||||||
- **bind\_addr**: (optional) ``ip:port`` to listen on for Raft connections. If not specified the ``self_addr`` will be used.
|
|
||||||
- **partner\_addrs**: list of other Patroni nodes in the cluster in format: ['ip1:port', 'ip2:port', 'etc...']
|
|
||||||
- **data\_dir**: directory where to store Raft log and snapshot. If not specified the current working directory is used.
|
|
||||||
- **password**: (optional) Encrypt Raft traffic with a specified password, requires ``cryptography`` python module.
|
|
||||||
|
|
||||||
Short FAQ about Raft implementation
|
|
||||||
|
|
||||||
- Q: How to list all the nodes providing consensus?
|
|
||||||
|
|
||||||
A: ``syncobj_admin -conn host:port -status`` where the host:port is the address of one of the cluster nodes
|
|
||||||
|
|
||||||
- Q: Node that was a part of consensus and has gone and I can't reuse the same IP for other node. How to remove this node from the consensus?
|
|
||||||
|
|
||||||
A: ``syncobj_admin -conn host:port -remove host2:port2`` where the ``host2:port2`` is the address of the node you want to remove from consensus.
|
|
||||||
|
|
||||||
- Q: Where to get the ``syncobj_admin`` utility?
|
|
||||||
|
|
||||||
A: It is installed together with ``pysyncobj`` module (python RAFT implementation), which is Patroni dependency.
|
|
||||||
|
|
||||||
- Q: it is possible to run Patroni node without adding in to the consensus?
|
|
||||||
|
|
||||||
A: Yes, just comment out or remove ``raft.self_addr`` from Patroni configuration.
|
|
||||||
|
|
||||||
- Q: It is possible to run Patroni and PostgreSQL only on two nodes?
|
|
||||||
|
|
||||||
A: Yes, on the third node you can run ``patroni_raft_controller`` (without Patroni and PostgreSQL). In such a setup, one can temporarily lose one node without affecting the primary.
|
|
||||||
|
|
||||||
|
|
||||||
.. _postgresql_settings:
|
|
||||||
|
|
||||||
PostgreSQL
|
|
||||||
----------
|
|
||||||
- **postgresql**:
|
|
||||||
- **authentication**:
|
|
||||||
- **superuser**:
|
|
||||||
- **username**: name for the superuser, set during initialization (initdb) and later used by Patroni to connect to the postgres.
|
|
||||||
- **password**: password for the superuser, set during initialization (initdb).
|
|
||||||
- **sslmode**: (optional) maps to the `sslmode <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLMODE>`__ connection parameter, which allows a client to specify the type of TLS negotiation mode with the server. For more information on how each mode works, please visit the `PostgreSQL documentation <https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS>`__. The default mode is ``prefer``.
|
|
||||||
- **sslkey**: (optional) maps to the `sslkey <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLKEY>`__ connection parameter, which specifies the location of the secret key used with the client's certificate.
|
|
||||||
- **sslpassword**: (optional) maps to the `sslpassword <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLPASSWORD>`__ connection parameter, which specifies the password for the secret key specified in ``sslkey``.
|
|
||||||
- **sslcert**: (optional) maps to the `sslcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCERT>`__ connection parameter, which specifies the location of the client certificate.
|
|
||||||
- **sslrootcert**: (optional) maps to the `sslrootcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLROOTCERT>`__ connection parameter, which specifies the location of a file containing one ore more certificate authorities (CA) certificates that the client will use to verify a server's certificate.
|
|
||||||
- **sslcrl**: (optional) maps to the `sslcrl <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRL>`__ connection parameter, which specifies the location of a file containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
|
||||||
- **sslcrldir**: (optional) maps to the `sslcrldir <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRLDIR>`__ connection parameter, which specifies the location of a directory with files containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
|
||||||
- **gssencmode**: (optional) maps to the `gssencmode <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-GSSENCMODE>`__ connection parameter, which determines whether or with what priority a secure GSS TCP/IP connection will be negotiated with the server
|
|
||||||
- **channel_binding**: (optional) maps to the `channel_binding <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-CHANNEL-BINDING>`__ connection parameter, which controls the client's use of channel binding.
|
|
||||||
- **replication**:
|
|
||||||
- **username**: replication username; the user will be created during initialization. Replicas will use this user to access the replication source via streaming replication
|
|
||||||
- **password**: replication password; the user will be created during initialization.
|
|
||||||
- **sslmode**: (optional) maps to the `sslmode <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLMODE>`__ connection parameter, which allows a client to specify the type of TLS negotiation mode with the server. For more information on how each mode works, please visit the `PostgreSQL documentation <https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS>`__. The default mode is ``prefer``.
|
|
||||||
- **sslkey**: (optional) maps to the `sslkey <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLKEY>`__ connection parameter, which specifies the location of the secret key used with the client's certificate.
|
|
||||||
- **sslpassword**: (optional) maps to the `sslpassword <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLPASSWORD>`__ connection parameter, which specifies the password for the secret key specified in ``sslkey``.
|
|
||||||
- **sslcert**: (optional) maps to the `sslcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCERT>`__ connection parameter, which specifies the location of the client certificate.
|
|
||||||
- **sslrootcert**: (optional) maps to the `sslrootcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLROOTCERT>`__ connection parameter, which specifies the location of a file containing one ore more certificate authorities (CA) certificates that the client will use to verify a server's certificate.
|
|
||||||
- **sslcrl**: (optional) maps to the `sslcrl <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRL>`__ connection parameter, which specifies the location of a file containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
|
||||||
- **sslcrldir**: (optional) maps to the `sslcrldir <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRLDIR>`__ connection parameter, which specifies the location of a directory with files containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
|
||||||
- **gssencmode**: (optional) maps to the `gssencmode <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-GSSENCMODE>`__ connection parameter, which determines whether or with what priority a secure GSS TCP/IP connection will be negotiated with the server
|
|
||||||
- **channel_binding**: (optional) maps to the `channel_binding <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-CHANNEL-BINDING>`__ connection parameter, which controls the client's use of channel binding.
|
|
||||||
- **rewind**:
|
|
||||||
- **username**: name for the user for ``pg_rewind``; the user will be created during initialization of postgres 11+ and all necessary `permissions <https://www.postgresql.org/docs/11/app-pgrewind.html#id-1.9.5.8.8>`__ will be granted.
|
|
||||||
- **password**: password for the user for ``pg_rewind``; the user will be created during initialization.
|
|
||||||
- **sslmode**: (optional) maps to the `sslmode <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLMODE>`__ connection parameter, which allows a client to specify the type of TLS negotiation mode with the server. For more information on how each mode works, please visit the `PostgreSQL documentation <https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS>`__. The default mode is ``prefer``.
|
|
||||||
- **sslkey**: (optional) maps to the `sslkey <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLKEY>`__ connection parameter, which specifies the location of the secret key used with the client's certificate.
|
|
||||||
- **sslpassword**: (optional) maps to the `sslpassword <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLPASSWORD>`__ connection parameter, which specifies the password for the secret key specified in ``sslkey``.
|
|
||||||
- **sslcert**: (optional) maps to the `sslcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCERT>`__ connection parameter, which specifies the location of the client certificate.
|
|
||||||
- **sslrootcert**: (optional) maps to the `sslrootcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLROOTCERT>`__ connection parameter, which specifies the location of a file containing one ore more certificate authorities (CA) certificates that the client will use to verify a server's certificate.
|
|
||||||
- **sslcrl**: (optional) maps to the `sslcrl <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRL>`__ connection parameter, which specifies the location of a file containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
|
||||||
- **sslcrldir**: (optional) maps to the `sslcrldir <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRLDIR>`__ connection parameter, which specifies the location of a directory with files containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
|
||||||
- **gssencmode**: (optional) maps to the `gssencmode <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-GSSENCMODE>`__ connection parameter, which determines whether or with what priority a secure GSS TCP/IP connection will be negotiated with the server
|
|
||||||
- **channel_binding**: (optional) maps to the `channel_binding <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-CHANNEL-BINDING>`__ connection parameter, which controls the client's use of channel binding.
|
|
||||||
- **callbacks**: callback scripts to run on certain actions. Patroni will pass the action, role and cluster name. (See scripts/aws.py as an example of how to write them.)
|
|
||||||
- **on\_reload**: run this script when configuration reload is triggered.
|
|
||||||
- **on\_restart**: run this script when the postgres restarts (without changing role).
|
|
||||||
- **on\_role\_change**: run this script when the postgres is being promoted or demoted.
|
|
||||||
- **on\_start**: run this script when the postgres starts.
|
|
||||||
- **on\_stop**: run this script when the postgres stops.
|
|
||||||
- **connect\_address**: IP address + port through which Postgres is accessible from other nodes and applications.
|
|
||||||
- **proxy\_address**: IP address + port through which a connection pool (e.g. pgbouncer) running next to Postgres is accessible. The value is written to the member key in DCS as ``proxy_url`` and could be used/useful for service discovery.
|
|
||||||
- **create\_replica\_methods**: an ordered list of the create methods for turning a Patroni node into a new replica.
|
|
||||||
"basebackup" is the default method; other methods are assumed to refer to scripts, each of which is configured as its
|
|
||||||
own config item. See :ref:`custom replica creation methods documentation <custom_replica_creation>` for further explanation.
|
|
||||||
- **data\_dir**: The location of the Postgres data directory, either :ref:`existing <existing_data>` or to be initialized by Patroni.
|
|
||||||
- **config\_dir**: The location of the Postgres configuration directory, defaults to the data directory. Must be writable by Patroni.
|
|
||||||
- **bin\_dir**: Path to PostgreSQL binaries (pg_ctl, pg_rewind, pg_basebackup, postgres). The default value is an empty string meaning that PATH environment variable will be used to find the executables.
|
|
||||||
- **listen**: IP address + port that Postgres listens to; must be accessible from other nodes in the cluster, if you're using streaming replication. Multiple comma-separated addresses are permitted, as long as the port component is appended after to the last one with a colon, i.e. ``listen: 127.0.0.1,127.0.0.2:5432``. Patroni will use the first address from this list to establish local connections to the PostgreSQL node.
|
|
||||||
- **use\_unix\_socket**: specifies that Patroni should prefer to use unix sockets to connect to the cluster. Default value is ``false``. If ``unix_socket_directories`` is defined, Patroni will use the first suitable value from it to connect to the cluster and fallback to tcp if nothing is suitable. If ``unix_socket_directories`` is not specified in ``postgresql.parameters``, Patroni will assume that the default value should be used and omit ``host`` from the connection parameters.
|
|
||||||
- **use\_unix\_socket\_repl**: specifies that Patroni should prefer to use unix sockets for replication user cluster connection. Default value is ``false``. If ``unix_socket_directories`` is defined, Patroni will use the first suitable value from it to connect to the cluster and fallback to tcp if nothing is suitable. If ``unix_socket_directories`` is not specified in ``postgresql.parameters``, Patroni will assume that the default value should be used and omit ``host`` from the connection parameters.
|
|
||||||
- **pgpass**: path to the `.pgpass <https://www.postgresql.org/docs/current/static/libpq-pgpass.html>`__ password file. Patroni creates this file before executing pg\_basebackup, the post_init script and under some other circumstances. The location must be writable by Patroni.
|
|
||||||
- **recovery\_conf**: additional configuration settings written to recovery.conf when configuring follower.
|
|
||||||
- **custom\_conf** : path to an optional custom ``postgresql.conf`` file, that will be used in place of ``postgresql.base.conf``. The file must exist on all cluster nodes, be readable by PostgreSQL and will be included from its location on the real ``postgresql.conf``. Note that Patroni will not monitor this file for changes, nor backup it. However, its settings can still be overridden by Patroni's own configuration facilities - see :ref:`dynamic configuration <dynamic_configuration>` for details.
|
|
||||||
- **parameters**: list of configuration settings for Postgres. Many of these are required for replication to work.
|
|
||||||
- **pg\_hba**: list of lines that Patroni will use to generate ``pg_hba.conf``. This parameter has higher priority than ``bootstrap.pg_hba``. Together with :ref:`dynamic configuration <dynamic_configuration>` it simplifies management of ``pg_hba.conf``.
|
|
||||||
- **- host all all 0.0.0.0/0 md5**.
|
|
||||||
- **- host replication replicator 127.0.0.1/32 md5**: A line like this is required for replication.
|
|
||||||
- **pg\_ident**: list of lines that Patroni will use to generate ``pg_ident.conf``. Together with :ref:`dynamic configuration <dynamic_configuration>` it simplifies management of ``pg_ident.conf``.
|
|
||||||
- **- mapname1 systemname1 pguser1**.
|
|
||||||
- **- mapname1 systemname2 pguser2**.
|
|
||||||
- **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 the PostgreSQL data directory and recreate the replica. Otherwise it will try to follow the new leader. Default value is **false**.
|
|
||||||
- **remove\_data\_directory\_on\_diverged\_timelines**: Patroni will remove the PostgreSQL data directory and recreate the replica if it notices that timelines are diverging and the former primary can not start streaming from the new primary. This option is useful when ``pg_rewind`` can not be used. While performing timelines divergence check on PostgreSQL v10 and older Patroni will try to connect with replication credential to the "postgres" database. Hence, such access should be allowed in the pg_hba.conf. Default value is **false**.
|
|
||||||
- **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".
|
|
||||||
- **pre\_promote**: a fencing script that executes during a failover after acquiring the leader lock but before promoting the replica. If the script exits with a non-zero code, Patroni does not promote the replica and removes the leader key from DCS.
|
|
||||||
|
|
||||||
.. _restapi_settings:
|
|
||||||
|
|
||||||
REST API
|
|
||||||
--------
|
|
||||||
- **restapi**:
|
|
||||||
- **connect\_address**: IP address (or hostname) and port, to access the Patroni's :ref:`REST API <rest_api>`. All the members of the cluster must be able to connect to this address, so unless the Patroni setup is intended for a demo inside the localhost, this address must be a non "localhost" or loopback address (ie: "localhost" or "127.0.0.1"). It can serve as an endpoint for HTTP health checks (read below about the "listen" REST API parameter), and also for user queries (either directly or via the REST API), as well as for the health checks done by the cluster members during leader elections (for example, to determine whether the leader is still running, or if there is a node which has a WAL position that is ahead of the one doing the query; etc.) The connect_address is put in the member key in DCS, making it possible to translate the member name into the address to connect to its REST API.
|
|
||||||
|
|
||||||
- **listen**: IP address (or hostname) and port that Patroni will listen to for the REST API - to provide also the same health checks and cluster messaging between the participating nodes, as described above. to provide health-check information for HAProxy (or any other load balancer capable of doing a HTTP "OPTION" or "GET" checks).
|
|
||||||
|
|
||||||
- **authentication**: (optional)
|
|
||||||
- **username**: Basic-auth username to protect unsafe REST API endpoints.
|
|
||||||
- **password**: Basic-auth password to protect unsafe REST API endpoints.
|
|
||||||
- **certfile**: (optional): Specifies the file with the certificate in the PEM format. If the certfile is not specified or is left empty, the API server will work without SSL.
|
|
||||||
- **keyfile**: (optional): Specifies the file with the secret key in the PEM format.
|
|
||||||
- **keyfile\_password**: (optional): Specifies a password for decrypting the keyfile.
|
|
||||||
- **cafile**: (optional): Specifies the file with the CA_BUNDLE with certificates of trusted CAs to use while verifying client certs.
|
|
||||||
- **ciphers**: (optional): Specifies the permitted cipher suites (e.g. "ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA256:!SSLv1:!SSLv2:!SSLv3:!TLSv1:!TLSv1.1")
|
|
||||||
- **verify\_client**: (optional): ``none`` (default), ``optional`` or ``required``. When ``none`` REST API will not check client certificates. When ``required`` client certificates are required for all REST API calls. When ``optional`` client certificates are required for all unsafe REST API endpoints. When ``required`` is used, then client authentication succeeds, if the certificate signature verification succeeds. For ``optional`` the client cert will only be checked for ``PUT``, ``POST``, ``PATCH``, and ``DELETE`` requests.
|
|
||||||
- **allowlist**: (optional): Specifies the set of hosts that are allowed to call unsafe REST API endpoints. The single element could be a host name, an IP address or a network address using CIDR notation. By default ``allow all`` is used. In case if ``allowlist`` or ``allowlist_include_members`` are set, anything that is not included is rejected.
|
|
||||||
- **allowlist\_include\_members**: (optional): If set to ``true`` it allows accessing unsafe REST API endpoints from other cluster members registered in DCS (IP address or hostname is taken from the members ``api_url``). Be careful, it might happen that OS will use a different IP for outgoing connections.
|
|
||||||
- **http\_extra\_headers**: (optional): HTTP headers let the REST API server pass additional information with an HTTP response.
|
|
||||||
- **https\_extra\_headers**: (optional): HTTPS headers let the REST API server pass additional information with an HTTP response when TLS is enabled. This will also pass additional information set in ``http_extra_headers``.
|
|
||||||
|
|
||||||
Here is an example of both **http_extra_headers** and **https_extra_headers**:
|
|
||||||
|
|
||||||
.. code:: YAML
|
|
||||||
|
|
||||||
restapi:
|
|
||||||
listen: <listen>
|
|
||||||
connect_address: <connect_address>
|
|
||||||
authentication:
|
|
||||||
username: <username>
|
|
||||||
password: <password>
|
|
||||||
http_extra_headers:
|
|
||||||
'X-Frame-Options': 'SAMEORIGIN'
|
|
||||||
'X-XSS-Protection': '1; mode=block'
|
|
||||||
'X-Content-Type-Options': 'nosniff'
|
|
||||||
cafile: <ca file>
|
|
||||||
certfile: <cert>
|
|
||||||
keyfile: <key>
|
|
||||||
https_extra_headers:
|
|
||||||
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains'
|
|
||||||
|
|
||||||
.. _patronictl_settings:
|
|
||||||
|
|
||||||
CTL
|
|
||||||
---
|
|
||||||
- **ctl**: (optional)
|
|
||||||
- **insecure**: Allow connections to REST API without verifying SSL certs.
|
|
||||||
- **cacert**: Specifies the file with the CA_BUNDLE file or directory with certificates of trusted CAs to use while verifying REST API SSL certs. If not provided patronictl will use the value provided for REST API "cafile" parameter.
|
|
||||||
- **certfile**: Specifies the file with the client certificate in the PEM format. If not provided patronictl will use the value provided for REST API "certfile" parameter.
|
|
||||||
- **keyfile**: Specifies the file with the client secret key in the PEM format. If not provided patronictl will use the value provided for REST API "keyfile" parameter.
|
|
||||||
- **keyfile\_password**: Specifies a password for decrypting the keyfile. If not provided patronictl will use the value provided for REST API "keyfile\_password" parameter.
|
|
||||||
|
|
||||||
Watchdog
|
|
||||||
--------
|
|
||||||
- **mode**: ``off``, ``automatic`` or ``required``. When ``off`` watchdog is disabled. When ``automatic`` watchdog will be used if available, but ignored if it is not. When ``required`` the node will not become a leader unless watchdog can be successfully enabled.
|
|
||||||
- **device**: Path to watchdog device. Defaults to ``/dev/watchdog``.
|
|
||||||
- **safety_margin**: Number of seconds of safety margin between watchdog triggering and leader key expiration.
|
|
||||||
|
|
||||||
.. _tags_settings:
|
|
||||||
|
|
||||||
Tags
|
|
||||||
----
|
|
||||||
- **nofailover**: ``true`` or ``false``, controls whether this node is allowed to participate in the leader race and become a leader. Defaults to ``false``
|
|
||||||
- **clonefrom**: ``true`` or ``false``. If set to ``true`` other nodes might prefer to use this node for bootstrap (take ``pg_basebackup`` from). If there are several nodes with ``clonefrom`` tag set to ``true`` the node to bootstrap from will be chosen randomly. The default value is ``false``.
|
|
||||||
- **noloadbalance**: ``true`` or ``false``. If set to ``true`` the node will return HTTP Status Code 503 for the ``GET /replica`` REST API health-check and therefore will be excluded from the load-balancing. Defaults to ``false``.
|
|
||||||
- **replicatefrom**: The IP address/hostname of another replica. Used to support cascading replication.
|
|
||||||
- **nosync**: ``true`` or ``false``. If set to ``true`` the node will never be selected as a synchronous replica.
|
|
||||||
|
|
||||||
In addition to these predefined tags, you can also add your own ones:
|
|
||||||
|
|
||||||
- **key1**: ``true``
|
|
||||||
- **key2**: ``false``
|
|
||||||
- **key3**: ``1.4``
|
|
||||||
- **key4**: ``"RandomString"``
|
|
||||||
|
|
||||||
Tags are visible in the :ref:`REST API <rest_api>` and ``patronictl list`` You can also check for an instance health using these tags. If the tag isn't defined for an instance, or if the respective value doesn't match the querying value, it will return HTTP Status Code 503.
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<mxfile host="app.diagrams.net" modified="2023-03-13T14:29:21.924Z" agent="5.0 (X11; Ubuntu)" etag="sukwsRuBYbiX8e-LLYnw" version="21.0.6" type="device"><diagram name="Page-1" id="Xu3tU9JEMeQEUPilRV_D">7Vxtb9s2EP41BrYPNfTmt4+Jk2bFOixrihXYF4O2aEkNLaoUZTv99SMlUhZF+i2RE8dVEsDiiTxKd88deXeMO+54sb4jIAn/wj5EHcfy1x33puM4tud67INTngrKkLc4ISCRLzptCA/RTyiIlqBmkQ9TpSPFGNEoUYkzHMdwRhUaIASv1G5zjNRZExBAjfAwA0infot8Ggqq3R9tbvwBoyCk8v0GxY0FkJ3Fm6Qh8PGqQnJvO+6YYEyLq8V6DBEXnpRLMe7jlrvlgxEY00MG/L3474v99d/Hb+jnn596d99vsjj7ILgsAcrEC9+MhYJS+iSFkOAoprkge9fsj80ztjo9dmfMW12nVyPU2wOVYOstzkMl1NsDlWDX2du1+e36A1YIWkthb9XmtyoPyP7ca5xRFMVwXELOYsSAAD9iqhhjhAmjxThm0rsO6QKxls0uV2FE4UMCZlyqK2YujDbHMRWgtx3ZFoLnXBmsKWBzEcEj1wQkt0tYKKTogxBI0mhajiJwlpE0WsIvMC2YcyoDYMKvF+uA22oXrFKvGxCcJfnjf2JzGe9O2OVkhnDmcyaU4EcoX7LjuOz3Iwfc9TxCqPbyS0hoxGzpCkUB500xnwqIFoJzyjkyiURx8Dlv3biWkIJpCh+kIfTF6+j4l2Bms8J1hSTs4Q7iBaTkiXURd3vSNoVz8kRztbF0T9LCipF7chwQ3iUoWW8MkF0IGzzCHh3NHu8Bk3gcaTZpELemm95VfzzsVwVnb9VKHXk1HZSsTCiugFzXyk6/c7CqbHvAzXq3shyrpyur7Ni4slxdWXd8TJxSEDP5OH3EAT4l7CrIoc7o/vRJ02X6COksFII3epdtFrHF6xxmpYw+z3/qpiUR8hlMIbrHaUSj3DdMMaV4sdewZ5D7KBUX+xwdSJPibefRGvrbvBWBKc7IDBa+ivm51OS1/OlE6mAiRX5CZI5UJzLQcdk3+JD+qVDpHYtKAoHPOhCYIKbTFpyvB04u+YmU+wkR2lMRar81RHstRFuIKhB13DODaF+D6O3X8Q0PNFGWcu04lh4nKTisKE8BR76BSmthgIoqK/8x4bDEWz0k61pWHmR1+24t+BLxVY06MlKLOK3Wc7SF8SAfze4bmNg1mjOs9c0Dqb12olmE2XDqWH/MppDEkIm5GxVIT2R4wxTkn8zPDlUQu44O4qEpnBieCMQDDcQtZFvIViHbPzPEDjWAQj+AcqHDhIY4wDFAtxvqNcFZ7JdY3fT5jLmkczR/h5Q+ieUTZBS/JGYtVtAd/URmkAISwF38xBLDX3CnqghEgEZLNSFpkrwYes/trLK01r2S56ksigcVo2r6Kx/j+SodtU6odUI7nRDT23l5IVl8eNduaPBWbuhlorcvQPT9A0U/Oi/R6475ckU/OC/R66nkG74WFHkQtujFil76PzJeNSyWxA9iTbziYiQwF6nsIPMn7JMtgqPiChWUjwVb2aEt+bUlv03Jb4ZJggmgcOIDCiblPmJ7iel05T+9itVQ+c9Ttx1vX/2zDbn7yyz/ucfqyrbPrfpnt1nsS89iH49Sr16lfvNEtq0nAVuY/uIwdZzzg+lQg6lWcNFDwzZxdFGJo+P97blVXOw229mC9p3VXJyzznYK8e5N/JSnw/dlfuRKc+l1lzIebl1R64reS+XFgNF36IvkLuANfNHLpO9ehPSHB0pfyvFcpO/9UtKXRvL60n+kftr/Z/jtGi7DW39l/0juoeEwv8yM+NGyUkepJUsq9RRDv5xkKNzwhMKHIk/Pyzb9ZN3ZUrY5fjqVxGc65BFsd88zHMzIa4pRrylG+8R7MKNBU4yGTTEaNcSIeYCGGNlNMXKaYtQUsp1tyL6vWGXBTDPWX5qsuKV6BJIHDPJfax11bapvk+cIr2YhILTLq5JTkPIV0FSROtmG2altmAc9bb/slrnV6o6510Di1Lhu6QfVj1x87KYMbSsjY73hQLic2as0xigh0QJw9e+UhnHhf4aVMXRT1bT2BqqLyPeLHSY/UAA2Jw3UQJ5HxXxTKQ4d5Far5ABEkcdQr65UWdjW95RVuUGt2OHqUe7IFOWeymbPZKNvOFO1a2u8d0fvntWGXi/PS2MJ3WearjVDIE2VQTSiCL6Cw9l6BixntBKo5axiTBYAGZk9UALBIooD1u1LUWbME1iO9RtIn+JZSHCMs/T3ildRD4nt80FcsltcEEdFnjY7nR/SEb7L+jT/UX6JiJikU/2eDpNfsbqW5wwV1yJt4Lm5Y9kFz+cpPDItzJqbbxMpum++k8W9/R8=</diagram></mxfile>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<mxfile host="app.diagrams.net" modified="2023-03-13T14:25:32.295Z" agent="5.0 (X11; Ubuntu)" etag="EcVEbU6F-AyuIGXoP3hl" version="21.0.6" type="device"><diagram id="SVgELWPNXIlR7V7eDs_m" name="Page-1">7Vtbc5s4FP41frQHgY3tx9hO2u5kZ9Km7c70xSODDGxlxAoRO/31K4FkgwS2k+DGTZxmpuggjsS5fOci0nGmq80HCpPwb+Ij3LEtf9NxZh3bBn2nz/8TlMeCMhyOCkJAI19O2hHuo19IEi1JzSIfpZWJjBDMoqRK9EgcI49VaJBSsq5OWxJcXTWBATII9x7EJvWfyGehpAJ3vLvxEUVBKJce2cPixgqqyfJN0hD6ZF0iOdcdZ0oJYcXVajNFWAhPyaV47qbh7nZjFMXsmAf8b7fdj9cAuzc/wOTz92/W3Y+vXafg8gBxJl94NrXlftmjEkJCopjlghxM+C9fZ2p1BvzOVIx69kAj6ONhlQDMkeBRJejjYZUAdPZAWx/oGywRjFGFvaWtb5U2yH+dCckYjmI03ZqcxYkBhX7EVTElmFBOi0nMpTcJ2QrzEeCX6zBi6D6BnpDqmrsLpy1JzKTRA1uNpeAFV27WDPK1qOSRawLR6wdUKKSYgzFM0mixfYoiL6Np9IC+oLRgLqjcABNxvdoEwld7cJ32ewElWZJv/xNfq/bunF/OPUwyXzBhlPxE6iU7tsP/3QiDmywjjLWXf0CURdyXrnAUCN6MiKWgHGG0ZIIjl0gUB7f5aOZYUgp1S/gwDZEvX8e0f+kSYlW0KZGkP3xAZIUYfeRT5N2+JX3zUY2L4Xrn6Y5y37Dk5I56Dkp0Cbasdw7IL6QPPsEf3b7hkHeQizyODKeskbehnMGVOx25ZcmBRrXopqcpYcuqzoxLVm6qZS/wHK0roOvKNnQF6nQFHPtEunKAoavrr9OZCEY4S7mX8itgqC39iZgXShmX5Fax7VzGqQYVJX13hAmKnzqlL/MfBRYl2O5ZVg7EPdfRAFpisEYd11ILLNdmjhsYD/On+f0aJkCj2SNtbg62ylhv4QLhO5JGLMpxakEYI6sSnHhIIGOjeevo9zNbIBojLuZelCPfJFEQyBXkn8yM7aoZ25aJOaMaM+6PXm7F6ch2HA96nxfjq1/hp+zT9C+vaxpxx3axAGY/euCXAcuduiAtqE7ha9bMy0klq3f/y0Sak2NKtwhJV3yCm2yKh+TtFy1XJYmVjtkCcA7s4WhG/bYYDdpidEi8RzMatsVo1BajcUuMuIu3xAi0xchui1Fblm03WfZdySsLZoazvmtyBZb0NCCP2qqktKu5gB6rlpisvRBS1vMhgwuYooY8rCEaHRvImqNWvxq1RMWlJ8rD3sAMW4MWEuXasGXvD1tHQEhbftbI6O6DeIk4ZTDmOnqatZzZq7TGKKHRCgr175VGbdx/hpNxL2BVzzqYLK4i3xeP8xqavw7c1dTVZFpkpjBjRJbXudNW8nBZkdUUaaWS3+6f0mfBoOKzwDEzzXFdpnmq2tYsbWeiBig0yYuduNaycpjrylpI2FZCUZON8uJnXLWOgm2DeVzaWu+6reURmhAKGZqLqDrfQkJzF+V0LS6zUdNai+vMOlxgbHa46gLlLgZQBHk5blGUYC7p/Q2VWhtuaic22PZxLc5yo6WitIMNi/0mszXDkosdcieYJsXbLqMN8pt8gkc0klEPFR4hAlydb/iLuVLFXEh+ruR+dGuv/1Qb3UYmaaSu2dpza2zUPZWJqu1cGnvvt7H3dCN+xcZe/VGCWSK9zaOEp6vq8LHPbz5KAGYXdjZ1LgnrJWF9F+ewQ/fsstThJUt921kqeLKVnl2a6hpWhvwAKZETykISkBji6x11woEk9rdWsJtzS4R+cz3+ixh7lIoUPa5nBWUl3kKZ+95CSlFsfa8WKMKQcSytYEydUOWjdwKid9rrAg1kOMxUeTBIA8TkY5putvt4gbrMfOxSVbyzquIZoHN2ZYX7TsqKZ+jq7D5RskfnESLUl7s5wu6T+fj3BIOBFsoNDRRh63SxwEwvVUYZOnlEgGmxE3XKwSKGUQsHc219pnDwqLDxQCdntJbmL1jFhK4grmV2/xh7IccWkqV84pcix8sRvemI55lH9ULqDadEwhbzaNI52UnikYikvLnmrFB+/i5X6ZS/MK9Dqi4P7mPHqfiA+hLsua6lppDlMkUn8Rp7/Ieh2fA3odn4ldFM2WUDmh0GE/MrrBNCYFuffLXH6ALKLZD/DAhXp58vhnCO4CN3WEVw59wR3DF72q+J4IebE9aRUK/+FqA9qH9Za6j/h8n52JCqFHIuch68VTm33pVrkDMf7v4EsoCZ3R+SOtf/Aw==</diagram></mxfile>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
+86
-71
@@ -34,30 +34,36 @@ There are only a few simple rules you need to follow:
|
|||||||
|
|
||||||
After that you just need to start Patroni and it will handle the rest:
|
After that you just need to start Patroni and it will handle the rest:
|
||||||
|
|
||||||
|
0. Patroni will set ``bootstrap.dcs.synchronous_mode`` to :ref:`quorum <quorum_mode>`
|
||||||
|
if it is not explicitly set to any other value.
|
||||||
1. ``citus`` extension will be automatically added to ``shared_preload_libraries``.
|
1. ``citus`` extension will be automatically added to ``shared_preload_libraries``.
|
||||||
2. If ``max_prepared_transactions`` isn't explicitly set in the global
|
2. If ``max_prepared_transactions`` isn't explicitly set in the global
|
||||||
:ref:`dynamic configuration <dynamic_configuration>` Patroni will
|
:ref:`dynamic configuration <dynamic_configuration>` Patroni will
|
||||||
automatically set it to ``2*max_connections``.
|
automatically set it to ``2*max_connections``.
|
||||||
3. The ``citus.database`` will be automatically created followed by ``CREATE EXTENSION citus``.
|
3. The ``citus.local_hostname`` GUC value will be adjusted from ``localhost`` to the
|
||||||
4. Current superuser :ref:`credentials <postgresql_settings>` will be added to the ``pg_dist_authinfo``
|
value that Patroni is using in order to connect to the local PostgreSQL
|
||||||
|
instance. The value sometimes should be different from the ``localhost``
|
||||||
|
because PostgreSQL might be not listening on it.
|
||||||
|
4. The ``citus.database`` will be automatically created followed by ``CREATE EXTENSION citus``.
|
||||||
|
5. Current superuser :ref:`credentials <postgresql_settings>` will be added to the ``pg_dist_authinfo``
|
||||||
table to allow cross-node communication. Don't forget to update them if
|
table to allow cross-node communication. Don't forget to update them if
|
||||||
later you decide to change superuser username/password/sslcert/sslkey!
|
later you decide to change superuser username/password/sslcert/sslkey!
|
||||||
5. The coordinator primary node will automatically discover worker primary
|
6. The coordinator primary node will automatically discover worker primary
|
||||||
nodes and add them to the ``pg_dist_node`` table using the
|
nodes and add them to the ``pg_dist_node`` table using the
|
||||||
``citus_add_node()`` function.
|
``citus_add_node()`` function.
|
||||||
6. Patroni will also maintain ``pg_dist_node`` in case failover/switchover
|
7. Patroni will also maintain ``pg_dist_node`` in case failover/switchover
|
||||||
on the coordinator or worker clusters occurs.
|
on the coordinator or worker clusters occurs.
|
||||||
|
|
||||||
patronictl
|
patronictl
|
||||||
----------
|
----------
|
||||||
|
|
||||||
Coordinator and worker clusters are physically different PostgreSQL/Patroni
|
Coordinator and worker clusters are physically different PostgreSQL/Patroni
|
||||||
clusters that are just logically groupped together using the
|
clusters that are just logically grouped together using the
|
||||||
`Citus <https://github.com/citusdata/citus>`__ database extension to
|
`Citus <https://github.com/citusdata/citus>`__ database extension to
|
||||||
PostgreSQL. Therefore in most cases it is not possible to manage them as a
|
PostgreSQL. Therefore in most cases it is not possible to manage them as a
|
||||||
single entity.
|
single entity.
|
||||||
|
|
||||||
It results in two major differences in ``patronictl`` behaviour when
|
It results in two major differences in :ref:`patronictl` behaviour when
|
||||||
``patroni.yaml`` has the ``citus`` section comparing with the usual:
|
``patroni.yaml`` has the ``citus`` section comparing with the usual:
|
||||||
|
|
||||||
1. The ``list`` and the ``topology`` by default output all members of the Citus
|
1. The ``list`` and the ``topology`` by default output all members of the Citus
|
||||||
@@ -65,44 +71,44 @@ It results in two major differences in ``patronictl`` behaviour when
|
|||||||
which Citus group they belong to.
|
which Citus group they belong to.
|
||||||
2. For all ``patronictl`` commands the new option is introduced, named
|
2. For all ``patronictl`` commands the new option is introduced, named
|
||||||
``--group``. For some commands the default value for the group might be
|
``--group``. For some commands the default value for the group might be
|
||||||
taken from the ``patroni.yaml``. For example, ``patronictl pause`` will
|
taken from the ``patroni.yaml``. For example, :ref:`patronictl_pause` will
|
||||||
enable the maintenance mode by default for the ``group`` that is set in the
|
enable the maintenance mode by default for the ``group`` that is set in the
|
||||||
``citus`` section, but for example for ``patronictl switchover`` or
|
``citus`` section, but for example for :ref:`patronictl_switchover` or
|
||||||
``patronictl remove`` the group must be explicitly specified.
|
:ref:`patronictl_remove` the group must be explicitly specified.
|
||||||
|
|
||||||
An example of ``patronictl list`` output for the Citus cluster::
|
An example of :ref:`patronictl_list` output for the Citus cluster::
|
||||||
|
|
||||||
postgres@coord1:~$ patronictl list demo
|
postgres@coord1:~$ patronictl list demo
|
||||||
+ Citus cluster: demo ----------+--------------+---------+----+-----------+
|
+ Citus cluster: demo ----------+----------------+---------+----+-----------+
|
||||||
| Group | Member | Host | Role | State | TL | Lag in MB |
|
| Group | Member | Host | Role | State | TL | Lag in MB |
|
||||||
+-------+---------+-------------+--------------+---------+----+-----------+
|
+-------+---------+-------------+----------------+---------+----+-----------+
|
||||||
| 0 | coord1 | 172.27.0.10 | Replica | running | 1 | 0 |
|
| 0 | coord1 | 172.27.0.10 | Replica | running | 1 | 0 |
|
||||||
| 0 | coord2 | 172.27.0.6 | Sync Standby | running | 1 | 0 |
|
| 0 | coord2 | 172.27.0.6 | Quorum Standby | running | 1 | 0 |
|
||||||
| 0 | coord3 | 172.27.0.4 | Leader | running | 1 | |
|
| 0 | coord3 | 172.27.0.4 | Leader | running | 1 | |
|
||||||
| 1 | work1-1 | 172.27.0.8 | Sync Standby | running | 1 | 0 |
|
| 1 | work1-1 | 172.27.0.8 | Quorum Standby | running | 1 | 0 |
|
||||||
| 1 | work1-2 | 172.27.0.2 | Leader | running | 1 | |
|
| 1 | work1-2 | 172.27.0.2 | Leader | running | 1 | |
|
||||||
| 2 | work2-1 | 172.27.0.5 | Sync Standby | running | 1 | 0 |
|
| 2 | work2-1 | 172.27.0.5 | Quorum Standby | running | 1 | 0 |
|
||||||
| 2 | work2-2 | 172.27.0.7 | Leader | running | 1 | |
|
| 2 | work2-2 | 172.27.0.7 | Leader | running | 1 | |
|
||||||
+-------+---------+-------------+--------------+---------+----+-----------+
|
+-------+---------+-------------+----------------+---------+----+-----------+
|
||||||
|
|
||||||
If we add the ``--group`` option, the output will change to::
|
If we add the ``--group`` option, the output will change to::
|
||||||
|
|
||||||
postgres@coord1:~$ patronictl list demo --group 0
|
postgres@coord1:~$ patronictl list demo --group 0
|
||||||
+ Citus cluster: demo (group: 0, 7179854923829112860) -----------+
|
+ Citus cluster: demo (group: 0, 7179854923829112860) -+-----------+
|
||||||
| Member | Host | Role | State | TL | Lag in MB |
|
| Member | Host | Role | State | TL | Lag in MB |
|
||||||
+--------+-------------+--------------+---------+----+-----------+
|
+--------+-------------+----------------+---------+----+-----------+
|
||||||
| coord1 | 172.27.0.10 | Replica | running | 1 | 0 |
|
| coord1 | 172.27.0.10 | Replica | running | 1 | 0 |
|
||||||
| coord2 | 172.27.0.6 | Sync Standby | running | 1 | 0 |
|
| coord2 | 172.27.0.6 | Quorum Standby | running | 1 | 0 |
|
||||||
| coord3 | 172.27.0.4 | Leader | running | 1 | |
|
| coord3 | 172.27.0.4 | Leader | running | 1 | |
|
||||||
+--------+-------------+--------------+---------+----+-----------+
|
+--------+-------------+----------------+---------+----+-----------+
|
||||||
|
|
||||||
postgres@coord1:~$ patronictl list demo --group 1
|
postgres@coord1:~$ patronictl list demo --group 1
|
||||||
+ Citus cluster: demo (group: 1, 7179854923881963547) -----------+
|
+ Citus cluster: demo (group: 1, 7179854923881963547) -+-----------+
|
||||||
| Member | Host | Role | State | TL | Lag in MB |
|
| Member | Host | Role | State | TL | Lag in MB |
|
||||||
+---------+------------+--------------+---------+----+-----------+
|
+---------+------------+----------------+---------+----+-----------+
|
||||||
| work1-1 | 172.27.0.8 | Sync Standby | running | 1 | 0 |
|
| work1-1 | 172.27.0.8 | Quorum Standby | running | 1 | 0 |
|
||||||
| work1-2 | 172.27.0.2 | Leader | running | 1 | |
|
| work1-2 | 172.27.0.2 | Leader | running | 1 | |
|
||||||
+---------+------------+--------------+---------+----+-----------+
|
+---------+------------+----------------+---------+----+-----------+
|
||||||
|
|
||||||
Citus worker switchover
|
Citus worker switchover
|
||||||
-----------------------
|
-----------------------
|
||||||
@@ -115,33 +121,33 @@ the coordinator for the shards hosted on a worker node. The switchover then
|
|||||||
happens while the traffic is kept on the coordinator, and resumes as soon as a
|
happens while the traffic is kept on the coordinator, and resumes as soon as a
|
||||||
new primary worker node is ready to accept read-write queries.
|
new primary worker node is ready to accept read-write queries.
|
||||||
|
|
||||||
An example of ``patronictl switchover`` on the worker cluster::
|
An example of :ref:`patronictl_switchover` on the worker cluster::
|
||||||
|
|
||||||
postgres@coord1:~$ patronictl switchover demo
|
postgres@coord1:~$ patronictl switchover demo
|
||||||
+ Citus cluster: demo ----------+--------------+---------+----+-----------+
|
+ Citus cluster: demo ----------+----------------+---------+----+-----------+
|
||||||
| Group | Member | Host | Role | State | TL | Lag in MB |
|
| Group | Member | Host | Role | State | TL | Lag in MB |
|
||||||
+-------+---------+-------------+--------------+---------+----+-----------+
|
+-------+---------+-------------+----------------+---------+----+-----------+
|
||||||
| 0 | coord1 | 172.27.0.10 | Replica | running | 1 | 0 |
|
| 0 | coord1 | 172.27.0.10 | Replica | running | 1 | 0 |
|
||||||
| 0 | coord2 | 172.27.0.6 | Sync Standby | running | 1 | 0 |
|
| 0 | coord2 | 172.27.0.6 | Quorum Standby | running | 1 | 0 |
|
||||||
| 0 | coord3 | 172.27.0.4 | Leader | running | 1 | |
|
| 0 | coord3 | 172.27.0.4 | Leader | running | 1 | |
|
||||||
| 1 | work1-1 | 172.27.0.8 | Leader | running | 1 | |
|
| 1 | work1-1 | 172.27.0.8 | Leader | running | 1 | |
|
||||||
| 1 | work1-2 | 172.27.0.2 | Sync Standby | running | 1 | 0 |
|
| 1 | work1-2 | 172.27.0.2 | Quorum Standby | running | 1 | 0 |
|
||||||
| 2 | work2-1 | 172.27.0.5 | Sync Standby | running | 1 | 0 |
|
| 2 | work2-1 | 172.27.0.5 | Quorum Standby | running | 1 | 0 |
|
||||||
| 2 | work2-2 | 172.27.0.7 | Leader | running | 1 | |
|
| 2 | work2-2 | 172.27.0.7 | Leader | running | 1 | |
|
||||||
+-------+---------+-------------+--------------+---------+----+-----------+
|
+-------+---------+-------------+----------------+---------+----+-----------+
|
||||||
Citus group: 2
|
Citus group: 2
|
||||||
Primary [work2-2]:
|
Primary [work2-2]:
|
||||||
Candidate ['work2-1'] []:
|
Candidate ['work2-1'] []:
|
||||||
When should the switchover take place (e.g. 2022-12-22T08:02 ) [now]:
|
When should the switchover take place (e.g. 2024-08-26T08:02 ) [now]:
|
||||||
Current cluster topology
|
Current cluster topology
|
||||||
+ Citus cluster: demo (group: 2, 7179854924063375386) -----------+
|
+ Citus cluster: demo (group: 2, 7179854924063375386) -+-----------+
|
||||||
| Member | Host | Role | State | TL | Lag in MB |
|
| Member | Host | Role | State | TL | Lag in MB |
|
||||||
+---------+------------+--------------+---------+----+-----------+
|
+---------+------------+----------------+---------+----+-----------+
|
||||||
| work2-1 | 172.27.0.5 | Sync Standby | running | 1 | 0 |
|
| work2-1 | 172.27.0.5 | Quorum Standby | running | 1 | 0 |
|
||||||
| work2-2 | 172.27.0.7 | Leader | running | 1 | |
|
| work2-2 | 172.27.0.7 | Leader | running | 1 | |
|
||||||
+---------+------------+--------------+---------+----+-----------+
|
+---------+------------+----------------+---------+----+-----------+
|
||||||
Are you sure you want to switchover cluster demo, demoting current primary work2-2? [y/N]: y
|
Are you sure you want to switchover cluster demo, demoting current primary work2-2? [y/N]: y
|
||||||
2022-12-22 07:02:40.33003 Successfully switched over to "work2-1"
|
2024-08-26 07:02:40.33003 Successfully switched over to "work2-1"
|
||||||
+ Citus cluster: demo (group: 2, 7179854924063375386) ------+
|
+ Citus cluster: demo (group: 2, 7179854924063375386) ------+
|
||||||
| Member | Host | Role | State | TL | Lag in MB |
|
| Member | Host | Role | State | TL | Lag in MB |
|
||||||
+---------+------------+---------+---------+----+-----------+
|
+---------+------------+---------+---------+----+-----------+
|
||||||
@@ -150,32 +156,41 @@ An example of ``patronictl switchover`` on the worker cluster::
|
|||||||
+---------+------------+---------+---------+----+-----------+
|
+---------+------------+---------+---------+----+-----------+
|
||||||
|
|
||||||
postgres@coord1:~$ patronictl list demo
|
postgres@coord1:~$ patronictl list demo
|
||||||
+ Citus cluster: demo ----------+--------------+---------+----+-----------+
|
+ Citus cluster: demo ----------+----------------+---------+----+-----------+
|
||||||
| Group | Member | Host | Role | State | TL | Lag in MB |
|
| Group | Member | Host | Role | State | TL | Lag in MB |
|
||||||
+-------+---------+-------------+--------------+---------+----+-----------+
|
+-------+---------+-------------+----------------+---------+----+-----------+
|
||||||
| 0 | coord1 | 172.27.0.10 | Replica | running | 1 | 0 |
|
| 0 | coord1 | 172.27.0.10 | Replica | running | 1 | 0 |
|
||||||
| 0 | coord2 | 172.27.0.6 | Sync Standby | running | 1 | 0 |
|
| 0 | coord2 | 172.27.0.6 | Quorum Standby | running | 1 | 0 |
|
||||||
| 0 | coord3 | 172.27.0.4 | Leader | running | 1 | |
|
| 0 | coord3 | 172.27.0.4 | Leader | running | 1 | |
|
||||||
| 1 | work1-1 | 172.27.0.8 | Leader | running | 1 | |
|
| 1 | work1-1 | 172.27.0.8 | Leader | running | 1 | |
|
||||||
| 1 | work1-2 | 172.27.0.2 | Sync Standby | running | 1 | 0 |
|
| 1 | work1-2 | 172.27.0.2 | Quorum Standby | running | 1 | 0 |
|
||||||
| 2 | work2-1 | 172.27.0.5 | Leader | running | 2 | |
|
| 2 | work2-1 | 172.27.0.5 | Leader | running | 2 | |
|
||||||
| 2 | work2-2 | 172.27.0.7 | Sync Standby | running | 2 | 0 |
|
| 2 | work2-2 | 172.27.0.7 | Quorum Standby | running | 2 | 0 |
|
||||||
+-------+---------+-------------+--------------+---------+----+-----------+
|
+-------+---------+-------------+----------------+---------+----+-----------+
|
||||||
|
|
||||||
And this is how it looks on the coordinator side::
|
And this is how it looks on the coordinator side::
|
||||||
|
|
||||||
# The worker primary notifies the coordinator that it is going to execute "pg_ctl stop".
|
# The worker primary notifies the coordinator that it is going to execute "pg_ctl stop".
|
||||||
2022-12-22 07:02:38,636 DEBUG: query("BEGIN")
|
2024-08-26 07:02:38,636 DEBUG: query(BEGIN, ())
|
||||||
2022-12-22 07:02:38,636 DEBUG: query("SELECT pg_catalog.citus_update_node(3, '172.27.0.7-demoted', 5432, true, 10000)")
|
2024-08-26 07:02:38,636 DEBUG: query(SELECT pg_catalog.citus_update_node(%s, %s, %s, true, %s), (3, '172.19.0.7-demoted', 5432, 10000))
|
||||||
# From this moment all application traffic on the coordinator to the worker group 2 is paused.
|
# From this moment all application traffic on the coordinator to the worker group 2 is paused.
|
||||||
|
|
||||||
|
# The old worker primary is assigned as a secondary.
|
||||||
|
2024-08-26 07:02:40,084 DEBUG: query(SELECT pg_catalog.citus_update_node(%s, %s, %s, true, %s), (7, '172.19.0.7', 5432, 10000))
|
||||||
|
|
||||||
# The future worker primary notifies the coordinator that it acquired the leader lock in DCS and about to run "pg_ctl promote".
|
# The future worker primary notifies the coordinator that it acquired the leader lock in DCS and about to run "pg_ctl promote".
|
||||||
2022-12-22 07:02:40,085 DEBUG: query("SELECT pg_catalog.citus_update_node(3, '172.27.0.5', 5432)")
|
2024-08-26 07:02:40,085 DEBUG: query(SELECT pg_catalog.citus_update_node(%s, %s, %s, true, %s), (3, '172.19.0.5', 5432, 10000))
|
||||||
|
|
||||||
# The new worker primary just finished promote and notifies coordinator that it is ready to accept read-write traffic.
|
# The new worker primary just finished promote and notifies coordinator that it is ready to accept read-write traffic.
|
||||||
2022-12-22 07:02:41,485 DEBUG: query("COMMIT")
|
2024-08-26 07:02:41,485 DEBUG: query(COMMIT, ())
|
||||||
# From this moment the application traffic on the coordinator to the worker group 2 is unblocked.
|
# From this moment the application traffic on the coordinator to the worker group 2 is unblocked.
|
||||||
|
|
||||||
|
Secondary nodes
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Starting from Patroni v4.0.0 Citus secondary nodes without ``noloadbalance`` :ref:`tag <tags_settings>` are also registered in ``pg_dist_node``.
|
||||||
|
However, to use secondary nodes for read-only queries applications need to change `citus.use_secondary_nodes <https://docs.citusdata.com/en/latest/develop/api_guc.html#citus-use-secondary-nodes-enum>`__ GUC.
|
||||||
|
|
||||||
Peek into DCS
|
Peek into DCS
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
@@ -331,7 +346,7 @@ new Kubernetes objects ConfigMaps or Endpoints, it automatically puts the
|
|||||||
You can find a complete example of Patroni deployment on Kubernetes with Citus
|
You can find a complete example of Patroni deployment on Kubernetes with Citus
|
||||||
support in the `kubernetes`__ folder of the Patroni repository.
|
support in the `kubernetes`__ folder of the Patroni repository.
|
||||||
|
|
||||||
__ https://github.com/zalando/patroni/tree/master/kubernetes
|
__ https://github.com/patroni/patroni/tree/master/kubernetes
|
||||||
|
|
||||||
There are two important files for you:
|
There are two important files for you:
|
||||||
|
|
||||||
@@ -343,7 +358,7 @@ Citus upgrades and PostgreSQL major upgrades
|
|||||||
|
|
||||||
First, please read about upgrading Citus version in the `documentation`__.
|
First, please read about upgrading Citus version in the `documentation`__.
|
||||||
There is one minor change in the process. When executing upgrade, you have to
|
There is one minor change in the process. When executing upgrade, you have to
|
||||||
use ``patronictl restart`` instead of ``systemctl restart`` to restart
|
use :ref:`patronictl_restart` instead of ``systemctl restart`` to restart
|
||||||
PostgreSQL.
|
PostgreSQL.
|
||||||
|
|
||||||
__ https://docs.citusdata.com/en/latest/admin_guide/upgrading_citus.html
|
__ https://docs.citusdata.com/en/latest/admin_guide/upgrading_citus.html
|
||||||
|
|||||||
+147
-13
@@ -20,10 +20,17 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from sphinx.application import ENV_PICKLE_FILENAME
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath('..'))
|
sys.path.insert(0, os.path.abspath('..'))
|
||||||
|
|
||||||
from patroni.version import __version__
|
from patroni.version import __version__
|
||||||
|
|
||||||
|
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
module_dir = os.path.abspath(os.path.join(project_root, 'patroni'))
|
||||||
|
excludes = ['tests', 'setup.py', 'conf']
|
||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
# If your documentation needs a minimal Sphinx version, state it here.
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
@@ -33,11 +40,28 @@ from patroni.version import __version__
|
|||||||
# Add any Sphinx extension module names here, as strings. They can be
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
# ones.
|
# ones.
|
||||||
extensions = ['sphinx.ext.intersphinx',
|
extensions = [
|
||||||
|
'sphinx.ext.intersphinx',
|
||||||
'sphinx.ext.todo',
|
'sphinx.ext.todo',
|
||||||
'sphinx.ext.mathjax',
|
'sphinx.ext.mathjax',
|
||||||
'sphinx.ext.ifconfig',
|
'sphinx.ext.ifconfig',
|
||||||
'sphinx.ext.viewcode']
|
# 'sphinx.ext.viewcode',
|
||||||
|
'sphinx_github_style', # Generate "View on GitHub" for source code
|
||||||
|
'sphinxcontrib.apidoc', # For generating module docs from code
|
||||||
|
'sphinx.ext.autodoc', # For generating module docs from docstrings
|
||||||
|
'sphinx.ext.napoleon', # For Google and Numpy formatted docstrings
|
||||||
|
]
|
||||||
|
apidoc_module_dir = module_dir
|
||||||
|
apidoc_output_dir = 'modules'
|
||||||
|
apidoc_excluded_paths = excludes
|
||||||
|
apidoc_separate_modules = True
|
||||||
|
|
||||||
|
# Include autodoc for all members, including private ones and the ones that are missing a docstring.
|
||||||
|
autodoc_default_options = {
|
||||||
|
"members": True,
|
||||||
|
"undoc-members": True,
|
||||||
|
"private-members": True,
|
||||||
|
}
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ['_templates']
|
templates_path = ['_templates']
|
||||||
@@ -53,8 +77,8 @@ master_doc = 'index'
|
|||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = 'Patroni'
|
project = 'Patroni'
|
||||||
copyright = '2015 Compose, Zalando SE'
|
copyright = '2025 Compose, Zalando SE, Patroni Contributors'
|
||||||
author = 'Zalando SE'
|
author = 'Patroni Contributors'
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
# |version| and |release|, also used in various other places throughout the
|
# |version| and |release|, also used in various other places throughout the
|
||||||
@@ -70,7 +94,7 @@ release = __version__
|
|||||||
#
|
#
|
||||||
# This is also used if you do content translation via gettext catalogs.
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
# Usually you set "language" from the command line for these cases.
|
# Usually you set "language" from the command line for these cases.
|
||||||
language = None
|
language = 'en'
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
# List of patterns, relative to source directory, that match files and
|
||||||
# directories to ignore when looking for source files.
|
# directories to ignore when looking for source files.
|
||||||
@@ -90,11 +114,8 @@ todo_include_todos = True
|
|||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
html_theme = 'sphinx_rtd_theme'
|
||||||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||||
if not on_rtd: # only import and set the theme if we're building docs locally
|
|
||||||
import sphinx_rtd_theme
|
|
||||||
html_theme = 'sphinx_rtd_theme'
|
|
||||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
# further. For a list of options available for each theme, see the
|
# further. For a list of options available for each theme, see the
|
||||||
@@ -107,6 +128,34 @@ if not on_rtd: # only import and set the theme if we're building docs locally
|
|||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
html_static_path = ['_static']
|
html_static_path = ['_static']
|
||||||
|
|
||||||
|
# Replace "source" links with "edit on GitHub" when using rtd theme
|
||||||
|
html_context = {
|
||||||
|
'display_github': True,
|
||||||
|
'github_user': 'patroni',
|
||||||
|
'github_repo': 'patroni',
|
||||||
|
'github_version': 'master',
|
||||||
|
'conf_py_path': '/docs/',
|
||||||
|
}
|
||||||
|
|
||||||
|
# sphinx-github-style options, https://sphinx-github-style.readthedocs.io/en/latest/index.html
|
||||||
|
|
||||||
|
# The name of the top-level package.
|
||||||
|
top_level = "patroni"
|
||||||
|
|
||||||
|
# The blob to link to on GitHub - any of "head", "last_tag", or "{blob}"
|
||||||
|
# linkcode_blob = 'head'
|
||||||
|
|
||||||
|
# The link to your GitHub repository formatted as https://github.com/user/repo
|
||||||
|
# If not provided, will attempt to create the link from the html_context dict
|
||||||
|
# linkcode_url = f"https://github.com/{html_context['github_user']}/" \
|
||||||
|
# f"{html_context['github_repo']}/{html_context['github_version']}"
|
||||||
|
|
||||||
|
# The text to use for the linkcode link
|
||||||
|
# linkcode_link_text: str = "View on GitHub"
|
||||||
|
|
||||||
|
# A linkcode_resolve() function to use for resolving the link target
|
||||||
|
# linkcode_resolve: types.FunctionType
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTMLHelp output ------------------------------------------
|
# -- Options for HTMLHelp output ------------------------------------------
|
||||||
|
|
||||||
@@ -139,7 +188,7 @@ latex_elements = {
|
|||||||
# author, documentclass [howto, manual, or own class]).
|
# author, documentclass [howto, manual, or own class]).
|
||||||
latex_documents = [
|
latex_documents = [
|
||||||
(master_doc, 'Patroni.tex', 'Patroni Documentation',
|
(master_doc, 'Patroni.tex', 'Patroni Documentation',
|
||||||
'Zalando SE', 'manual'),
|
'Patroni Contributors', 'manual'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -165,7 +214,6 @@ texinfo_documents = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for Epub output ----------------------------------------------
|
# -- Options for Epub output ----------------------------------------------
|
||||||
|
|
||||||
# Bibliographic Dublin Core info.
|
# Bibliographic Dublin Core info.
|
||||||
@@ -187,9 +235,88 @@ epub_copyright = copyright
|
|||||||
epub_exclude_files = ['search.html']
|
epub_exclude_files = ['search.html']
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Example configuration for intersphinx: refer to the Python standard library.
|
# Example configuration for intersphinx: refer to the Python standard library.
|
||||||
intersphinx_mapping = {'https://docs.python.org/': None}
|
intersphinx_mapping = {'python': ('https://docs.python.org/', None)}
|
||||||
|
|
||||||
|
|
||||||
|
# Remove these pages from index, references, toc trees, etc.
|
||||||
|
# If the builder is not 'html' then add the API docs modules index to pages to be removed.
|
||||||
|
exclude_from_builder = {
|
||||||
|
'latex': ['modules/'],
|
||||||
|
'epub': ['modules/'],
|
||||||
|
}
|
||||||
|
# Internal holding list, anything added here will always be excluded
|
||||||
|
_docs_to_remove = []
|
||||||
|
|
||||||
|
|
||||||
|
def config_inited(app, config):
|
||||||
|
"""Run during Sphinx `config-inited` phase.
|
||||||
|
|
||||||
|
rtd reuses the environment, and there is no way to customize this behavior.
|
||||||
|
Thus we remove the saved env.
|
||||||
|
"""
|
||||||
|
pickle_file = os.path.join(app.doctreedir, ENV_PICKLE_FILENAME)
|
||||||
|
if on_rtd and os.path.exists(pickle_file):
|
||||||
|
os.remove(pickle_file)
|
||||||
|
|
||||||
|
|
||||||
|
def builder_inited(app):
|
||||||
|
"""Run during Sphinx `builder-inited` phase.
|
||||||
|
|
||||||
|
Set a config value to builder name and add module docs to `docs_to_remove`.
|
||||||
|
"""
|
||||||
|
print(f'The builder is: {app.builder.name}')
|
||||||
|
app.add_config_value('builder', app.builder.name, 'env')
|
||||||
|
|
||||||
|
# Remove pages when builder matches any referenced in exclude_from_builder
|
||||||
|
if exclude_from_builder.get(app.builder.name):
|
||||||
|
_docs_to_remove.extend(exclude_from_builder[app.builder.name])
|
||||||
|
|
||||||
|
|
||||||
|
def _to_be_removed(doc):
|
||||||
|
for remove in _docs_to_remove:
|
||||||
|
if doc.startswith(remove):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def env_get_outdated(app, env, added, changed, removed):
|
||||||
|
"""Run during Sphinx `env-get-outdated` phase.
|
||||||
|
|
||||||
|
Remove the items listed in `docs_to_remove` from known pages.
|
||||||
|
"""
|
||||||
|
to_remove = set()
|
||||||
|
if hasattr(env, 'found_docs'):
|
||||||
|
for doc in env.found_docs:
|
||||||
|
if _to_be_removed(doc):
|
||||||
|
to_remove.add(doc)
|
||||||
|
added.difference_update(to_remove)
|
||||||
|
changed.difference_update(to_remove)
|
||||||
|
removed.update(to_remove)
|
||||||
|
if hasattr(env, 'project'):
|
||||||
|
env.project.docnames.difference_update(to_remove)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def doctree_read(app, doctree):
|
||||||
|
"""Run during Sphinx `doctree-read` phase.
|
||||||
|
|
||||||
|
Remove the items listed in `docs_to_remove` from the table of contents.
|
||||||
|
"""
|
||||||
|
from sphinx import addnodes
|
||||||
|
for toc_tree_node in doctree.traverse(addnodes.toctree):
|
||||||
|
for e in toc_tree_node['entries']:
|
||||||
|
if _to_be_removed(str(e[1])):
|
||||||
|
toc_tree_node['entries'].remove(e)
|
||||||
|
|
||||||
|
|
||||||
|
def autodoc_skip(app, what, name, obj, would_skip, options):
|
||||||
|
"""Include autodoc of ``__init__`` methods, which are skipped by default."""
|
||||||
|
if name == "__init__":
|
||||||
|
return False
|
||||||
|
return would_skip
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# A possibility to have an own stylesheet, to add new rules or override existing ones
|
# A possibility to have an own stylesheet, to add new rules or override existing ones
|
||||||
# For the latter case, the CSS specificity of the rules should be higher than the default ones
|
# For the latter case, the CSS specificity of the rules should be higher than the default ones
|
||||||
@@ -198,3 +325,10 @@ def setup(app):
|
|||||||
app.add_css_file('custom.css')
|
app.add_css_file('custom.css')
|
||||||
else:
|
else:
|
||||||
app.add_stylesheet('custom.css')
|
app.add_stylesheet('custom.css')
|
||||||
|
|
||||||
|
# Run extra steps to remove module docs when running with a non-html builder
|
||||||
|
app.connect('config-inited', config_inited)
|
||||||
|
app.connect('builder-inited', builder_inited)
|
||||||
|
app.connect('env-get-outdated', env_get_outdated)
|
||||||
|
app.connect('doctree-read', doctree_read)
|
||||||
|
app.connect("autodoc-skip-member", autodoc_skip)
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
.. _contributing_guidelines:
|
||||||
|
|
||||||
|
Contributing guidelines
|
||||||
|
=======================
|
||||||
|
|
||||||
|
.. _chatting:
|
||||||
|
|
||||||
|
Chatting
|
||||||
|
--------
|
||||||
|
|
||||||
|
If you have a question, looking for an interactive troubleshooting help or want to chat with other Patroni users, join us on channel `#patroni <https://postgresteam.slack.com/archives/C9XPYG92A>`__ in the `PostgreSQL Slack <https://pgtreats.info/slack-invite>`__.
|
||||||
|
|
||||||
|
.. _reporting_bugs:
|
||||||
|
|
||||||
|
Reporting bugs
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Before reporting a bug please make sure to **reproduce it with the latest Patroni version**!
|
||||||
|
Also please double check if the issue already exists in our `Issues Tracker <https://github.com/patroni/patroni/issues>`__.
|
||||||
|
|
||||||
|
Running tests
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Requirements for running behave tests:
|
||||||
|
|
||||||
|
#. PostgreSQL packages including `contrib <https://www.postgresql.org/docs/current/contrib.html>`__ modules need to be installed.
|
||||||
|
#. PostgreSQL binaries must be available in your `PATH`. You may need to add them to the path with something like `PATH=/usr/lib/postgresql/11/bin:$PATH python -m behave`.
|
||||||
|
#. If you'd like to test with external DCSs (e.g., Etcd, Consul, and Zookeeper) you'll need the packages installed and respective services running and accepting unencrypted/unprotected connections on localhost and default port. In the case of Etcd or Consul, the behave test suite could start them up if binaries are available in the `PATH`.
|
||||||
|
|
||||||
|
Install dependencies:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
# You may want to use Virtualenv or specify pip3.
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install -r requirements.dev.txt
|
||||||
|
|
||||||
|
After you have all dependencies installed, you can run the various test suites:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
# You may want to use Virtualenv or specify python3.
|
||||||
|
|
||||||
|
# Run flake8 to check syntax and formatting:
|
||||||
|
python setup.py flake8
|
||||||
|
|
||||||
|
# Run the pytest suite in tests/:
|
||||||
|
python setup.py test
|
||||||
|
|
||||||
|
# Moreover, you may want to run tests in different scopes for debugging purposes,
|
||||||
|
# the -s option include print output during test execution.
|
||||||
|
# Tests in pytest typically follow the pattern: FILEPATH::CLASSNAME::TESTNAME.
|
||||||
|
pytest -s tests/test_api.py
|
||||||
|
pytest -s tests/test_api.py::TestRestApiHandler
|
||||||
|
pytest -s tests/test_api.py::TestRestApiHandler::test_do_GET
|
||||||
|
|
||||||
|
# Run the behave (https://behave.readthedocs.io/en/latest/) test suite in features/;
|
||||||
|
# modify DCS as desired (raft has no dependencies so is the easiest to start with):
|
||||||
|
DCS=raft python -m behave
|
||||||
|
|
||||||
|
Testing with tox
|
||||||
|
----------------
|
||||||
|
|
||||||
|
To run tox tests you only need to install one dependency (other than Python)
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pip install tox>=4
|
||||||
|
|
||||||
|
If you wish to run `behave` tests then you also need docker installed.
|
||||||
|
|
||||||
|
Tox configuration in `tox.ini` has "environments" to run the following tasks:
|
||||||
|
|
||||||
|
* lint: Python code lint with `flake8`
|
||||||
|
* test: unit tests for all available python interpreters with `pytest`,
|
||||||
|
generates XML reports or HTML reports if a TTY is detected
|
||||||
|
* dep: detect package dependency conflicts using `pipdeptree`
|
||||||
|
* type: static type checking with `pyright`
|
||||||
|
* black: code formatting with `black`
|
||||||
|
* docker-build: build docker image used for the `behave` env
|
||||||
|
* docker-cmd: run arbitrary command with the above image
|
||||||
|
* docker-behave-etcd: run tox for behave tests with above image
|
||||||
|
* py*behave: run behave with available python interpreters (without docker, although
|
||||||
|
this is what is called inside docker containers)
|
||||||
|
* docs: build docs with `sphinx`
|
||||||
|
|
||||||
|
Running tox
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
To run the default env list; dep, lint, test, and docs, just run:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
tox
|
||||||
|
|
||||||
|
The `test` envs can be run with the label `test`:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
tox -m test
|
||||||
|
|
||||||
|
The `behave` docker tests can be run with the label `behave`:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
tox -m behave
|
||||||
|
|
||||||
|
Similarly, docs has the label `docs`.
|
||||||
|
|
||||||
|
All other envs can be run with their respective env names:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
tox -e lint
|
||||||
|
tox -e py39-test-lin
|
||||||
|
|
||||||
|
It is also possible to select partial env lists using `factors`. For example, if you want to run
|
||||||
|
all envs for python 3.10:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
tox -f py310
|
||||||
|
|
||||||
|
This is equivalent to running all the envs listed below:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ tox -l -f py310
|
||||||
|
py310-test-lin
|
||||||
|
py310-test-mac
|
||||||
|
py310-test-win
|
||||||
|
py310-type-lin
|
||||||
|
py310-type-mac
|
||||||
|
py310-type-win
|
||||||
|
py310-behave-etcd-lin
|
||||||
|
py310-behave-etcd-win
|
||||||
|
py310-behave-etcd-mac
|
||||||
|
|
||||||
|
|
||||||
|
You can list all configured combinations of environments with tox (>=v4) like so
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
tox l
|
||||||
|
|
||||||
|
The envs `test` and `docs` will attempt to open the HTML output files
|
||||||
|
when the job completes, if tox is run with an active terminal. This
|
||||||
|
is intended to be for benefit of the developer running this env locally.
|
||||||
|
It will attempt to run `open` on a mac and `xdg-open` on Linux.
|
||||||
|
To use a different command set the env var `OPEN_CMD` to the name or path of
|
||||||
|
the command. If this step fails it will not fail the run overall.
|
||||||
|
If you want to disable this facility set the env var `OPEN_CMD` to the `:` no-op command.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
OPEN_CMD=: tox -m docs
|
||||||
|
|
||||||
|
Behave tests
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Behave tests with `-m behave` will build docker images based on PG_MAJOR version 11 through 16 and then run all
|
||||||
|
behave tests. This can take quite a long time to run so you might want to limit the scope to a select version of
|
||||||
|
Postgres or to a specific feature set or steps.
|
||||||
|
|
||||||
|
To specify the version of postgres include the full name of the dependent image build env that you want and then the
|
||||||
|
behave env name. For instance if you want Postgres 14 use:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
tox -e pg14-docker-build,pg14-docker-behave-etcd-lin
|
||||||
|
|
||||||
|
If on the other hand you want to test a specific feature you can pass positional arguments to behave. This will run
|
||||||
|
the watchdog behave feature test scenario with all versions of Postgres.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
tox -m behave -- features/watchdog.feature
|
||||||
|
|
||||||
|
Of course you can combine the two.
|
||||||
|
|
||||||
|
Contributing a pull request
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
#. Fork the repository, develop and test your code changes.
|
||||||
|
#. Reflect changes in the user documentation.
|
||||||
|
#. Submit a pull request with a clear description of the changes objective. Link an existing issue if necessary.
|
||||||
|
|
||||||
|
You'll get feedback about your pull request as soon as possible.
|
||||||
|
|
||||||
|
Happy Patroni hacking ;-)
|
||||||
@@ -23,7 +23,7 @@ In general, it is impossible to distinguish between these two from a single node
|
|||||||
DCS Failsafe Mode
|
DCS Failsafe Mode
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
We introduce a new special option, the ``failsafe_mode``. It could be enabled only via global configuration stored in the DCS ``/config`` key. If the failsafe mode is enabled and the leader lock update in DCS failed due to reasons different from the version/value/index mismatch, Postgres may continue to run as a primary if it can access all known members of the cluster via Patroni REST API.
|
We introduce a new special option, the ``failsafe_mode``. It could be enabled only via global :ref:`dynamic configuration <dynamic_configuration>` stored in the DCS ``/config`` key. If the failsafe mode is enabled and the leader lock update in DCS failed due to reasons different from the version/value/index mismatch, Postgres may continue to run as a primary if it can access all known members of the cluster via Patroni REST API.
|
||||||
|
|
||||||
|
|
||||||
Low-level implementation details
|
Low-level implementation details
|
||||||
@@ -53,11 +53,11 @@ F.A.Q.
|
|||||||
- What if all members of the Patroni cluster are lost while DCS is down?
|
- What if all members of the Patroni cluster are lost while DCS is down?
|
||||||
|
|
||||||
Patroni could be configured to create the new replica from the backup even when the cluster doesn't have a leader. But, if the new member isn't present in the ``/failsafe`` key, it will not be able to grab the leader lock and promote.
|
Patroni could be configured to create the new replica from the backup even when the cluster doesn't have a leader. But, if the new member isn't present in the ``/failsafe`` key, it will not be able to grab the leader lock and promote.
|
||||||
|
|
||||||
- What will happen if the primary lost access to DCS while replicas didn't?
|
- What will happen if the primary lost access to DCS while replicas didn't?
|
||||||
|
|
||||||
The primary will execute the failsafe code and contact all known replicas. These replicas will use this information as an indicator that the primary is alive and will not start the leader race even if the leader lock in DCS has expired.
|
The primary will execute the failsafe code and contact all known replicas. These replicas will use this information as an indicator that the primary is alive and will not start the leader race even if the leader lock in DCS has expired.
|
||||||
|
|
||||||
- How to enable the Failsafe Mode?
|
- How to enable the Failsafe Mode?
|
||||||
|
|
||||||
Before enabling the ``failsafe_mode`` please make sure that Patroni version on all members is up-to-date. After that, you can use either the ``PATCH /config`` :ref:`REST API <rest_api>` or ``patronictl edit-config -s failsafe_mode=true``
|
Before enabling the ``failsafe_mode`` please make sure that Patroni version on all members is up-to-date. After that, you can use either the ``PATCH /config`` :ref:`REST API <rest_api>` or :ref:`patronictl edit-config -s failsafe_mode=true <patronictl_edit_config_parameters>`
|
||||||
|
|||||||
@@ -1,89 +1,117 @@
|
|||||||
.. _dynamic_configuration:
|
.. _dynamic_configuration:
|
||||||
|
|
||||||
Patroni configuration
|
==============================
|
||||||
=====================
|
Dynamic Configuration Settings
|
||||||
|
==============================
|
||||||
|
|
||||||
Patroni configuration is stored in the DCS (Distributed Configuration Store). There are 3 types of configuration:
|
Dynamic configuration is stored in the DCS (Distributed Configuration Store) and applied on all cluster nodes.
|
||||||
|
|
||||||
- Dynamic configuration.
|
In order to change the dynamic configuration you can use either :ref:`patronictl_edit_config` tool or Patroni :ref:`REST API <rest_api>`.
|
||||||
These options can be set in DCS at any time. If the options changed are not part of the startup configuration,
|
|
||||||
they are applied asynchronously (upon the next wake up cycle) to every node, which gets subsequently reloaded.
|
|
||||||
If the node requires a restart to apply the configuration (for options with context postmaster, if their values
|
|
||||||
have changed), a special flag, ``pending_restart`` indicating this, is set in the members.data JSON.
|
|
||||||
Additionally, the node status also indicates this, by showing ``"restart_pending": true``.
|
|
||||||
|
|
||||||
- Local :ref:`configuration <settings>` (patroni.yml).
|
- **loop\_wait**: the number of seconds the loop will sleep. Default value: 10, minimum possible value: 1
|
||||||
These options are defined in the configuration file and take precedence over dynamic configuration.
|
- **ttl**: the TTL to acquire the leader lock (in seconds). Think of it as the length of time before initiation of the automatic failover process. Default value: 30, minimum possible value: 20
|
||||||
patroni.yml could be changed and reloaded in runtime (without restart of Patroni) by sending SIGHUP to the Patroni process, performing ``POST /reload`` REST-API request or executing ``patronictl reload``.
|
- **retry\_timeout**: timeout for DCS and PostgreSQL operation retries (in seconds). DCS or network issues shorter than this will not cause Patroni to demote the leader. Default value: 10, minimum possible value: 3
|
||||||
|
|
||||||
- Environment :ref:`configuration <environment>`.
|
.. warning::
|
||||||
It is possible to set/override some of the "Local" configuration parameters with environment variables.
|
when changing values of **loop_wait**, **retry_timeout**, or **ttl** you have to follow the rule:
|
||||||
Environment configuration is very useful when you are running in a dynamic environment and you don't know some of the parameters in advance (for example it's not possible to know your external IP address when you are running inside ``docker``).
|
|
||||||
|
|
||||||
The local configuration can be either a single YAML file or a directory. When it is a directory, all YAML files in that directory are loaded one by one in sorted order. In case a key is defined in multiple files, the occurrence in the last file takes precedence.
|
.. code-block:: python
|
||||||
|
|
||||||
Some of the PostgreSQL parameters must hold the same values on the primary and the replicas. For those, values set either in the local patroni configuration files or via the environment variables take no effect. To alter or set their values one must change the shared configuration in the DCS. Below is the actual list of such parameters together with the default values:
|
loop_wait + 2 * retry_timeout <= ttl
|
||||||
|
|
||||||
- max_connections: 100
|
|
||||||
- max_locks_per_transaction: 64
|
|
||||||
- max_worker_processes: 8
|
|
||||||
- max_prepared_transactions: 0
|
|
||||||
- wal_level: hot_standby
|
|
||||||
- wal_log_hints: on
|
|
||||||
- track_commit_timestamp: off
|
|
||||||
|
|
||||||
For the parameters below, PostgreSQL does not require equal values among the primary and all the replicas. However, considering the possibility of a replica to become the primary at any time, it doesn't really make sense to set them differently; therefore, Patroni restricts setting their values to the Dynamic configuration
|
|
||||||
|
|
||||||
- max_wal_senders: 5
|
|
||||||
- max_replication_slots: 5
|
|
||||||
- wal_keep_segments: 8
|
|
||||||
- wal_keep_size: 128MB
|
|
||||||
|
|
||||||
These parameters are validated to ensure they are sane, or meet a minimum value.
|
|
||||||
|
|
||||||
There are some other Postgres parameters controlled by Patroni:
|
|
||||||
|
|
||||||
- listen_addresses - is set either from ``postgresql.listen`` or from ``PATRONI_POSTGRESQL_LISTEN`` environment variable
|
|
||||||
- port - is set either from ``postgresql.listen`` or from ``PATRONI_POSTGRESQL_LISTEN`` environment variable
|
|
||||||
- cluster_name - is set either from ``scope`` or from ``PATRONI_SCOPE`` environment variable
|
|
||||||
- hot_standby: on
|
|
||||||
|
|
||||||
To be on the safe side parameters from the above lists are not written into ``postgresql.conf``, but passed as a list of arguments to the ``pg_ctl start`` which gives them the highest precedence, even above `ALTER SYSTEM <https://www.postgresql.org/docs/current/static/sql-altersystem.html>`__
|
|
||||||
|
|
||||||
|
|
||||||
When applying the local or dynamic configuration options, the following actions are taken:
|
- **maximum\_lag\_on\_failover**: the maximum bytes a follower may lag to be able to participate in leader election.
|
||||||
|
- **maximum\_lag\_on\_syncnode**: the maximum bytes a synchronous follower may lag before it is considered as an unhealthy candidate and swapped by healthy asynchronous follower. Patroni utilize the max replica lsn if there is more than one follower, otherwise it will use leader's current wal lsn. Default is -1, Patroni will not take action to swap synchronous unhealthy follower when the value is set to 0 or below. Please set the value high enough so Patroni won't swap synchrounous follower frequently during high transaction volume.
|
||||||
|
- **max\_timelines\_history**: maximum number of timeline history items kept in DCS. Default value: 0. When set to 0, it keeps the full history in DCS.
|
||||||
|
- **primary\_start\_timeout**: the amount of time a primary is allowed to recover from failures before failover is triggered (in seconds). Default is 300 seconds. When set to 0 failover is done immediately after a crash is detected if possible. When using asynchronous replication a failover can cause lost transactions. Worst case failover time for primary failure is: loop\_wait + primary\_start\_timeout + loop\_wait, unless primary\_start\_timeout is zero, in which case it's just loop\_wait. Set the value according to your durability/availability tradeoff.
|
||||||
|
- **primary\_stop\_timeout**: The number of seconds Patroni is allowed to wait when stopping Postgres and effective only when synchronous_mode is enabled. When set to > 0 and the synchronous_mode is enabled, Patroni sends SIGKILL to the postmaster if the stop operation is running for more than the value set by primary\_stop\_timeout. Set the value according to your durability/availability tradeoff. If the parameter is not set or set <= 0, primary\_stop\_timeout does not apply.
|
||||||
|
- **synchronous\_mode**: turns on synchronous replication mode. Possible values: ``off``, ``on``, ``quorum``. In this mode the leader takes care of management of ``synchronous_standby_names``, and only the last known leader, or one of synchronous replicas, are allowed to participate in leader race. Synchronous mode makes sure that successfully committed transactions will not be lost at failover, at the cost of losing availability for writes when Patroni cannot ensure transaction durability. See :ref:`replication modes documentation <replication_modes>` for details.
|
||||||
|
- **synchronous\_mode\_strict**: prevents disabling synchronous replication if no synchronous replicas are available, blocking all client writes to the primary. See :ref:`replication modes documentation <replication_modes>` for details.
|
||||||
|
- **synchronous\_node\_count**: if ``synchronous_mode`` is enabled, this parameter is used by Patroni to manage the precise number of synchronous standby instances and adjusts the state in DCS and the ``synchronous_standby_names`` parameter in PostgreSQL as members join and leave. If the parameter is set to a value higher than the number of eligible nodes, it will be automatically adjusted. Defaults to ``1``.
|
||||||
|
- **failsafe\_mode**: Enables :ref:`DCS Failsafe Mode <dcs_failsafe_mode>`. Defaults to `false`.
|
||||||
|
- **postgresql**:
|
||||||
|
|
||||||
- The node first checks if there is a postgresql.base.conf or if the ``custom_conf`` parameter is set.
|
- **use\_pg\_rewind**: whether or not to use pg_rewind. Defaults to `false`. Note that either the cluster must be initialized with ``data page checksums`` (``--data-checksums`` option for ``initdb``) and/or ``wal_log_hints`` must be set to ``on``, or ``pg_rewind`` will not work.
|
||||||
- If the `custom_conf` parameter is set, it will take the file specified on it as a base configuration, ignoring `postgresql.base.conf` and `postgresql.conf`.
|
- **use\_slots**: whether or not to use replication slots. Defaults to `true` on PostgreSQL 9.4+.
|
||||||
- If the `custom_conf` parameter is not set and `postgresql.base.conf` exists, it contains the renamed "original" configuration and it will be used as a base configuration.
|
- **recovery\_conf**: additional configuration settings written to recovery.conf when configuring follower. There is no recovery.conf anymore in PostgreSQL 12, but you may continue using this section, because Patroni handles it transparently.
|
||||||
- If there is no `custom_conf` nor `postgresql.base.conf`, the original postgresql.conf is taken and renamed to postgresql.base.conf.
|
- **parameters**: configuration parameters (GUCs) for Postgres in format ``{max_connections: 100, wal_level: "replica", max_wal_senders: 10, wal_log_hints: "on"}``. Many of these are required for replication to work.
|
||||||
- The dynamic options (with the exceptions above) are dumped into the postgresql.conf and an include is set in
|
|
||||||
postgresql.conf to the used base configuration (either postgresql.base.conf or what is on ``custom_conf``). Therefore, we would be able to apply new options without re-reading the configuration file to check if the include is present not.
|
|
||||||
- Some parameters that are essential for Patroni to manage the cluster are overridden using the command line.
|
|
||||||
- If some of the options that require restart are changed (we should look at the context in pg_settings and at the actual
|
|
||||||
values of those options), a pending_restart flag of a given node is set. This flag is reset on any restart.
|
|
||||||
|
|
||||||
The parameters would be applied in the following order (run-time are given the highest priority):
|
- **pg\_hba**: list of lines that Patroni will use to generate ``pg_hba.conf``. Patroni ignores this parameter if ``hba_file`` PostgreSQL parameter is set to a non-default value.
|
||||||
|
|
||||||
1. load parameters from file `postgresql.base.conf` (or from a `custom_conf` file, if set)
|
- **- host all all 0.0.0.0/0 md5**
|
||||||
2. load parameters from file `postgresql.conf`
|
- **- host replication replicator 127.0.0.1/32 md5**: A line like this is required for replication.
|
||||||
3. load parameters from file `postgresql.auto.conf`
|
|
||||||
4. run-time parameter using `-o --name=value`
|
|
||||||
|
|
||||||
This allows configuration for all the nodes (2), configuration for a specific node using `ALTER SYSTEM` (3) and ensures that parameters essential to the running of Patroni are enforced (4), as well as leaves room for configuration tools that manage `postgresql.conf` directly without involving Patroni (1).
|
- **pg\_ident**: list of lines that Patroni will use to generate ``pg_ident.conf``. Patroni ignores this parameter if ``ident_file`` PostgreSQL parameter is set to a non-default value.
|
||||||
|
|
||||||
|
- **- mapname1 systemname1 pguser1**
|
||||||
|
- **- mapname1 systemname2 pguser2**
|
||||||
|
|
||||||
|
- **standby\_cluster**: if this section is defined, we want to bootstrap a standby cluster.
|
||||||
|
|
||||||
|
- **host**: an address of remote node
|
||||||
|
- **port**: a port of remote node
|
||||||
|
- **primary\_slot\_name**: which slot on the remote node to use for replication. This parameter is optional, the default value is derived from the instance name (see function `slot_name_from_member_name`).
|
||||||
|
- **create\_replica\_methods**: an ordered list of methods that can be used to bootstrap standby leader from the remote primary, can be different from the list defined in :ref:`postgresql_settings`
|
||||||
|
- **restore\_command**: command to restore WAL records from the remote primary to nodes in a standby cluster, can be different from the list defined in :ref:`postgresql_settings`
|
||||||
|
- **archive\_cleanup\_command**: cleanup command for standby leader
|
||||||
|
- **recovery\_min\_apply\_delay**: how long to wait before actually apply WAL records on a standby leader
|
||||||
|
|
||||||
|
- **member_slots_ttl**: retention time of physical replication slots for replicas when they are shut down. Default value: `30min`. Set it to `0` if you want to keep the old behavior (when the member key expires from DCS, the slot is immediately removed). The feature works only starting from PostgreSQL 11.
|
||||||
|
- **slots**: define permanent replication slots. These slots will be preserved during switchover/failover. Permanent slots that don't exist will be created by Patroni. With PostgreSQL 11 onwards permanent physical slots are created on all nodes and their position is advanced every **loop_wait** seconds. For PostgreSQL versions older than 11 permanent physical replication slots are maintained only on the current primary. The logical slots are copied from the primary to a standby with restart, and after that their position advanced every **loop_wait** seconds (if necessary). Copying logical slot files performed via ``libpq`` connection and using either rewind or superuser credentials (see **postgresql.authentication** section). There is always a chance that the logical slot position on the replica is a bit behind the former primary, therefore application should be prepared that some messages could be received the second time after the failover. The easiest way of doing so - tracking ``confirmed_flush_lsn``. Enabling permanent replication slots requires **postgresql.use_slots** to be set to ``true``. If there are permanent logical replication slots defined Patroni will automatically enable the ``hot_standby_feedback``. Since the failover of logical replication slots is unsafe on PostgreSQL 9.6 and older and PostgreSQL version 10 is missing some important functions, the feature only works with PostgreSQL 11+.
|
||||||
|
|
||||||
|
- **my\_slot\_name**: the name of the permanent replication slot. If the permanent slot name matches with the name of the current node it will not be created on this node. If you add a permanent physical replication slot which name matches the name of a Patroni member, Patroni will ensure that the slot that was created is not removed even if the corresponding member becomes unresponsive, situation which would normally result in the slot's removal by Patroni. Although this can be useful in some situations, such as when you want replication slots used by members to persist during temporary failures or when importing existing members to a new Patroni cluster (see :ref:`Convert a Standalone to a Patroni Cluster <existing_data>` for details), caution should be exercised by the operator that these clashes in names are not persisted in the DCS, when the slot is no longer required, due to its effect on normal functioning of Patroni.
|
||||||
|
|
||||||
|
- **type**: slot type. Could be ``physical`` or ``logical``. If the slot is logical, you have to additionally define ``database`` and ``plugin``. If the slot is physical, you can optionally define ``cluster_type``.
|
||||||
|
- **database**: the database name where logical slots should be created.
|
||||||
|
- **plugin**: the plugin name for the logical slot.
|
||||||
|
- **cluster_type**: the type of cluster (``primary`` or ``standby``) the slot should only be created on, otherwise it will not be created or an already existing slot will be dropped.
|
||||||
|
|
||||||
|
- **ignore\_slots**: list of sets of replication slot properties for which Patroni should ignore matching slots. This configuration/feature/etc. is useful when some replication slots are managed outside of Patroni. Any subset of matching properties will cause a slot to be ignored.
|
||||||
|
|
||||||
|
- **name**: the name of the replication slot.
|
||||||
|
- **type**: slot type. Can be ``physical`` or ``logical``. If the slot is logical, you may additionally define ``database`` and/or ``plugin``.
|
||||||
|
- **database**: the database name (when matching a ``logical`` slot).
|
||||||
|
- **plugin**: the logical decoding plugin (when matching a ``logical`` slot).
|
||||||
|
|
||||||
|
Note: **slots** is a hashmap while **ignore_slots** is an array. For example:
|
||||||
|
|
||||||
|
.. code:: YAML
|
||||||
|
|
||||||
|
slots:
|
||||||
|
permanent_logical_slot_name:
|
||||||
|
type: logical
|
||||||
|
database: my_db
|
||||||
|
plugin: test_decoding
|
||||||
|
permanent_physical_slot_name:
|
||||||
|
type: physical
|
||||||
|
...
|
||||||
|
ignore_slots:
|
||||||
|
- name: ignored_logical_slot_name
|
||||||
|
type: logical
|
||||||
|
database: my_db
|
||||||
|
plugin: test_decoding
|
||||||
|
- name: ignored_physical_slot_name
|
||||||
|
type: physical
|
||||||
|
...
|
||||||
|
|
||||||
|
Note: When running PostgreSQL v11 or newer Patroni maintains physical replication slots on all nodes that could potentially become a leader, so that replica nodes keep WAL segments reserved if they are potentially required by other nodes. In case the node is absent and its member key in DCS gets expired, the corresponding replication slot is dropped after ``member_slots_ttl`` (default value is `30min`). You can increase or decrease retention based on your needs. Alternatively, if your cluster topology is static (fixed number of nodes that never change their names) you can configure permanent physical replication slots with names corresponding to the names of the nodes to avoid slots removal and recycling of WAL files while replica is temporarily down:
|
||||||
|
|
||||||
|
.. code:: YAML
|
||||||
|
|
||||||
|
slots:
|
||||||
|
node_name1:
|
||||||
|
type: physical
|
||||||
|
node_name2:
|
||||||
|
type: physical
|
||||||
|
node_name3:
|
||||||
|
type: physical
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
Also, the following Patroni configuration options can be changed only dynamically:
|
.. warning::
|
||||||
|
Permanent replication slots are synchronized only from the ``primary``/``standby_leader`` to replica nodes. That means, applications are supposed to be using them only from the leader node. Using them on replica nodes will cause indefinite growth of ``pg_wal`` on all other nodes in the cluster.
|
||||||
|
An exception to that rule are physical slots that match the Patroni member names (created and maintained by Patroni). Those will be synchronized among all nodes as they are used for replication among them.
|
||||||
|
|
||||||
- ttl: 30
|
|
||||||
- loop_wait: 10
|
|
||||||
- retry_timeouts: 10
|
|
||||||
- maximum_lag_on_failover: 1048576
|
|
||||||
- max_timelines_history: 0
|
|
||||||
- check_timeline: false
|
|
||||||
- postgresql.use_slots: true
|
|
||||||
|
|
||||||
Upon changing these options, Patroni will read the relevant section of the configuration stored in DCS and change its
|
.. warning::
|
||||||
run-time values.
|
Setting ``nostream`` tag on standby disables copying and synchronization of permanent logical replication slots on the node itself and all its cascading replicas if any.
|
||||||
|
|
||||||
Patroni nodes are dumping the state of the DCS options to disk upon for every change of the configuration into the file ``patroni.dynamic.json`` located in the Postgres data directory. Only the leader is allowed to restore these options from the on-disk dump if these are completely absent from the DCS or if they are invalid.
|
|
||||||
|
|||||||
+56
-16
@@ -10,18 +10,58 @@ To deploy a Patroni cluster without using a pre-existing PostgreSQL instance, se
|
|||||||
Procedure
|
Procedure
|
||||||
---------
|
---------
|
||||||
|
|
||||||
A Patroni cluster can be started with a data directory from a single-node PostgreSQL database. This is achieved by following closely these steps:
|
You can find below an overview of steps for converting an existing Postgres cluster to a Patroni managed cluster. In the steps we assume all nodes that are part of the existing cluster are currently up and running, and that you *do not* intend to change Postgres configuration while the migration is ongoing. The steps:
|
||||||
|
|
||||||
1. Manually start PostgreSQL daemon
|
#. Create the Postgres users as explained for :ref:`authentication <postgresql_settings>` section of the Patroni configuration. You can find sample SQL commands to create the users in the code block below, in which you need to replace the usernames and passwords as per your environment. If you already have the relevant users, then you can skip this step.
|
||||||
2. Create Patroni superuser and replication users as defined in the :ref:`authentication <postgresql_settings>` section of the Patroni configuration. If this user is created in SQL, the following queries achieve this:
|
|
||||||
|
|
||||||
.. code-block:: sql
|
.. code-block:: sql
|
||||||
|
|
||||||
CREATE USER $PATRONI_SUPERUSER_USERNAME WITH SUPERUSER ENCRYPTED PASSWORD '$PATRONI_SUPERUSER_PASSWORD';
|
-- Patroni superuser
|
||||||
CREATE USER $PATRONI_REPLICATION_USERNAME WITH REPLICATION ENCRYPTED PASSWORD '$PATRONI_REPLICATION_PASSWORD';
|
-- Replace PATRONI_SUPERUSER_USERNAME and PATRONI_SUPERUSER_PASSWORD accordingly
|
||||||
|
CREATE USER PATRONI_SUPERUSER_USERNAME WITH SUPERUSER ENCRYPTED PASSWORD 'PATRONI_SUPERUSER_PASSWORD';
|
||||||
|
|
||||||
3. Start Patroni (e.g. ``patroni /etc/patroni/patroni.yml``). It automatically detects that PostgreSQL daemon is already running but its configuration might be out-of-date.
|
-- Patroni replication user
|
||||||
4. Ask Patroni to restart the node with ``patronictl restart cluster-name node-name``. This step is only required if PostgreSQL configuration is out-of-date.
|
-- Replace PATRONI_REPLICATION_USERNAME and PATRONI_REPLICATION_PASSWORD accordingly
|
||||||
|
CREATE USER PATRONI_REPLICATION_USERNAME WITH REPLICATION ENCRYPTED PASSWORD 'PATRONI_REPLICATION_PASSWORD';
|
||||||
|
|
||||||
|
-- Patroni rewind user, if you intend to enable use_pg_rewind in your Patroni configuration
|
||||||
|
-- Replace PATRONI_REWIND_USERNAME and PATRONI_REWIND_PASSWORD accordingly
|
||||||
|
CREATE USER PATRONI_REWIND_USERNAME WITH ENCRYPTED PASSWORD 'PATRONI_REWIND_PASSWORD';
|
||||||
|
GRANT EXECUTE ON function pg_catalog.pg_ls_dir(text, boolean, boolean) TO PATRONI_REWIND_USERNAME;
|
||||||
|
GRANT EXECUTE ON function pg_catalog.pg_stat_file(text, boolean) TO PATRONI_REWIND_USERNAME;
|
||||||
|
GRANT EXECUTE ON function pg_catalog.pg_read_binary_file(text) TO PATRONI_REWIND_USERNAME;
|
||||||
|
GRANT EXECUTE ON function pg_catalog.pg_read_binary_file(text, bigint, bigint, boolean) TO PATRONI_REWIND_USERNAME;
|
||||||
|
|
||||||
|
#. Perform the following steps on all Postgres nodes. Perform all steps on one node before proceeding with the next node. Start with the primary node, then proceed with each standby node:
|
||||||
|
|
||||||
|
#. If you are running Postgres through systemd, then disable the Postgres systemd unit. This is performed as Patroni manages starting and stopping the Postgres daemon.
|
||||||
|
|
||||||
|
#. Create a YAML configuration file for Patroni. You can use :ref:`Patroni configuration generation and validation tooling <validate_generate_config>` for that.
|
||||||
|
|
||||||
|
* **Note (specific for the primary node):** If you have replication slots being used for replication between cluster members, then it is recommended that you enable ``use_slots`` and configure the existing replication slots as permanent via the ``slots`` configuration item. Be aware that Patroni automatically creates replication slots for replication between members, and drops replication slots that it does not recognize, when ``use_slots`` is enabled. The idea of using permanent slots here is to allow your existing slots to persist while the migration to Patroni is in progress. See :ref:`YAML Configuration Settings <yaml_configuration>` for details.
|
||||||
|
|
||||||
|
#. Start Patroni using the ``patroni`` systemd service unit. It automatically detects that Postgres is already running and starts monitoring the instance.
|
||||||
|
|
||||||
|
#. Hand over Postgres "start up procedure" to Patroni. In order to do that you need to restart the cluster members through :ref:`patronictl restart cluster-name member-name <patronictl_restart_parameters>` command. For minimal downtime you might want to split this step into:
|
||||||
|
|
||||||
|
#. Immediate restart of the standby nodes.
|
||||||
|
#. Scheduled restart of the primary node within a maintenance window.
|
||||||
|
|
||||||
|
#. If you configured permanent slots in step ``1.2.``, then you should remove them from ``slots`` configuration through :ref:`patronictl edit-config cluster-name member-name <patronictl_edit_config_parameters>` command once the ``restart_lsn`` of the slots created by Patroni is able to catch up with the ``restart_lsn`` of the original slots for the corresponding members. By removing the slots from ``slots`` configuration you will allow Patroni to drop the original slots from your cluster once they are not needed anymore. You can find below an example query to check the ``restart_lsn`` of a couple slots, so you can compare them:
|
||||||
|
|
||||||
|
.. code-block:: sql
|
||||||
|
|
||||||
|
-- Assume original_slot_for_member_x is the name of the slot in your original
|
||||||
|
-- cluster for replicating changes to member X, and slot_for_member_x is the
|
||||||
|
-- slot created by Patroni for that purpose. You need restart_lsn of
|
||||||
|
-- slot_for_member_x to be >= restart_lsn of original_slot_for_member_x
|
||||||
|
SELECT slot_name,
|
||||||
|
restart_lsn
|
||||||
|
FROM pg_replication_slots
|
||||||
|
WHERE slot_name IN (
|
||||||
|
'original_slot_for_member_x',
|
||||||
|
'slot_for_member_x'
|
||||||
|
)
|
||||||
|
|
||||||
.. _major_upgrade:
|
.. _major_upgrade:
|
||||||
|
|
||||||
@@ -30,14 +70,14 @@ Major Upgrade of PostgreSQL Version
|
|||||||
|
|
||||||
The only possible way to do a major upgrade currently is:
|
The only possible way to do a major upgrade currently is:
|
||||||
|
|
||||||
1. Stop Patroni
|
#. Stop Patroni
|
||||||
2. Upgrade PostgreSQL binaries and perform `pg_upgrade <https://www.postgresql.org/docs/current/pgupgrade.html>`_ on the primary node
|
#. Upgrade PostgreSQL binaries and perform `pg_upgrade <https://www.postgresql.org/docs/current/pgupgrade.html>`_ on the primary node
|
||||||
3. Update patroni.yml
|
#. Update patroni.yml
|
||||||
4. Remove the initialize key from DCS or wipe complete cluster state from DCS. The second one could be achieved by running ``patronictl remove <cluster-name>``. It is necessary because pg_upgrade runs initdb which actually creates a new database with a new PostgreSQL system identifier.
|
#. Remove the initialize key from DCS or wipe complete cluster state from DCS. The second one could be achieved by running :ref:`patronictl remove cluster-name <patronictl_remove_parameters>` . It is necessary because pg_upgrade runs initdb which actually creates a new database with a new PostgreSQL system identifier.
|
||||||
5. If you wiped the cluster state in the previous step, you may wish to copy patroni.dynamic.json from old data dir to the new one. It will help you to retain some PostgreSQL parameters you had set before.
|
#. If you wiped the cluster state in the previous step, you may wish to copy patroni.dynamic.json from old data dir to the new one. It will help you to retain some PostgreSQL parameters you had set before.
|
||||||
6. Start Patroni on the primary node.
|
#. Start Patroni on the primary node.
|
||||||
7. Upgrade PostgreSQL binaries, update patroni.yml and wipe the data_dir on standby nodes.
|
#. Upgrade PostgreSQL binaries, update patroni.yml and wipe the data_dir on standby nodes.
|
||||||
8. Start Patroni on the standby nodes and wait for the replication to complete.
|
#. Start Patroni on the standby nodes and wait for the replication to complete.
|
||||||
|
|
||||||
Running pg_upgrade on standby nodes is not supported by PostgreSQL. If you know what you are doing, you can try the rsync procedure described in https://www.postgresql.org/docs/current/pgupgrade.html instead of wiping data_dir on standby nodes. The safest way is however to let Patroni replicate the data for you.
|
Running pg_upgrade on standby nodes is not supported by PostgreSQL. If you know what you are doing, you can try the rsync procedure described in https://www.postgresql.org/docs/current/pgupgrade.html instead of wiping data_dir on standby nodes. The safest way is however to let Patroni replicate the data for you.
|
||||||
|
|
||||||
|
|||||||
+332
@@ -0,0 +1,332 @@
|
|||||||
|
.. _faq:
|
||||||
|
|
||||||
|
FAQ
|
||||||
|
===
|
||||||
|
|
||||||
|
In this section you will find answers for the most frequently asked questions about Patroni.
|
||||||
|
Each sub-section attempts to focus on different kinds of questions.
|
||||||
|
|
||||||
|
We hope that this helps you to clarify most of your questions.
|
||||||
|
If you still have further concerns or find yourself facing an unexpected issue, please refer to :ref:`chatting` and :ref:`reporting_bugs` for instructions on how to get help or report issues.
|
||||||
|
|
||||||
|
Comparison with other HA solutions
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
Why does Patroni require a separate cluster of DCS nodes while other solutions like ``repmgr`` do not?
|
||||||
|
There are different ways of implementing HA solutions, each of them with their pros and cons.
|
||||||
|
|
||||||
|
Software like ``repmgr`` performs communication among the nodes to decide when actions should be taken.
|
||||||
|
|
||||||
|
Patroni on the other hand relies on the state stored in the DCS. The DCS acts as a source of truth for Patroni to decide what it should do.
|
||||||
|
|
||||||
|
While having a separate DCS cluster can make you bloat your architecture, this approach also makes it less likely for split-brain scenarios to happen in your Postgres cluster.
|
||||||
|
|
||||||
|
What is the difference between Patroni and other HA solutions in regards to Postgres management?
|
||||||
|
Patroni does not just manage the high availability of the Postgres cluster but also manages Postgres itself.
|
||||||
|
|
||||||
|
If Postgres nodes do not exist yet, it takes care of bootstrapping the primary and the standby nodes, and also manages Postgres configuration of the nodes. If the Postgres nodes already exist, Patroni will take over management of the cluster.
|
||||||
|
|
||||||
|
Besides the above, Patroni also has self-healing capabilities. In other words, if a primary node fails, Patroni will not only fail over to a replica, but also attempt to rejoin the former primary as a replica of the new primary. Similarly, if a replica fails, Patroni will attempt to rejoin that replica.
|
||||||
|
|
||||||
|
That is way we call Patroni as a "template for HA solutions". It goes further than just managing physical replication: it manages Postgres as a whole.
|
||||||
|
|
||||||
|
DCS
|
||||||
|
---
|
||||||
|
|
||||||
|
Can I use the same ``etcd`` cluster to store data from two or more Patroni clusters?
|
||||||
|
Yes, you can!
|
||||||
|
|
||||||
|
Information about a Patroni cluster is stored in the DCS under a path prefixed with the ``namespace`` and ``scope`` Patroni settings.
|
||||||
|
|
||||||
|
As long as you do not have conflicting namespace and scope across different Patroni clusters, you should be able to use the same DCS cluster to store information from multiple Patroni clusters.
|
||||||
|
|
||||||
|
What occurs if I attempt to use the same combination of ``namespace`` and ``scope`` for different Patroni clusters that point to the same DCS cluster?
|
||||||
|
The second Patroni cluster that attempts to use the same ``namespace`` and ``scope`` will not be able to manage Postgres because it will find information related with that same combination in the DCS, but with an incompatible Postgres system identifier.
|
||||||
|
The mismatch on the system identifier causes Patroni to abort the management of the second cluster, as it assumes that refers to a different cluster and that the user has misconfigured Patroni.
|
||||||
|
|
||||||
|
Make sure to use different ``namespace`` / ``scope`` when dealing with different Patroni clusters that share the same DCS cluster.
|
||||||
|
|
||||||
|
What occurs if I lose my DCS cluster?
|
||||||
|
The DCS is used to store basically status and the dynamic configuration of the Patroni cluster.
|
||||||
|
|
||||||
|
They very first consequence is that all the Patroni clusters that rely on that DCS will go to read-only mode -- unless :ref:`dcs_failsafe_mode` is enabled.
|
||||||
|
|
||||||
|
What should I do if I lose my DCS cluster?
|
||||||
|
There are three possible outcomes upon losing your DCS cluster:
|
||||||
|
|
||||||
|
1. The DCS cluster is fully recovered: this requires no action from the Patroni side. Once the DCS cluster is recovered, Patroni should be able to recover too;
|
||||||
|
2. The DCS cluster is re-created in place, and the endpoints remain the same. No changes are required on the Patroni side;
|
||||||
|
3. A new DCS cluster is created with different endpoints. You will need to update the DCS endpoints in the Patroni configuration of each Patroni node.
|
||||||
|
|
||||||
|
If you face scenario ``2.`` or ``3.`` Patroni will take care of creating the status information again based on the current status of the cluster, and recreate the dynamic configuration on the DCS based on a backup file named ``patroni.dynamic.json`` which is stored inside the Postgres data directory of each member of the Patroni cluster.
|
||||||
|
|
||||||
|
What occurs if I lose majority in my DCS cluster?
|
||||||
|
The DCS will become unresponsive, which will cause Patroni to demote the current read/write Postgres node.
|
||||||
|
|
||||||
|
Remember: Patroni relies on the state of the DCS to take actions on the cluster.
|
||||||
|
|
||||||
|
You can use the :ref:`dcs_failsafe_mode` to alleviate that situation.
|
||||||
|
|
||||||
|
patronictl
|
||||||
|
----------
|
||||||
|
|
||||||
|
Do I need to run :ref:`patronictl` in the Patroni host?
|
||||||
|
No, you do not need to do that.
|
||||||
|
|
||||||
|
Running :ref:`patronictl` in the Patroni host is handy if you have access to the Patroni host because you can use the very same configuration file from the ``patroni`` agent for the :ref:`patronictl` application.
|
||||||
|
|
||||||
|
However, :ref:`patronictl` is basically a client and it can be executed from remote machines. You just need to provide it with enough configuration so it can reach the DCS and the REST API of the Patroni member(s).
|
||||||
|
|
||||||
|
Why did the information from one of my Patroni members disappear from the output of :ref:`patronictl_list` command?
|
||||||
|
Information shown by :ref:`patronictl_list` is based on the contents of the DCS.
|
||||||
|
|
||||||
|
If information about a member disappeared from the DCS it is very likely that the Patroni agent on that node is not running anymore, or it is not able to communicate with the DCS.
|
||||||
|
|
||||||
|
As the member is not able to update the information, the information eventually expires from the DCS, and consequently the member is not shown anymore in the output of :ref:`patronictl_list`.
|
||||||
|
|
||||||
|
Why is the information about one of my Patroni members not up-to-date in the output of :ref:`patronictl_list` command?
|
||||||
|
Information shown by :ref:`patronictl_list` is based on the contents of the DCS.
|
||||||
|
|
||||||
|
By default, that information is updated by Patroni roughly every ``loop_wait`` seconds.
|
||||||
|
In other words, even if everything is normally functional you may still see a "delay" of up to ``loop_wait`` seconds in the information stored in the DCS.
|
||||||
|
|
||||||
|
Be aware that that is not a rule, though. Some operations performed by Patroni cause it to immediately update the DCS information.
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
-------------
|
||||||
|
|
||||||
|
What is the difference between dynamic configuration and local configuration?
|
||||||
|
Dynamic configuration (or global configuration) is the configuration stored in the DCS, and which is applied to all members of the Patroni cluster.
|
||||||
|
This is primarily where you should store your configuration.
|
||||||
|
|
||||||
|
Settings that are specific to a node, or settings that you would like to overwrite the global configuration with, you should set only on the desired Patroni member as a local configuration.
|
||||||
|
That local configuration can be specified either through the configuration file or through environment variables.
|
||||||
|
|
||||||
|
See more in :ref:`patroni_configuration`.
|
||||||
|
|
||||||
|
What are the types of configuration in Patroni, and what is the precedence?
|
||||||
|
The types are:
|
||||||
|
|
||||||
|
* Dynamic configuration: applied to all members;
|
||||||
|
* Local configuration: applied to the local member, overrides dynamic configuration;
|
||||||
|
* Environment configuration: applied to the local member, overrides both dynamic and local configuration.
|
||||||
|
|
||||||
|
**Note:** some Postgres GUCs can only be set globally, i.e., through dynamic configuration. Besides that, there are GUCs which Patroni enforces a hard-coded value.
|
||||||
|
|
||||||
|
See more in :ref:`patroni_configuration`.
|
||||||
|
|
||||||
|
Is there any facility to help me create my Patroni configuration file?
|
||||||
|
Yes, there is.
|
||||||
|
|
||||||
|
You can use ``patroni --generate-sample-config`` or ``patroni --generate-config`` commands to generate a sample Patroni configuration or a Patroni configuration based on an existing Postgres instance, respectively.
|
||||||
|
|
||||||
|
Please refer to :ref:`generate_sample_config` and :ref:`generate_config` for more details.
|
||||||
|
|
||||||
|
I changed my parameters under ``bootstrap.dcs`` configuration but Patroni is not applying the changes to the cluster members. What is wrong?
|
||||||
|
The values configured under ``bootstrap.dcs`` are only used when bootstrapping a fresh cluster. Those values will be written to the DCS during the bootstrap.
|
||||||
|
|
||||||
|
After the bootstrap phase finishes, you will only be able to change the dynamic configuration through the DCS.
|
||||||
|
|
||||||
|
Refer to the next question for more details.
|
||||||
|
|
||||||
|
How can I change my dynamic configuration?
|
||||||
|
You need to change the configuration in the DCS. That is accomplished either through:
|
||||||
|
|
||||||
|
* :ref:`patronictl_edit_config`; or
|
||||||
|
* A ``PATCH`` request to :ref:`config_endpoint`.
|
||||||
|
|
||||||
|
How can I change my local configuration?
|
||||||
|
You need to change the configuration file of the corresponding Patroni member and signal the Patroni agent with ``SIHGUP``. You can do that using either of these approaches:
|
||||||
|
|
||||||
|
* Send a ``POST`` request to the REST API :ref:`reload_endpoint`; or
|
||||||
|
* Run :ref:`patronictl_reload`; or
|
||||||
|
* Locally signal the Patroni process with ``SIGHUP``:
|
||||||
|
|
||||||
|
* If you started Patroni through systemd, you can use the command ``systemctl reload PATRONI_UNIT.service``, ``PATRONI_UNIT`` being the name of the Patroni service; or
|
||||||
|
* If you started Patroni through other means, you will need to identify the ``patroni`` process and run ``kill -s HUP PID``, ``PID`` being the process ID of the ``patroni`` process.
|
||||||
|
|
||||||
|
**Note:** there are cases where a reload through the :ref:`patronictl_reload` may not work:
|
||||||
|
|
||||||
|
* Expired REST API certificates: you can mitigate that by using the ``-k`` option of the :ref:`patronictl`;
|
||||||
|
* Wrong credentials: for example when changing ``restapi`` or ``ctl`` credentials in the configuration file, and using that same configuration file for Patroni and :ref:`patronictl`.
|
||||||
|
|
||||||
|
How can I change my environment configuration?
|
||||||
|
The environment configuration is only read by Patroni during startup.
|
||||||
|
|
||||||
|
With that in mind, if you change the environment configuration you will need to restart the corresponding Patroni agent.
|
||||||
|
|
||||||
|
Take care to not cause a failover in the cluster! You might be interested in checking :ref:`patronictl_pause`.
|
||||||
|
|
||||||
|
What occurs if I change a Postgres GUC that requires a reload?
|
||||||
|
When you change the dynamic or the local configuration as explained in the previous questions, Patroni will take care of reloading the Postgres configuration for you.
|
||||||
|
|
||||||
|
What occurs if I change a Postgres GUC that requires a restart?
|
||||||
|
Patroni will mark the affected members with a flag of ``pending restart``.
|
||||||
|
|
||||||
|
It is up to you to determine when and how to restart the members. That can be accomplished either through:
|
||||||
|
|
||||||
|
* :ref:`patronictl_restart`; or
|
||||||
|
* A ``POST`` request to :ref:`restart_endpoint`.
|
||||||
|
|
||||||
|
**Note:** some Postgres GUCs require a special management in terms of the order for restarting the Postgres nodes. Refer to :ref:`shared_memory_gucs` for more details.
|
||||||
|
|
||||||
|
What is the difference between ``etcd`` and ``etcd3`` in Patroni configuration?
|
||||||
|
``etcd`` uses the API version 2 of ``etcd``, while ``etcd3`` uses the API version 3 of ``etcd``.
|
||||||
|
|
||||||
|
Be aware that information stored by the API version 2 is not manageable by API version 3 and vice-versa.
|
||||||
|
|
||||||
|
We recommend that you configure ``etcd3`` instead of ``etcd`` because:
|
||||||
|
|
||||||
|
* API version 2 is disabled by default from Etcd v3.4 onward;
|
||||||
|
* API version 2 will be completely removed on Etcd v3.6.
|
||||||
|
|
||||||
|
I have ``use_slots`` enabled in my Patroni configuration, but when a cluster member goes offline for some time, the replication slot used by that member is dropped on the upstream node. What can I do to avoid that issue?
|
||||||
|
There are two options:
|
||||||
|
|
||||||
|
1. You can tune ``member_slots_ttl`` (default value ``30min``, available since Patroni ``4.0.0`` and PostgreSQL 11 onwards) and replication slots for absent members will not be removed when the members downtime is shorter than the configured threshold.
|
||||||
|
2. You can configure permanent physical replication slots for the members.
|
||||||
|
|
||||||
|
Since Patroni ``3.2.0`` it is now possible to have member slots as permanent slots managed by Patroni.
|
||||||
|
|
||||||
|
Patroni will create the permanent physical slots on all nodes, and make sure to not remove the slots, as well as to advance the slots' LSN on all nodes according to the LSN that has been consumed by the member.
|
||||||
|
|
||||||
|
Later, if you decide to remove the corresponding member, it's **your responsibility** to adjust the permanent slots configuration, otherwise Patroni will keep the slots around forever.
|
||||||
|
|
||||||
|
**Note:** on Patroni older than ``3.2.0`` you could still have member slots configured as permanent physical slots, however they would be managed only on the current leader. That is, in case of failover/switchover these slots would be created on the new leader, but that wouldn't guarantee that it had all WAL segments for the absent node.
|
||||||
|
|
||||||
|
**Note:** even with Patroni ``3.2.0`` there might be a small race condition. In the very beginning, when the slot is created on the replica it could be ahead of the same slot on the leader and in case if nobody is consuming the slot there is still a chance that some files could be missing after failover. With that in mind, it is recommended that you configure continuous archiving, which makes it possible to restore required WALs or perform PITR.
|
||||||
|
|
||||||
|
What is the difference between ``loop_wait``, ``retry_timeout`` and ``ttl``?
|
||||||
|
Patroni performs what we call a HA cycle from time to time. On each HA cycle it takes care of performing a series of checks on the cluster to determine its healthiness, and depending on the status it may take actions, like failing over to a standby.
|
||||||
|
|
||||||
|
``loop_wait`` determines for how long, in seconds, Patroni should sleep before performing a new cycle of HA checks.
|
||||||
|
|
||||||
|
``retry_timeout`` sets the timeout for retry operations on the DCS and on Postgres. For example: if the DCS is unresponsive for more than ``retry_timeout`` seconds, Patroni might demote the primary node as a security action.
|
||||||
|
|
||||||
|
``ttl`` sets the lease time on the ``leader`` lock in the DCS. If the current leader of the cluster is not able to renew the lease during its HA cycles for longer than ``ttl``, then the lease will expire and that will trigger a ``leader race`` in the cluster.
|
||||||
|
|
||||||
|
**Note:** when modifying these settings, please keep in mind that Patroni enforces the rule and minimal values described in :ref:`dynamic_configuration` section of the docs.
|
||||||
|
|
||||||
|
Postgres management
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Can I change Postgres GUCs directly in Postgres configuration?
|
||||||
|
You can, but you should avoid that.
|
||||||
|
|
||||||
|
Postgres configuration is managed by Patroni, and attempts to edit the configuration files may end up being frustrated by Patroni as it may eventually overwrite them.
|
||||||
|
|
||||||
|
There are a few options available to overcome the management performed by Patroni:
|
||||||
|
|
||||||
|
* Change Postgres GUCs through ``$PGDATA/postgresql.base.conf``; or
|
||||||
|
* Define a ``postgresql.custom_conf`` which will be used instead of ``postgresql.base.conf`` so you can manage that externally; or
|
||||||
|
* Change GUCs using ``ALTER SYSTEM`` / ``ALTER DATABASE`` / ``ALTER USER``.
|
||||||
|
|
||||||
|
You can find more information about that in the section :ref:`important_configuration_rules`.
|
||||||
|
|
||||||
|
In any case we recommend that you manage all the Postgres configuration through Patroni. That will centralize the management and make it easier to debug Patroni when needed.
|
||||||
|
|
||||||
|
Can I restart Postgres nodes directly?
|
||||||
|
No, you should **not** attempt to manage Postgres directly!
|
||||||
|
|
||||||
|
Any attempt of bouncing the Postgres server without Patroni can lead your cluster to face failovers.
|
||||||
|
|
||||||
|
If you need to manage the Postgres server, do that through the ways exposed by Patroni.
|
||||||
|
|
||||||
|
Is Patroni able to take over management of an already existing Postgres cluster?
|
||||||
|
Yes, it can!
|
||||||
|
|
||||||
|
Please refer to :ref:`existing_data` for detailed instructions.
|
||||||
|
|
||||||
|
How does Patroni manage Postgres?
|
||||||
|
Patroni takes care of bringing Postgres up and down by running the Postgres binaries, like ``pg_ctl`` and ``postgres``.
|
||||||
|
|
||||||
|
With that in mind you **MUST** disable any other sources that could manage the Postgres clusters, like the systemd units, e.g. ``postgresql.service``. Only Patroni should be able to start, stop and promote Postgres instances in the cluster. Not doing so may result in split-brain scenarios. For example: if the node running as a primary failed and the unit ``postgresql.service`` is enabled, it may bring Postgres back up and cause a split-brain.
|
||||||
|
|
||||||
|
Concepts and requirements
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
Which are the applications that make part of Patroni?
|
||||||
|
Patroni basically ships a couple applications:
|
||||||
|
|
||||||
|
* ``patroni``: This is the Patroni agent, which takes care of managing a Postgres node;
|
||||||
|
* ``patronictl``: This is a command-line utility used to interact with a Patroni cluster (perform switchovers, restarts, changes in the configuration, etc.). Please find more information in :ref:`patronictl`.
|
||||||
|
|
||||||
|
What is a ``standby cluster`` in Patroni?
|
||||||
|
It is a cluster that does not have any primary Postgres node running, i.e., there is no read/write member in the cluster.
|
||||||
|
|
||||||
|
These kinds of clusters exist to replicate data from another cluster and are usually useful when you want to replicate data across data centers.
|
||||||
|
|
||||||
|
There will be a leader in the cluster which will be a standby in charge of replicating changes from a remote Postgres node.
|
||||||
|
Then, there will be a set of standbys configured with cascading replication from such leader member.
|
||||||
|
|
||||||
|
**Note:** the standby cluster doesn't know anything about the source cluster which it is replicating from -- it can even use ``restore_command`` instead of WAL streaming, and may use an absolutely independent DCS cluster.
|
||||||
|
|
||||||
|
Refer to :ref:`standby_cluster` for more details.
|
||||||
|
|
||||||
|
What is a ``leader`` in Patroni?
|
||||||
|
A ``leader`` in Patroni is like a coordinator of the cluster.
|
||||||
|
|
||||||
|
In a regular Patroni cluster, the ``leader`` will be the read/write node.
|
||||||
|
|
||||||
|
In a standby Patroni cluster, the ``leader`` (AKA ``standby leader``) will be in charge of replicating from a remote Postgres node, and cascading those changes to the other members of the standby cluster.
|
||||||
|
|
||||||
|
Does Patroni require a minimum number of Postgres nodes in the cluster?
|
||||||
|
No, you can run Patroni with any number of Postgres nodes.
|
||||||
|
|
||||||
|
Remember: Patroni is decoupled from the DCS.
|
||||||
|
|
||||||
|
What does ``pause`` mean in Patroni?
|
||||||
|
Pause is an operation exposed by Patroni so the user can ask Patroni to step back in regards to Postgres management.
|
||||||
|
|
||||||
|
That is mainly useful when you want to perform maintenance on the cluster, and would like to avoid that Patroni takes decisions related with HA, like failing over to a standby when you stop the primary.
|
||||||
|
|
||||||
|
You can find more information about that in :ref:`pause`.
|
||||||
|
|
||||||
|
Automatic failover
|
||||||
|
------------------
|
||||||
|
|
||||||
|
How does the automatic failover mechanism of Patroni work?
|
||||||
|
Patroni automatic failover is based on what we call ``leader race``.
|
||||||
|
|
||||||
|
Patroni stores the cluster's status in the DCS, among them a ``leader`` lock which holds the name of the Patroni member which is the current ``leader`` of the cluster.
|
||||||
|
|
||||||
|
That ``leader`` lock has a time-to-live associated with it. If the leader node fails to update the lease of the ``leader`` lock in time, the key will eventually expire from the DCS.
|
||||||
|
|
||||||
|
When the ``leader`` lock expires, it triggers what Patroni calls a ``leader race``: all nodes start performing checks to determine if they are the best candidates for taking over the ``leader`` role.
|
||||||
|
Some of these checks include calls to the REST API of all other Patroni members.
|
||||||
|
|
||||||
|
All Patroni members that find themselves as the best candidate for taking over the ``leader`` lock will attempt to do so.
|
||||||
|
The first Patroni member that is able to take the ``leader`` lock will promote itself to a read/write node (or ``standby leader``), and the others will be configured to follow it.
|
||||||
|
|
||||||
|
Can I temporarily disable automatic failover in the Patroni cluster?
|
||||||
|
Yes, you can!
|
||||||
|
|
||||||
|
You can achieve that by temporarily pausing the cluster.
|
||||||
|
This is typically useful for performing maintenance.
|
||||||
|
|
||||||
|
When you want to resume the automatic failover of the cluster, you just need to unpause it.
|
||||||
|
|
||||||
|
You can find more information about that in :ref:`pause`.
|
||||||
|
|
||||||
|
Bootstrapping and standbys creation
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
How does Patroni create a primary Postgres node? What about a standby Postgres node?
|
||||||
|
By default Patroni will use ``initdb`` to bootstrap a fresh cluster, and ``pg_basebackup`` to create standby nodes from a copy of the ``leader`` member.
|
||||||
|
|
||||||
|
You can customize that behavior by writing your custom bootstrap methods, and your custom replica creation methods.
|
||||||
|
|
||||||
|
Custom methods are usually useful when you want to restore backups created by backup tools like pgBackRest or Barman, for example.
|
||||||
|
|
||||||
|
For detailed information please refer to :ref:`custom_bootstrap` and :ref:`custom_replica_creation`.
|
||||||
|
|
||||||
|
Monitoring
|
||||||
|
----------
|
||||||
|
|
||||||
|
How can I monitor my Patroni cluster?
|
||||||
|
Patroni exposes a couple handy endpoints in its :ref:`rest_api`:
|
||||||
|
|
||||||
|
* ``/metrics``: exposes monitoring metrics in a format that can be consumed by Prometheus;
|
||||||
|
* ``/patroni``: exposes the status of the cluster in a JSON format. The information shown here is very similar to what is shown by the ``/metrics`` endpoint.
|
||||||
|
|
||||||
|
You can use those endpoints to implement monitoring checks.
|
||||||
@@ -109,7 +109,7 @@ digraph G {
|
|||||||
subgraph cluster_process_healthy_cluster {
|
subgraph cluster_process_healthy_cluster {
|
||||||
label = "process_healthy_cluster"
|
label = "process_healthy_cluster"
|
||||||
"healthy_has_lock" [label="Am I the owner of the leader lock?", shape=diamond]
|
"healthy_has_lock" [label="Am I the owner of the leader lock?", shape=diamond]
|
||||||
"healthy_is_leader" [label="Is Postgres running as master?", shape=diamond]
|
"healthy_is_leader" [label="Is Postgres running as primary?", shape=diamond]
|
||||||
"healthy_no_lock" [label="Follow the leader (async,\ncreate/update recovery.conf and restart if necessary)"]
|
"healthy_no_lock" [label="Follow the leader (async,\ncreate/update recovery.conf and restart if necessary)"]
|
||||||
"healthy_has_lock" -> "healthy_no_lock" [label="no" color="red"]
|
"healthy_has_lock" -> "healthy_no_lock" [label="no" color="red"]
|
||||||
"healthy_has_lock" -> "healthy_update_leader_lock" [label="yes" color="green"]
|
"healthy_has_lock" -> "healthy_update_leader_lock" [label="yes" color="green"]
|
||||||
@@ -119,7 +119,7 @@ digraph G {
|
|||||||
"healthy_update_success" -> "healthy_is_leader" [label="yes" color="green"]
|
"healthy_update_success" -> "healthy_is_leader" [label="yes" color="green"]
|
||||||
"healthy_update_success" -> "healthy_demote" [label="no" color="red"]
|
"healthy_update_success" -> "healthy_demote" [label="no" color="red"]
|
||||||
"healthy_demote" [label="Demote (async,\nrestart in read-only)"]
|
"healthy_demote" [label="Demote (async,\nrestart in read-only)"]
|
||||||
"healthy_failover" [label="Promote Postgres to master"]
|
"healthy_failover" [label="Promote Postgres to primary"]
|
||||||
"healthy_is_leader" -> "healthy_failover" [label="no" color="red"]
|
"healthy_is_leader" -> "healthy_failover" [label="no" color="red"]
|
||||||
}
|
}
|
||||||
"healthy_demote" -> "update_member"
|
"healthy_demote" -> "update_member"
|
||||||
@@ -134,10 +134,10 @@ digraph G {
|
|||||||
"unhealthy_leader_race" [label="Try to create leader key"]
|
"unhealthy_leader_race" [label="Try to create leader key"]
|
||||||
"unhealthy_leader_race" -> "unhealthy_acquire_lock"
|
"unhealthy_leader_race" -> "unhealthy_acquire_lock"
|
||||||
"unhealthy_acquire_lock" [label="Was I able to get the lock?", shape="diamond"]
|
"unhealthy_acquire_lock" [label="Was I able to get the lock?", shape="diamond"]
|
||||||
"unhealthy_is_leader" [label="Is Postgres running as master?", shape=diamond]
|
"unhealthy_is_leader" [label="Is Postgres running as primary?", shape=diamond]
|
||||||
"unhealthy_acquire_lock" -> "unhealthy_is_leader" [label="yes" color="green"]
|
"unhealthy_acquire_lock" -> "unhealthy_is_leader" [label="yes" color="green"]
|
||||||
"unhealthy_is_leader" -> "unhealthy_promote" [label="no" color="red"]
|
"unhealthy_is_leader" -> "unhealthy_promote" [label="no" color="red"]
|
||||||
"unhealthy_promote" [label="Promote to master"]
|
"unhealthy_promote" [label="Promote to primary"]
|
||||||
"unhealthy_is_healthiest" -> "unhealthy_follow" [label="no" color="red"]
|
"unhealthy_is_healthiest" -> "unhealthy_follow" [label="no" color="red"]
|
||||||
"unhealthy_follow" [label="try to follow somebody else()"]
|
"unhealthy_follow" [label="try to follow somebody else()"]
|
||||||
"unhealthy_acquire_lock" -> "unhealthy_follow" [label="no" color="red"]
|
"unhealthy_acquire_lock" -> "unhealthy_follow" [label="no" color="red"]
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 507 KiB After Width: | Height: | Size: 524 KiB |
@@ -0,0 +1,54 @@
|
|||||||
|
.. _ha_multi_dc:
|
||||||
|
|
||||||
|
===================
|
||||||
|
HA multi datacenter
|
||||||
|
===================
|
||||||
|
|
||||||
|
The high availability of a PostgreSQL cluster deployed in multiple data centers is based on replication, which can be synchronous or asynchronous (`replication_modes <replication_modes.rst>`_).
|
||||||
|
|
||||||
|
In both cases, it is important to be clear about the following concepts:
|
||||||
|
|
||||||
|
- Postgres can run as primary or standby leader only when it owns the leading key and can update the leading key.
|
||||||
|
- You should run the odd number of etcd, ZooKeeper or Consul nodes: 3 or 5!
|
||||||
|
|
||||||
|
Synchronous Replication
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
To have a multi DC cluster that can automatically tolerate a zone drop, a minimum of 3 is required.
|
||||||
|
|
||||||
|
The architecture diagram would be the following:
|
||||||
|
|
||||||
|
.. image:: _static/multi-dc-synchronous-replication.png
|
||||||
|
|
||||||
|
We must deploy a cluster of etcd, ZooKeeper or Consul through the different DC, with a minimum of 3 nodes, one in each zone.
|
||||||
|
|
||||||
|
Regarding postgres, we must deploy at least 2 nodes, in different DC. Then you have to set ``synchronous_mode: true`` in the global :ref:`dynamic configuration <dynamic_configuration>`.
|
||||||
|
|
||||||
|
This enables sync replication and the primary node will choose one of the nodes as synchronous.
|
||||||
|
|
||||||
|
Asynchronous Replication
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
With only two data centers it would be better to have two independent etcd clusters and run Patroni :ref:`standby cluster <standby_cluster>` in the second data center. If the first site is down, you can MANUALLY promote the ``standby_cluster``.
|
||||||
|
|
||||||
|
The architecture diagram would be the following:
|
||||||
|
|
||||||
|
.. image:: _static/multi-dc-asynchronous-replication.png
|
||||||
|
|
||||||
|
Automatic promotion is not possible, because DC2 will never able to figure out the state of DC1.
|
||||||
|
|
||||||
|
You should not use ``pg_ctl promote`` in this scenario, you need "manually promote" the healthy cluster by removing ``standby_cluster`` section from the :ref:`dynamic configuration <dynamic_configuration>`.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
If the source cluster is still up and running and you promote the standby cluster you create a split-brain.
|
||||||
|
|
||||||
|
In case you want to return to the "initial" state, there are only two ways of resolving it:
|
||||||
|
|
||||||
|
- Add the standby_cluster section back and it will trigger ``pg_rewind``; however, for ``pg_rewind`` to function properly, either the cluster must be initialized with ``data page checksums`` (``--data-checksums`` option for ``initdb``) and/or ``wal_log_hints`` must be set to ``on``, but there are still chances that ``pg_rewind`` might fail due to other factors.
|
||||||
|
- Rebuild the standby cluster from scratch.
|
||||||
|
|
||||||
|
Before promoting standby cluster one have to manually ensure that the source cluster is down (STONITH). When DC1 recovers, the cluster has to be converted to a standby cluster.
|
||||||
|
|
||||||
|
Before doing that you may manually examine the database and extract all changes that happened between the time when network between DC1 and DC2 has stopped working and the time when you manually stopped the cluster in DC1.
|
||||||
|
|
||||||
|
Once extracted, you may also manually apply these changes to the cluster in DC2.
|
||||||
+25
-14
@@ -6,11 +6,11 @@
|
|||||||
Introduction
|
Introduction
|
||||||
============
|
============
|
||||||
|
|
||||||
Patroni is a template for you to create your own customized, high-availability solution using Python and - for maximum accessibility - a distributed configuration store like `ZooKeeper <https://zookeeper.apache.org/>`__, `etcd <https://github.com/coreos/etcd>`__, `Consul <https://github.com/hashicorp/consul>`__ or `Kubernetes <https://kubernetes.io>`__. Database engineers, DBAs, DevOps engineers, and SREs who are looking to quickly deploy HA PostgreSQL in the datacenter-or anywhere else-will hopefully find it useful.
|
Patroni is a template for high availability (HA) PostgreSQL solutions using Python. For maximum accessibility, Patroni supports a variety of distributed configuration stores like `ZooKeeper <https://zookeeper.apache.org/>`__, `etcd <https://github.com/coreos/etcd>`__, `Consul <https://github.com/hashicorp/consul>`__ or `Kubernetes <https://kubernetes.io>`__. Database engineers, DBAs, DevOps engineers, and SREs who are looking to quickly deploy HA PostgreSQL in datacenters — or anywhere else — will hopefully find it useful.
|
||||||
|
|
||||||
We call Patroni a "template" because it is far from being a one-size-fits-all or plug-and-play replication system. It will have its own caveats. Use wisely. There are many ways to run high availability with PostgreSQL; for a list, see the `PostgreSQL Documentation <https://wiki.postgresql.org/wiki/Replication,_Clustering,_and_Connection_Pooling>`__.
|
We call Patroni a "template" because it is far from being a one-size-fits-all or plug-and-play replication system. It will have its own caveats. Use wisely. There are many ways to run high availability with PostgreSQL; for a list, see the `PostgreSQL Documentation <https://wiki.postgresql.org/wiki/Replication,_Clustering,_and_Connection_Pooling>`__.
|
||||||
|
|
||||||
Currently supported PostgreSQL versions: 9.3 to 15.
|
Currently supported PostgreSQL versions: 9.3 to 17.
|
||||||
|
|
||||||
**Note to Citus users**: Starting from 3.0 Patroni nicely integrates with the `Citus <https://github.com/citusdata/citus>`__ database extension to Postgres. Please check the :ref:`Citus support page <citus>` in the Patroni documentation for more info about how to use Patroni high availability together with a Citus distributed cluster.
|
**Note to Citus users**: Starting from 3.0 Patroni nicely integrates with the `Citus <https://github.com/citusdata/citus>`__ database extension to Postgres. Please check the :ref:`Citus support page <citus>` in the Patroni documentation for more info about how to use Patroni high availability together with a Citus distributed cluster.
|
||||||
|
|
||||||
@@ -22,25 +22,36 @@ Currently supported PostgreSQL versions: 9.3 to 15.
|
|||||||
:caption: Contents:
|
:caption: Contents:
|
||||||
|
|
||||||
README
|
README
|
||||||
citus
|
installation
|
||||||
dynamic_configuration
|
patroni_configuration
|
||||||
dcs_failsafe_mode
|
|
||||||
rest_api
|
rest_api
|
||||||
existing_data
|
patronictl
|
||||||
ENVIRONMENT
|
|
||||||
SETTINGS
|
|
||||||
security
|
|
||||||
replica_bootstrap
|
replica_bootstrap
|
||||||
replication_modes
|
replication_modes
|
||||||
pause
|
standby_cluster
|
||||||
kubernetes
|
|
||||||
watchdog
|
watchdog
|
||||||
|
pause
|
||||||
|
dcs_failsafe_mode
|
||||||
|
kubernetes
|
||||||
|
citus
|
||||||
|
existing_data
|
||||||
|
tools_integration
|
||||||
|
security
|
||||||
|
ha_multi_dc
|
||||||
|
faq
|
||||||
releases
|
releases
|
||||||
CONTRIBUTING
|
CONTRIBUTING
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
==================
|
==================
|
||||||
|
|
||||||
* :ref:`genindex`
|
.. ifconfig:: builder == 'html'
|
||||||
* :ref:`modindex`
|
|
||||||
* :ref:`search`
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
||||||
|
|
||||||
|
.. ifconfig:: builder != 'html'
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`search`
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
.. _installation:
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
Pre-requirements for Mac OS
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
To install requirements on a Mac, run the following:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
brew install postgresql etcd haproxy libyaml python
|
||||||
|
|
||||||
|
.. _psycopg2_install_options:
|
||||||
|
|
||||||
|
Psycopg
|
||||||
|
-------
|
||||||
|
|
||||||
|
Starting from `psycopg2-2.8`_ the binary version of psycopg2 will no longer be installed by default. Installing it from
|
||||||
|
the source code requires C compiler and postgres+python dev packages. Since in the python world it is not possible to
|
||||||
|
specify dependency as ``psycopg2 OR psycopg2-binary`` you will have to decide how to install it.
|
||||||
|
|
||||||
|
There are a few options available:
|
||||||
|
|
||||||
|
1. Use the package manager from your distro
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sudo apt-get install python3-psycopg2 # install psycopg2 module on Debian/Ubuntu
|
||||||
|
sudo yum install python3-psycopg2 # install psycopg2 on RedHat/Fedora/CentOS
|
||||||
|
|
||||||
|
2. Specify one of `psycopg`, `psycopg2`, or `psycopg2-binary` in the :ref:`list of dependencies <extras>` when installing Patroni with pip.
|
||||||
|
|
||||||
|
|
||||||
|
.. _extras:
|
||||||
|
|
||||||
|
General installation for pip
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Patroni can be installed with pip:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
pip install patroni[dependencies]
|
||||||
|
|
||||||
|
where ``dependencies`` can be either empty, or consist of one or more of the following:
|
||||||
|
|
||||||
|
etcd or etcd3
|
||||||
|
`python-etcd` module in order to use Etcd as Distributed Configuration Store (DCS)
|
||||||
|
consul
|
||||||
|
`py-consul` module in order to use Consul as DCS
|
||||||
|
zookeeper
|
||||||
|
`kazoo` module in order to use Zookeeper as DCS
|
||||||
|
exhibitor
|
||||||
|
`kazoo` module in order to use Exhibitor as DCS (same dependencies as for Zookeeper)
|
||||||
|
kubernetes
|
||||||
|
`kubernetes` module in order to use Kubernetes as DCS in Patroni
|
||||||
|
raft
|
||||||
|
`pysyncobj` module in order to use python Raft implementation as DCS
|
||||||
|
aws
|
||||||
|
`boto3` in order to use AWS callbacks
|
||||||
|
jsonlogger
|
||||||
|
`python-json-logger` module in order to enable :ref:`logging <log_settings>` in json format
|
||||||
|
systemd
|
||||||
|
`systemd-python` in order to use sd_notify integration
|
||||||
|
all
|
||||||
|
all of the above (except psycopg family)
|
||||||
|
psycopg
|
||||||
|
`psycopg[binary]>=3.0.0` module
|
||||||
|
psycopg2
|
||||||
|
`psycopg2>=2.5.4` module
|
||||||
|
psycopg2-binary
|
||||||
|
`psycopg2-binary` module
|
||||||
|
|
||||||
|
For example, the command in order to install Patroni together with psycopg3, dependencies for Etcd as a DCS, and AWS callbacks is:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
pip install patroni[psycopg3,etcd3,aws]
|
||||||
|
|
||||||
|
Note that external tools to call in the replica creation or custom bootstrap scripts (i.e. WAL-E) should be installed
|
||||||
|
independently of Patroni.
|
||||||
|
|
||||||
|
.. _package_installation:
|
||||||
|
|
||||||
|
Package installation on Linux
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
Patroni packages may be available for your operating system, produced by the Postgres community for:
|
||||||
|
|
||||||
|
* RHEL, RockyLinux, AlmaLinux;
|
||||||
|
* Debian and Ubuntu;
|
||||||
|
* SUSE Enterprise Linux.
|
||||||
|
|
||||||
|
You can also find packages for direct dependencies of Patroni, like python modules that might not be available in
|
||||||
|
the official operating system repositories.
|
||||||
|
|
||||||
|
For more information see the `PGDG repository`_ documentation.
|
||||||
|
|
||||||
|
If you are on a RedHat Enterprise Linux derivative operating system you may also require packages from EPEL, see
|
||||||
|
`EPEL repository`_ documentation.
|
||||||
|
|
||||||
|
Once you have installed the PGDG repository for your OS you can install patroni.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Patroni packages are not maintained by the Patroni developers, but rather by the Postgres community. If you
|
||||||
|
require support please first try connecting on `Postgres slack`_.
|
||||||
|
|
||||||
|
Installing on Debian derivatives
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
With PGDG repo installed, see :ref:`above <package_installation>`, install Patroni via apt run:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
apt-get install patroni
|
||||||
|
|
||||||
|
Installing on RedHat derivatives
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
With PGDG repo installed, see :ref:`above <package_installation>`, install patroni with an etcd DCS via dnf on RHEL 9
|
||||||
|
(and derivatives) run:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
dnf install patroni patroni-etcd
|
||||||
|
|
||||||
|
You can install etcd from PGDG if your RedHat derivative distribution does not provide packages. On the nodes that will
|
||||||
|
host the DCS run:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
dnf install 'dnf-command(config-manager)'
|
||||||
|
dnf config-manager --enable pgdg-rhel9-extras
|
||||||
|
dnf install etcd
|
||||||
|
|
||||||
|
You can replace the version of RHEL with `8` in the repo to make `pgdg-rhel8-extras` if needed. The repo name is still
|
||||||
|
`pgdg-rhelN-extras` on RockyLinux, AlmaLinux, Oracle Linux, etc...
|
||||||
|
|
||||||
|
Installing on SUSE Enterprise Linux
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
You might need to enable the SUSE PackageHub repositories for some dependencies. see `SUSE PackageHub`_ documentation.
|
||||||
|
|
||||||
|
For SLES 15 with PGDG repo installed, see :ref:`above <package_installation>`, you can install patroni using:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
zypper install patroni patroni-etcd
|
||||||
|
|
||||||
|
With the SUSE PackageHub repo enabled you can also install etcd:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
SUSEConnect -p PackageHub/15.5/x86_64
|
||||||
|
zypper install etcd
|
||||||
|
|
||||||
|
Upgrading
|
||||||
|
---------
|
||||||
|
|
||||||
|
Upgrading patroni is a very simple process, just update the software installation and restart the Patroni daemon on
|
||||||
|
each node in the cluster.
|
||||||
|
|
||||||
|
However, restarting the Patroni daemon will result in a Postgres database restart. In some situations this may cause
|
||||||
|
a failover of the primary node in your cluster, therefore it is recommended to put the cluster into maintenance mode
|
||||||
|
until the Patroni daemon restart has been completed.
|
||||||
|
|
||||||
|
To put the cluster in maintenance mode, run the following command on one of the patroni nodes:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
patronictl pause --wait
|
||||||
|
|
||||||
|
Then on each node in the cluster, perform the package upgrade required for your OS:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
apt-get update && apt-get install patroni patroni-etcd
|
||||||
|
|
||||||
|
Restart the patroni daemon process on each node:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
systemctl restart patroni
|
||||||
|
|
||||||
|
Then finally resume monitoring of Postgres with patroni to take it out of maintenance mode:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
patronictl resume --wait
|
||||||
|
|
||||||
|
The cluster will now be full operational with the new version of Patroni.
|
||||||
|
|
||||||
|
.. _psycopg2-2.8: http://initd.org/psycopg/articles/2019/04/04/psycopg-28-released/
|
||||||
|
.. _PGDG repository: https://www.postgresql.org/download/linux/
|
||||||
|
.. _EPEL repository: https://docs.fedoraproject.org/en-US/epel/
|
||||||
|
.. _SUSE PackageHub: https://packagehub.suse.com/how-to-use/
|
||||||
|
.. _Postgres slack: http://pgtreats.info/slack-invite
|
||||||
+54
-2
@@ -32,10 +32,62 @@ Configuration
|
|||||||
|
|
||||||
Patroni Kubernetes :ref:`settings <kubernetes_settings>` and :ref:`environment variables <kubernetes_environment>` are described in the general chapters of the documentation.
|
Patroni Kubernetes :ref:`settings <kubernetes_settings>` and :ref:`environment variables <kubernetes_environment>` are described in the general chapters of the documentation.
|
||||||
|
|
||||||
|
.. _kubernetes_role_values:
|
||||||
|
|
||||||
|
Customize role label
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
By default, Patroni will set corresponding labels on the pod it runs in based on node's role, such as ``role=primary``.
|
||||||
|
The key and value of label can be customized by `kubernetes.role_label`, `kubernetes.leader_label_value`, `kubernetes.follower_label_value` and `kubernetes.standby_leader_label_value`.
|
||||||
|
|
||||||
|
Note that if you migrate from default role labels to custom ones, you can reduce downtime by following migration steps:
|
||||||
|
|
||||||
|
1. Add a temporary label using original role value for the pod with `kubernetes.tmp_role_label` (like ``tmp_role``). Once pods are restarted they will get following labels set by Patroni:
|
||||||
|
|
||||||
|
.. code:: YAML
|
||||||
|
|
||||||
|
labels:
|
||||||
|
cluster-name: foo
|
||||||
|
role: primary
|
||||||
|
tmp_role: primary
|
||||||
|
|
||||||
|
2. After all pods have been updated, modify the service selector to select the temporary label.
|
||||||
|
|
||||||
|
.. code:: YAML
|
||||||
|
|
||||||
|
selector:
|
||||||
|
cluster-name: foo
|
||||||
|
tmp_role: primary
|
||||||
|
|
||||||
|
3. Add your custom role label (e.g., set `kubernetes.leader_label_value=primary`). Once pods are restarted they will get following new labels set by Patroni:
|
||||||
|
|
||||||
|
.. code:: YAML
|
||||||
|
|
||||||
|
labels:
|
||||||
|
cluster-name: foo
|
||||||
|
role: primary
|
||||||
|
tmp_role: primary
|
||||||
|
|
||||||
|
4. After all pods have been updated again, modify the service selector to use new role value.
|
||||||
|
|
||||||
|
.. code:: YAML
|
||||||
|
|
||||||
|
selector:
|
||||||
|
cluster-name: foo
|
||||||
|
role: primary
|
||||||
|
|
||||||
|
5. Finally, remove the temporary label from your configuration and update all pods.
|
||||||
|
|
||||||
|
.. code:: YAML
|
||||||
|
|
||||||
|
labels:
|
||||||
|
cluster-name: foo
|
||||||
|
role: primary
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
|
||||||
- The `kubernetes <https://github.com/zalando/patroni/tree/master/kubernetes>`__ folder of the Patroni repository contains
|
- The `kubernetes <https://github.com/patroni/patroni/tree/master/kubernetes>`__ folder of the Patroni repository contains
|
||||||
examples of the Docker image, and the Kubernetes manifest to test Patroni Kubernetes setup.
|
examples of the Docker image, and the Kubernetes manifest to test Patroni Kubernetes setup.
|
||||||
Note that in the current state it will not be able to use PersistentVolumes because of permission issues.
|
Note that in the current state it will not be able to use PersistentVolumes because of permission issues.
|
||||||
|
|
||||||
@@ -46,5 +98,5 @@ Examples
|
|||||||
to deploy the Spilo image configured with Patroni running using Kubernetes.
|
to deploy the Spilo image configured with Patroni running using Kubernetes.
|
||||||
|
|
||||||
- In order to run your database clusters at scale using Patroni and Spilo, take a look at the
|
- In order to run your database clusters at scale using Patroni and Spilo, take a look at the
|
||||||
`postgres-operator <https://github.com/zalando-incubator/postgres-operator>`_ project. It implements the operator pattern
|
`postgres-operator <https://github.com/zalando/postgres-operator>`_ project. It implements the operator pattern
|
||||||
to manage Spilo clusters.
|
to manage Spilo clusters.
|
||||||
|
|||||||
@@ -0,0 +1,259 @@
|
|||||||
|
.. _patroni_configuration:
|
||||||
|
|
||||||
|
Patroni configuration
|
||||||
|
=====================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:hidden:
|
||||||
|
|
||||||
|
dynamic_configuration
|
||||||
|
yaml_configuration
|
||||||
|
ENVIRONMENT
|
||||||
|
|
||||||
|
|
||||||
|
There are 3 types of Patroni configuration:
|
||||||
|
|
||||||
|
- Global :ref:`dynamic configuration <dynamic_configuration>`.
|
||||||
|
These options are stored in the DCS (Distributed Configuration Store) and applied on all cluster nodes.
|
||||||
|
Dynamic configuration can be set at any time using :ref:`patronictl_edit_config` tool or Patroni :ref:`REST API <rest_api>`.
|
||||||
|
If the options changed are not part of the startup configuration, they are applied asynchronously (upon the next wake up cycle)
|
||||||
|
to every node, which gets subsequently reloaded.
|
||||||
|
If the node requires a restart to apply the configuration (for `PostgreSQL parameters <https://www.postgresql.org/docs/current/view-pg-settings.html>`__ with context postmaster, if their values
|
||||||
|
have changed), a special flag ``pending_restart`` indicating this is set in the members.data JSON.
|
||||||
|
Additionally, the node status indicates this by showing ``"restart_pending": true``.
|
||||||
|
|
||||||
|
- Local :ref:`configuration file <yaml_configuration>` (patroni.yml).
|
||||||
|
These options are defined in the configuration file and take precedence over dynamic configuration.
|
||||||
|
``patroni.yml`` can be changed and reloaded at runtime (without restart of Patroni) by sending SIGHUP to the Patroni process, performing ``POST /reload`` REST-API request or executing :ref:`patronictl_reload`. Local configuration can be either a single YAML file or a directory. When it is a directory, all YAML files in that directory are loaded one by one in sorted order. In case a key is defined in multiple files, the occurrence in the last file takes precedence.
|
||||||
|
|
||||||
|
- :ref:`Environment configuration <environment>`.
|
||||||
|
It is possible to set/override some of the "Local" configuration parameters with environment variables.
|
||||||
|
Environment configuration is very useful when you are running in a dynamic environment and you don't know some of the parameters in advance (for example it's not possible to know your external IP address when you are running inside ``docker``).
|
||||||
|
|
||||||
|
.. _important_configuration_rules:
|
||||||
|
|
||||||
|
Important rules
|
||||||
|
---------------
|
||||||
|
|
||||||
|
PostgreSQL parameters controlled by Patroni
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Some of the PostgreSQL parameters **must hold the same values on the primary and the replicas**. For those, **values set either in the local patroni configuration files or via the environment variables take no effect**. To alter or set their values one must change the shared configuration in the DCS. Below is the actual list of such parameters together with the default values:
|
||||||
|
|
||||||
|
- **max_connections**: 100
|
||||||
|
- **max_locks_per_transaction**: 64
|
||||||
|
- **max_worker_processes**: 8
|
||||||
|
- **max_prepared_transactions**: 0
|
||||||
|
- **wal_level**: hot_standby
|
||||||
|
- **track_commit_timestamp**: off
|
||||||
|
|
||||||
|
For the parameters below, PostgreSQL does not require equal values among the primary and all the replicas. However, considering the possibility of a replica to become the primary at any time, it doesn't really make sense to set them differently; therefore, **Patroni restricts setting their values to the** :ref:`dynamic configuration <dynamic_configuration>`.
|
||||||
|
|
||||||
|
- **max_wal_senders**: 10
|
||||||
|
- **max_replication_slots**: 10
|
||||||
|
- **wal_keep_segments**: 8
|
||||||
|
- **wal_keep_size**: 128MB
|
||||||
|
- **wal_log_hints**: on
|
||||||
|
|
||||||
|
These parameters are validated to ensure they are sane, or meet a minimum value.
|
||||||
|
|
||||||
|
There are some other Postgres parameters controlled by Patroni:
|
||||||
|
|
||||||
|
- **listen_addresses** - is set either from ``postgresql.listen`` or from ``PATRONI_POSTGRESQL_LISTEN`` environment variable
|
||||||
|
- **port** - is set either from ``postgresql.listen`` or from ``PATRONI_POSTGRESQL_LISTEN`` environment variable
|
||||||
|
- **cluster_name** - is set either from ``scope`` or from ``PATRONI_SCOPE`` environment variable
|
||||||
|
- **hot_standby: on**
|
||||||
|
|
||||||
|
To be on the safe side parameters from the above lists are written into ``postgresql.conf``, and passed as a list of arguments to the ``postgres`` which gives them the highest precedence (except ``wal_keep_segments`` and ``wal_keep_size``), even above `ALTER SYSTEM <https://www.postgresql.org/docs/current/static/sql-altersystem.html>`__
|
||||||
|
|
||||||
|
There also are some parameters like **postgresql.listen**, **postgresql.data_dir** that **can be set only locally**, i.e. in the Patroni :ref:`config file <yaml_configuration>` or via :ref:`configuration <environment>` variable. In most cases the local configuration will override the dynamic configuration.
|
||||||
|
|
||||||
|
|
||||||
|
When applying the local or dynamic configuration options, the following actions are taken:
|
||||||
|
|
||||||
|
- The node first checks if there is a `postgresql.base.conf` file or if the ``custom_conf`` parameter is set.
|
||||||
|
- If the ``custom_conf`` parameter is set, the file it specifies is used as the base configuration, ignoring `postgresql.base.conf` and `postgresql.conf`.
|
||||||
|
- If the ``custom_conf`` parameter is not set and `postgresql.base.conf` exists, it contains the renamed "original" configuration and is used as the base configuration.
|
||||||
|
- If there is no ``custom_conf`` nor `postgresql.base.conf`, the original `postgresql.conf` is renamed to `postgresql.base.conf` and used as the base configuration.
|
||||||
|
- The dynamic options (with the exceptions above) are dumped into the `postgresql.conf` and an include is set in
|
||||||
|
`postgresql.conf` to the base configuration (either `postgresql.base.conf` or the file at ``custom_conf``).
|
||||||
|
Therefore, we would be able to apply new options without re-reading the configuration file to check if the include is present or not.
|
||||||
|
- Some parameters that are essential for Patroni to manage the cluster are overridden using the command line.
|
||||||
|
- If an option that requires restart is changed (we should look at the context in pg_settings and at the actual
|
||||||
|
values of those options), a pending_restart flag is set on that node. This flag is reset on any restart.
|
||||||
|
|
||||||
|
The parameters would be applied in the following order (run-time are given the highest priority):
|
||||||
|
|
||||||
|
1. load parameters from file `postgresql.base.conf` (or from a ``custom_conf`` file, if set)
|
||||||
|
2. load parameters from file `postgresql.conf`
|
||||||
|
3. load parameters from file `postgresql.auto.conf`
|
||||||
|
4. run-time parameter using `-o --name=value`
|
||||||
|
|
||||||
|
This allows configuration for all the nodes (2), configuration for a specific node using ``ALTER SYSTEM`` (3) and ensures that parameters essential to the running of Patroni are enforced (4), as well as leaves room for configuration tools that manage `postgresql.conf` directly without involving Patroni (1).
|
||||||
|
|
||||||
|
.. _shared_memory_gucs:
|
||||||
|
|
||||||
|
PostgreSQL parameters that touch shared memory
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
PostgreSQL has some parameters that determine the size of the shared memory used by them:
|
||||||
|
|
||||||
|
- **max_connections**
|
||||||
|
- **max_prepared_transactions**
|
||||||
|
- **max_locks_per_transaction**
|
||||||
|
- **max_wal_senders**
|
||||||
|
- **max_worker_processes**
|
||||||
|
|
||||||
|
Changing these parameters require a PostgreSQL restart to take effect, and their shared memory structures cannot be smaller on the standby nodes than on the primary node.
|
||||||
|
|
||||||
|
As explained before, Patroni restrict changing their values through :ref:`dynamic configuration <dynamic_configuration>`, which usually consists of:
|
||||||
|
|
||||||
|
1. Applying changes through :ref:`patronictl_edit_config` (or via REST API ``/config`` endpoint)
|
||||||
|
2. Restarting nodes through :ref:`patronictl_restart` (or via REST API ``/restart`` endpoint)
|
||||||
|
|
||||||
|
**Note:** please keep in mind that you should perform a restart of the PostgreSQL nodes through :ref:`patronictl_restart` command, or via REST API ``/restart`` endpoint. An attempt to restart PostgreSQL by restarting the Patroni daemon, e.g. by executing ``systemctl restart patroni``, can cause a failover to occur in the cluster, if you are restarting the primary node.
|
||||||
|
|
||||||
|
However, as those settings manage shared memory, some extra care should be taken when restarting the nodes:
|
||||||
|
|
||||||
|
* If you want to **increase** the value of any of those settings:
|
||||||
|
|
||||||
|
1. Restart all standbys first
|
||||||
|
2. Restart the primary after that
|
||||||
|
|
||||||
|
* If you want to **decrease** the value of any of those settings:
|
||||||
|
|
||||||
|
1. Restart the primary first
|
||||||
|
2. Restart all standbys after that
|
||||||
|
|
||||||
|
**Note:** if you attempt to restart all nodes in one go after **decreasing** the value of any of those settings, Patroni will ignore the change and restart the standby with the original setting value, thus requiring that you restart the standbys again later. Patroni does that to prevent the standby to enter in an infinite crash loop, because PostgreSQL quits with a `FATAL` message if you attempt to set any of those parameters to a value lower than what is visible in ``pg_controldata`` on the Standby node. In other words, we can only decrease the setting on the standby once its ``pg_controldata`` is up-to-date with the primary in regards to these changes on the primary.
|
||||||
|
|
||||||
|
More information about that can be found at `PostgreSQL Administrator's Overview <https://www.postgresql.org/docs/current/hot-standby.html#HOT-STANDBY-ADMIN>`__.
|
||||||
|
|
||||||
|
Patroni configuration parameters
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Also the following Patroni configuration options **can be changed only dynamically**:
|
||||||
|
|
||||||
|
- **ttl**: 30
|
||||||
|
- **loop_wait**: 10
|
||||||
|
- **retry_timeouts**: 10
|
||||||
|
- **maximum_lag_on_failover**: 1048576
|
||||||
|
- **max_timelines_history**: 0
|
||||||
|
- **check_timeline**: false
|
||||||
|
- **postgresql.use_slots**: true
|
||||||
|
|
||||||
|
Upon changing these options, Patroni will read the relevant section of the configuration stored in DCS and change its run-time values.
|
||||||
|
|
||||||
|
Patroni nodes are dumping the state of the DCS options to disk upon for every change of the configuration into the file ``patroni.dynamic.json`` located in the Postgres data directory. Only the leader is allowed to restore these options from the on-disk dump if these are completely absent from the DCS or if they are invalid.
|
||||||
|
|
||||||
|
|
||||||
|
.. _validate_generate_config:
|
||||||
|
|
||||||
|
Configuration generation and validation
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
Patroni provides command-line interfaces for a Patroni :ref:`local configuration <yaml_configuration>` generation and validation. Using the ``patroni`` executable you can:
|
||||||
|
|
||||||
|
- Create a sample local Patroni configuration;
|
||||||
|
- Create a Patroni configuration file for the locally running PostgreSQL instance (e.g. as a preparation step for the :ref:`Patroni integration <existing_data>`);
|
||||||
|
- Validate a given Patroni configuration file.
|
||||||
|
|
||||||
|
.. _generate_sample_config:
|
||||||
|
|
||||||
|
Sample Patroni configuration
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code:: text
|
||||||
|
|
||||||
|
patroni --generate-sample-config [configfile]
|
||||||
|
|
||||||
|
Description
|
||||||
|
"""""""""""
|
||||||
|
|
||||||
|
Generate a sample Patroni configuration file in ``yaml`` format.
|
||||||
|
Parameter values are defined using the :ref:`Environment configuration <environment>`, otherwise, if not set, the defaults used in Patroni or the ``#FIXME`` string for the values that should be later defined by the user.
|
||||||
|
|
||||||
|
Some default values are defined based on the local setup:
|
||||||
|
|
||||||
|
- **postgresql.listen**: the IP address returned by ``gethostname`` call for the current machine's hostname and the standard ``5432`` port.
|
||||||
|
- **postgresql.connect_address**: the IP address returned by ``gethostname`` call for the current machine's hostname and the standard ``5432`` port.
|
||||||
|
- **postgresql.authentication.rewind**: is only defined if the PostgreSQL version can be defined from the binary and the version is 11 or later.
|
||||||
|
- **restapi.listen**: IP address returned by ``gethostname`` call for the current machine's hostname and the standard ``8008`` port.
|
||||||
|
- **restapi.connect_address**: IP address returned by ``gethostname`` call for the current machine's hostname and the standard ``8008`` port.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
""""""""""
|
||||||
|
|
||||||
|
``configfile`` - full path to the configuration file used to store the result. If not provided, the result is sent to ``stdout``.
|
||||||
|
|
||||||
|
.. _generate_config:
|
||||||
|
|
||||||
|
Patroni configuration for a running instance
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code:: text
|
||||||
|
|
||||||
|
patroni --generate-config [--dsn DSN] [configfile]
|
||||||
|
|
||||||
|
Description
|
||||||
|
"""""""""""
|
||||||
|
|
||||||
|
Generate a Patroni configuration in ``yaml`` format for the locally running PostgreSQL instance.
|
||||||
|
Either the provided DSN (takes precedence) or PostgreSQL `environment variables <https://www.postgresql.org/docs/current/libpq-envars.html>`__ will be used for the PostgreSQL connection. If the password is not provided, it should be entered via prompt.
|
||||||
|
|
||||||
|
All the non-internal GUCs defined in the source Postgres instance, independently if they were set through a configuration file, through the postmaster command-line, or through environment variables, will be used as the source for the following Patroni configuration parameters:
|
||||||
|
|
||||||
|
- **scope**: ``cluster_name`` GUC value;
|
||||||
|
- **postgresql.listen**: ``listen_addresses`` and ``port`` GUC values;
|
||||||
|
- **postgresql.datadir**: ``data_directory`` GUC value;
|
||||||
|
- **postgresql.parameters**: ``archive_command``, ``restore_command``, ``archive_cleanup_command``, ``recovery_end_command``, ``ssl_passphrase_command``, ``hba_file``, ``ident_file``, ``config_file`` GUC values;
|
||||||
|
- **bootstrap.dcs**: all other gathered PostgreSQL GUCs.
|
||||||
|
|
||||||
|
If ``scope``, ``postgresql.listen`` or ``postgresql.datadir`` is not set from the Postgres GUCs, the respective :ref:`Environment configuration <environment>` value is used.
|
||||||
|
|
||||||
|
Other rules applied for the values definition:
|
||||||
|
|
||||||
|
- **name**: ``PATRONI_NAME`` environment variable value if set, otherwise the current machine's hostname.
|
||||||
|
- **postgresql.bin_dir**: path to the Postgres binaries gathered from the running instance.
|
||||||
|
- **postgresql.connect_address**: the IP address returned by ``gethostname`` call for the current machine's hostname and the port used for the instance connection or the ``port`` GUC value.
|
||||||
|
- **postgresql.authentication.superuser**: the configuration used for the instance connection;
|
||||||
|
- **postgresql.pg_hba**: the lines gathered from the source instance's ``hba_file``.
|
||||||
|
- **postgresql.pg_ident**: the lines gathered from the source instance's ``ident_file``.
|
||||||
|
- **restapi.listen**: IP address returned by ``gethostname`` call for the current machine's hostname and the standard ``8008`` port.
|
||||||
|
- **restapi.connect_address**: IP address returned by ``gethostname`` call for the current machine's hostname and the standard ``8008`` port.
|
||||||
|
|
||||||
|
Other parameters defined using :ref:`Environment configuration <environment>` are also included into the configuration.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
""""""""""
|
||||||
|
|
||||||
|
``configfile``
|
||||||
|
Full path to the configuration file used to store the result. If not provided, result is sent to ``stdout``.
|
||||||
|
|
||||||
|
``dsn``
|
||||||
|
Optional DSN string for the local PostgreSQL instance to get GUC values from.
|
||||||
|
|
||||||
|
|
||||||
|
Validate Patroni configuration
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code:: text
|
||||||
|
|
||||||
|
patroni --validate-config [configfile] [--ignore-listen-port | -i]
|
||||||
|
|
||||||
|
Description
|
||||||
|
"""""""""""
|
||||||
|
|
||||||
|
Validate the given Patroni configuration and print the information about the failed checks.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
""""""""""
|
||||||
|
|
||||||
|
``configfile``
|
||||||
|
Full path to the configuration file to check. If not given or file does not exist, will try to read from the ``PATRONI_CONFIG_VARIABLE`` environment variable or, if not set, from the :ref:`Patroni environment variables <environment>`.
|
||||||
|
|
||||||
|
``--ignore-listen-port | -i``
|
||||||
|
Optional flag to ignore bind failures for ``listen`` ports that are already in use when validating the ``configfile``.
|
||||||
|
|
||||||
|
``--print | -p``
|
||||||
|
Optional flag to print out local configuration (including environment configuration overrides) after it has been successfully validated.
|
||||||
+1964
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -19,7 +19,7 @@ When Patroni runs in a paused mode, it does not change the state of PostgreSQL,
|
|||||||
|
|
||||||
- For the Postgres primary with the leader lock Patroni updates the lock. If the node with the leader lock stops being the primary (i.e. is demoted manually), Patroni will release the lock instead of promoting the node back.
|
- For the Postgres primary with the leader lock Patroni updates the lock. If the node with the leader lock stops being the primary (i.e. is demoted manually), Patroni will release the lock instead of promoting the node back.
|
||||||
|
|
||||||
- Manual unscheduled restart, reinitialize and manual failover are allowed. Manual failover is only allowed if the node to failover to is specified. In the paused mode, manual failover does not require a running primary node.
|
- Manual unscheduled restart, manual unscheduled failover/switchover and reinitialize are allowed. No scheduled action is allowed. Manual switchover is only allowed if the node to switch over to is specified.
|
||||||
|
|
||||||
- If 'parallel' primaries are detected by Patroni, it emits a warning, but does not demote the primary without the leader lock.
|
- If 'parallel' primaries are detected by Patroni, it emits a warning, but does not demote the primary without the leader lock.
|
||||||
|
|
||||||
@@ -32,6 +32,6 @@ When Patroni runs in a paused mode, it does not change the state of PostgreSQL,
|
|||||||
User guide
|
User guide
|
||||||
----------
|
----------
|
||||||
|
|
||||||
``patronictl`` supports ``pause`` and ``resume`` commands.
|
``patronictl`` supports :ref:`pause <patronictl_pause>` and :ref:`resume <patronictl_resume>` commands.
|
||||||
|
|
||||||
One can also issue a ``PATCH`` request to the ``{namespace}/{cluster}/config`` key with ``{"pause": true/false/null}``
|
One can also issue a ``PATCH`` request to the ``{namespace}/{cluster}/config`` key with ``{"pause": true/false/null}``
|
||||||
|
|||||||
+1271
-303
File diff suppressed because it is too large
Load Diff
+55
-61
@@ -1,3 +1,5 @@
|
|||||||
|
.. _replica_imaging_and_bootstrap:
|
||||||
|
|
||||||
Replica imaging and bootstrap
|
Replica imaging and bootstrap
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
@@ -43,19 +45,49 @@ in the configuration files, Patroni supplies two cluster-specific ones:
|
|||||||
|
|
||||||
Passing these two additional flags can be disabled by setting a special ``no_params`` parameter to ``True``.
|
Passing these two additional flags can be disabled by setting a special ``no_params`` parameter to ``True``.
|
||||||
|
|
||||||
If the bootstrap script returns 0, Patroni tries to configure and start the PostgreSQL instance produced by it. If any
|
If the bootstrap script returns ``0``, Patroni tries to configure and start the PostgreSQL instance produced by it. If any
|
||||||
of the intermediate steps fail, or the script returns a non-zero value, Patroni assumes that the bootstrap has failed,
|
of the intermediate steps fail, or the script returns a non-zero value, Patroni assumes that the bootstrap has failed,
|
||||||
cleans up after itself and releases the initialize lock to give another node the opportunity to bootstrap.
|
cleans up after itself and releases the initialize lock to give another node the opportunity to bootstrap.
|
||||||
|
|
||||||
If a ``recovery_conf`` block is defined in the same section as the custom bootstrap method, Patroni will generate a
|
If a ``recovery_conf`` block is defined in the same section as the custom bootstrap method, Patroni will generate a
|
||||||
``recovery.conf`` before starting the newly bootstrapped instance. Typically, such recovery.conf should contain at least
|
``recovery.conf`` before starting the newly bootstrapped instance (or set the recovery settings on Postgres configuration if
|
||||||
one of the ``recovery_target_*`` parameters, together with the ``recovery_target_timeline`` set to ``promote``.
|
running PostgreSQL >= 12).
|
||||||
|
Typically, such recovery configuration should contain at least one of the ``recovery_target_*`` parameters, together with the ``recovery_target_action`` set to ``promote``.
|
||||||
|
|
||||||
If ``keep_existing_recovery_conf`` is defined and set to ``True``, Patroni will not remove the existing ``recovery.conf`` file if it exists.
|
If ``keep_existing_recovery_conf`` is defined and set to ``True``, Patroni will not remove the existing ``recovery.conf`` file if it exists (PostgreSQL <= 11).
|
||||||
This is useful when bootstrapping from a backup with tools like pgBackRest that generate the appropriate ``recovery.conf`` for you.
|
Similarly, in that case Patroni will not remove the existing ``recovery.signal`` or ``standby.signal`` if either exists, nor will it override the configured recovery settings (PostgreSQL >= 12).
|
||||||
|
This is useful when bootstrapping from a backup with tools like pgBackRest that generate the appropriate recovery configuration for you.
|
||||||
|
|
||||||
|
Besides that, any additional key/value pairs informed in the custom bootstrap method configuration will be passed as arguments to ``command`` in the format ``--name=value``. For example:
|
||||||
|
|
||||||
|
.. code:: YAML
|
||||||
|
|
||||||
|
bootstrap:
|
||||||
|
method: <custom_bootstrap_method_name>
|
||||||
|
<custom_bootstrap_method_name>:
|
||||||
|
command: <path_to_custom_bootstrap_script>
|
||||||
|
arg1: value1
|
||||||
|
arg2: value2
|
||||||
|
|
||||||
|
Makes the configured ``command`` to be called additionally with ``--arg1=value1 --arg2=value2`` command-line arguments.
|
||||||
|
|
||||||
.. note:: Bootstrap methods are neither chained, nor fallen-back to the default one in case the primary one fails
|
.. note:: Bootstrap methods are neither chained, nor fallen-back to the default one in case the primary one fails
|
||||||
|
|
||||||
|
As an example, you are able to bootstrap a fresh Patroni cluster from a Barman backup with a configuration like this:
|
||||||
|
|
||||||
|
.. code:: YAML
|
||||||
|
|
||||||
|
bootstrap:
|
||||||
|
method: barman
|
||||||
|
barman:
|
||||||
|
keep_existing_recovery_conf: true
|
||||||
|
command: patroni_barman --api-url https://barman-host:7480 recover
|
||||||
|
barman-server: my_server
|
||||||
|
ssh-command: ssh postgres@patroni-host
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
``patroni_barman recover`` requires that you have both Barman and ``pg-backup-api`` configured in the Barman host, so it can execute a remote ``barman recover`` through the backup API.
|
||||||
|
The above example uses a subset of the available parameters. You can get more information running ``patroni_barman recover --help``.
|
||||||
|
|
||||||
.. _custom_replica_creation:
|
.. _custom_replica_creation:
|
||||||
|
|
||||||
@@ -110,6 +142,24 @@ example: pgbackrest
|
|||||||
basebackup:
|
basebackup:
|
||||||
max-rate: '100M'
|
max-rate: '100M'
|
||||||
|
|
||||||
|
example: Barman
|
||||||
|
|
||||||
|
.. code:: YAML
|
||||||
|
|
||||||
|
postgresql:
|
||||||
|
create_replica_methods:
|
||||||
|
- barman
|
||||||
|
- basebackup
|
||||||
|
barman:
|
||||||
|
command: patroni_barman --api-url https://barman-host:7480 recover
|
||||||
|
barman-server: my_server
|
||||||
|
ssh-command: ssh postgres@patroni-host
|
||||||
|
basebackup:
|
||||||
|
max-rate: '100M'
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
``patroni_barman recover`` requires that you have both Barman and ``pg-backup-api`` configured in the Barman host, so it can execute a remote ``barman recover`` through the backup API.
|
||||||
|
The above example uses a subset of the available parameters. You can get more information running ``patroni_barman recover --help``.
|
||||||
|
|
||||||
The ``create_replica_methods`` 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
|
stop on the first one that returns 0. Each method should define a separate section in the configuration file, listing the command
|
||||||
@@ -169,59 +219,3 @@ and
|
|||||||
- waldir: /pg-wal-mount/external-waldir
|
- waldir: /pg-wal-mount/external-waldir
|
||||||
|
|
||||||
If all replica creation methods fail, Patroni will try again all methods in order during the next event loop cycle.
|
If all replica creation methods fail, Patroni will try again all methods in order during the next event loop cycle.
|
||||||
|
|
||||||
.. _standby_cluster:
|
|
||||||
|
|
||||||
Standby cluster
|
|
||||||
---------------
|
|
||||||
|
|
||||||
Another available option is to run a "standby cluster", that contains only of
|
|
||||||
standby nodes replicating from some remote node. This type of clusters has:
|
|
||||||
|
|
||||||
* "standby leader", that behaves pretty much like a regular cluster leader,
|
|
||||||
except it replicates from a remote node.
|
|
||||||
|
|
||||||
* cascade replicas, that are replicating from standby leader.
|
|
||||||
|
|
||||||
Standby leader holds and updates a leader lock in DCS. If the leader lock
|
|
||||||
expires, cascade replicas will perform an election to choose another leader
|
|
||||||
from the standbys.
|
|
||||||
|
|
||||||
There is no further relationship between the standby cluster and the primary
|
|
||||||
cluster it replicates from, in particular, they must not share the same DCS
|
|
||||||
scope if they use the same DCS. They do not know anything else from each other
|
|
||||||
apart from replication information. Also, the standby cluster is not being
|
|
||||||
displayed in ``patronictl list`` or ``patronictl topology`` output on the
|
|
||||||
primary cluster.
|
|
||||||
|
|
||||||
For the sake of flexibility, you can specify methods of creating a replica and
|
|
||||||
recovery WAL records when a cluster is in the "standby mode" by providing
|
|
||||||
`create_replica_methods` key in `standby_cluster` section. It is distinct from
|
|
||||||
creating replicas, when cluster is detached and functions as a normal cluster,
|
|
||||||
which is controlled by `create_replica_methods` in `postgresql` section. Both
|
|
||||||
"standby" and "normal" `create_replica_methods` reference keys in `postgresql`
|
|
||||||
section.
|
|
||||||
|
|
||||||
To configure such cluster you need to specify the section ``standby_cluster``
|
|
||||||
in a patroni configuration:
|
|
||||||
|
|
||||||
.. code:: YAML
|
|
||||||
|
|
||||||
bootstrap:
|
|
||||||
dcs:
|
|
||||||
standby_cluster:
|
|
||||||
host: 1.2.3.4
|
|
||||||
port: 5432
|
|
||||||
primary_slot_name: patroni
|
|
||||||
create_replica_methods:
|
|
||||||
- basebackup
|
|
||||||
|
|
||||||
Note, that these options will be applied only once during cluster bootstrap,
|
|
||||||
and the only way to change them afterwards is through DCS.
|
|
||||||
|
|
||||||
Patroni expects to find `postgresql.conf` or `postgresql.conf.backup` in PGDATA
|
|
||||||
of the remote primary and will not start if it does not find it after a
|
|
||||||
basebackup. If the remote primary keeps its `postgresql.conf` elsewhere, it is
|
|
||||||
your responsibility to copy it to PGDATA.
|
|
||||||
|
|
||||||
If you use replication slots on the standby cluster, you must also create the corresponding replication slot on the primary cluster. It will not be done automatically by the standby cluster implementation. You can use Patroni's permanent replication slots feature on the primary cluster to maintain a replication slot with the same name as ``primary_slot_name``, or its default value if ``primary_slot_name`` is not provided.
|
|
||||||
|
|||||||
+112
-15
@@ -6,8 +6,9 @@ Replication modes
|
|||||||
|
|
||||||
Patroni uses PostgreSQL streaming replication. For more information about streaming replication, see the `Postgres documentation <http://www.postgresql.org/docs/current/static/warm-standby.html#STREAMING-REPLICATION>`__. By default Patroni configures PostgreSQL for asynchronous replication. Choosing your replication schema is dependent on your business considerations. Investigate both async and sync replication, as well as other HA solutions, to determine which solution is best for you.
|
Patroni uses PostgreSQL streaming replication. For more information about streaming replication, see the `Postgres documentation <http://www.postgresql.org/docs/current/static/warm-standby.html#STREAMING-REPLICATION>`__. By default Patroni configures PostgreSQL for asynchronous replication. Choosing your replication schema is dependent on your business considerations. Investigate both async and sync replication, as well as other HA solutions, to determine which solution is best for you.
|
||||||
|
|
||||||
|
|
||||||
Asynchronous mode durability
|
Asynchronous mode durability
|
||||||
----------------------------
|
============================
|
||||||
|
|
||||||
In asynchronous mode the cluster is allowed to lose some committed transactions to ensure availability. When the primary server fails or becomes unavailable for any other reason Patroni will automatically promote a sufficiently healthy standby to primary. Any transactions that have not been replicated to that standby remain in a "forked timeline" on the primary, and are effectively unrecoverable [1]_.
|
In asynchronous mode the cluster is allowed to lose some committed transactions to ensure availability. When the primary server fails or becomes unavailable for any other reason Patroni will automatically promote a sufficiently healthy standby to primary. Any transactions that have not been replicated to that standby remain in a "forked timeline" on the primary, and are effectively unrecoverable [1]_.
|
||||||
|
|
||||||
@@ -15,10 +16,11 @@ The amount of transactions that can be lost is controlled via ``maximum_lag_on_f
|
|||||||
|
|
||||||
By default, when running leader elections, Patroni does not take into account the current timeline of replicas, what in some cases could be undesirable behavior. You can prevent the node not having the same timeline as a former primary become the new leader by changing the value of ``check_timeline`` parameter to ``true``.
|
By default, when running leader elections, Patroni does not take into account the current timeline of replicas, what in some cases could be undesirable behavior. You can prevent the node not having the same timeline as a former primary become the new leader by changing the value of ``check_timeline`` parameter to ``true``.
|
||||||
|
|
||||||
PostgreSQL synchronous replication
|
|
||||||
----------------------------------
|
|
||||||
|
|
||||||
You can use Postgres's `synchronous replication <http://www.postgresql.org/docs/current/static/warm-standby.html#SYNCHRONOUS-REPLICATION>`__ with Patroni. Synchronous replication ensures consistency across a cluster by confirming that writes are written to a secondary before returning to the connecting client with a success. The cost of synchronous replication: reduced throughput on writes. This throughput will be entirely based on network performance.
|
PostgreSQL synchronous replication
|
||||||
|
==================================
|
||||||
|
|
||||||
|
You can use Postgres's `synchronous replication <http://www.postgresql.org/docs/current/static/warm-standby.html#SYNCHRONOUS-REPLICATION>`__ with Patroni. Synchronous replication ensures consistency across a cluster by confirming that writes are written to a secondary before returning to the connecting client with a success. The cost of synchronous replication: increased latency and reduced throughput on writes. This throughput will be entirely based on network performance.
|
||||||
|
|
||||||
In hosted datacenter environments (like AWS, Rackspace, or any network you do not control), synchronous replication significantly increases the variability of write performance. If followers become inaccessible from the leader, the leader effectively becomes read-only.
|
In hosted datacenter environments (like AWS, Rackspace, or any network you do not control), synchronous replication significantly increases the variability of write performance. If followers become inaccessible from the leader, the leader effectively becomes read-only.
|
||||||
|
|
||||||
@@ -33,10 +35,11 @@ When using PostgreSQL synchronous replication, use at least three Postgres data
|
|||||||
|
|
||||||
Using PostgreSQL synchronous replication does not guarantee zero lost transactions under all circumstances. When the primary and the secondary that is currently acting as a synchronous replica fail simultaneously a third node that might not contain all transactions will be promoted.
|
Using PostgreSQL synchronous replication does not guarantee zero lost transactions under all circumstances. When the primary and the secondary that is currently acting as a synchronous replica fail simultaneously a third node that might not contain all transactions will be promoted.
|
||||||
|
|
||||||
|
|
||||||
.. _synchronous_mode:
|
.. _synchronous_mode:
|
||||||
|
|
||||||
Synchronous mode
|
Synchronous mode
|
||||||
----------------
|
================
|
||||||
|
|
||||||
For use cases where losing committed transactions is not permissible you can turn on Patroni's ``synchronous_mode``. When ``synchronous_mode`` is turned on Patroni will not promote a standby unless it is certain that the standby contains all transactions that may have returned a successful commit status to client [2]_. This means that the system may be unavailable for writes even though some servers are available. System administrators can still use manual failover commands to promote a standby even if it results in transaction loss.
|
For use cases where losing committed transactions is not permissible you can turn on Patroni's ``synchronous_mode``. When ``synchronous_mode`` is turned on Patroni will not promote a standby unless it is certain that the standby contains all transactions that may have returned a successful commit status to client [2]_. This means that the system may be unavailable for writes even though some servers are available. System administrators can still use manual failover commands to promote a standby even if it results in transaction loss.
|
||||||
|
|
||||||
@@ -49,36 +52,130 @@ on at least two nodes, enable ``synchronous_mode_strict`` in addition to the
|
|||||||
``synchronous_mode``. This parameter prevents Patroni from switching off the
|
``synchronous_mode``. This parameter prevents Patroni from switching off the
|
||||||
synchronous replication on the primary when no synchronous standby candidates
|
synchronous replication on the primary when no synchronous standby candidates
|
||||||
are available. As a downside, the primary is not be available for writes
|
are available. As a downside, the primary is not be available for writes
|
||||||
(unless the Postgres transaction explicitly turns of ``synchronous_mode``),
|
(unless the Postgres transaction explicitly turns off ``synchronous_mode``),
|
||||||
blocking all client write requests until at least one synchronous replica comes
|
blocking all client write requests until at least one synchronous replica comes
|
||||||
up.
|
up.
|
||||||
|
|
||||||
You can ensure that a standby never becomes the synchronous standby by setting ``nosync`` tag to true. This is recommended to set for standbys that are behind slow network connections and would cause performance degradation when becoming a synchronous standby.
|
You can ensure that a standby never becomes the synchronous standby by setting ``nosync`` tag to true. This is recommended to set for standbys that are behind slow network connections and would cause performance degradation when becoming a synchronous standby. Setting tag ``nostream`` to true will also have the same effect.
|
||||||
|
|
||||||
Synchronous mode can be switched on and off via Patroni REST interface. See :ref:`dynamic configuration <dynamic_configuration>` for instructions.
|
Synchronous mode can be switched on and off using ``patronictl edit-config`` command or via Patroni REST interface. See :ref:`dynamic configuration <dynamic_configuration>` for instructions.
|
||||||
|
|
||||||
Note: Because of the way synchronous replication is implemented in PostgreSQL it is still possible to lose transactions even when using ``synchronous_mode_strict``. If the PostgreSQL backend is cancelled while waiting to acknowledge replication (as a result of packet cancellation due to client timeout or backend failure) transaction changes become visible for other backends. Such changes are not yet replicated and may be lost in case of standby promotion.
|
Note: Because of the way synchronous replication is implemented in PostgreSQL it is still possible to lose transactions even when using ``synchronous_mode_strict``. If the PostgreSQL backend is cancelled while waiting to acknowledge replication (as a result of packet cancellation due to client timeout or backend failure) transaction changes become visible for other backends. Such changes are not yet replicated and may be lost in case of standby promotion.
|
||||||
|
|
||||||
|
|
||||||
Synchronous Replication Factor
|
Synchronous Replication Factor
|
||||||
------------------------------
|
==============================
|
||||||
The parameter ``synchronous_node_count`` is used by Patroni to manage number of synchronous standby databases. It is set to 1 by default. It has no effect when ``synchronous_mode`` is set to off. When enabled, Patroni manages precise number of synchronous standby databases based on parameter ``synchronous_node_count`` and adjusts the state in DCS & synchronous_standby_names as members join and leave.
|
|
||||||
|
The parameter ``synchronous_node_count`` is used by Patroni to manage the number of synchronous standby databases. It is set to ``1`` by default. It has no effect when ``synchronous_mode`` is set to ``off``. When enabled, Patroni manages the precise number of synchronous standby databases based on parameter ``synchronous_node_count`` and adjusts the state in DCS & ``synchronous_standby_names`` in PostgreSQL as members join and leave. If the parameter is set to a value higher than the number of eligible nodes it will be automatically reduced by Patroni.
|
||||||
|
|
||||||
|
|
||||||
|
Maximum lag on synchronous node
|
||||||
|
===============================
|
||||||
|
|
||||||
|
By default Patroni sticks to nodes that are declared as ``synchronous``, according to the ``pg_stat_replication`` view, even when there are other nodes ahead of it. This is done to minimize the number of changes of ``synchronous_standby_names``. To change this behavior one may use ``maximum_lag_on_syncnode`` parameter. It controls how much lag the replica can have to still be considered as "synchronous".
|
||||||
|
|
||||||
|
Patroni utilizes the max replica LSN if there is more than one standby, otherwise it will use leader's current wal LSN. The default is ``-1``, and Patroni will not take action to swap a synchronous unhealthy standby when the value is set to ``0`` or less. Please set the value high enough so that Patroni won't swap synchronous standbys frequently during high transaction volume.
|
||||||
|
|
||||||
|
|
||||||
Synchronous mode implementation
|
Synchronous mode implementation
|
||||||
-------------------------------
|
===============================
|
||||||
|
|
||||||
When in synchronous mode Patroni maintains synchronization state in the DCS, containing the latest primary and current synchronous standby databases. This state is updated with strict ordering constraints to ensure the following invariants:
|
When in synchronous mode Patroni maintains synchronization state in the DCS (``/sync`` key), containing the latest primary and current synchronous standby databases. This state is updated with strict ordering constraints to ensure the following invariants:
|
||||||
|
|
||||||
- A node must be marked as the latest leader whenever it can accept write transactions. Patroni crashing or PostgreSQL not shutting down can cause violations of this invariant.
|
- A node must be marked as the latest leader whenever it can accept write transactions. Patroni crashing or PostgreSQL not shutting down can cause violations of this invariant.
|
||||||
|
|
||||||
- A node must be set as the synchronous standby in PostgreSQL as long as it is published as the synchronous standby.
|
- A node must be set as the synchronous standby in PostgreSQL as long as it is published as the synchronous standby in the ``/sync`` key in DCS..
|
||||||
|
|
||||||
- A node that is not the leader or current synchronous standby is not allowed to promote itself automatically.
|
- A node that is not the leader or current synchronous standby is not allowed to promote itself automatically.
|
||||||
|
|
||||||
Patroni will only assign one or more synchronous standby nodes based on ``synchronous_node_count`` parameter to ``synchronous_standby_names``.
|
Patroni will only assign one or more synchronous standby nodes based on ``synchronous_node_count`` parameter to ``synchronous_standby_names``.
|
||||||
|
|
||||||
On each HA loop iteration Patroni re-evaluates synchronous standby nodes choice. If the current list of synchronous standby nodes are connected and has not requested its synchronous status to be removed it remains picked. Otherwise the cluster member available for sync that is furthest ahead in replication is picked.
|
On each HA loop iteration Patroni re-evaluates synchronous standby nodes choice. If the current list of synchronous standby nodes are connected and has not requested its synchronous status to be removed it remains picked. Otherwise the cluster members available for sync that are furthest ahead in replication are picked.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
---------
|
||||||
|
|
||||||
|
``/config`` key in DCS
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code-block:: YAML
|
||||||
|
|
||||||
|
synchronous_mode: on
|
||||||
|
synchronous_node_count: 2
|
||||||
|
...
|
||||||
|
|
||||||
|
``/sync`` key in DCS
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code-block:: JSON
|
||||||
|
|
||||||
|
{
|
||||||
|
"leader": "node0",
|
||||||
|
"sync_standby": "node1,node2"
|
||||||
|
}
|
||||||
|
|
||||||
|
postgresql.conf
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code-block:: INI
|
||||||
|
|
||||||
|
synchronous_standby_names = 'FIRST 2 (node1,node2)'
|
||||||
|
|
||||||
|
|
||||||
.. [1] The data is still there, but recovering it requires a manual recovery effort by data recovery specialists. When Patroni is allowed to rewind with ``use_pg_rewind`` the forked timeline will be automatically erased to rejoin the failed primary with the cluster.
|
In the above examples only nodes ``node1`` and ``node2`` are known to be synchronous and allowed to be automatically promoted if the primary (``node0``) fails.
|
||||||
|
|
||||||
|
|
||||||
|
.. _quorum_mode:
|
||||||
|
|
||||||
|
Quorum commit mode
|
||||||
|
==================
|
||||||
|
|
||||||
|
Starting from PostgreSQL v10 Patroni supports quorum-based synchronous replication.
|
||||||
|
|
||||||
|
In this mode, Patroni maintains synchronization state in the DCS, containing the latest known primary, the number of nodes required for quorum, and the nodes currently eligible to vote on quorum. In steady state, the nodes voting on quorum are the leader and all synchronous standbys. This state is updated with strict ordering constraints, with regards to node promotion and ``synchronous_standby_names``, to ensure that at all times any subset of voters that can achieve quorum includes at least one node with the latest successful commit.
|
||||||
|
|
||||||
|
On each iteration of HA loop, Patroni re-evaluates synchronous standby choices and quorum, based on node availability and requested cluster configuration. In PostgreSQL versions above 9.6 all eligible nodes are added as synchronous standbys as soon as their replication catches up to leader.
|
||||||
|
|
||||||
|
Quorum commit helps to reduce worst case latencies, even during normal operation, as a higher latency of replicating to one standby can be compensated by other standbys.
|
||||||
|
|
||||||
|
The quorum-based synchronous mode could be enabled by setting ``synchronous_mode`` to ``quorum`` using ``patronictl edit-config`` command or via Patroni REST interface. See :ref:`dynamic configuration <dynamic_configuration>` for instructions.
|
||||||
|
|
||||||
|
Other parameters, like ``synchronous_node_count``, ``maximum_lag_on_syncnode``, and ``synchronous_mode_strict`` continue to work the same way as with ``synchronous_mode=on``.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
---------
|
||||||
|
|
||||||
|
``/config`` key in DCS
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code-block:: YAML
|
||||||
|
|
||||||
|
synchronous_mode: quorum
|
||||||
|
synchronous_node_count: 2
|
||||||
|
...
|
||||||
|
|
||||||
|
``/sync`` key in DCS
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code-block:: JSON
|
||||||
|
|
||||||
|
{
|
||||||
|
"leader": "node0",
|
||||||
|
"sync_standby": "node1,node2,node3",
|
||||||
|
"quorum": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
postgresql.conf
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code-block:: INI
|
||||||
|
|
||||||
|
synchronous_standby_names = 'ANY 2 (node1,node2,node3)'
|
||||||
|
|
||||||
|
|
||||||
|
If the primary (``node0``) failed, in the above example two of the ``node1``, ``node2``, ``node3`` will have the latest transaction received, but we don't know which ones. To figure out whether the node ``node1`` has received the latest transaction, we need to compare its LSN with the LSN on **at least** one node (``quorum=1`` in the ``/sync`` key) among ``node2`` and ``node3``. If ``node1`` isn't behind of at least one of them, we can guarantee that there will be no user visible data loss if ``node1`` is promoted.
|
||||||
|
|
||||||
|
|
||||||
|
.. [1] The data is still there, but recovering it requires a manual recovery effort by data recovery specialists. When Patroni is allowed to rewind with ``use_pg_rewind`` the forked timeline will be automatically erased to rejoin the failed primary with the cluster. However, for ``use_pg_rewind`` to function properly, either the cluster must be initialized with ``data page checksums`` (``--data-checksums`` option for ``initdb``) and/or ``wal_log_hints`` must be set to ``on``.
|
||||||
|
|
||||||
.. [2] Clients can change the behavior per transaction using PostgreSQL's ``synchronous_commit`` setting. Transactions with ``synchronous_commit`` values of ``off`` and ``local`` may be lost on fail over, but will not be blocked by replication delays.
|
.. [2] Clients can change the behavior per transaction using PostgreSQL's ``synchronous_commit`` setting. Transactions with ``synchronous_commit`` values of ``off`` and ``local`` may be lost on fail over, but will not be blocked by replication delays.
|
||||||
|
|||||||
+377
-52
@@ -3,7 +3,7 @@
|
|||||||
Patroni REST API
|
Patroni REST API
|
||||||
================
|
================
|
||||||
|
|
||||||
Patroni has a rich REST API, which is used by Patroni itself during the leader race, by the ``patronictl`` tool in order to perform failovers/switchovers/reinitialize/restarts/reloads, by HAProxy or any other kind of load balancer to perform HTTP health checks, and of course could also be used for monitoring. Below you will find the list of Patroni REST API endpoints.
|
Patroni has a rich REST API, which is used by Patroni itself during the leader race, by the :ref:`patronictl` tool in order to perform failovers/switchovers/reinitialize/restarts/reloads, by HAProxy or any other kind of load balancer to perform HTTP health checks, and of course could also be used for monitoring. Below you will find the list of Patroni REST API endpoints.
|
||||||
|
|
||||||
Health check endpoints
|
Health check endpoints
|
||||||
----------------------
|
----------------------
|
||||||
@@ -30,7 +30,8 @@ For all health check ``GET`` requests Patroni returns a JSON document with the s
|
|||||||
|
|
||||||
- ``GET /replica?tag_key1=value1&tag_key2=value2``: replica check endpoint. In addition, It will also check for user defined tags ``key1`` and ``key2`` and their respective values in the **tags** section of the yaml configuration management. If the tag isn't defined for an instance, or if the value in the yaml configuration doesn't match the querying value, it will return HTTP Status Code 503.
|
- ``GET /replica?tag_key1=value1&tag_key2=value2``: replica check endpoint. In addition, It will also check for user defined tags ``key1`` and ``key2`` and their respective values in the **tags** section of the yaml configuration management. If the tag isn't defined for an instance, or if the value in the yaml configuration doesn't match the querying value, it will return HTTP Status Code 503.
|
||||||
|
|
||||||
In the following requests, since we are checking for the leader or standby-leader status, Patroni doesn't apply any of the user defined tags and they will be ignored.
|
In the following requests, since we are checking for the leader or standby-leader status, Patroni doesn't apply any of the user defined tags and they will be ignored.
|
||||||
|
|
||||||
- ``GET /?tag_key1=value1&tag_key2=value2``
|
- ``GET /?tag_key1=value1&tag_key2=value2``
|
||||||
- ``GET /leader?tag_key1=value1&tag_key2=value2``
|
- ``GET /leader?tag_key1=value1&tag_key2=value2``
|
||||||
- ``GET /primary?tag_key1=value1&tag_key2=value2``
|
- ``GET /primary?tag_key1=value1&tag_key2=value2``
|
||||||
@@ -44,6 +45,10 @@ For all health check ``GET`` requests Patroni returns a JSON document with the s
|
|||||||
|
|
||||||
- ``GET /read-only-sync``: like the above endpoint, but also includes the primary.
|
- ``GET /read-only-sync``: like the above endpoint, but also includes the primary.
|
||||||
|
|
||||||
|
- ``GET /quorum``: returns HTTP status code **200** only when this Patroni node is listed as a quorum node in ``synchronous_standby_names`` on the primary.
|
||||||
|
|
||||||
|
- ``GET /read-only-quorum``: like the above endpoint, but also includes the primary.
|
||||||
|
|
||||||
- ``GET /asynchronous`` or ``GET /async``: returns HTTP status code **200** only when the Patroni node is running as an asynchronous standby.
|
- ``GET /asynchronous`` or ``GET /async``: returns HTTP status code **200** only when the Patroni node is running as an asynchronous standby.
|
||||||
|
|
||||||
|
|
||||||
@@ -91,26 +96,263 @@ Monitoring endpoint
|
|||||||
|
|
||||||
The ``GET /patroni`` is used by Patroni during the leader race. It also could be used by your monitoring system. The JSON document produced by this endpoint has the same structure as the JSON produced by the health check endpoints.
|
The ``GET /patroni`` is used by Patroni during the leader race. It also could be used by your monitoring system. The JSON document produced by this endpoint has the same structure as the JSON produced by the health check endpoints.
|
||||||
|
|
||||||
|
**Example:** A healthy cluster
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ curl -s http://localhost:8008/patroni | jq .
|
$ curl -s http://localhost:8008/patroni | jq .
|
||||||
{
|
{
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"postmaster_start_time": "2019-09-24 09:22:32.555 CEST",
|
"postmaster_start_time": "2024-08-28 19:39:26.352526+00:00",
|
||||||
"role": "master",
|
"role": "primary",
|
||||||
"server_version": 110005,
|
"server_version": 160004,
|
||||||
"cluster_unlocked": false,
|
|
||||||
"xlog": {
|
"xlog": {
|
||||||
"location": 25624640
|
"location": 67395656
|
||||||
},
|
},
|
||||||
"timeline": 3,
|
"timeline": 1,
|
||||||
"database_system_identifier": "6739877027151648096",
|
"replication": [
|
||||||
|
{
|
||||||
|
"usename": "replicator",
|
||||||
|
"application_name": "patroni2",
|
||||||
|
"client_addr": "10.89.0.6",
|
||||||
|
"state": "streaming",
|
||||||
|
"sync_state": "async",
|
||||||
|
"sync_priority": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"usename": "replicator",
|
||||||
|
"application_name": "patroni3",
|
||||||
|
"client_addr": "10.89.0.2",
|
||||||
|
"state": "streaming",
|
||||||
|
"sync_state": "async",
|
||||||
|
"sync_priority": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dcs_last_seen": 1692356718,
|
||||||
|
"tags": {
|
||||||
|
"clonefrom": true
|
||||||
|
},
|
||||||
|
"database_system_identifier": "7268616322854375442",
|
||||||
"patroni": {
|
"patroni": {
|
||||||
"version": "1.6.0",
|
"version": "4.0.0",
|
||||||
"scope": "batman"
|
"scope": "demo",
|
||||||
|
"name": "patroni1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
**Example:** An unlocked cluster
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ curl -s http://localhost:8008/patroni | jq .
|
||||||
|
{
|
||||||
|
"state": "running",
|
||||||
|
"postmaster_start_time": "2024-08-28 19:39:26.352526+00:00",
|
||||||
|
"role": "replica",
|
||||||
|
"server_version": 160004,
|
||||||
|
"xlog": {
|
||||||
|
"received_location": 67419744,
|
||||||
|
"replayed_location": 67419744,
|
||||||
|
"replayed_timestamp": null,
|
||||||
|
"paused": false
|
||||||
|
},
|
||||||
|
"timeline": 1,
|
||||||
|
"replication": [
|
||||||
|
{
|
||||||
|
"usename": "replicator",
|
||||||
|
"application_name": "patroni2",
|
||||||
|
"client_addr": "10.89.0.6",
|
||||||
|
"state": "streaming",
|
||||||
|
"sync_state": "async",
|
||||||
|
"sync_priority": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"usename": "replicator",
|
||||||
|
"application_name": "patroni3",
|
||||||
|
"client_addr": "10.89.0.2",
|
||||||
|
"state": "streaming",
|
||||||
|
"sync_state": "async",
|
||||||
|
"sync_priority": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cluster_unlocked": true,
|
||||||
|
"dcs_last_seen": 1692356928,
|
||||||
|
"tags": {
|
||||||
|
"clonefrom": true
|
||||||
|
},
|
||||||
|
"database_system_identifier": "7268616322854375442",
|
||||||
|
"patroni": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"scope": "demo",
|
||||||
|
"name": "patroni1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example:** An unlocked cluster with :ref:`DCS failsafe mode <dcs_failsafe_mode>` enabled
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ curl -s http://localhost:8008/patroni | jq .
|
||||||
|
{
|
||||||
|
"state": "running",
|
||||||
|
"postmaster_start_time": "2024-08-28 19:39:26.352526+00:00",
|
||||||
|
"role": "replica",
|
||||||
|
"server_version": 160004,
|
||||||
|
"xlog": {
|
||||||
|
"location": 67420024
|
||||||
|
},
|
||||||
|
"timeline": 1,
|
||||||
|
"replication": [
|
||||||
|
{
|
||||||
|
"usename": "replicator",
|
||||||
|
"application_name": "patroni2",
|
||||||
|
"client_addr": "10.89.0.6",
|
||||||
|
"state": "streaming",
|
||||||
|
"sync_state": "async",
|
||||||
|
"sync_priority": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"usename": "replicator",
|
||||||
|
"application_name": "patroni3",
|
||||||
|
"client_addr": "10.89.0.2",
|
||||||
|
"state": "streaming",
|
||||||
|
"sync_state": "async",
|
||||||
|
"sync_priority": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cluster_unlocked": true,
|
||||||
|
"failsafe_mode_is_active": true,
|
||||||
|
"dcs_last_seen": 1692356928,
|
||||||
|
"tags": {
|
||||||
|
"clonefrom": true
|
||||||
|
},
|
||||||
|
"database_system_identifier": "7268616322854375442",
|
||||||
|
"patroni": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"scope": "demo",
|
||||||
|
"name": "patroni1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example:** A cluster with the :ref:`pause mode <pause>` enabled
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ curl -s http://localhost:8008/patroni | jq .
|
||||||
|
{
|
||||||
|
"state": "running",
|
||||||
|
"postmaster_start_time": "2024-08-28 19:39:26.352526+00:00",
|
||||||
|
"role": "replica",
|
||||||
|
"server_version": 160004,
|
||||||
|
"xlog": {
|
||||||
|
"location": 67420024
|
||||||
|
},
|
||||||
|
"timeline": 1,
|
||||||
|
"replication": [
|
||||||
|
{
|
||||||
|
"usename": "replicator",
|
||||||
|
"application_name": "patroni2",
|
||||||
|
"client_addr": "10.89.0.6",
|
||||||
|
"state": "streaming",
|
||||||
|
"sync_state": "async",
|
||||||
|
"sync_priority": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"usename": "replicator",
|
||||||
|
"application_name": "patroni3",
|
||||||
|
"client_addr": "10.89.0.2",
|
||||||
|
"state": "streaming",
|
||||||
|
"sync_state": "async",
|
||||||
|
"sync_priority": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pause": true,
|
||||||
|
"dcs_last_seen": 1724874295,
|
||||||
|
"tags": {
|
||||||
|
"clonefrom": true
|
||||||
|
},
|
||||||
|
"database_system_identifier": "7268616322854375442",
|
||||||
|
"patroni": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"scope": "demo",
|
||||||
|
"name": "patroni1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Retrieve the Patroni metrics in Prometheus format through the ``GET /metrics`` endpoint.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ curl http://localhost:8008/metrics
|
||||||
|
|
||||||
|
# HELP patroni_version Patroni semver without periods. \
|
||||||
|
# TYPE patroni_version gauge
|
||||||
|
patroni_version{scope="batman",name="patroni1"} 040000
|
||||||
|
# HELP patroni_postgres_running Value is 1 if Postgres is running, 0 otherwise.
|
||||||
|
# TYPE patroni_postgres_running gauge
|
||||||
|
patroni_postgres_running{scope="batman",name="patroni1"} 1
|
||||||
|
# HELP patroni_postmaster_start_time Epoch seconds since Postgres started.
|
||||||
|
# TYPE patroni_postmaster_start_time gauge
|
||||||
|
patroni_postmaster_start_time{scope="batman",name="patroni1"} 1724873966.352526
|
||||||
|
# HELP patroni_primary Value is 1 if this node is the leader, 0 otherwise.
|
||||||
|
# TYPE patroni_primary gauge
|
||||||
|
patroni_primary{scope="batman",name="patroni1"} 1
|
||||||
|
# HELP patroni_xlog_location Current location of the Postgres transaction log, 0 if this node is not the leader.
|
||||||
|
# TYPE patroni_xlog_location counter
|
||||||
|
patroni_xlog_location{scope="batman",name="patroni1"} 22320573386952
|
||||||
|
# HELP patroni_standby_leader Value is 1 if this node is the standby_leader, 0 otherwise.
|
||||||
|
# TYPE patroni_standby_leader gauge
|
||||||
|
patroni_standby_leader{scope="batman",name="patroni1"} 0
|
||||||
|
# HELP patroni_replica Value is 1 if this node is a replica, 0 otherwise.
|
||||||
|
# TYPE patroni_replica gauge
|
||||||
|
patroni_replica{scope="batman",name="patroni1"} 0
|
||||||
|
# HELP patroni_sync_standby Value is 1 if this node is a sync standby replica, 0 otherwise.
|
||||||
|
# TYPE patroni_sync_standby gauge
|
||||||
|
patroni_sync_standby{scope="batman",name="patroni1"} 0
|
||||||
|
# HELP patroni_quorum_standby Value is 1 if this node is a quorum standby replica, 0 otherwise.
|
||||||
|
# TYPE patroni_quorum_standby gauge
|
||||||
|
patroni_quorum_standby{scope="batman",name="patroni1"} 0
|
||||||
|
# HELP patroni_xlog_received_location Current location of the received Postgres transaction log, 0 if this node is not a replica.
|
||||||
|
# TYPE patroni_xlog_received_location counter
|
||||||
|
patroni_xlog_received_location{scope="batman",name="patroni1"} 0
|
||||||
|
# HELP patroni_xlog_replayed_location Current location of the replayed Postgres transaction log, 0 if this node is not a replica.
|
||||||
|
# TYPE patroni_xlog_replayed_location counter
|
||||||
|
patroni_xlog_replayed_location{scope="batman",name="patroni1"} 0
|
||||||
|
# HELP patroni_xlog_replayed_timestamp Current timestamp of the replayed Postgres transaction log, 0 if null.
|
||||||
|
# TYPE patroni_xlog_replayed_timestamp gauge
|
||||||
|
patroni_xlog_replayed_timestamp{scope="batman",name="patroni1"} 0
|
||||||
|
# HELP patroni_xlog_paused Value is 1 if the Postgres xlog is paused, 0 otherwise.
|
||||||
|
# TYPE patroni_xlog_paused gauge
|
||||||
|
patroni_xlog_paused{scope="batman",name="patroni1"} 0
|
||||||
|
# HELP patroni_postgres_streaming Value is 1 if Postgres is streaming, 0 otherwise.
|
||||||
|
# TYPE patroni_postgres_streaming gauge
|
||||||
|
patroni_postgres_streaming{scope="batman",name="patroni1"} 1
|
||||||
|
# HELP patroni_postgres_in_archive_recovery Value is 1 if Postgres is replicating from archive, 0 otherwise.
|
||||||
|
# TYPE patroni_postgres_in_archive_recovery gauge
|
||||||
|
patroni_postgres_in_archive_recovery{scope="batman",name="patroni1"} 0
|
||||||
|
# HELP patroni_postgres_server_version Version of Postgres (if running), 0 otherwise.
|
||||||
|
# TYPE patroni_postgres_server_version gauge
|
||||||
|
patroni_postgres_server_version{scope="batman",name="patroni1"} 160004
|
||||||
|
# HELP patroni_cluster_unlocked Value is 1 if the cluster is unlocked, 0 if locked.
|
||||||
|
# TYPE patroni_cluster_unlocked gauge
|
||||||
|
patroni_cluster_unlocked{scope="batman",name="patroni1"} 0
|
||||||
|
# HELP patroni_postgres_timeline Postgres timeline of this node (if running), 0 otherwise.
|
||||||
|
# TYPE patroni_postgres_timeline counter
|
||||||
|
patroni_failsafe_mode_is_active{scope="batman",name="patroni1"} 0
|
||||||
|
# HELP patroni_postgres_timeline Postgres timeline of this node (if running), 0 otherwise.
|
||||||
|
# TYPE patroni_postgres_timeline counter
|
||||||
|
patroni_postgres_timeline{scope="batman",name="patroni1"} 24
|
||||||
|
# HELP patroni_dcs_last_seen Epoch timestamp when DCS was last contacted successfully by Patroni.
|
||||||
|
# TYPE patroni_dcs_last_seen gauge
|
||||||
|
patroni_dcs_last_seen{scope="batman",name="patroni1"} 1724874235
|
||||||
|
# HELP patroni_pending_restart Value is 1 if the node needs a restart, 0 otherwise.
|
||||||
|
# TYPE patroni_pending_restart gauge
|
||||||
|
patroni_pending_restart{scope="batman",name="patroni1"} 1
|
||||||
|
# HELP patroni_is_paused Value is 1 if auto failover is disabled, 0 otherwise.
|
||||||
|
# TYPE patroni_is_paused gauge
|
||||||
|
patroni_is_paused{scope="batman",name="patroni1"} 1
|
||||||
|
|
||||||
|
|
||||||
Cluster status endpoints
|
Cluster status endpoints
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
@@ -122,24 +364,24 @@ Cluster status endpoints
|
|||||||
{
|
{
|
||||||
"members": [
|
"members": [
|
||||||
{
|
{
|
||||||
"name": "postgresql0",
|
"name": "patroni1",
|
||||||
"host": "127.0.0.1",
|
|
||||||
"port": 5432,
|
|
||||||
"role": "leader",
|
"role": "leader",
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"api_url": "http://127.0.0.1:8008/patroni",
|
"api_url": "http://10.89.0.4:8008/patroni",
|
||||||
|
"host": "10.89.0.4",
|
||||||
|
"port": 5432,
|
||||||
"timeline": 5,
|
"timeline": 5,
|
||||||
"tags": {
|
"tags": {
|
||||||
"clonefrom": true
|
"clonefrom": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "postgresql1",
|
"name": "patroni2",
|
||||||
"host": "127.0.0.1",
|
|
||||||
"port": 5433,
|
|
||||||
"role": "replica",
|
"role": "replica",
|
||||||
"state": "running",
|
"state": "streaming",
|
||||||
"api_url": "http://127.0.0.1:8009/patroni",
|
"api_url": "http://10.89.0.6:8008/patroni",
|
||||||
|
"host": "10.89.0.6",
|
||||||
|
"port": 5433,
|
||||||
"timeline": 5,
|
"timeline": 5,
|
||||||
"tags": {
|
"tags": {
|
||||||
"clonefrom": true
|
"clonefrom": true
|
||||||
@@ -147,9 +389,11 @@ Cluster status endpoints
|
|||||||
"lag": 0
|
"lag": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"scope": "demo",
|
||||||
"scheduled_switchover": {
|
"scheduled_switchover": {
|
||||||
"at": "2019-09-24T10:36:00+02:00",
|
"at": "2023-09-24T10:36:00+02:00",
|
||||||
"from": "postgresql0"
|
"from": "patroni1",
|
||||||
|
"to": "patroni3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +430,7 @@ Cluster status endpoints
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
.. _config_endpoint:
|
||||||
|
|
||||||
Config endpoint
|
Config endpoint
|
||||||
---------------
|
---------------
|
||||||
@@ -194,7 +439,7 @@ Config endpoint
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ curl -s localhost:8008/config | jq .
|
$ curl -s http://localhost:8008/config | jq .
|
||||||
{
|
{
|
||||||
"ttl": 30,
|
"ttl": 30,
|
||||||
"loop_wait": 10,
|
"loop_wait": 10,
|
||||||
@@ -205,7 +450,6 @@ Config endpoint
|
|||||||
"use_pg_rewind": true,
|
"use_pg_rewind": true,
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"hot_standby": "on",
|
"hot_standby": "on",
|
||||||
"wal_log_hints": "on",
|
|
||||||
"wal_level": "hot_standby",
|
"wal_level": "hot_standby",
|
||||||
"max_wal_senders": 5,
|
"max_wal_senders": 5,
|
||||||
"max_replication_slots": 5,
|
"max_replication_slots": 5,
|
||||||
@@ -232,7 +476,6 @@ Config endpoint
|
|||||||
"use_pg_rewind": true,
|
"use_pg_rewind": true,
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"hot_standby": "on",
|
"hot_standby": "on",
|
||||||
"wal_log_hints": "on",
|
|
||||||
"wal_level": "hot_standby",
|
"wal_level": "hot_standby",
|
||||||
"max_wal_senders": 5,
|
"max_wal_senders": 5,
|
||||||
"max_replication_slots": 5,
|
"max_replication_slots": 5,
|
||||||
@@ -249,19 +492,29 @@ Let's check that the node processed this configuration. First of all it should s
|
|||||||
|
|
||||||
$ curl -s http://localhost:8008/patroni | jq .
|
$ curl -s http://localhost:8008/patroni | jq .
|
||||||
{
|
{
|
||||||
"pending_restart": true,
|
|
||||||
"database_system_identifier": "6287881213849985952",
|
"database_system_identifier": "6287881213849985952",
|
||||||
"postmaster_start_time": "2016-06-13 13:13:05.211 CEST",
|
"postmaster_start_time": "2024-08-28 19:39:26.352526+00:00",
|
||||||
"xlog": {
|
"xlog": {
|
||||||
"location": 2197818976
|
"location": 2197818976
|
||||||
},
|
},
|
||||||
|
"timeline": 1,
|
||||||
|
"dcs_last_seen": 1724874545,
|
||||||
|
"database_system_identifier": "7408277255830290455",
|
||||||
|
"pending_restart": true,
|
||||||
|
"pending_restart_reason": {
|
||||||
|
"max_connections": {
|
||||||
|
"old_value": "100",
|
||||||
|
"new_value": "101"
|
||||||
|
}
|
||||||
|
},
|
||||||
"patroni": {
|
"patroni": {
|
||||||
|
"version": "4.0.0",
|
||||||
"scope": "batman",
|
"scope": "batman",
|
||||||
"version": "1.0"
|
"name": "patroni1"
|
||||||
},
|
},
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"role": "master",
|
"role": "primary",
|
||||||
"server_version": 90503
|
"server_version": 160004
|
||||||
}
|
}
|
||||||
|
|
||||||
Removing parameters:
|
Removing parameters:
|
||||||
@@ -285,7 +538,6 @@ If you want to remove (reset) some setting just patch it with ``null``:
|
|||||||
"hot_standby": "on",
|
"hot_standby": "on",
|
||||||
"unix_socket_directories": ".",
|
"unix_socket_directories": ".",
|
||||||
"wal_level": "hot_standby",
|
"wal_level": "hot_standby",
|
||||||
"wal_log_hints": "on",
|
|
||||||
"max_wal_senders": 5,
|
"max_wal_senders": 5,
|
||||||
"max_replication_slots": 5
|
"max_replication_slots": 5
|
||||||
}
|
}
|
||||||
@@ -299,7 +551,7 @@ The above call removes ``postgresql.parameters.max_connections`` from the dynami
|
|||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ curl -s -XPUT -d \
|
$ curl -s -XPUT -d \
|
||||||
'{"maximum_lag_on_failover":1048576,"retry_timeout":10,"postgresql":{"use_slots":true,"use_pg_rewind":true,"parameters":{"hot_standby":"on","wal_log_hints":"on","wal_level":"hot_standby","unix_socket_directories":".","max_wal_senders":5}},"loop_wait":3,"ttl":20}' \
|
'{"maximum_lag_on_failover":1048576,"retry_timeout":10,"postgresql":{"use_slots":true,"use_pg_rewind":true,"parameters":{"hot_standby":"on","wal_level":"hot_standby","unix_socket_directories":".","max_wal_senders":5}},"loop_wait":3,"ttl":20}' \
|
||||||
http://localhost:8008/config | jq .
|
http://localhost:8008/config | jq .
|
||||||
{
|
{
|
||||||
"ttl": 20,
|
"ttl": 20,
|
||||||
@@ -311,7 +563,6 @@ The above call removes ``postgresql.parameters.max_connections`` from the dynami
|
|||||||
"hot_standby": "on",
|
"hot_standby": "on",
|
||||||
"unix_socket_directories": ".",
|
"unix_socket_directories": ".",
|
||||||
"wal_level": "hot_standby",
|
"wal_level": "hot_standby",
|
||||||
"wal_log_hints": "on",
|
|
||||||
"max_wal_senders": 5
|
"max_wal_senders": 5
|
||||||
},
|
},
|
||||||
"use_pg_rewind": true
|
"use_pg_rewind": true
|
||||||
@@ -323,41 +574,114 @@ The above call removes ``postgresql.parameters.max_connections`` from the dynami
|
|||||||
Switchover and failover endpoints
|
Switchover and failover endpoints
|
||||||
---------------------------------
|
---------------------------------
|
||||||
|
|
||||||
``POST /switchover`` or ``POST /failover``. These endpoints are very similar to each other. There are a couple of minor differences though:
|
.. _switchover_api:
|
||||||
|
|
||||||
1. The failover endpoint allows to perform a manual failover when there are no healthy nodes, but at the same time it will not allow you to schedule a switchover.
|
Switchover
|
||||||
|
^^^^^^^^^^
|
||||||
|
|
||||||
2. The switchover endpoint is the opposite. It works only when the cluster is healthy (there is a leader) and allows to schedule a switchover at a given time.
|
``/switchover`` endpoint only works when the cluster is healthy (there is a leader). It also allows to schedule a switchover at a given time.
|
||||||
|
|
||||||
|
When calling ``/switchover`` endpoint a candidate can be specified but is not required, in contrast to ``/failover`` endpoint. If a candidate is not provided, all the eligible nodes of the cluster will participate in the leader race after the leader stepped down.
|
||||||
|
|
||||||
In the JSON body of the ``POST`` request you must specify at least the ``leader`` or ``candidate`` fields and optionally the ``scheduled_at`` field if you want to schedule a switchover at a specific time.
|
In the JSON body of the ``POST`` request you must specify the ``leader`` field. The ``candidate`` and the ``scheduled_at`` fields are optional and can be used to schedule a switchover at a specific time.
|
||||||
|
|
||||||
|
Depending on the situation, requests might return different HTTP status codes and bodies. Status code **200** is returned when the switchover or failover successfully completed. If the switchover was successfully scheduled, Patroni will return HTTP status code **202**. In case something went wrong, the error status code (one of **400**, **412**, or **503**) will be returned with some details in the response body.
|
||||||
|
|
||||||
Example: perform a failover to the specific node:
|
``DELETE /switchover`` can be used to delete the currently scheduled switchover.
|
||||||
|
|
||||||
|
**Example:** perform a switchover to any healthy standby
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ curl -s http://localhost:8009/failover -XPOST -d '{"candidate":"postgresql1"}'
|
$ curl -s http://localhost:8008/switchover -XPOST -d '{"leader":"postgresql1"}'
|
||||||
Successfully failed over to "postgresql1"
|
Successfully switched over to "postgresql2"
|
||||||
|
|
||||||
|
|
||||||
Example: schedule a switchover from the leader to any other healthy replica in the cluster at a specific time:
|
**Example:** perform a switchover to a specific node
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ curl -s http://localhost:8008/switchover -XPOST -d \
|
$ curl -s http://localhost:8008/switchover -XPOST -d \
|
||||||
'{"leader":"postgresql0","scheduled_at":"2019-09-24T12:00+00"}'
|
'{"leader":"postgresql1","candidate":"postgresql2"}'
|
||||||
Switchover scheduled
|
Successfully switched over to "postgresql2"
|
||||||
|
|
||||||
|
|
||||||
Depending on the situation the request might finish with a different HTTP status code and body. The status code **200** is returned when the switchover or failover successfully completed. If the switchover was successfully scheduled, Patroni will return HTTP status code **202**. In case something went wrong, the error status code (one of **400**, **412** or **503**) will be returned with some details in the response body. For more information please check the source code of ``patroni/api.py:do_POST_failover()`` method.
|
**Example:** schedule a switchover from the leader to any other healthy standby in the cluster at a specific time.
|
||||||
|
|
||||||
- ``DELETE /switchover``: delete the scheduled switchover
|
.. code-block:: bash
|
||||||
|
|
||||||
The ``POST /switchover`` and ``POST failover`` endpoints are used by ``patronictl switchover`` and ``patronictl failover``, respectively.
|
$ curl -s http://localhost:8008/switchover -XPOST -d \
|
||||||
The ``DELETE /switchover`` is used by ``patronictl flush <cluster-name> switchover``.
|
'{"leader":"postgresql0","scheduled_at":"2019-09-24T12:00+00"}'
|
||||||
|
Switchover scheduled
|
||||||
|
|
||||||
|
|
||||||
|
Failover
|
||||||
|
^^^^^^^^
|
||||||
|
|
||||||
|
``/failover`` endpoint can be used to perform a manual failover when there are no healthy nodes (e.g. to an asynchronous standby if all synchronous standbys are not healthy enough to promote). However there is no requirement for a cluster not to have leader - failover can also be run on a healthy cluster.
|
||||||
|
|
||||||
|
In the JSON body of the ``POST`` request you must specify the ``candidate`` field. If the ``leader`` field is specified, a switchover is triggered instead.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ curl -s http://localhost:8008/failover -XPOST -d '{"candidate":"postgresql1"}'
|
||||||
|
Successfully failed over to "postgresql1"
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
:ref:`Be very careful <failover_healthcheck>` when using this endpoint, as this can cause data loss in certain situations. In most cases, :ref:`the switchover endpoint <switchover_api>` satisfies the administrator's needs.
|
||||||
|
|
||||||
|
|
||||||
|
``POST /switchover`` and ``POST /failover`` endpoints are used by :ref:`patronictl_switchover` and :ref:`patronictl_failover`, respectively.
|
||||||
|
|
||||||
|
``DELETE /switchover`` is used by :ref:`patronictl flush cluster-name switchover <patronictl_flush_parameters>`.
|
||||||
|
|
||||||
|
.. list-table:: Failover/Switchover comparison
|
||||||
|
:widths: 25 25 25
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* -
|
||||||
|
- Failover
|
||||||
|
- Switchover
|
||||||
|
* - Requires leader specified
|
||||||
|
- no
|
||||||
|
- yes
|
||||||
|
* - Requires candidate specified
|
||||||
|
- yes
|
||||||
|
- no
|
||||||
|
* - Can be run in pause
|
||||||
|
- yes
|
||||||
|
- yes (only to a specific candidate)
|
||||||
|
* - Can be scheduled
|
||||||
|
- no
|
||||||
|
- yes (if not in pause)
|
||||||
|
|
||||||
|
.. _failover_healthcheck:
|
||||||
|
|
||||||
|
Healthy standby
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
There are a couple of checks that a member of a cluster should pass to be able to participate in the leader race during a switchover or to become a leader as a failover/switchover candidate:
|
||||||
|
|
||||||
|
- be reachable via Patroni API;
|
||||||
|
- not have ``nofailover`` tag set to ``true``;
|
||||||
|
- have watchdog fully functional (if required by the configuration);
|
||||||
|
- in case of a switchover in a healthy cluster or an automatic failover, not exceed maximum replication lag (``maximum_lag_on_failover`` :ref:`configuration parameter <dynamic_configuration>`);
|
||||||
|
- in case of a switchover in a healthy cluster or an automatic failover, not have a timeline number smaller than the cluster timeline if ``check_timeline`` :ref:`configuration parameter <dynamic_configuration>` is set to ``true``;
|
||||||
|
- in :ref:`synchronous mode <synchronous_mode>`:
|
||||||
|
|
||||||
|
- In case of a switchover (both with and without a candidate): be listed in the ``/sync`` key members;
|
||||||
|
- For a failover in both healthy and unhealthy clusters, this check is omitted.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
In case of a manual failover in a cluster without a leader, a candidate will be allowed to promote even if:
|
||||||
|
- it is not in the ``/sync`` key members when synchronous mode is enabled;
|
||||||
|
- its lag exceeds the maximum replication lag allowed;
|
||||||
|
- it has the timeline number smaller than the last known cluster timeline.
|
||||||
|
|
||||||
|
.. _restart_endpoint:
|
||||||
|
|
||||||
Restart endpoint
|
Restart endpoint
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
@@ -371,15 +695,16 @@ Restart endpoint
|
|||||||
|
|
||||||
- ``DELETE /restart``: delete the scheduled restart
|
- ``DELETE /restart``: delete the scheduled restart
|
||||||
|
|
||||||
``POST /restart`` and ``DELETE /restart`` endpoints are used by ``patronictl restart`` and ``patronictl flush <cluster-name> restart`` respectively.
|
``POST /restart`` and ``DELETE /restart`` endpoints are used by :ref:`patronictl_restart` and :ref:`patronictl flush cluster-name restart <patronictl_flush_parameters>` respectively.
|
||||||
|
|
||||||
|
.. _reload_endpoint:
|
||||||
|
|
||||||
Reload endpoint
|
Reload endpoint
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
The ``POST /reload`` call will order Patroni to re-read and apply the configuration file. This is the equivalent of sending the ``SIGHUP`` signal to the Patroni process. In case you changed some of the Postgres parameters which require a restart (like **shared_buffers**), you still have to explicitly do the restart of Postgres by either calling the ``POST /restart`` endpoint or with the help of ``patronictl restart``.
|
The ``POST /reload`` call will order Patroni to re-read and apply the configuration file. This is the equivalent of sending the ``SIGHUP`` signal to the Patroni process. In case you changed some of the Postgres parameters which require a restart (like **shared_buffers**), you still have to explicitly do the restart of Postgres by either calling the ``POST /restart`` endpoint or with the help of :ref:`patronictl_restart`.
|
||||||
|
|
||||||
The reload endpoint is used by ``patronictl reload``.
|
The reload endpoint is used by :ref:`patronictl_reload`.
|
||||||
|
|
||||||
|
|
||||||
Reinitialize endpoint
|
Reinitialize endpoint
|
||||||
@@ -389,4 +714,4 @@ Reinitialize endpoint
|
|||||||
|
|
||||||
The call might fail if Patroni is in a loop trying to recover (restart) a failed Postgres. In order to overcome this problem one can specify ``{"force":true}`` in the request body.
|
The call might fail if Patroni is in a loop trying to recover (restart) a failed Postgres. In order to overcome this problem one can specify ``{"force":true}`` in the request body.
|
||||||
|
|
||||||
The reinitialize endpoint is used by ``patronictl reinit``.
|
The reinitialize endpoint is used by :ref:`patronictl_reinit`.
|
||||||
|
|||||||
+4
-4
@@ -9,11 +9,11 @@ A Patroni cluster has two interfaces to be protected from unauthorized access: t
|
|||||||
Protecting DCS
|
Protecting DCS
|
||||||
==============
|
==============
|
||||||
|
|
||||||
Patroni and patronictl both store and retrieve data to/from the DCS.
|
Patroni and :ref:`patronictl` both store and retrieve data to/from the DCS.
|
||||||
|
|
||||||
Despite DCS doesn't contain any sensitive information, it allows changing some of Patroni/Postgres configuration. Therefore the very first thing that should be protected is DCS itself.
|
Despite DCS doesn't contain any sensitive information, it allows changing some of Patroni/Postgres configuration. Therefore the very first thing that should be protected is DCS itself.
|
||||||
|
|
||||||
The details of protection depend on the type of DCS used. The authentication and encryption parameters (tokens/basic-auth/client certificates) for the supported types of DCS are covered in :ref:`SETTINGS <bootstrap_settings>`
|
The details of protection depend on the type of DCS used. The authentication and encryption parameters (tokens/basic-auth/client certificates) for the supported types of DCS are covered in :ref:`settings <yaml_configuration>`.
|
||||||
|
|
||||||
The general recommendation is to enable TLS for all DCS communication.
|
The general recommendation is to enable TLS for all DCS communication.
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ Protecting the REST API
|
|||||||
|
|
||||||
Protecting the REST API is a more complicated task.
|
Protecting the REST API is a more complicated task.
|
||||||
|
|
||||||
The Patroni REST API is used by Patroni itself during the leader race, by the ``patronictl`` tool in order to perform failovers/switchovers/reinitialize/restarts/reloads, by HAProxy or any other kind of load balancer to perform HTTP health checks, and of course could also be used for monitoring.
|
The Patroni REST API is used by Patroni itself during the leader race, by the :ref:`patronictl` tool in order to perform failovers/switchovers/reinitialize/restarts/reloads, by HAProxy or any other kind of load balancer to perform HTTP health checks, and of course could also be used for monitoring.
|
||||||
|
|
||||||
From the point of view of security, REST API contains safe (``GET`` requests, only retrieve information) and unsafe (``PUT``, ``POST``, ``PATCH`` and ``DELETE`` requests, change the state of nodes) endpoints.
|
From the point of view of security, REST API contains safe (``GET`` requests, only retrieve information) and unsafe (``PUT``, ``POST``, ``PATCH`` and ``DELETE`` requests, change the state of nodes) endpoints.
|
||||||
|
|
||||||
@@ -32,6 +32,6 @@ When TLS for the REST API is enabled and a PKI is established, mutual authentica
|
|||||||
|
|
||||||
The ``restapi`` section parameters enable TLS client authentication to the server. Depending on the value of the ``verify_client`` parameter, the API server requires a successful client certificate verification for both safe and unsafe API calls (``verify_client: required``), or only for unsafe API calls (``verify_client: optional``), or for no API calls (``verify_client: none``).
|
The ``restapi`` section parameters enable TLS client authentication to the server. Depending on the value of the ``verify_client`` parameter, the API server requires a successful client certificate verification for both safe and unsafe API calls (``verify_client: required``), or only for unsafe API calls (``verify_client: optional``), or for no API calls (``verify_client: none``).
|
||||||
|
|
||||||
The ``ctl`` section parameters enable TLS server authentication to the client (the ``patronictl`` tool which uses the same config as patroni). Set ``insecure: true`` to disable the server certificate verification by the client. See :ref:`SETTINGS <patronictl_settings>` for a detailed description of the TLS client parameters.
|
The ``ctl`` section parameters enable TLS server authentication to the client (the :ref:`patronictl` tool which uses the same config as patroni). Set ``insecure: true`` to disable the server certificate verification by the client. See :ref:`settings <patronictl_settings>` for a detailed description of the TLS client parameters.
|
||||||
|
|
||||||
Protecting the PostgreSQL database proper from unauthorized access is beyond the scope of this document and is covered in https://www.postgresql.org/docs/current/client-authentication.html
|
Protecting the PostgreSQL database proper from unauthorized access is beyond the scope of this document and is covered in https://www.postgresql.org/docs/current/client-authentication.html
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
.. _standby_cluster:
|
||||||
|
|
||||||
|
Standby cluster
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Patroni also support running cascading replication to a remote datacenter
|
||||||
|
(region) using a feature that is called "standby cluster". This type of
|
||||||
|
clusters has:
|
||||||
|
|
||||||
|
* "standby leader", that behaves pretty much like a regular cluster leader,
|
||||||
|
except it replicates from a remote node.
|
||||||
|
|
||||||
|
* cascade replicas, that are replicating from standby leader.
|
||||||
|
|
||||||
|
Standby leader holds and updates a leader lock in DCS. If the leader lock
|
||||||
|
expires, cascade replicas will perform an election to choose another leader
|
||||||
|
from the standbys.
|
||||||
|
|
||||||
|
There is no further relationship between the standby cluster and the primary
|
||||||
|
cluster it replicates from, in particular, they must not share the same DCS
|
||||||
|
scope if they use the same DCS. They do not know anything else from each other
|
||||||
|
apart from replication information. Also, the standby cluster is not being
|
||||||
|
displayed in :ref:`patronictl_list` or :ref:`patronictl_topology` output on the
|
||||||
|
primary cluster.
|
||||||
|
|
||||||
|
For the sake of flexibility, you can specify methods of creating a replica and
|
||||||
|
recovery WAL records when a cluster is in the "standby mode" by providing
|
||||||
|
:ref:`create_replica_methods <custom_replica_creation>` key in
|
||||||
|
`standby_cluster` section. It is distinct from creating replicas, when cluster
|
||||||
|
is detached and functions as a normal cluster, which is controlled by
|
||||||
|
`create_replica_methods` in `postgresql` section. Both "standby" and "normal"
|
||||||
|
`create_replica_methods` reference keys in `postgresql` section.
|
||||||
|
|
||||||
|
To configure such cluster you need to specify the section ``standby_cluster``
|
||||||
|
in a patroni configuration:
|
||||||
|
|
||||||
|
.. code:: YAML
|
||||||
|
|
||||||
|
bootstrap:
|
||||||
|
dcs:
|
||||||
|
standby_cluster:
|
||||||
|
host: 1.2.3.4
|
||||||
|
port: 5432
|
||||||
|
primary_slot_name: patroni
|
||||||
|
create_replica_methods:
|
||||||
|
- basebackup
|
||||||
|
|
||||||
|
Note, that these options will be applied only once during cluster bootstrap,
|
||||||
|
and the only way to change them afterwards is through DCS.
|
||||||
|
|
||||||
|
Patroni expects to find `postgresql.conf` or `postgresql.conf.backup` in PGDATA
|
||||||
|
of the remote primary and will not start if it does not find it after a
|
||||||
|
basebackup. If the remote primary keeps its `postgresql.conf` elsewhere, it is
|
||||||
|
your responsibility to copy it to PGDATA.
|
||||||
|
|
||||||
|
If you use replication slots on the standby cluster, you must also create the
|
||||||
|
corresponding replication slot on the primary cluster. It will not be done
|
||||||
|
automatically by the standby cluster implementation. You can use Patroni's
|
||||||
|
permanent replication slots feature on the primary cluster to maintain a
|
||||||
|
replication slot with the same name as ``primary_slot_name``, or its default
|
||||||
|
value if ``primary_slot_name`` is not provided.
|
||||||
|
|
||||||
|
In case the remote site doesn't provide a single endpoint that connects to a
|
||||||
|
primary, one could list all hosts of the source cluster in the
|
||||||
|
``standby_cluster.host`` section. When ``standby_cluster.host`` contains
|
||||||
|
multiple hosts separated by commas, Patroni will:
|
||||||
|
|
||||||
|
* add ``target_session_attrs=read-write`` to the ``primary_conninfo`` on the
|
||||||
|
standby leader node.
|
||||||
|
* use ``target_session_attrs=read-write`` when trying to determine whether we
|
||||||
|
need to run ``pg_rewind`` or when executing ``pg_rewind`` on all nodes of the
|
||||||
|
standby cluster.
|
||||||
|
* It is important to note that for ``pg_rewind`` to operate successfully,
|
||||||
|
either the cluster must be initialized with ``data page checksums``
|
||||||
|
(``--data-checksums`` option for ``initdb``) and/or ``wal_log_hints`` must be set to ``on``.
|
||||||
|
Otherwise, ``pg_rewind`` will not function properly.
|
||||||
|
|
||||||
|
There is also a possibility to replicate the standby cluster from another
|
||||||
|
standby cluster or from a standby member of the primary cluster: for that, you
|
||||||
|
need to define a single host in the ``standby_cluster.host`` section. However,
|
||||||
|
you need to beware that in this case ``pg_rewind`` will fail to execute on the
|
||||||
|
standby cluster.
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
.. _tools_integration:
|
||||||
|
|
||||||
|
Integration with other tools
|
||||||
|
============================
|
||||||
|
|
||||||
|
Patroni is able to integrate with other tools in your stack. In this section you
|
||||||
|
will find a list of examples, which although not an exhaustive list, might
|
||||||
|
provide you with ideas on how Patroni can integrate with other tools.
|
||||||
|
|
||||||
|
Barman
|
||||||
|
------
|
||||||
|
|
||||||
|
Patroni delivers an application named ``patroni_barman`` which has logic to
|
||||||
|
communicate with ``pg-backup-api``, so you are able to perform Barman operations
|
||||||
|
remotely.
|
||||||
|
|
||||||
|
This application currently has a couple of sub-commands: ``recover`` and
|
||||||
|
``config-switch``.
|
||||||
|
|
||||||
|
patroni_barman recover
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The ``recover`` sub-command can be used as a custom bootstrap or custom replica
|
||||||
|
creation method. You can find more information about that in
|
||||||
|
:ref:`replica_imaging_and_bootstrap`.
|
||||||
|
|
||||||
|
patroni_barman config-switch
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The ``config-switch`` sub-command is designed to be used as an ``on_role_change``
|
||||||
|
callback in Patroni. As an example, assume you are streaming WALs from your
|
||||||
|
current primary to your Barman host. In the event of a failover in the cluster
|
||||||
|
you might want to start streaming WALs from the new primary. You can accomplish
|
||||||
|
this by using ``patroni_barman config-switch`` as the ``on_role_change`` callback.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
That sub-command relies on the ``barman config-switch`` command, which is in
|
||||||
|
charge of overriding the configuration of a Barman server by applying a
|
||||||
|
pre-defined model on top of it. This command is available since Barman 3.10.
|
||||||
|
Please consult the Barman documentation for more details.
|
||||||
|
|
||||||
|
This is an example of how you can configure Patroni to apply a configuration
|
||||||
|
model in case this Patroni node is promoted to primary:
|
||||||
|
|
||||||
|
.. code:: YAML
|
||||||
|
|
||||||
|
postgresql:
|
||||||
|
callbacks:
|
||||||
|
on_role_change: >
|
||||||
|
patroni_barman
|
||||||
|
--api-url YOUR_API_URL
|
||||||
|
config-switch
|
||||||
|
--barman-server YOUR_BARMAN_SERVER_NAME
|
||||||
|
--barman-model YOUR_BARMAN_MODEL_NAME
|
||||||
|
--switch-when promoted
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
``patroni_barman config-switch`` requires that you have both Barman and
|
||||||
|
``pg-backup-api`` configured in the Barman host, so it can execute a remote
|
||||||
|
``barman config-switch`` through the backup API. Also, it requires that you
|
||||||
|
have pre-configured Barman models to be applied. The above example uses a
|
||||||
|
subset of the available parameters. You can get more information running
|
||||||
|
``patroni_barman config-switch --help``, and by consulting the Barman
|
||||||
|
documentation.
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
.. _yaml_configuration:
|
||||||
|
|
||||||
|
============================
|
||||||
|
YAML Configuration Settings
|
||||||
|
============================
|
||||||
|
|
||||||
|
|
||||||
|
Global/Universal
|
||||||
|
----------------
|
||||||
|
- **name**: the name of the host. Must be unique for the cluster.
|
||||||
|
- **namespace**: path within the configuration store where Patroni will keep information about the cluster. Default value: "/service"
|
||||||
|
- **scope**: cluster name
|
||||||
|
|
||||||
|
.. _log_settings:
|
||||||
|
|
||||||
|
Log
|
||||||
|
---
|
||||||
|
- **type**: sets the format of logs. Can be either **plain** or **json**. To use **json** format, you must have the :ref:`jsonlogger <extras>` installed. The default value is **plain**.
|
||||||
|
- **level**: sets the general logging level. Default value is **INFO** (see `the docs for Python logging <https://docs.python.org/3.6/library/logging.html#levels>`_)
|
||||||
|
- **traceback\_level**: sets the level where tracebacks will be visible. Default value is **ERROR**. Set it to **DEBUG** if you want to see tracebacks only if you enable **log.level=DEBUG**.
|
||||||
|
- **format**: sets the log formatting string. If the log type is **plain**, the log format should be a string. Refer to
|
||||||
|
`the LogRecord attributes <https://docs.python.org/3.6/library/logging.html#logrecord-attributes>`_ for
|
||||||
|
available attributes. If the log type is **json**, the log format can be a list in addition to a string. Each list
|
||||||
|
item should correspond to LogRecord attributes. Be cautious that only the field name is required, and the **%(**
|
||||||
|
and **)** should be omitted. If you wish to print a log field with a different key name, use a dictionary where
|
||||||
|
the dictionary key is the log field, and the value is the name of the field you want to be printed in the log.
|
||||||
|
Default value is **%(asctime)s %(levelname)s: %(message)s**
|
||||||
|
- **dateformat**: sets the datetime formatting string. (see the `formatTime() documentation <https://docs.python.org/3.6/library/logging.html#logging.Formatter.formatTime>`_)
|
||||||
|
- **static_fields**: add additional fields to the log. This option is only available when the log type is set to **json**.
|
||||||
|
- **max\_queue\_size**: Patroni is using two-step logging. Log records are written into the in-memory queue and there is a separate thread which pulls them from the queue and writes to stderr or file. The maximum size of the internal queue is limited by default by **1000** records, which is enough to keep logs for the past 1h20m.
|
||||||
|
- **dir**: Directory to write application logs to. The directory must exist and be writable by the user executing Patroni. If you set this value, the application will retain 4 25MB logs by default. You can tune those retention values with `file_num` and `file_size` (see below).
|
||||||
|
- **mode**: Permissions for log files (for example, ``0644``). If not specified, permissions will be set based on the current umask value.
|
||||||
|
- **file\_num**: The number of application logs to retain.
|
||||||
|
- **file\_size**: Size of patroni.log file (in bytes) that triggers a log rolling.
|
||||||
|
- **loggers**: This section allows redefining logging level per python module
|
||||||
|
|
||||||
|
- **patroni.postmaster: WARNING**
|
||||||
|
- **urllib3: DEBUG**
|
||||||
|
- **deduplicate_heartbeat_logs**: If set to ``true``, successive heartbeat logs that are identical shall not be output. Default value is ``false``.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
The time the HA loop executes at can be very valuable information in diagnosing failovers due to resource exhaustion and similar problems. When ``deduplicate_heartbeat_logs`` is set to ``true`` there will be no log generated for the HA loop execution (unless the leader changes) and hence this potentially useful information will not be available from the logs.
|
||||||
|
|
||||||
|
Here is an example of how to config patroni to log in json format.
|
||||||
|
|
||||||
|
.. code:: YAML
|
||||||
|
|
||||||
|
log:
|
||||||
|
type: json
|
||||||
|
format:
|
||||||
|
- message
|
||||||
|
- module
|
||||||
|
- asctime: '@timestamp'
|
||||||
|
- levelname: level
|
||||||
|
static_fields:
|
||||||
|
app: patroni
|
||||||
|
|
||||||
|
.. _bootstrap_settings:
|
||||||
|
|
||||||
|
Bootstrap configuration
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Once Patroni has initialized the cluster for the first time and settings have been stored in the DCS, all future
|
||||||
|
changes to the ``bootstrap.dcs`` section of the YAML configuration will not take any effect! If you want to change
|
||||||
|
them please use either :ref:`patronictl_edit_config` or the Patroni :ref:`REST API <rest_api>`.
|
||||||
|
|
||||||
|
- **bootstrap**:
|
||||||
|
|
||||||
|
- **dcs**: This section will be written into `/<namespace>/<scope>/config` of the given configuration store after initializing the new cluster. The global dynamic configuration for the cluster. You can put any of the parameters described in the :ref:`Dynamic Configuration settings <dynamic_configuration>` under ``bootstrap.dcs`` and after Patroni has initialized (bootstrapped) the new cluster, it will write this section into `/<namespace>/<scope>/config` of the configuration store.
|
||||||
|
- **method**: custom script to use for bootstrapping this cluster.
|
||||||
|
|
||||||
|
See :ref:`custom bootstrap methods documentation <custom_bootstrap>` for details.
|
||||||
|
When ``initdb`` is specified revert to the default ``initdb`` command. ``initdb`` is also triggered when no ``method``
|
||||||
|
parameter is present in the configuration file.
|
||||||
|
- **initdb**: (optional) list options to be passed on to initdb.
|
||||||
|
|
||||||
|
- **- data-checksums**: Must be enabled when pg_rewind is needed on 9.3.
|
||||||
|
- **- encoding: UTF8**: default encoding for new databases.
|
||||||
|
- **- locale: UTF8**: default locale for new databases.
|
||||||
|
- **post\_bootstrap** or **post\_init**: An additional script that will be executed after initializing the cluster. The script receives a connection string URL (with the cluster superuser as a user name). The PGPASSFILE variable is set to the location of pgpass file.
|
||||||
|
|
||||||
|
.. _citus_settings:
|
||||||
|
|
||||||
|
Citus
|
||||||
|
-----
|
||||||
|
Enables integration Patroni with `Citus <https://docs.citusdata.com>`__. If configured, Patroni will take care of registering Citus worker nodes on the coordinator. You can find more information about Citus support :ref:`here <citus>`.
|
||||||
|
|
||||||
|
- **group**: the Citus group id, integer. Use ``0`` for coordinator and ``1``, ``2``, etc... for workers
|
||||||
|
- **database**: the database where ``citus`` extension should be created. Must be the same on the coordinator and all workers. Currently only one database is supported.
|
||||||
|
|
||||||
|
.. _consul_settings:
|
||||||
|
|
||||||
|
Consul
|
||||||
|
------
|
||||||
|
Most of the parameters are optional, but you have to specify one of the **host** or **url**
|
||||||
|
|
||||||
|
- **host**: the host:port for the Consul local agent.
|
||||||
|
- **url**: url for the Consul local agent, in format: http(s)://host:port.
|
||||||
|
- **port**: (optional) Consul port.
|
||||||
|
- **scheme**: (optional) **http** or **https**, defaults to **http**.
|
||||||
|
- **token**: (optional) ACL token.
|
||||||
|
- **verify**: (optional) whether to verify the SSL certificate for HTTPS requests.
|
||||||
|
- **cacert**: (optional) The ca certificate. If present it will enable validation.
|
||||||
|
- **cert**: (optional) file with the client certificate.
|
||||||
|
- **key**: (optional) file with the client key. Can be empty if the key is part of **cert**.
|
||||||
|
- **dc**: (optional) Datacenter to communicate with. By default the datacenter of the host is used.
|
||||||
|
- **consistency**: (optional) Select consul consistency mode. Possible values are ``default``, ``consistent``, or ``stale`` (more details in `consul API reference <https://www.consul.io/api/features/consistency.html/>`__)
|
||||||
|
- **checks**: (optional) list of Consul health checks used for the session. By default an empty list is used.
|
||||||
|
- **register\_service**: (optional) whether or not to register a service with the name defined by the scope parameter and the tag master, primary, replica, or standby-leader depending on the node's role. Defaults to **false**.
|
||||||
|
- **service\_tags**: (optional) additional static tags to add to the Consul service apart from the role (``primary``/``replica``/``standby-leader``). By default an empty list is used.
|
||||||
|
- **service\_check\_interval**: (optional) how often to perform health check against registered url. Defaults to '5s'.
|
||||||
|
- **service\_check\_tls\_server\_name**: (optional) override SNI host when connecting via TLS, see also `consul agent check API reference <https://www.consul.io/api-docs/agent/check#tlsservername>`__.
|
||||||
|
|
||||||
|
The ``token`` needs to have the following ACL permissions:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
service_prefix "${scope}" {
|
||||||
|
policy = "write"
|
||||||
|
}
|
||||||
|
key_prefix "${namespace}/${scope}" {
|
||||||
|
policy = "write"
|
||||||
|
}
|
||||||
|
session_prefix "" {
|
||||||
|
policy = "write"
|
||||||
|
}
|
||||||
|
|
||||||
|
Etcd
|
||||||
|
----
|
||||||
|
Most of the parameters are optional, but you have to specify one of the **host**, **hosts**, **url**, **proxy** or **srv**
|
||||||
|
|
||||||
|
- **host**: the host:port for the etcd endpoint.
|
||||||
|
- **hosts**: list of etcd endpoint in format host1:port1,host2:port2,etc... Could be a comma separated string or an actual yaml list.
|
||||||
|
- **use\_proxies**: If this parameter is set to true, Patroni will consider **hosts** as a list of proxies and will not perform a topology discovery of etcd cluster.
|
||||||
|
- **url**: url for the etcd.
|
||||||
|
- **proxy**: proxy url for the etcd. If you are connecting to the etcd using proxy, use this parameter instead of **url**.
|
||||||
|
- **srv**: Domain to search the SRV record(s) for cluster autodiscovery. Patroni will try to query these SRV service names for specified domain (in that order until first success): ``_etcd-client-ssl``, ``_etcd-client``, ``_etcd-ssl``, ``_etcd``, ``_etcd-server-ssl``, ``_etcd-server``. If SRV records for ``_etcd-server-ssl`` or ``_etcd-server`` are retrieved then ETCD peer protocol is used do query ETCD for available members. Otherwise hosts from SRV records will be used.
|
||||||
|
- **srv\_suffix**: Configures a suffix to the SRV name that is queried during discovery. Use this flag to differentiate between multiple etcd clusters under the same domain. Works only with conjunction with **srv**. For example, if ``srv_suffix: foo`` and ``srv: example.org`` are set, the following DNS SRV query is made:``_etcd-client-ssl-foo._tcp.example.com`` (and so on for every possible ETCD SRV service name).
|
||||||
|
- **protocol**: (optional) http or https, if not specified http is used. If the **url** or **proxy** is specified - will take protocol from them.
|
||||||
|
- **username**: (optional) username for etcd authentication.
|
||||||
|
- **password**: (optional) password for etcd authentication.
|
||||||
|
- **cacert**: (optional) The ca certificate. If present it will enable validation.
|
||||||
|
- **cert**: (optional) file with the client certificate.
|
||||||
|
- **key**: (optional) file with the client key. Can be empty if the key is part of **cert**.
|
||||||
|
|
||||||
|
Etcdv3
|
||||||
|
------
|
||||||
|
If you want that Patroni works with Etcd cluster via protocol version 3, you need to use the ``etcd3`` section in the Patroni configuration file. All configuration parameters are the same as for ``etcd``.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
Keys created with protocol version 2 are not visible with protocol version 3 and the other way around, therefore it is not possible to switch from ``etcd`` to ``etcd3`` just by updating Patroni config file. In addition, Patroni uses Etcd's gRPC-gateway (proxy) to communicate with the V3 API, which means that TLS common name authentication is not possible.
|
||||||
|
|
||||||
|
|
||||||
|
ZooKeeper
|
||||||
|
----------
|
||||||
|
- **hosts**: List of ZooKeeper cluster members in format: ['host1:port1', 'host2:port2', 'etc...'].
|
||||||
|
- **use_ssl**: (optional) Whether SSL is used or not. Defaults to ``false``. If set to ``false``, all SSL specific parameters are ignored.
|
||||||
|
- **cacert**: (optional) The CA certificate. If present it will enable validation.
|
||||||
|
- **cert**: (optional) File with the client certificate.
|
||||||
|
- **key**: (optional) File with the client key.
|
||||||
|
- **key_password**: (optional) The client key password.
|
||||||
|
- **verify**: (optional) Whether to verify certificate or not. Defaults to ``true``.
|
||||||
|
- **set_acls**: (optional) If set, configure Kazoo to apply a default ACL to each ZNode that it creates. ACLs will assume 'x509' schema and should be specified as a dictionary with the principal as the key and one or more permissions as a list in the value. Permissions may be one of ``CREATE``, ``READ``, ``WRITE``, ``DELETE`` or ``ADMIN``. For example, ``set_acls: {CN=principal1: [CREATE, READ], CN=principal2: [ALL]}``.
|
||||||
|
- **auth_data**: (optional) Authentication credentials to use for the connection. Should be a dictionary in the form that `scheme` is the key and `credential` is the value. Defaults to empty dictionary.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
It is required to install ``kazoo>=2.6.0`` to support SSL.
|
||||||
|
|
||||||
|
|
||||||
|
Exhibitor
|
||||||
|
---------
|
||||||
|
- **hosts**: initial list of Exhibitor (ZooKeeper) nodes in format: 'host1,host2,etc...'. This list updates automatically whenever the Exhibitor (ZooKeeper) cluster topology changes.
|
||||||
|
- **poll\_interval**: how often the list of ZooKeeper and Exhibitor nodes should be updated from Exhibitor.
|
||||||
|
- **port**: Exhibitor port.
|
||||||
|
|
||||||
|
.. _kubernetes_settings:
|
||||||
|
|
||||||
|
Kubernetes
|
||||||
|
----------
|
||||||
|
- **bypass\_api\_service**: (optional) When communicating with the Kubernetes API, Patroni is usually relying on the `kubernetes` service, the address of which is exposed in the pods via the `KUBERNETES_SERVICE_HOST` environment variable. If `bypass_api_service` is set to ``true``, Patroni will resolve the list of API nodes behind the service and connect directly to them.
|
||||||
|
- **namespace**: (optional) Kubernetes namespace where Patroni pod is running. Default value is `default`.
|
||||||
|
- **labels**: Labels in format ``{label1: value1, label2: value2}``. These labels will be used to find existing objects (Pods and either Endpoints or ConfigMaps) associated with the current cluster. Also Patroni will set them on every object (Endpoint or ConfigMap) it creates.
|
||||||
|
- **scope\_label**: (optional) name of the label containing cluster name. Default value is `cluster-name`.
|
||||||
|
- **bootstrap\_labels**: (optional) Labels in format ``{label1: value1, label2: value2}``. These labels will be assigned to a Patroni pod when its state is either ``initializing new cluster``, ``running custom bootstrap script``, ``starting after custom bootstrap`` or ``creating replica``.
|
||||||
|
- **role\_label**: (optional) name of the label containing role (`primary`, `replica`, or other custom value). Patroni will set this label on the pod it runs in. Default value is ``role``.
|
||||||
|
- **leader\_label\_value**: (optional) value of the pod label when Postgres role is ``primary``. Default value is ``primary``.
|
||||||
|
- **follower\_label\_value**: (optional) value of the pod label when Postgres role is ``replica``. Default value is ``replica``.
|
||||||
|
- **standby\_leader\_label\_value**: (optional) value of the pod label when Postgres role is ``standby_leader``. Default value is ``primary``.
|
||||||
|
- **tmp\_role\_label**: (optional) name of the temporary label containing role (`primary` or `replica`). Value of this label will always use the default of corresponding role. Set only when necessary.
|
||||||
|
- **use\_endpoints**: (optional) if set to true, Patroni will use Endpoints instead of ConfigMaps to run leader elections and keep cluster state.
|
||||||
|
- **pod\_ip**: (optional) IP address of the pod Patroni is running in. This value is required when `use_endpoints` is enabled and is used to populate the leader endpoint subsets when the pod's PostgreSQL is promoted.
|
||||||
|
- **ports**: (optional) if the Service object has the name for the port, the same name must appear in the Endpoint object, otherwise service won't work. For example, if your service is defined as ``{Kind: Service, spec: {ports: [{name: postgresql, port: 5432, targetPort: 5432}]}}``, then you have to set ``kubernetes.ports: [{"name": "postgresql", "port": 5432}]`` and Patroni will use it for updating subsets of the leader Endpoint. This parameter is used only if `kubernetes.use_endpoints` is set.
|
||||||
|
- **cacert**: (optional) Specifies the file with the CA_BUNDLE file with certificates of trusted CAs to use while verifying Kubernetes API SSL certs. If not provided, patroni will use the value provided by the ServiceAccount secret.
|
||||||
|
- **retriable\_http\_codes**: (optional) list of HTTP status codes from K8s API to retry on. By default Patroni is retrying on ``500``, ``503``, and ``504``, or if K8s API response has ``retry-after`` HTTP header.
|
||||||
|
|
||||||
|
|
||||||
|
.. _raft_settings:
|
||||||
|
|
||||||
|
Raft (deprecated)
|
||||||
|
-----------------
|
||||||
|
- **self\_addr**: ``ip:port`` to listen on for Raft connections. The ``self_addr`` must be accessible from other nodes of the cluster. If not set, the node will not participate in consensus.
|
||||||
|
- **bind\_addr**: (optional) ``ip:port`` to listen on for Raft connections. If not specified the ``self_addr`` will be used.
|
||||||
|
- **partner\_addrs**: list of other Patroni nodes in the cluster in format: ['ip1:port', 'ip2:port', 'etc...']
|
||||||
|
- **data\_dir**: directory where to store Raft log and snapshot. If not specified the current working directory is used.
|
||||||
|
- **password**: (optional) Encrypt Raft traffic with a specified password, requires ``cryptography`` python module.
|
||||||
|
|
||||||
|
Short FAQ about Raft implementation
|
||||||
|
|
||||||
|
- Q: How to list all the nodes providing consensus?
|
||||||
|
|
||||||
|
A: ``syncobj_admin -conn host:port -status`` where the host:port is the address of one of the cluster nodes
|
||||||
|
|
||||||
|
- Q: Node that was a part of consensus and has gone and I can't reuse the same IP for other node. How to remove this node from the consensus?
|
||||||
|
|
||||||
|
A: ``syncobj_admin -conn host:port -remove host2:port2`` where the ``host2:port2`` is the address of the node you want to remove from consensus.
|
||||||
|
|
||||||
|
- Q: Where to get the ``syncobj_admin`` utility?
|
||||||
|
|
||||||
|
A: It is installed together with ``pysyncobj`` module (python RAFT implementation), which is Patroni dependency.
|
||||||
|
|
||||||
|
- Q: it is possible to run Patroni node without adding in to the consensus?
|
||||||
|
|
||||||
|
A: Yes, just comment out or remove ``raft.self_addr`` from Patroni configuration.
|
||||||
|
|
||||||
|
- Q: It is possible to run Patroni and PostgreSQL only on two nodes?
|
||||||
|
|
||||||
|
A: Yes, on the third node you can run ``patroni_raft_controller`` (without Patroni and PostgreSQL). In such a setup, one can temporarily lose one node without affecting the primary.
|
||||||
|
|
||||||
|
|
||||||
|
.. _postgresql_settings:
|
||||||
|
|
||||||
|
PostgreSQL
|
||||||
|
----------
|
||||||
|
- **postgresql**:
|
||||||
|
|
||||||
|
- **authentication**:
|
||||||
|
|
||||||
|
- **superuser**:
|
||||||
|
|
||||||
|
- **username**: name for the superuser, set during initialization (initdb) and later used by Patroni to connect to the postgres.
|
||||||
|
- **password**: password for the superuser, set during initialization (initdb).
|
||||||
|
- **sslmode**: (optional) maps to the `sslmode <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLMODE>`__ connection parameter, which allows a client to specify the type of TLS negotiation mode with the server. For more information on how each mode works, please visit the `PostgreSQL documentation <https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS>`__. The default mode is ``prefer``.
|
||||||
|
- **sslkey**: (optional) maps to the `sslkey <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLKEY>`__ connection parameter, which specifies the location of the secret key used with the client's certificate.
|
||||||
|
- **sslpassword**: (optional) maps to the `sslpassword <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLPASSWORD>`__ connection parameter, which specifies the password for the secret key specified in ``sslkey``.
|
||||||
|
- **sslcert**: (optional) maps to the `sslcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCERT>`__ connection parameter, which specifies the location of the client certificate.
|
||||||
|
- **sslrootcert**: (optional) maps to the `sslrootcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLROOTCERT>`__ connection parameter, which specifies the location of a file containing one or more certificate authorities (CA) certificates that the client will use to verify a server's certificate.
|
||||||
|
- **sslcrl**: (optional) maps to the `sslcrl <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRL>`__ connection parameter, which specifies the location of a file containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
||||||
|
- **sslcrldir**: (optional) maps to the `sslcrldir <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRLDIR>`__ connection parameter, which specifies the location of a directory with files containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
||||||
|
- **sslnegotiation**: (optional) maps to the `sslnegotiation <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLNEGOTIATION>`__ connection parameter, which controls how SSL encryption is negotiated with the server, if SSL is used.
|
||||||
|
- **gssencmode**: (optional) maps to the `gssencmode <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-GSSENCMODE>`__ connection parameter, which determines whether or with what priority a secure GSS TCP/IP connection will be negotiated with the server
|
||||||
|
- **channel_binding**: (optional) maps to the `channel_binding <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-CHANNEL-BINDING>`__ connection parameter, which controls the client's use of channel binding.
|
||||||
|
- **replication**:
|
||||||
|
|
||||||
|
- **username**: replication username; the user will be created during initialization. Replicas will use this user to access the replication source via streaming replication
|
||||||
|
- **password**: replication password; the user will be created during initialization.
|
||||||
|
- **sslmode**: (optional) maps to the `sslmode <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLMODE>`__ connection parameter, which allows a client to specify the type of TLS negotiation mode with the server. For more information on how each mode works, please visit the `PostgreSQL documentation <https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS>`__. The default mode is ``prefer``.
|
||||||
|
- **sslkey**: (optional) maps to the `sslkey <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLKEY>`__ connection parameter, which specifies the location of the secret key used with the client's certificate.
|
||||||
|
- **sslpassword**: (optional) maps to the `sslpassword <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLPASSWORD>`__ connection parameter, which specifies the password for the secret key specified in ``sslkey``.
|
||||||
|
- **sslcert**: (optional) maps to the `sslcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCERT>`__ connection parameter, which specifies the location of the client certificate.
|
||||||
|
- **sslrootcert**: (optional) maps to the `sslrootcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLROOTCERT>`__ connection parameter, which specifies the location of a file containing one or more certificate authorities (CA) certificates that the client will use to verify a server's certificate.
|
||||||
|
- **sslcrl**: (optional) maps to the `sslcrl <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRL>`__ connection parameter, which specifies the location of a file containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
||||||
|
- **sslcrldir**: (optional) maps to the `sslcrldir <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRLDIR>`__ connection parameter, which specifies the location of a directory with files containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
||||||
|
- **sslnegotiation**: (optional) maps to the `sslnegotiation <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLNEGOTIATION>`__ connection parameter, which controls how SSL encryption is negotiated with the server, if SSL is used.
|
||||||
|
- **gssencmode**: (optional) maps to the `gssencmode <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-GSSENCMODE>`__ connection parameter, which determines whether or with what priority a secure GSS TCP/IP connection will be negotiated with the server
|
||||||
|
- **channel_binding**: (optional) maps to the `channel_binding <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-CHANNEL-BINDING>`__ connection parameter, which controls the client's use of channel binding.
|
||||||
|
- **rewind**:
|
||||||
|
|
||||||
|
- **username**: (optional) name for the user for ``pg_rewind``; the user will be created during initialization of postgres 11+ and all necessary `permissions <https://www.postgresql.org/docs/11/app-pgrewind.html#id-1.9.5.8.8>`__ will be granted.
|
||||||
|
- **password**: (optional) password for the user for ``pg_rewind``; the user will be created during initialization.
|
||||||
|
- **sslmode**: (optional) maps to the `sslmode <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLMODE>`__ connection parameter, which allows a client to specify the type of TLS negotiation mode with the server. For more information on how each mode works, please visit the `PostgreSQL documentation <https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS>`__. The default mode is ``prefer``.
|
||||||
|
- **sslkey**: (optional) maps to the `sslkey <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLKEY>`__ connection parameter, which specifies the location of the secret key used with the client's certificate.
|
||||||
|
- **sslpassword**: (optional) maps to the `sslpassword <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLPASSWORD>`__ connection parameter, which specifies the password for the secret key specified in ``sslkey``.
|
||||||
|
- **sslcert**: (optional) maps to the `sslcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCERT>`__ connection parameter, which specifies the location of the client certificate.
|
||||||
|
- **sslrootcert**: (optional) maps to the `sslrootcert <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLROOTCERT>`__ connection parameter, which specifies the location of a file containing one or more certificate authorities (CA) certificates that the client will use to verify a server's certificate.
|
||||||
|
- **sslcrl**: (optional) maps to the `sslcrl <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRL>`__ connection parameter, which specifies the location of a file containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
||||||
|
- **sslcrldir**: (optional) maps to the `sslcrldir <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCRLDIR>`__ connection parameter, which specifies the location of a directory with files containing a certificate revocation list. A client will reject connecting to any server that has a certificate present in this list.
|
||||||
|
- **sslnegotiation**: (optional) maps to the `sslnegotiation <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLNEGOTIATION>`__ connection parameter, which controls how SSL encryption is negotiated with the server, if SSL is used.
|
||||||
|
- **gssencmode**: (optional) maps to the `gssencmode <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-GSSENCMODE>`__ connection parameter, which determines whether or with what priority a secure GSS TCP/IP connection will be negotiated with the server
|
||||||
|
- **channel_binding**: (optional) maps to the `channel_binding <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-CHANNEL-BINDING>`__ connection parameter, which controls the client's use of channel binding.
|
||||||
|
|
||||||
|
- **callbacks**: callback scripts to run on certain actions. Patroni will pass the action, role and cluster name. (See scripts/aws.py as an example of how to write them.)
|
||||||
|
|
||||||
|
- **on\_reload**: run this script when configuration reload is triggered.
|
||||||
|
- **on\_restart**: run this script when the postgres restarts (without changing role).
|
||||||
|
- **on\_role\_change**: run this script when the postgres is being promoted or demoted.
|
||||||
|
- **on\_start**: run this script when the postgres starts.
|
||||||
|
- **on\_stop**: run this script when the postgres stops.
|
||||||
|
- **connect\_address**: IP address + port through which Postgres is accessible from other nodes and applications.
|
||||||
|
- **proxy\_address**: IP address + port through which a connection pool (e.g. pgbouncer) running next to Postgres is accessible. The value is written to the member key in DCS as ``proxy_url`` and could be used/useful for service discovery.
|
||||||
|
- **create\_replica\_methods**: an ordered list of the create methods for turning a Patroni node into a new replica.
|
||||||
|
"basebackup" is the default method; other methods are assumed to refer to scripts, each of which is configured as its
|
||||||
|
own config item. See :ref:`custom replica creation methods documentation <custom_replica_creation>` for further explanation.
|
||||||
|
- **data\_dir**: The location of the Postgres data directory, either :ref:`existing <existing_data>` or to be initialized by Patroni.
|
||||||
|
- **config\_dir**: The location of the Postgres configuration directory, defaults to the data directory. Must be writable by Patroni.
|
||||||
|
- **bin\_dir**: (optional) Path to PostgreSQL binaries (pg_ctl, initdb, pg_controldata, pg_basebackup, postgres, pg_isready, pg_rewind). If not provided or is an empty string, PATH environment variable will be used to find the executables.
|
||||||
|
- **bin\_name**: (optional) Make it possible to override Postgres binary names, if you are using a custom Postgres distribution:
|
||||||
|
|
||||||
|
- **pg\_ctl**: (optional) Custom name for ``pg_ctl`` binary.
|
||||||
|
- **initdb**: (optional) Custom name for ``initdb`` binary.
|
||||||
|
- **pg\controldata**: (optional) Custom name for ``pg_controldata`` binary.
|
||||||
|
- **pg\_basebackup**: (optional) Custom name for ``pg_basebackup`` binary.
|
||||||
|
- **postgres**: (optional) Custom name for ``postgres`` binary.
|
||||||
|
- **pg\_isready**: (optional) Custom name for ``pg_isready`` binary.
|
||||||
|
- **pg\_rewind**: (optional) Custom name for ``pg_rewind`` binary.
|
||||||
|
- **listen**: IP address + port that Postgres listens to; must be accessible from other nodes in the cluster, if you're using streaming replication. Multiple comma-separated addresses are permitted, as long as the port component is appended after to the last one with a colon, i.e. ``listen: 127.0.0.1,127.0.0.2:5432``. Patroni will use the first address from this list to establish local connections to the PostgreSQL node.
|
||||||
|
- **use\_unix\_socket**: specifies that Patroni should prefer to use unix sockets to connect to the cluster. Default value is ``false``. If ``unix_socket_directories`` is defined, Patroni will use the first suitable value from it to connect to the cluster and fallback to tcp if nothing is suitable. If ``unix_socket_directories`` is not specified in ``postgresql.parameters``, Patroni will assume that the default value should be used and omit ``host`` from the connection parameters.
|
||||||
|
- **use\_unix\_socket\_repl**: specifies that Patroni should prefer to use unix sockets for replication user cluster connection. Default value is ``false``. If ``unix_socket_directories`` is defined, Patroni will use the first suitable value from it to connect to the cluster and fallback to tcp if nothing is suitable. If ``unix_socket_directories`` is not specified in ``postgresql.parameters``, Patroni will assume that the default value should be used and omit ``host`` from the connection parameters.
|
||||||
|
- **pgpass**: path to the `.pgpass <https://www.postgresql.org/docs/current/static/libpq-pgpass.html>`__ password file. Patroni creates this file before executing pg\_basebackup, the post_init script and under some other circumstances. The location must be writable by Patroni.
|
||||||
|
- **recovery\_conf**: additional configuration settings written to recovery.conf when configuring follower.
|
||||||
|
- **custom\_conf** : path to an optional custom ``postgresql.conf`` file, that will be used in place of ``postgresql.base.conf``. The file must exist on all cluster nodes, be readable by PostgreSQL and will be included from its location on the real ``postgresql.conf``. Note that Patroni will not monitor this file for changes, nor backup it. However, its settings can still be overridden by Patroni's own configuration facilities - see :ref:`dynamic configuration <patroni_configuration>` for details.
|
||||||
|
- **parameters**: configuration parameters (GUCs) for Postgres in format ``{ssl: "on", ssl_cert_file: "cert_file"}``.
|
||||||
|
- **pg\_hba**: list of lines that Patroni will use to generate ``pg_hba.conf``. Patroni ignores this parameter if ``hba_file`` PostgreSQL parameter is set to a non-default value. Together with :ref:`dynamic configuration <dynamic_configuration>` this parameter simplifies management of ``pg_hba.conf``.
|
||||||
|
|
||||||
|
- **- host all all 0.0.0.0/0 md5**
|
||||||
|
- **- host replication replicator 127.0.0.1/32 md5**: A line like this is required for replication.
|
||||||
|
- **pg\_ident**: list of lines that Patroni will use to generate ``pg_ident.conf``. Patroni ignores this parameter if ``ident_file`` PostgreSQL parameter is set to a non-default value. Together with :ref:`dynamic configuration <dynamic_configuration>` this parameter simplifies management of ``pg_ident.conf``.
|
||||||
|
|
||||||
|
- **- mapname1 systemname1 pguser1**
|
||||||
|
- **- mapname1 systemname2 pguser2**
|
||||||
|
- **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. Either the cluster must be initialized with ``data page checksums`` (``--data-checksums`` option for ``initdb``) and/or ``wal_log_hints`` must be set to ``on``, or ``pg_rewind`` will not work.
|
||||||
|
- **remove\_data\_directory\_on\_rewind\_failure**: If this option is enabled, Patroni will remove the PostgreSQL data directory and recreate the replica. Otherwise it will try to follow the new leader. Default value is **false**.
|
||||||
|
- **remove\_data\_directory\_on\_diverged\_timelines**: Patroni will remove the PostgreSQL data directory and recreate the replica if it notices that timelines are diverging and the former primary can not start streaming from the new primary. This option is useful when ``pg_rewind`` can not be used. While performing timelines divergence check on PostgreSQL v10 and older Patroni will try to connect with replication credential to the "postgres" database. Hence, such access should be allowed in the pg_hba.conf. Default value is **false**.
|
||||||
|
- **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".
|
||||||
|
- **pre\_promote**: a fencing script that executes during a failover after acquiring the leader lock but before promoting the replica. If the script exits with a non-zero code, Patroni does not promote the replica and removes the leader key from DCS.
|
||||||
|
- **before\_stop**: a script that executes immediately prior to stopping postgres. As opposed to a callback, this script runs synchronously, blocking shutdown until it has completed. The return code of this script does not impact whether shutdown proceeds afterwards.
|
||||||
|
|
||||||
|
.. _restapi_settings:
|
||||||
|
|
||||||
|
REST API
|
||||||
|
--------
|
||||||
|
- **restapi**:
|
||||||
|
|
||||||
|
- **connect\_address**: IP address (or hostname) and port, to access the Patroni's :ref:`REST API <rest_api>`. All the members of the cluster must be able to connect to this address, so unless the Patroni setup is intended for a demo inside the localhost, this address must be a non "localhost" or loopback address (ie: "localhost" or "127.0.0.1"). It can serve as an endpoint for HTTP health checks (read below about the "listen" REST API parameter), and also for user queries (either directly or via the REST API), as well as for the health checks done by the cluster members during leader elections (for example, to determine whether the leader is still running, or if there is a node which has a WAL position that is ahead of the one doing the query; etc.) The connect_address is put in the member key in DCS, making it possible to translate the member name into the address to connect to its REST API.
|
||||||
|
- **listen**: IP address (or hostname) and port that Patroni will listen to for the REST API - to provide also the same health checks and cluster messaging between the participating nodes, as described above. to provide health-check information for HAProxy (or any other load balancer capable of doing a HTTP "OPTION" or "GET" checks).
|
||||||
|
- **authentication**: (optional)
|
||||||
|
|
||||||
|
- **username**: Basic-auth username to protect unsafe REST API endpoints.
|
||||||
|
- **password**: Basic-auth password to protect unsafe REST API endpoints.
|
||||||
|
- **certfile**: (optional): Specifies the file with the certificate in the PEM format. If the certfile is not specified or is left empty, the API server will work without SSL.
|
||||||
|
- **keyfile**: (optional): Specifies the file with the secret key in the PEM format.
|
||||||
|
- **keyfile\_password**: (optional): Specifies a password for decrypting the keyfile.
|
||||||
|
- **cafile**: (optional): Specifies the file with the CA_BUNDLE with certificates of trusted CAs to use while verifying client certs.
|
||||||
|
- **ciphers**: (optional): Specifies the permitted cipher suites (e.g. "ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA256:!SSLv1:!SSLv2:!SSLv3:!TLSv1:!TLSv1.1")
|
||||||
|
- **verify\_client**: (optional): ``none`` (default), ``optional`` or ``required``. When ``none`` REST API will not check client certificates. When ``required`` client certificates are required for all REST API calls. When ``optional`` client certificates are required for all unsafe REST API endpoints. When ``required`` is used, then client authentication succeeds, if the certificate signature verification succeeds. For ``optional`` the client cert will only be checked for ``PUT``, ``POST``, ``PATCH``, and ``DELETE`` requests.
|
||||||
|
- **allowlist**: (optional): Specifies the set of hosts that are allowed to call unsafe REST API endpoints. The single element could be a host name, an IP address or a network address using CIDR notation. By default ``allow all`` is used. In case if ``allowlist`` or ``allowlist_include_members`` are set, anything that is not included is rejected.
|
||||||
|
- **allowlist\_include\_members**: (optional): If set to ``true`` it allows accessing unsafe REST API endpoints from other cluster members registered in DCS (IP address or hostname is taken from the members ``api_url``). Be careful, it might happen that OS will use a different IP for outgoing connections.
|
||||||
|
- **http\_extra\_headers**: (optional): HTTP headers let the REST API server pass additional information with an HTTP response.
|
||||||
|
- **https\_extra\_headers**: (optional): HTTPS headers let the REST API server pass additional information with an HTTP response when TLS is enabled. This will also pass additional information set in ``http_extra_headers``.
|
||||||
|
- **request_queue_size**: (optional): Sets request queue size for TCP socket used by Patroni REST API. Once the queue is full, further requests get a "Connection denied" error. The default value is 5.
|
||||||
|
|
||||||
|
Here is an example of both **http_extra_headers** and **https_extra_headers**:
|
||||||
|
|
||||||
|
.. code:: YAML
|
||||||
|
|
||||||
|
restapi:
|
||||||
|
listen: <listen>
|
||||||
|
connect_address: <connect_address>
|
||||||
|
authentication:
|
||||||
|
username: <username>
|
||||||
|
password: <password>
|
||||||
|
http_extra_headers:
|
||||||
|
'X-Frame-Options': 'SAMEORIGIN'
|
||||||
|
'X-XSS-Protection': '1; mode=block'
|
||||||
|
'X-Content-Type-Options': 'nosniff'
|
||||||
|
cafile: <ca file>
|
||||||
|
certfile: <cert>
|
||||||
|
keyfile: <key>
|
||||||
|
https_extra_headers:
|
||||||
|
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains'
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
- The ``restapi.connect_address`` must be accessible from all nodes of a given Patroni cluster. Internally Patroni is using it during the leader race to find nodes with minimal replication lag.
|
||||||
|
- If you enabled client certificates validation (``restapi.verify_client`` is set to ``required``), you also **must** provide **valid client certificates** in the ``ctl.certfile``, ``ctl.keyfile``, ``ctl.keyfile_password``. If not provided, Patroni will not work correctly.
|
||||||
|
|
||||||
|
|
||||||
|
.. _patronictl_settings:
|
||||||
|
|
||||||
|
CTL
|
||||||
|
---
|
||||||
|
- **ctl**: (optional)
|
||||||
|
|
||||||
|
- **authentication**:
|
||||||
|
|
||||||
|
- **username**: Basic-auth username for accessing protected REST API endpoints. If not provided :ref:`patronictl` will use the value provided for REST API "username" parameter.
|
||||||
|
- **password**: Basic-auth password for accessing protected REST API endpoints. If not provided :ref:`patronictl` will use the value provided for REST API "password" parameter.
|
||||||
|
- **insecure**: Allow connections to REST API without verifying SSL certs.
|
||||||
|
- **cacert**: Specifies the file with the CA_BUNDLE file or directory with certificates of trusted CAs to use while verifying REST API SSL certs. If not provided :ref:`patronictl` will use the value provided for REST API "cafile" parameter.
|
||||||
|
- **certfile**: Specifies the file with the client certificate in the PEM format.
|
||||||
|
- **keyfile**: Specifies the file with the client secret key in the PEM format.
|
||||||
|
- **keyfile\_password**: Specifies a password for decrypting the client keyfile.
|
||||||
|
|
||||||
|
Watchdog
|
||||||
|
--------
|
||||||
|
- **mode**: ``off``, ``automatic`` or ``required``. When ``off`` watchdog is disabled. When ``automatic`` watchdog will be used if available, but ignored if it is not. When ``required`` the node will not become a leader unless watchdog can be successfully enabled.
|
||||||
|
- **device**: Path to watchdog device. Defaults to ``/dev/watchdog``.
|
||||||
|
- **safety_margin**: Number of seconds of safety margin between watchdog triggering and leader key expiration.
|
||||||
|
|
||||||
|
.. _tags_settings:
|
||||||
|
|
||||||
|
Tags
|
||||||
|
----
|
||||||
|
- **clonefrom**: ``true`` or ``false``. If set to ``true`` other nodes might prefer to use this node for bootstrap (take ``pg_basebackup`` from). If there are several nodes with ``clonefrom`` tag set to ``true`` the node to bootstrap from will be chosen randomly. The default value is ``false``.
|
||||||
|
- **noloadbalance**: ``true`` or ``false``. If set to ``true`` the node will return HTTP Status Code 503 for the ``GET /replica`` REST API health-check and therefore will be excluded from the load-balancing. Defaults to ``false``.
|
||||||
|
- **replicatefrom**: The name of another replica to replicate from. Used to support cascading replication.
|
||||||
|
- **nosync**: ``true`` or ``false``. If set to ``true`` the node will never be selected as a synchronous replica.
|
||||||
|
- **sync_priority**: integer, controls the priority this node should have during synchronous replica selection when ``synchronous_mode`` is set to ``on``. Nodes with higher priority will be preferred over lower-priority nodes. If the ``sync_priority`` is 0 or negative - such node is not allowed to be written to ``synchronous_standby_names`` PostgreSQL parameter (similar to ``nosync: true``). Keep in mind, that this parameter has the opposite meaning to ``sync_priority`` value reported in ``pg_stat_replication`` view.
|
||||||
|
- **nofailover**: ``true`` or ``false``, controls whether this node is allowed to participate in the leader race and become a leader. Defaults to ``false``, meaning this node _can_ participate in leader races.
|
||||||
|
- **failover_priority**: integer, controls the priority this node should have during failover. Nodes with higher priority will be preferred over lower-priority nodes if they received/replayed the same amount of WAL. However, nodes with higher values of receive/replay LSN are preferred regardless of their priority. If the ``failover_priority`` is 0 or negative - such node is not allowed to participate in the leader race and to become a leader (similar to ``nofailover: true``).
|
||||||
|
- **nostream**: ``true`` or ``false``. If set to ``true`` the node will not use replication protocol to stream WAL. It will rely instead on archive recovery (if ``restore_command`` is configured) and ``pg_wal``/``pg_xlog`` polling. It also disables copying and synchronization of permanent logical replication slots on the node itself and all its cascading replicas. Setting this tag on primary node has no effect.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
Provide only one of ``nofailover`` or ``failover_priority``. Providing ``nofailover: true`` is the same as ``failover_priority: 0``, and providing ``nofailover: false`` will give the node priority 1.
|
||||||
|
|
||||||
|
In addition to these predefined tags, you can also add your own ones:
|
||||||
|
|
||||||
|
- **key1**: ``true``
|
||||||
|
- **key2**: ``false``
|
||||||
|
- **key3**: ``1.4``
|
||||||
|
- **key4**: ``"RandomString"``
|
||||||
|
|
||||||
|
Tags are visible in the :ref:`REST API <rest_api>` and :ref:`patronictl_list` You can also check for an instance health using these tags. If the tag isn't defined for an instance, or if the respective value doesn't match the querying value, it will return HTTP Status Code 503.
|
||||||
@@ -6,7 +6,7 @@ Description=Runners to orchestrate a high-availability PostgreSQL
|
|||||||
After=syslog.target network.target
|
After=syslog.target network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=notify
|
||||||
|
|
||||||
User=postgres
|
User=postgres
|
||||||
Group=postgres
|
Group=postgres
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# syntax = docker/dockerfile:1.5
|
||||||
|
# Used only for running tests using tox, see ../tox.ini
|
||||||
|
ARG PG_MAJOR
|
||||||
|
ARG PGHOME=/home/postgres
|
||||||
|
ARG LC_ALL=C.UTF-8
|
||||||
|
ARG LANG=C.UTF-8
|
||||||
|
ARG BASE_IMAGE=postgres
|
||||||
|
|
||||||
|
FROM ${BASE_IMAGE}:${PG_MAJOR}
|
||||||
|
|
||||||
|
ARG PGHOME
|
||||||
|
ARG LC_ALL
|
||||||
|
ARG LANG
|
||||||
|
|
||||||
|
ENV PGHOME="$PGHOME"
|
||||||
|
ENV PG_USER="${PG_USER:-postgres}"
|
||||||
|
ENV PG_GROUP="${PG_GROUP:-$PG_USER}"
|
||||||
|
ENV LC_ALL="$LC_ALL"
|
||||||
|
ENV LANG="$LANG"
|
||||||
|
|
||||||
|
ARG ETCDVERSION=3.3.13
|
||||||
|
ENV ETCDVERSION="$ETCDVERSION"
|
||||||
|
ARG ETCDURL="https://github.com/coreos/etcd/releases/download/v$ETCDVERSION"
|
||||||
|
|
||||||
|
USER root
|
||||||
|
RUN set -ex \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get reinstall init-system-helpers \
|
||||||
|
&& apt-get install -y \
|
||||||
|
python3-dev \
|
||||||
|
python3-venv \
|
||||||
|
rsync \
|
||||||
|
curl \
|
||||||
|
gcc \
|
||||||
|
golang \
|
||||||
|
jq \
|
||||||
|
locales \
|
||||||
|
sudo \
|
||||||
|
busybox \
|
||||||
|
net-tools \
|
||||||
|
iputils-ping \
|
||||||
|
&& rm -rf /var/cache/apt \
|
||||||
|
\
|
||||||
|
&& python3 -m venv /tox \
|
||||||
|
&& /tox/bin/pip install --no-cache-dir tox>=4 \
|
||||||
|
\
|
||||||
|
&& mkdir -p "$PGHOME" \
|
||||||
|
&& sed -i "s|/var/lib/postgresql.*|$PGHOME:/bin/bash|" /etc/passwd \
|
||||||
|
&& chown -R "$PG_USER:$PG_GROUP" /var/log /home/postgres \
|
||||||
|
\
|
||||||
|
# Download etcd \
|
||||||
|
&& curl -sL "$ETCDURL/etcd-v$ETCDVERSION-linux-$(dpkg --print-architecture).tar.gz" \
|
||||||
|
| tar xz -C /usr/local/bin --strip=1 --wildcards --no-anchored etcd etcdctl
|
||||||
|
|
||||||
|
ENV PATH="/tox/bin:$PATH"
|
||||||
|
|
||||||
|
# This Dockerfile syntax only works with docker buildx and the syntax
|
||||||
|
# line at the top of this file.
|
||||||
|
COPY <<EOF /tox-wrapper.sh
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -ex
|
||||||
|
copy_output() {
|
||||||
|
if [[ -d "\$PGHOME/src/features/output" && /src/features ]] ;then
|
||||||
|
cp -a "\$PGHOME/src/features/output" "/src/features/output-\$HOSTNAME"
|
||||||
|
find "/src/features/output-\$HOSTNAME" -type f -exec chmod 666 {} \\;
|
||||||
|
find "/src/features/output-\$HOSTNAME" -type d -exec chmod 777 {} \\;
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure the copy is ran if the container is stopped with `docker stop` or `docker kill`
|
||||||
|
trap 'copy_output' SIGTERM
|
||||||
|
# For architectures such as aarch we need to get the respective GOARCH
|
||||||
|
# so we can tell etcd we're ok with running an unsupported architecture.
|
||||||
|
export ETCD_UNSUPPORTED_ARCH=$(go env GOARCH)
|
||||||
|
cd /src
|
||||||
|
runuser -u "\$PG_USER" -- \\
|
||||||
|
find . ! -readable 2>/dev/null \\
|
||||||
|
| sed 's|^./||' >/tmp/copy_exclude.lst \\
|
||||||
|
|| true
|
||||||
|
runuser -u "\$PG_USER" -- \\
|
||||||
|
rsync -a \\
|
||||||
|
--exclude=.tox \\
|
||||||
|
--exclude="features/output*" \\
|
||||||
|
--exclude-from="/tmp/copy_exclude.lst" \\
|
||||||
|
. "\$PGHOME/src/"
|
||||||
|
cd "\$PGHOME/src"
|
||||||
|
runuser -u "\$PG_USER" -w ETCD_UNSUPPORTED_ARCH -- "\$@" &
|
||||||
|
wait $!
|
||||||
|
# SIGINT whilst child proc is running is not seen by trap so we run a copy here instead of using
|
||||||
|
# trap copy_output SIGINT EXIT
|
||||||
|
copy_output
|
||||||
|
EOF
|
||||||
|
RUN chmod +x /tox-wrapper.sh
|
||||||
|
|
||||||
|
VOLUME /src
|
||||||
|
|
||||||
|
ENTRYPOINT ["/tox-wrapper.sh"]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
import os
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -3,12 +3,18 @@ import argparse
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--datadir", required=True)
|
parser.add_argument("--datadir", required=True)
|
||||||
parser.add_argument("--dbname", required=True)
|
parser.add_argument("--dbname", required=True)
|
||||||
parser.add_argument("--walmethod", required=True, choices=("fetch", "stream", "none"))
|
parser.add_argument("--walmethod", required=True, choices=("fetch", "stream", "none"))
|
||||||
|
parser.add_argument("--sleep", required=False, type=int)
|
||||||
args, _ = parser.parse_known_args()
|
args, _ = parser.parse_known_args()
|
||||||
|
|
||||||
|
if args.sleep:
|
||||||
|
sleep(args.sleep)
|
||||||
|
|
||||||
walmethod = ["-X", args.walmethod] if args.walmethod != "none" else []
|
walmethod = ["-X", args.walmethod] if args.walmethod != "none" else []
|
||||||
sys.exit(subprocess.call(["pg_basebackup", "-D", args.datadir, "-c", "fast", "-d", args.dbname] + walmethod))
|
sys.exit(subprocess.call(["pg_basebackup", "-D", args.datadir, "-c", "fast", "-d", args.dbname] + walmethod))
|
||||||
|
|||||||
@@ -2,10 +2,17 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--datadir", required=True)
|
parser.add_argument("--datadir", required=True)
|
||||||
parser.add_argument("--sourcedir", required=True)
|
parser.add_argument("--sourcedir", required=True)
|
||||||
|
parser.add_argument("--test-argument", required=True)
|
||||||
|
parser.add_argument("--sleep", required=False, type=int)
|
||||||
args, _ = parser.parse_known_args()
|
args, _ = parser.parse_known_args()
|
||||||
|
|
||||||
|
if args.sleep:
|
||||||
|
sleep(args.sleep)
|
||||||
|
|
||||||
shutil.copytree(args.sourcedir, args.datadir)
|
shutil.copytree(args.sourcedir, args.datadir)
|
||||||
|
|||||||
@@ -2,57 +2,57 @@ Feature: basic replication
|
|||||||
We should check that the basic bootstrapping, replication and failover works.
|
We should check that the basic bootstrapping, replication and failover works.
|
||||||
|
|
||||||
Scenario: check replication of a single table
|
Scenario: check replication of a single table
|
||||||
Given I start postgres0
|
Given I start postgres-0
|
||||||
Then postgres0 is a leader after 10 seconds
|
Then postgres-0 is a leader after 10 seconds
|
||||||
And there is a non empty initialize key in DCS after 15 seconds
|
And there is a non empty initialize key in DCS after 15 seconds
|
||||||
When I issue a PATCH request to http://127.0.0.1:8008/config with {"ttl": 20, "synchronous_mode": true}
|
When I issue a PATCH request to http://127.0.0.1:8008/config with {"ttl": 20, "synchronous_mode": true}
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
When I start postgres1
|
When I start postgres-1
|
||||||
And I configure and start postgres2 with a tag replicatefrom postgres0
|
And I configure and start postgres-2 with a tag replicatefrom postgres-0
|
||||||
And "sync" key in DCS has leader=postgres0 after 20 seconds
|
And "sync" key in DCS has leader=postgres-0 after 20 seconds
|
||||||
And I add the table foo to postgres0
|
And I add the table foo to postgres-0
|
||||||
Then table foo is present on postgres1 after 20 seconds
|
Then table foo is present on postgres-1 after 20 seconds
|
||||||
Then table foo is present on postgres2 after 20 seconds
|
Then table foo is present on postgres-2 after 20 seconds
|
||||||
|
|
||||||
Scenario: check restart of sync replica
|
Scenario: check restart of sync replica
|
||||||
Given I shut down postgres2
|
Given I shut down postgres-2
|
||||||
Then "sync" key in DCS has sync_standby=postgres1 after 5 seconds
|
Then "sync" key in DCS has sync_standby=postgres-1 after 5 seconds
|
||||||
When I start postgres2
|
When I start postgres-2
|
||||||
And I shut down postgres1
|
And I shut down postgres-1
|
||||||
Then "sync" key in DCS has sync_standby=postgres2 after 10 seconds
|
Then "sync" key in DCS has sync_standby=postgres-2 after 10 seconds
|
||||||
When I start postgres1
|
When I start postgres-1
|
||||||
Then "members/postgres1" key in DCS has state=running after 10 seconds
|
Then "members/postgres-1" key in DCS has state=running after 10 seconds
|
||||||
And Status code on GET http://127.0.0.1:8010/sync is 200 after 3 seconds
|
And Status code on GET http://127.0.0.1:8010/sync is 200 after 3 seconds
|
||||||
And Status code on GET http://127.0.0.1:8009/async is 200 after 3 seconds
|
And Status code on GET http://127.0.0.1:8009/async is 200 after 3 seconds
|
||||||
|
|
||||||
Scenario: check stuck sync replica
|
Scenario: check stuck sync replica
|
||||||
Given I issue a PATCH request to http://127.0.0.1:8008/config with {"pause": true, "maximum_lag_on_syncnode": 15000000, "postgresql": {"parameters": {"synchronous_commit": "remote_apply"}}}
|
Given I issue a PATCH request to http://127.0.0.1:8008/config with {"pause": true, "maximum_lag_on_syncnode": 15000000, "postgresql": {"parameters": {"synchronous_commit": "remote_apply"}}}
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
And I create table on postgres0
|
And I create table on postgres-0
|
||||||
And table mytest is present on postgres1 after 2 seconds
|
And table mytest is present on postgres-1 after 2 seconds
|
||||||
And table mytest is present on postgres2 after 2 seconds
|
And table mytest is present on postgres-2 after 2 seconds
|
||||||
When I pause wal replay on postgres2
|
When I pause wal replay on postgres-2
|
||||||
And I load data on postgres0
|
And I load data on postgres-0
|
||||||
Then "sync" key in DCS has sync_standby=postgres1 after 15 seconds
|
Then "sync" key in DCS has sync_standby=postgres-1 after 15 seconds
|
||||||
And I resume wal replay on postgres2
|
And I resume wal replay on postgres-2
|
||||||
And Status code on GET http://127.0.0.1:8009/sync is 200 after 3 seconds
|
And Status code on GET http://127.0.0.1:8009/sync is 200 after 3 seconds
|
||||||
And Status code on GET http://127.0.0.1:8010/async is 200 after 3 seconds
|
And Status code on GET http://127.0.0.1:8010/async is 200 after 3 seconds
|
||||||
When I issue a PATCH request to http://127.0.0.1:8008/config with {"pause": null, "maximum_lag_on_syncnode": -1, "postgresql": {"parameters": {"synchronous_commit": "on"}}}
|
When I issue a PATCH request to http://127.0.0.1:8008/config with {"pause": null, "maximum_lag_on_syncnode": -1, "postgresql": {"parameters": {"synchronous_commit": "on"}}}
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
And I drop table on postgres0
|
And I drop table on postgres-0
|
||||||
|
|
||||||
Scenario: check multi sync replication
|
Scenario: check multi sync replication
|
||||||
Given I issue a PATCH request to http://127.0.0.1:8008/config with {"synchronous_node_count": 2}
|
Given I issue a PATCH request to http://127.0.0.1:8008/config with {"synchronous_node_count": 2}
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
Then "sync" key in DCS has sync_standby=postgres1,postgres2 after 10 seconds
|
Then "sync" key in DCS has sync_standby=postgres-1,postgres-2 after 10 seconds
|
||||||
And Status code on GET http://127.0.0.1:8010/sync is 200 after 3 seconds
|
And Status code on GET http://127.0.0.1:8010/sync is 200 after 3 seconds
|
||||||
And Status code on GET http://127.0.0.1:8009/sync is 200 after 3 seconds
|
And Status code on GET http://127.0.0.1:8009/sync is 200 after 3 seconds
|
||||||
When I issue a PATCH request to http://127.0.0.1:8008/config with {"synchronous_node_count": 1}
|
When I issue a PATCH request to http://127.0.0.1:8008/config with {"synchronous_node_count": 1}
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
And I shut down postgres1
|
And I shut down postgres-1
|
||||||
Then "sync" key in DCS has sync_standby=postgres2 after 10 seconds
|
Then "sync" key in DCS has sync_standby=postgres-2 after 10 seconds
|
||||||
When I start postgres1
|
When I start postgres-1
|
||||||
Then "members/postgres1" key in DCS has state=running after 10 seconds
|
Then "members/postgres-1" key in DCS has state=running after 10 seconds
|
||||||
And Status code on GET http://127.0.0.1:8010/sync is 200 after 3 seconds
|
And Status code on GET http://127.0.0.1:8010/sync is 200 after 3 seconds
|
||||||
And Status code on GET http://127.0.0.1:8009/async is 200 after 3 seconds
|
And Status code on GET http://127.0.0.1:8009/async is 200 after 3 seconds
|
||||||
|
|
||||||
@@ -60,26 +60,26 @@ Feature: basic replication
|
|||||||
Given I run patronictl.py pause batman
|
Given I run patronictl.py pause batman
|
||||||
Then I receive a response returncode 0
|
Then I receive a response returncode 0
|
||||||
When I sleep for 2 seconds
|
When I sleep for 2 seconds
|
||||||
And I shut down postgres0
|
And I shut down postgres-0
|
||||||
And I run patronictl.py resume batman
|
And I run patronictl.py resume batman
|
||||||
Then I receive a response returncode 0
|
Then I receive a response returncode 0
|
||||||
And postgres2 role is the primary after 24 seconds
|
And postgres-2 role is the primary after 24 seconds
|
||||||
And Response on GET http://127.0.0.1:8010/history contains recovery after 10 seconds
|
And Response on GET http://127.0.0.1:8010/history contains recovery after 10 seconds
|
||||||
And there is a postgres2_cb.log with "on_role_change master batman" in postgres2 data directory
|
And there is a postgres-2_cb.log with "on_role_change primary batman" in postgres-2 data directory
|
||||||
When I issue a PATCH request to http://127.0.0.1:8010/config with {"synchronous_mode": null, "master_start_timeout": 0}
|
When I issue a PATCH request to http://127.0.0.1:8010/config with {"synchronous_mode": null, "master_start_timeout": 0}
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
When I add the table bar to postgres2
|
When I add the table bar to postgres-2
|
||||||
Then table bar is present on postgres1 after 20 seconds
|
Then table bar is present on postgres-1 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 postgres-0
|
||||||
And I start postgres0
|
And I start postgres-0
|
||||||
Then postgres0 role is the secondary after 20 seconds
|
Then postgres-0 role is the secondary after 20 seconds
|
||||||
When I add the table buz to postgres1
|
When I add the table buz to postgres-2
|
||||||
Then table buz is present on postgres0 after 20 seconds
|
Then table buz is present on postgres-0 after 20 seconds
|
||||||
|
|
||||||
|
@reject-duplicate-name
|
||||||
|
Scenario: check graceful rejection when two nodes have the same name
|
||||||
|
Given I start duplicate postgres-0 on port 8011
|
||||||
|
Then there is one of ["Can't start; there is already a node named 'postgres-0' running"] CRITICAL in the dup-postgres-0 patroni log after 5 seconds
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
Feature: bootstrap labels
|
||||||
|
Check that user-configurable bootstrap labels are set and removed with state change
|
||||||
|
|
||||||
|
Scenario: check label for cluster bootstrap
|
||||||
|
When I start postgres-0
|
||||||
|
Then postgres-0 is a leader after 10 seconds
|
||||||
|
When I start postgres-1 in a cluster batman1 as a long-running clone of postgres-0
|
||||||
|
Then "members/postgres-1" key in DCS has state=running custom bootstrap script after 20 seconds
|
||||||
|
And postgres-1 is labeled with "foo"
|
||||||
|
And postgres-1 is a leader of batman1 after 20 seconds
|
||||||
|
|
||||||
|
Scenario: check label for replica bootstrap
|
||||||
|
When I do a backup of postgres-1
|
||||||
|
And I start postgres-2 in cluster batman1 using long-running backup_restore
|
||||||
|
Then "members/postgres-2" key in DCS has state=creating replica after 20 seconds
|
||||||
|
And postgres-2 is labeled with "foo"
|
||||||
|
|
||||||
|
Scenario: check bootstrap label is removed
|
||||||
|
Given "members/postgres-1" key in DCS has state=running after 2 seconds
|
||||||
|
And "members/postgres-2" key in DCS has state=running after 20 seconds
|
||||||
|
Then postgres-1 is not labeled with "foo"
|
||||||
|
And postgres-2 is not labeled with "foo"
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
with open("data/{0}/{0}_cb.log".format(sys.argv[1]), "a+") as log:
|
with open("data/{0}/{0}_cb.log".format(sys.argv[1]), "a+") as log:
|
||||||
log.write(" ".join(sys.argv[-3:]) + "\n")
|
log.write(" ".join(sys.argv[-3:]) + "\n")
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ Feature: cascading replication
|
|||||||
We should check that patroni can do base backup and streaming from the replica
|
We should check that patroni can do base backup and streaming from the replica
|
||||||
|
|
||||||
Scenario: check a base backup and streaming replication from a replica
|
Scenario: check a base backup and streaming replication from a replica
|
||||||
Given I start postgres0
|
Given I start postgres-0
|
||||||
And postgres0 is a leader after 10 seconds
|
And postgres-0 is a leader after 10 seconds
|
||||||
And I configure and start postgres1 with a tag clonefrom true
|
And I configure and start postgres-1 with a tag clonefrom true
|
||||||
And replication works from postgres0 to postgres1 after 20 seconds
|
And replication works from postgres-0 to postgres-1 after 20 seconds
|
||||||
And I create label with "postgres0" in postgres0 data directory
|
And I create label with "postgres-0" in postgres-0 data directory
|
||||||
And I create label with "postgres1" in postgres1 data directory
|
And I create label with "postgres-1" in postgres-1 data directory
|
||||||
And "members/postgres1" key in DCS has state=running after 12 seconds
|
And "members/postgres-1" key in DCS has state=running after 12 seconds
|
||||||
And I configure and start postgres2 with a tag replicatefrom postgres1
|
And I configure and start postgres-2 with a tag replicatefrom postgres-1
|
||||||
Then replication works from postgres0 to postgres2 after 30 seconds
|
Then replication works from postgres-0 to postgres-2 after 30 seconds
|
||||||
And there is a label with "postgres1" in postgres2 data directory
|
And there is a label with "postgres-1" in postgres-2 data directory
|
||||||
|
|||||||
+54
-46
@@ -2,71 +2,79 @@ Feature: citus
|
|||||||
We should check that coordinator discovers and registers workers and clients don't have errors when worker cluster switches over
|
We should check that coordinator discovers and registers workers and clients don't have errors when worker cluster switches over
|
||||||
|
|
||||||
Scenario: check that worker cluster is registered in the coordinator
|
Scenario: check that worker cluster is registered in the coordinator
|
||||||
Given I start postgres0 in citus group 0
|
Given I start postgres-0 in citus group 0
|
||||||
And I start postgres2 in citus group 1
|
And I start postgres-2 in citus group 1
|
||||||
Then postgres0 is a leader in a group 0 after 10 seconds
|
Then postgres-0 is a leader in a group 0 after 10 seconds
|
||||||
And postgres2 is a leader in a group 1 after 10 seconds
|
And postgres-2 is a leader in a group 1 after 10 seconds
|
||||||
When I start postgres1 in citus group 0
|
When I start postgres-1 in citus group 0
|
||||||
And I start postgres3 in citus group 1
|
And I start postgres-3 in citus group 1
|
||||||
Then replication works from postgres0 to postgres1 after 15 seconds
|
Then replication works from postgres-0 to postgres-1 after 15 seconds
|
||||||
Then replication works from postgres2 to postgres3 after 15 seconds
|
Then replication works from postgres-2 to postgres-3 after 15 seconds
|
||||||
And postgres0 is registered in the coordinator postgres0 as the worker in group 0
|
And postgres-0 is registered in the postgres-0 as the primary in group 0 after 5 seconds
|
||||||
And postgres2 is registered in the coordinator postgres0 as the worker in group 1
|
And postgres-1 is registered in the postgres-0 as the secondary in group 0 after 5 seconds
|
||||||
|
And postgres-2 is registered in the postgres-0 as the primary in group 1 after 5 seconds
|
||||||
|
And postgres-3 is registered in the postgres-0 as the secondary in group 1 after 5 seconds
|
||||||
|
|
||||||
Scenario: coordinator failover updates pg_dist_node
|
Scenario: coordinator failover updates pg_dist_node
|
||||||
Given I run patronictl.py failover batman --group 0 --candidate postgres1 --force
|
Given I run patronictl.py failover batman --group 0 --candidate postgres-1 --force
|
||||||
Then postgres1 role is the primary after 10 seconds
|
Then postgres-1 role is the primary after 10 seconds
|
||||||
And replication works from postgres1 to postgres0 after 15 seconds
|
And "members/postgres-0" key in a group 0 in DCS has state=running after 15 seconds
|
||||||
And "sync" key in a group 0 in DCS has sync_standby=postgres0 after 15 seconds
|
And replication works from postgres-1 to postgres-0 after 15 seconds
|
||||||
And postgres1 is registered in the coordinator postgres1 as the worker in group 0
|
And postgres-1 is registered in the postgres-2 as the primary in group 0 after 5 seconds
|
||||||
When I run patronictl.py failover batman --group 0 --candidate postgres0 --force
|
And postgres-0 is registered in the postgres-2 as the secondary in group 0 after 15 seconds
|
||||||
Then postgres0 role is the primary after 10 seconds
|
And "sync" key in a group 0 in DCS has sync_standby=postgres-0 after 15 seconds
|
||||||
And replication works from postgres0 to postgres1 after 15 seconds
|
When I run patronictl.py switchover batman --group 0 --candidate postgres-0 --force
|
||||||
And "sync" key in a group 0 in DCS has sync_standby=postgres1 after 15 seconds
|
Then postgres-0 role is the primary after 10 seconds
|
||||||
And postgres0 is registered in the coordinator postgres0 as the worker in group 0
|
And replication works from postgres-0 to postgres-1 after 15 seconds
|
||||||
|
And postgres-0 is registered in the postgres-2 as the primary in group 0 after 5 seconds
|
||||||
|
And postgres-1 is registered in the postgres-2 as the secondary in group 0 after 15 seconds
|
||||||
|
And "sync" key in a group 0 in DCS has sync_standby=postgres-1 after 15 seconds
|
||||||
|
|
||||||
Scenario: worker switchover doesn't break client queries on the coordinator
|
Scenario: worker switchover doesn't break client queries on the coordinator
|
||||||
Given I create a distributed table on postgres0
|
Given I create a distributed table on postgres-0
|
||||||
And I start a thread inserting data on postgres0
|
And I start a thread inserting data on postgres-0
|
||||||
When I run patronictl.py switchover batman --group 1 --force
|
When I run patronictl.py switchover batman --group 1 --force
|
||||||
Then I receive a response returncode 0
|
Then I receive a response returncode 0
|
||||||
And postgres3 role is the primary after 10 seconds
|
And postgres-3 role is the primary after 10 seconds
|
||||||
And replication works from postgres3 to postgres2 after 15 seconds
|
And "members/postgres-2" key in a group 1 in DCS has state=running after 15 seconds
|
||||||
And "sync" key in a group 1 in DCS has sync_standby=postgres2 after 15 seconds
|
And replication works from postgres-3 to postgres-2 after 15 seconds
|
||||||
And postgres3 is registered in the coordinator postgres0 as the worker in group 1
|
And postgres-3 is registered in the postgres-0 as the primary in group 1 after 5 seconds
|
||||||
|
And postgres-2 is registered in the postgres-0 as the secondary in group 1 after 15 seconds
|
||||||
|
And "sync" key in a group 1 in DCS has sync_standby=postgres-2 after 15 seconds
|
||||||
And a thread is still alive
|
And a thread is still alive
|
||||||
When I run patronictl.py switchover batman --group 1 --force
|
When I run patronictl.py switchover batman --group 1 --force
|
||||||
Then I receive a response returncode 0
|
Then I receive a response returncode 0
|
||||||
And postgres2 role is the primary after 10 seconds
|
And postgres-2 role is the primary after 10 seconds
|
||||||
And replication works from postgres2 to postgres3 after 15 seconds
|
And replication works from postgres-2 to postgres-3 after 15 seconds
|
||||||
And "sync" key in a group 1 in DCS has sync_standby=postgres3 after 15 seconds
|
And postgres-2 is registered in the postgres-0 as the primary in group 1 after 5 seconds
|
||||||
And postgres2 is registered in the coordinator postgres0 as the worker in group 1
|
And postgres-3 is registered in the postgres-0 as the secondary in group 1 after 15 seconds
|
||||||
|
And "sync" key in a group 1 in DCS has sync_standby=postgres-3 after 15 seconds
|
||||||
And a thread is still alive
|
And a thread is still alive
|
||||||
When I stop a thread
|
When I stop a thread
|
||||||
Then a distributed table on postgres0 has expected rows
|
Then a distributed table on postgres-0 has expected rows
|
||||||
|
|
||||||
Scenario: worker primary restart doesn't break client queries on the coordinator
|
Scenario: worker primary restart doesn't break client queries on the coordinator
|
||||||
Given I cleanup a distributed table on postgres0
|
Given I cleanup a distributed table on postgres-0
|
||||||
And I start a thread inserting data on postgres0
|
And I start a thread inserting data on postgres-0
|
||||||
When I run patronictl.py restart batman postgres2 --group 1 --force
|
When I run patronictl.py restart batman postgres-2 --group 1 --force
|
||||||
Then I receive a response returncode 0
|
Then I receive a response returncode 0
|
||||||
And postgres2 role is the primary after 10 seconds
|
And postgres-2 role is the primary after 10 seconds
|
||||||
And replication works from postgres2 to postgres3 after 15 seconds
|
And replication works from postgres-2 to postgres-3 after 15 seconds
|
||||||
And postgres2 is registered in the coordinator postgres0 as the worker in group 1
|
And postgres-2 is registered in the postgres-0 as the primary in group 1 after 5 seconds
|
||||||
|
And postgres-3 is registered in the postgres-0 as the secondary in group 1 after 15 seconds
|
||||||
And a thread is still alive
|
And a thread is still alive
|
||||||
When I stop a thread
|
When I stop a thread
|
||||||
Then a distributed table on postgres0 has expected rows
|
Then a distributed table on postgres-0 has expected rows
|
||||||
|
|
||||||
Scenario: check that in-flight transaction is rolled back after timeout when other workers need to change pg_dist_node
|
Scenario: check that in-flight transaction is rolled back after timeout when other workers need to change pg_dist_node
|
||||||
Given I start postgres4 in citus group 2
|
Given I start postgres-4 in citus group 2
|
||||||
Then postgres4 is a leader in a group 2 after 10 seconds
|
Then postgres-4 is a leader in a group 2 after 10 seconds
|
||||||
And "members/postgres4" key in a group 2 in DCS has role=master after 3 seconds
|
And "members/postgres-4" key in a group 2 in DCS has role=primary after 3 seconds
|
||||||
When I run patronictl.py edit-config batman --group 2 -s ttl=20 --force
|
When I run patronictl.py edit-config batman --group 2 -s ttl=20 --force
|
||||||
Then I receive a response returncode 0
|
Then I receive a response returncode 0
|
||||||
And I receive a response output "+ttl: 20"
|
And I receive a response output "+ttl: 20"
|
||||||
When I sleep for 2 seconds
|
Then postgres-4 is registered in the postgres-2 as the primary in group 2 after 5 seconds
|
||||||
Then postgres4 is registered in the coordinator postgres0 as the worker in group 2
|
When I shut down postgres-4
|
||||||
When I shut down postgres4
|
Then there is a transaction in progress on postgres-0 changing pg_dist_node after 5 seconds
|
||||||
Then There is a transaction in progress on postgres0 changing pg_dist_node
|
When I run patronictl.py restart batman postgres-2 --group 1 --force
|
||||||
When I run patronictl.py restart batman postgres2 --group 1 --force
|
|
||||||
Then a transaction finishes in 20 seconds
|
Then a transaction finishes in 20 seconds
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ Feature: custom bootstrap
|
|||||||
We should check that patroni can bootstrap a new cluster from a backup
|
We should check that patroni can bootstrap a new cluster from a backup
|
||||||
|
|
||||||
Scenario: clone existing cluster using pg_basebackup
|
Scenario: clone existing cluster using pg_basebackup
|
||||||
Given I start postgres0
|
Given I start postgres-0
|
||||||
Then postgres0 is a leader after 10 seconds
|
Then postgres-0 is a leader after 10 seconds
|
||||||
When I add the table foo to postgres0
|
When I add the table foo to postgres-0
|
||||||
And I start postgres1 in a cluster batman1 as a clone of postgres0
|
And I start postgres-1 in a cluster batman1 as a clone of postgres-0
|
||||||
Then postgres1 is a leader of batman1 after 10 seconds
|
Then postgres-1 is a leader of batman1 after 10 seconds
|
||||||
Then table foo is present on postgres1 after 10 seconds
|
Then table foo is present on postgres-1 after 10 seconds
|
||||||
|
|
||||||
Scenario: make a backup and do a restore into a new cluster
|
Scenario: make a backup and do a restore into a new cluster
|
||||||
Given I add the table bar to postgres1
|
Given I add the table bar to postgres-1
|
||||||
And I do a backup of postgres1
|
And I do a backup of postgres-1
|
||||||
When I start postgres2 in a cluster batman2 from backup
|
When I start postgres-2 in a cluster batman2 from backup
|
||||||
Then postgres2 is a leader of batman2 after 30 seconds
|
Then postgres-2 is a leader of batman2 after 30 seconds
|
||||||
And table bar is present on postgres2 after 10 seconds
|
And table bar is present on postgres-2 after 10 seconds
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ Feature: dcs failsafe mode
|
|||||||
We should check the basic dcs failsafe mode functioning
|
We should check the basic dcs failsafe mode functioning
|
||||||
|
|
||||||
Scenario: check failsafe mode can be successfully enabled
|
Scenario: check failsafe mode can be successfully enabled
|
||||||
Given I start postgres0
|
Given I start postgres-0
|
||||||
And postgres0 is a leader after 10 seconds
|
And postgres-0 is a leader after 10 seconds
|
||||||
And I sleep for 3 seconds
|
Then "config" key in DCS has ttl=30 after 10 seconds
|
||||||
When I issue a PATCH request to http://127.0.0.1:8008/config with {"loop_wait": 2, "ttl": 20, "retry_timeout": 5, "failsafe_mode": true}
|
When I issue a PATCH request to http://127.0.0.1:8008/config with {"loop_wait": 2, "ttl": 20, "retry_timeout": 3, "failsafe_mode": true}
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
And Response on GET http://127.0.0.1:8008/failsafe contains postgres0 after 10 seconds
|
And Response on GET http://127.0.0.1:8008/failsafe contains postgres-0 after 10 seconds
|
||||||
When I issue a GET request to http://127.0.0.1:8008/failsafe
|
When I issue a GET request to http://127.0.0.1:8008/failsafe
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
And I receive a response postgres0 http://127.0.0.1:8008/patroni
|
And I receive a response postgres-0 http://127.0.0.1:8008/patroni
|
||||||
When I issue a PATCH request to http://127.0.0.1:8008/config with {"postgresql": {"parameters": {"wal_level": "logical"}}}
|
When I issue a PATCH request to http://127.0.0.1:8008/config with {"postgresql": {"parameters": {"wal_level": "logical"}},"slots":{"dcs_slot_1": null,"postgres_0":null}}
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
When I issue a PATCH request to http://127.0.0.1:8008/config with {"slots": {"dcs_slot_0": {"type": "logical", "database": "postgres", "plugin": "test_decoding"}}}
|
When I issue a PATCH request to http://127.0.0.1:8008/config with {"slots": {"dcs_slot_0": {"type": "logical", "database": "postgres", "plugin": "test_decoding"}}}
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
@@ -20,66 +20,99 @@ Feature: dcs failsafe mode
|
|||||||
Scenario: check one-node cluster is functioning while DCS is down
|
Scenario: check one-node cluster is functioning while DCS is down
|
||||||
Given DCS is down
|
Given DCS is down
|
||||||
Then Response on GET http://127.0.0.1:8008/primary contains failsafe_mode_is_active after 12 seconds
|
Then Response on GET http://127.0.0.1:8008/primary contains failsafe_mode_is_active after 12 seconds
|
||||||
And postgres0 role is the primary after 10 seconds
|
And postgres-0 role is the primary after 10 seconds
|
||||||
|
|
||||||
@dcs-failsafe
|
@dcs-failsafe
|
||||||
Scenario: check new replica isn't promoted when leader is down and DCS is up
|
Scenario: check new replica isn't promoted when leader is down and DCS is up
|
||||||
Given DCS is up
|
Given DCS is up
|
||||||
When I do a backup of postgres0
|
When I do a backup of postgres-0
|
||||||
And I shut down postgres0
|
And I shut down postgres-0
|
||||||
When I start postgres1 in a cluster batman from backup with no_leader
|
When I start postgres-1 in a cluster batman from backup with no_leader
|
||||||
And I sleep for 2 seconds
|
Then postgres-1 role is the replica after 12 seconds
|
||||||
Then postgres1 role is the replica after 12 seconds
|
|
||||||
|
|
||||||
Scenario: check leader and replica are both in /failsafe key after leader is back
|
Scenario: check leader and replica are both in /failsafe key after leader is back
|
||||||
Given I start postgres0
|
Given I start postgres-0
|
||||||
And I start postgres1
|
And I start postgres-1
|
||||||
Then "members/postgres0" key in DCS has state=running after 10 seconds
|
Then "members/postgres-0" key in DCS has state=running after 10 seconds
|
||||||
And "members/postgres1" key in DCS has state=running after 2 seconds
|
And "members/postgres-1" key in DCS has state=running after 2 seconds
|
||||||
And Response on GET http://127.0.0.1:8009/failsafe contains postgres1 after 10 seconds
|
And Response on GET http://127.0.0.1:8009/failsafe contains postgres-1 after 10 seconds
|
||||||
When I issue a GET request to http://127.0.0.1:8009/failsafe
|
When I issue a GET request to http://127.0.0.1:8009/failsafe
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
And I receive a response postgres0 http://127.0.0.1:8008/patroni
|
And I receive a response postgres-0 http://127.0.0.1:8008/patroni
|
||||||
And I receive a response postgres1 http://127.0.0.1:8009/patroni
|
And I receive a response postgres-1 http://127.0.0.1:8009/patroni
|
||||||
|
|
||||||
@dcs-failsafe
|
@dcs-failsafe
|
||||||
@slot-advance
|
@pg110000
|
||||||
Scenario: check leader and replica are functioning while DCS is down
|
Scenario: check leader and replica are functioning while DCS is down
|
||||||
Given logical slot dcs_slot_0 is in sync between postgres0 and postgres1 after 10 seconds
|
Given I get all changes from physical slot dcs_slot_1 on postgres-0
|
||||||
|
Then physical slot dcs_slot_1 is in sync between postgres-0 and postgres-1 after 10 seconds
|
||||||
|
And logical slot dcs_slot_0 is in sync between postgres-0 and postgres-1 after 10 seconds
|
||||||
And DCS is down
|
And DCS is down
|
||||||
Then Response on GET http://127.0.0.1:8008/primary contains failsafe_mode_is_active after 12 seconds
|
Then Response on GET http://127.0.0.1:8008/primary contains failsafe_mode_is_active after 12 seconds
|
||||||
Then postgres0 role is the primary after 10 seconds
|
Then postgres-0 role is the primary after 10 seconds
|
||||||
And postgres1 role is the replica after 2 seconds
|
And postgres-1 role is the replica after 2 seconds
|
||||||
And replication works from postgres0 to postgres1 after 10 seconds
|
And replication works from postgres-0 to postgres-1 after 10 seconds
|
||||||
And I get all changes from logical slot dcs_slot_0 on postgres0
|
When I get all changes from logical slot dcs_slot_0 on postgres-0
|
||||||
And logical slot dcs_slot_0 is in sync between postgres0 and postgres1 after 20 seconds
|
And I get all changes from physical slot dcs_slot_1 on postgres-0
|
||||||
|
Then logical slot dcs_slot_0 is in sync between postgres-0 and postgres-1 after 20 seconds
|
||||||
|
And physical slot dcs_slot_1 is in sync between postgres-0 and postgres-1 after 10 seconds
|
||||||
|
|
||||||
@dcs-failsafe
|
@dcs-failsafe
|
||||||
Scenario: check primary is demoted when one replica is shut down and DCS is down
|
Scenario: check primary is demoted when one replica is shut down and DCS is down
|
||||||
Given DCS is down
|
Given DCS is down
|
||||||
And I kill postgres1
|
And I kill postgres-1
|
||||||
And I kill postmaster on postgres1
|
And I kill postmaster on postgres-1
|
||||||
And I sleep for 2 seconds
|
Then postgres-0 role is the replica after 12 seconds
|
||||||
Then postgres0 role is the replica after 12 seconds
|
|
||||||
|
|
||||||
@dcs-failsafe
|
@dcs-failsafe
|
||||||
Scenario: check known replica is promoted when leader is down and DCS is up
|
Scenario: check known replica is promoted when leader is down and DCS is up
|
||||||
Given I shut down postgres0
|
Given I kill postgres-0
|
||||||
|
And I shut down postmaster on postgres-0
|
||||||
And DCS is up
|
And DCS is up
|
||||||
When I start postgres1
|
When I start postgres-1
|
||||||
Then "members/postgres1" key in DCS has state=running after 10 seconds
|
Then "members/postgres-1" key in DCS has state=running after 10 seconds
|
||||||
And postgres1 role is the primary after 25 seconds
|
And postgres-1 role is the primary after 25 seconds
|
||||||
|
|
||||||
|
@dcs-failsafe
|
||||||
|
Scenario: scale to three-node cluster
|
||||||
|
Given I start postgres-0
|
||||||
|
And I configure and start postgres-2 with a tag replicatefrom postgres-0
|
||||||
|
Then "members/postgres-2" key in DCS has state=running after 10 seconds
|
||||||
|
And "members/postgres-0" key in DCS has state=running after 20 seconds
|
||||||
|
And Response on GET http://127.0.0.1:8008/failsafe contains postgres-2 after 10 seconds
|
||||||
|
And replication works from postgres-1 to postgres-0 after 10 seconds
|
||||||
|
And replication works from postgres-1 to postgres-2 after 10 seconds
|
||||||
|
|
||||||
|
@dcs-failsafe
|
||||||
|
@pg110000
|
||||||
|
Scenario: make sure permanent slots exist on replicas
|
||||||
|
Given I issue a PATCH request to http://127.0.0.1:8009/config with {"slots":{"dcs_slot_0":null,"dcs_slot_2":{"type":"logical","database":"postgres","plugin":"test_decoding"}}}
|
||||||
|
Then logical slot dcs_slot_2 is in sync between postgres-1 and postgres-0 after 20 seconds
|
||||||
|
And logical slot dcs_slot_2 is in sync between postgres-1 and postgres-2 after 20 seconds
|
||||||
|
When I get all changes from physical slot dcs_slot_1 on postgres-1
|
||||||
|
Then physical slot dcs_slot_1 is in sync between postgres-1 and postgres-0 after 10 seconds
|
||||||
|
And physical slot dcs_slot_1 is in sync between postgres-1 and postgres-2 after 10 seconds
|
||||||
|
And physical slot postgres_0 is in sync between postgres-1 and postgres-2 after 10 seconds
|
||||||
|
And physical slot postgres_2 is in sync between postgres-0 and postgres-1 after 10 seconds
|
||||||
|
|
||||||
@dcs-failsafe
|
@dcs-failsafe
|
||||||
Scenario: check three-node cluster is functioning while DCS is down
|
Scenario: check three-node cluster is functioning while DCS is down
|
||||||
Given I start postgres0
|
|
||||||
And I start postgres2
|
|
||||||
Then "members/postgres2" key in DCS has state=running after 10 seconds
|
|
||||||
And "members/postgres0" key in DCS has state=running after 20 seconds
|
|
||||||
And Response on GET http://127.0.0.1:8008/failsafe contains postgres2 after 10 seconds
|
|
||||||
And replication works from postgres1 to postgres0 after 10 seconds
|
|
||||||
Given DCS is down
|
Given DCS is down
|
||||||
Then Response on GET http://127.0.0.1:8008/primary contains failsafe_mode_is_active after 12 seconds
|
Then Response on GET http://127.0.0.1:8009/primary contains failsafe_mode_is_active after 12 seconds
|
||||||
Then postgres1 role is the primary after 10 seconds
|
Then postgres-1 role is the primary after 10 seconds
|
||||||
And postgres0 role is the replica after 2 seconds
|
And postgres-0 role is the replica after 2 seconds
|
||||||
And postgres2 role is the replica after 2 seconds
|
And postgres-2 role is the replica after 2 seconds
|
||||||
|
|
||||||
|
@dcs-failsafe
|
||||||
|
@pg110000
|
||||||
|
Scenario: check that permanent slots are in sync between nodes while DCS is down
|
||||||
|
Given replication works from postgres-1 to postgres-0 after 10 seconds
|
||||||
|
And replication works from postgres-1 to postgres-2 after 10 seconds
|
||||||
|
When I get all changes from logical slot dcs_slot_2 on postgres-1
|
||||||
|
And I get all changes from physical slot dcs_slot_1 on postgres-1
|
||||||
|
Then logical slot dcs_slot_2 is in sync between postgres-1 and postgres-0 after 20 seconds
|
||||||
|
And logical slot dcs_slot_2 is in sync between postgres-1 and postgres-2 after 20 seconds
|
||||||
|
And physical slot dcs_slot_1 is in sync between postgres-1 and postgres-0 after 10 seconds
|
||||||
|
And physical slot dcs_slot_1 is in sync between postgres-1 and postgres-2 after 10 seconds
|
||||||
|
And physical slot postgres_0 is in sync between postgres-1 and postgres-2 after 10 seconds
|
||||||
|
And physical slot postgres_2 is in sync between postgres-0 and postgres-1 after 10 seconds
|
||||||
|
|||||||
+104
-57
@@ -1,28 +1,29 @@
|
|||||||
import abc
|
import abc
|
||||||
import datetime
|
import datetime
|
||||||
import glob
|
import glob
|
||||||
import os
|
|
||||||
import json
|
import json
|
||||||
import psutil
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import signal
|
import signal
|
||||||
import six
|
import stat
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
|
||||||
|
import psutil
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
import patroni.psycopg as psycopg
|
import patroni.psycopg as psycopg
|
||||||
|
|
||||||
from patroni.request import PatroniRequest
|
from patroni.request import PatroniRequest
|
||||||
from six.moves.BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
class AbstractController(abc.ABC):
|
||||||
class AbstractController(object):
|
|
||||||
|
|
||||||
def __init__(self, context, name, work_directory, output_dir):
|
def __init__(self, context, name, work_directory, output_dir):
|
||||||
self._context = context
|
self._context = context
|
||||||
@@ -53,15 +54,16 @@ class AbstractController(object):
|
|||||||
self._log = open(os.path.join(self._output_dir, self._name + '.log'), 'a')
|
self._log = open(os.path.join(self._output_dir, self._name + '.log'), 'a')
|
||||||
self._handle = self._start()
|
self._handle = self._start()
|
||||||
|
|
||||||
assert self._has_started(), "Process {0} is not running after being started".format(self._name)
|
if max_wait_limit < 0:
|
||||||
|
return
|
||||||
max_wait_limit *= self._context.timeout_multiplier
|
max_wait_limit *= self._context.timeout_multiplier
|
||||||
for _ in range(max_wait_limit):
|
for _ in range(max_wait_limit):
|
||||||
|
assert self._has_started(), "Process {0} is not running after being started".format(self._name)
|
||||||
if self._is_accessible():
|
if self._is_accessible():
|
||||||
break
|
break
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
else:
|
else:
|
||||||
assert False,\
|
assert False, \
|
||||||
"{0} instance is not available for queries after {1} seconds".format(self._name, max_wait_limit)
|
"{0} instance is not available for queries after {1} seconds".format(self._name, max_wait_limit)
|
||||||
|
|
||||||
def stop(self, kill=False, timeout=15, _=False):
|
def stop(self, kill=False, timeout=15, _=False):
|
||||||
@@ -164,9 +166,10 @@ class PatroniController(AbstractController):
|
|||||||
|
|
||||||
def stop(self, kill=False, timeout=15, postgres=False):
|
def stop(self, kill=False, timeout=15, postgres=False):
|
||||||
if postgres:
|
if postgres:
|
||||||
return subprocess.call(['pg_ctl', '-D', self._data_dir, 'stop', '-mi', '-w'])
|
mode = 'i' if kill else 'f'
|
||||||
|
return subprocess.call(['pg_ctl', '-D', self._data_dir, 'stop', '-m' + mode, '-w'])
|
||||||
super(PatroniController, self).stop(kill, timeout)
|
super(PatroniController, self).stop(kill, timeout)
|
||||||
if isinstance(self._context.dcs_ctl, KubernetesController):
|
if isinstance(self._context.dcs_ctl, KubernetesController) and not kill:
|
||||||
self._context.dcs_ctl.delete_pod(self._name[8:])
|
self._context.dcs_ctl.delete_pod(self._name[8:])
|
||||||
if self.watchdog:
|
if self.watchdog:
|
||||||
self.watchdog.stop()
|
self.watchdog.stop()
|
||||||
@@ -193,7 +196,7 @@ class PatroniController(AbstractController):
|
|||||||
config['raft'] = {'data_dir': self._output_dir, 'self_addr': 'localhost:' + os.environ['RAFT_PORT']}
|
config['raft'] = {'data_dir': self._output_dir, 'self_addr': 'localhost:' + os.environ['RAFT_PORT']}
|
||||||
|
|
||||||
host = config['restapi']['listen'].rsplit(':', 1)[0]
|
host = config['restapi']['listen'].rsplit(':', 1)[0]
|
||||||
config['restapi']['listen'] = config['restapi']['connect_address'] = '{0}:{1}'.format(host, 8008+int(name[-1]))
|
config['restapi']['listen'] = config['restapi']['connect_address'] = '{}:{}'.format(host, 8008 + int(name[-1]))
|
||||||
|
|
||||||
host = config['postgresql']['listen'].rsplit(':', 1)[0]
|
host = config['postgresql']['listen'].rsplit(':', 1)[0]
|
||||||
config['postgresql']['listen'] = config['postgresql']['connect_address'] = '{0}:{1}'.format(host, self.__PORT)
|
config['postgresql']['listen'] = config['postgresql']['connect_address'] = '{0}:{1}'.format(host, self.__PORT)
|
||||||
@@ -217,6 +220,8 @@ class PatroniController(AbstractController):
|
|||||||
'host replication replicator all md5',
|
'host replication replicator all md5',
|
||||||
'host all all all md5'
|
'host all all all md5'
|
||||||
]
|
]
|
||||||
|
if isinstance(self._context.dcs_ctl, KubernetesController):
|
||||||
|
config['kubernetes'] = {'bootstrap_labels': {'foo': 'bar'}}
|
||||||
|
|
||||||
if self._context.postgres_supports_ssl and self._context.certfile:
|
if self._context.postgres_supports_ssl and self._context.certfile:
|
||||||
config['postgresql']['parameters'].update({
|
config['postgresql']['parameters'].update({
|
||||||
@@ -246,6 +251,10 @@ class PatroniController(AbstractController):
|
|||||||
self.recursive_update(config, custom_config)
|
self.recursive_update(config, custom_config)
|
||||||
|
|
||||||
self.recursive_update(config, {
|
self.recursive_update(config, {
|
||||||
|
'log': {
|
||||||
|
'format': '%(asctime)s %(levelname)s [%(pathname)s:%(lineno)d - %(funcName)s]: %(message)s',
|
||||||
|
'loggers': {'patroni.postgresql.callback_executor': 'DEBUG'}
|
||||||
|
},
|
||||||
'bootstrap': {
|
'bootstrap': {
|
||||||
'dcs': {
|
'dcs': {
|
||||||
'loop_wait': 2,
|
'loop_wait': 2,
|
||||||
@@ -253,10 +262,18 @@ class PatroniController(AbstractController):
|
|||||||
'parameters': {
|
'parameters': {
|
||||||
'wal_keep_segments': 100,
|
'wal_keep_segments': 100,
|
||||||
'archive_mode': 'on',
|
'archive_mode': 'on',
|
||||||
'archive_command': (PatroniPoolController.ARCHIVE_RESTORE_SCRIPT +
|
'archive_command':
|
||||||
' --mode archive ' +
|
(PatroniPoolController.ARCHIVE_RESTORE_SCRIPT
|
||||||
'--dirname {} --filename %f --pathname %p').format(
|
+ ' --mode archive '
|
||||||
os.path.join(self._work_directory, 'data', 'wal_archive'))
|
+ '--dirname {} --filename %f --pathname %p').format(
|
||||||
|
os.path.join(self._work_directory, 'data',
|
||||||
|
f'wal_archive{str(self._citus_group or "")}')).replace('\\', '/'),
|
||||||
|
'restore_command':
|
||||||
|
(PatroniPoolController.ARCHIVE_RESTORE_SCRIPT
|
||||||
|
+ ' --mode restore '
|
||||||
|
+ '--dirname {} --filename %f --pathname %p').format(
|
||||||
|
os.path.join(self._work_directory, 'data',
|
||||||
|
f'wal_archive{str(self._citus_group or "")}')).replace('\\', '/')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,6 +362,13 @@ class PatroniController(AbstractController):
|
|||||||
'--datadir=' + os.path.join(self._work_directory, dest),
|
'--datadir=' + os.path.join(self._work_directory, dest),
|
||||||
'--dbname=' + self.backup_source])
|
'--dbname=' + self.backup_source])
|
||||||
|
|
||||||
|
def read_patroni_log(self, level):
|
||||||
|
try:
|
||||||
|
with open(str(os.path.join(self._output_dir or '', self._name + ".log"))) as f:
|
||||||
|
return [line for line in f.readlines() if line[24:24 + len(level)] == level]
|
||||||
|
except IOError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
class ProcessHang(object):
|
class ProcessHang(object):
|
||||||
|
|
||||||
@@ -480,6 +504,7 @@ class AbstractEtcdController(AbstractDcsController):
|
|||||||
|
|
||||||
def _is_running(self):
|
def _is_running(self):
|
||||||
from patroni.dcs.etcd import DnsCachingResolver
|
from patroni.dcs.etcd import DnsCachingResolver
|
||||||
|
|
||||||
# if etcd is running, but we didn't start it
|
# if etcd is running, but we didn't start it
|
||||||
try:
|
try:
|
||||||
self._client = self._client_cls({'host': 'localhost', 'port': 2379, 'retry_timeout': 30,
|
self._client = self._client_cls({'host': 'localhost', 'port': 2379, 'retry_timeout': 30,
|
||||||
@@ -596,7 +621,7 @@ class KubernetesController(AbstractExternalDcsController):
|
|||||||
api_process = 'kube-apiserver'
|
api_process = 'kube-apiserver'
|
||||||
elif context.startswith('k3d-'):
|
elif context.startswith('k3d-'):
|
||||||
container = '{0}-server-0'.format(context)
|
container = '{0}-server-0'.format(context)
|
||||||
api_process = 'k3s'
|
api_process = 'k3s server'
|
||||||
else:
|
else:
|
||||||
return super(KubernetesController, self)._is_running()
|
return super(KubernetesController, self)._is_running()
|
||||||
try:
|
try:
|
||||||
@@ -636,6 +661,10 @@ class KubernetesController(AbstractExternalDcsController):
|
|||||||
except Exception:
|
except Exception:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
def pod_labels(self, name):
|
||||||
|
pod = self._api.read_namespaced_pod(name, self._namespace)
|
||||||
|
return pod.metadata.labels or {}
|
||||||
|
|
||||||
def query(self, key, scope='batman', group=None):
|
def query(self, key, scope='batman', group=None):
|
||||||
if key.startswith('members/'):
|
if key.startswith('members/'):
|
||||||
pod = self._api.read_namespaced_pod(key[8:], self._namespace)
|
pod = self._api.read_namespaced_pod(key[8:], self._namespace)
|
||||||
@@ -644,9 +673,10 @@ class KubernetesController(AbstractExternalDcsController):
|
|||||||
try:
|
try:
|
||||||
if group is not None:
|
if group is not None:
|
||||||
scope = '{0}-{1}'.format(scope, group)
|
scope = '{0}-{1}'.format(scope, group)
|
||||||
ep = scope + {'leader': '', 'history': '-config', 'initialize': '-config'}.get(key, '-' + key)
|
rkey = 'leader' if key in ('status', 'failsafe') else key
|
||||||
|
ep = scope + {'leader': '', 'history': '-config', 'initialize': '-config'}.get(rkey, '-' + rkey)
|
||||||
e = self._api.read_namespaced_endpoints(ep, self._namespace)
|
e = self._api.read_namespaced_endpoints(ep, self._namespace)
|
||||||
if key != 'sync':
|
if key not in ('sync', 'status', 'failsafe'):
|
||||||
return e.metadata.annotations[key]
|
return e.metadata.annotations[key]
|
||||||
else:
|
else:
|
||||||
return json.dumps(e.metadata.annotations)
|
return json.dumps(e.metadata.annotations)
|
||||||
@@ -682,7 +712,7 @@ class ZooKeeperController(AbstractExternalDcsController):
|
|||||||
self._client = kazoo.client.KazooClient()
|
self._client = kazoo.client.KazooClient()
|
||||||
|
|
||||||
def process_name(self):
|
def process_name(self):
|
||||||
return "zookeeper"
|
return "java .*zookeeper"
|
||||||
|
|
||||||
def query(self, key, scope='batman', group=None):
|
def query(self, key, scope='batman', group=None):
|
||||||
import kazoo.exceptions
|
import kazoo.exceptions
|
||||||
@@ -803,7 +833,7 @@ class PatroniPoolController(object):
|
|||||||
raise Exception # this one should never happen because the previous line will always raise and exception
|
raise Exception # this one should never happen because the previous line will always raise and exception
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._context.postgres_supports_ssl = isinstance(e, subprocess.CalledProcessError)\
|
self._context.postgres_supports_ssl = isinstance(e, subprocess.CalledProcessError)\
|
||||||
and 'SSL is not supported by this build' not in e.output.decode()
|
and 'SSL is not supported by this build' not in e.output.decode()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def patroni_path(self):
|
def patroni_path(self):
|
||||||
@@ -828,7 +858,7 @@ class PatroniPoolController(object):
|
|||||||
|
|
||||||
def __getattr__(self, func):
|
def __getattr__(self, func):
|
||||||
if func not in ['stop', 'query', 'write_label', 'read_label', 'check_role_has_changed_to',
|
if func not in ['stop', 'query', 'write_label', 'read_label', 'check_role_has_changed_to',
|
||||||
'add_tag_to_config', 'get_watchdog', 'patroni_hang', 'backup']:
|
'add_tag_to_config', 'get_watchdog', 'patroni_hang', 'backup', 'read_patroni_log']:
|
||||||
raise AttributeError("PatroniPoolController instance has no attribute '{0}'".format(func))
|
raise AttributeError("PatroniPoolController instance has no attribute '{0}'".format(func))
|
||||||
|
|
||||||
def wrapper(name, *args, **kwargs):
|
def wrapper(name, *args, **kwargs):
|
||||||
@@ -848,15 +878,16 @@ class PatroniPoolController(object):
|
|||||||
os.makedirs(feature_dir)
|
os.makedirs(feature_dir)
|
||||||
self._output_dir = feature_dir
|
self._output_dir = feature_dir
|
||||||
|
|
||||||
def clone(self, from_name, cluster_name, to_name):
|
def clone(self, from_name, cluster_name, to_name, long_running=False):
|
||||||
f = self._processes[from_name]
|
f = self._processes[from_name]
|
||||||
custom_config = {
|
custom_config = {
|
||||||
'scope': cluster_name,
|
'scope': cluster_name,
|
||||||
'bootstrap': {
|
'bootstrap': {
|
||||||
'method': 'pg_basebackup',
|
'method': 'pg_basebackup',
|
||||||
'pg_basebackup': {
|
'pg_basebackup': {
|
||||||
'command': " ".join(self.BACKUP_SCRIPT +
|
'command': " ".join(self.BACKUP_SCRIPT
|
||||||
['--walmethod=stream', '--dbname="{0}"'.format(f.backup_source)])
|
+ ['--walmethod=stream', f'--dbname="{f.backup_source}"',
|
||||||
|
f'--sleep {5 if long_running else 0}'])
|
||||||
},
|
},
|
||||||
'dcs': {
|
'dcs': {
|
||||||
'postgresql': {
|
'postgresql': {
|
||||||
@@ -869,38 +900,48 @@ class PatroniPoolController(object):
|
|||||||
'postgresql': {
|
'postgresql': {
|
||||||
'parameters': {
|
'parameters': {
|
||||||
'archive_mode': 'on',
|
'archive_mode': 'on',
|
||||||
'archive_command': (self.ARCHIVE_RESTORE_SCRIPT + ' --mode archive ' +
|
'archive_command': (self.ARCHIVE_RESTORE_SCRIPT + ' --mode archive '
|
||||||
'--dirname {} --filename %f --pathname %p').format(
|
+ '--dirname {} --filename %f --pathname %p')
|
||||||
os.path.join(self.patroni_path, 'data', 'wal_archive_clone').replace('\\', '/'))
|
.format(os.path.join(self.patroni_path, 'data', 'wal_archive_clone').replace('\\', '/'))
|
||||||
},
|
},
|
||||||
'authentication': {
|
'authentication': {
|
||||||
'superuser': {'password': 'zalando1'},
|
'superuser': {'password': 'patroni1'},
|
||||||
'replication': {'password': 'rep-pass1'}
|
'replication': {'password': 'rep-pass1'}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.start(to_name, custom_config=custom_config)
|
kwargs = {'custom_config': custom_config}
|
||||||
|
if long_running:
|
||||||
|
kwargs['max_wait_limit'] = -1
|
||||||
|
self.start(to_name, **kwargs)
|
||||||
|
|
||||||
|
def backup_restore_config(self, params=None, long_running=False):
|
||||||
|
return {
|
||||||
|
'command': (self.BACKUP_RESTORE_SCRIPT
|
||||||
|
+ ' --sourcedir=' + os.path.join(self.patroni_path, 'data', 'basebackup')
|
||||||
|
+ f' --sleep {5 if long_running else 0}').replace('\\', '/'),
|
||||||
|
'test-argument': 'test-value', # test config mapping approach on custom bootstrap/replica creation
|
||||||
|
**(params or {}),
|
||||||
|
}
|
||||||
|
|
||||||
def bootstrap_from_backup(self, name, cluster_name):
|
def bootstrap_from_backup(self, name, cluster_name):
|
||||||
custom_config = {
|
custom_config = {
|
||||||
'scope': cluster_name,
|
'scope': cluster_name,
|
||||||
'bootstrap': {
|
'bootstrap': {
|
||||||
'method': 'backup_restore',
|
'method': 'backup_restore',
|
||||||
'backup_restore': {
|
'backup_restore': self.backup_restore_config({
|
||||||
'command': (self.BACKUP_RESTORE_SCRIPT + ' --sourcedir=' +
|
|
||||||
os.path.join(self.patroni_path, 'data', 'basebackup').replace('\\', '/')),
|
|
||||||
'recovery_conf': {
|
'recovery_conf': {
|
||||||
'recovery_target_action': 'promote',
|
'recovery_target_action': 'promote',
|
||||||
'recovery_target_timeline': 'latest',
|
'recovery_target_timeline': 'latest',
|
||||||
'restore_command': (self.ARCHIVE_RESTORE_SCRIPT + ' --mode restore ' +
|
'restore_command': (self.ARCHIVE_RESTORE_SCRIPT + ' --mode restore '
|
||||||
'--dirname {} --filename %f --pathname %p').format(
|
+ '--dirname {} --filename %f --pathname %p').format(
|
||||||
os.path.join(self.patroni_path, 'data', 'wal_archive_clone').replace('\\', '/'))
|
os.path.join(self.patroni_path, 'data', 'wal_archive_clone').replace('\\', '/'))
|
||||||
}
|
},
|
||||||
}
|
})
|
||||||
},
|
},
|
||||||
'postgresql': {
|
'postgresql': {
|
||||||
'authentication': {
|
'authentication': {
|
||||||
'superuser': {'password': 'zalando2'},
|
'superuser': {'password': 'patroni2'},
|
||||||
'replication': {'password': 'rep-pass2'}
|
'replication': {'password': 'rep-pass2'}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -911,17 +952,8 @@ class PatroniPoolController(object):
|
|||||||
custom_config = {
|
custom_config = {
|
||||||
'scope': cluster_name,
|
'scope': cluster_name,
|
||||||
'postgresql': {
|
'postgresql': {
|
||||||
'recovery_conf': {
|
|
||||||
'restore_command': (self.ARCHIVE_RESTORE_SCRIPT + ' --mode restore ' +
|
|
||||||
'--dirname {} --filename %f --pathname %p').format(
|
|
||||||
os.path.join(self.patroni_path, 'data', 'wal_archive').replace('\\', '/'))
|
|
||||||
},
|
|
||||||
'create_replica_methods': ['no_leader_bootstrap'],
|
'create_replica_methods': ['no_leader_bootstrap'],
|
||||||
'no_leader_bootstrap': {
|
'no_leader_bootstrap': self.backup_restore_config({'no_leader': '1'})
|
||||||
'command': (self.BACKUP_RESTORE_SCRIPT + ' --sourcedir=' +
|
|
||||||
os.path.join(self.patroni_path, 'data', 'basebackup').replace('\\', '/')),
|
|
||||||
'no_leader': '1'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.start(name, custom_config=custom_config)
|
self.start(name, custom_config=custom_config)
|
||||||
@@ -1062,9 +1094,11 @@ def before_all(context):
|
|||||||
try:
|
try:
|
||||||
with open(os.devnull, 'w') as null:
|
with open(os.devnull, 'w') as null:
|
||||||
ret = subprocess.call(['openssl', 'req', '-nodes', '-new', '-x509', '-subj', '/CN=batman.patroni',
|
ret = subprocess.call(['openssl', 'req', '-nodes', '-new', '-x509', '-subj', '/CN=batman.patroni',
|
||||||
'-keyout', context.keyfile, '-out', context.certfile], stdout=null, stderr=null)
|
'-addext', 'subjectAltName=IP:127.0.0.1', '-keyout', context.keyfile,
|
||||||
|
'-out', context.certfile], stdout=null, stderr=null)
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
raise Exception
|
raise Exception
|
||||||
|
os.chmod(context.keyfile, stat.S_IWRITE | stat.S_IREAD)
|
||||||
except Exception:
|
except Exception:
|
||||||
context.keyfile = context.certfile = None
|
context.keyfile = context.certfile = None
|
||||||
|
|
||||||
@@ -1075,7 +1109,9 @@ def before_all(context):
|
|||||||
'PATRONI_RESTAPI_CERTFILE': context.certfile,
|
'PATRONI_RESTAPI_CERTFILE': context.certfile,
|
||||||
'PATRONI_RESTAPI_KEYFILE': context.keyfile,
|
'PATRONI_RESTAPI_KEYFILE': context.keyfile,
|
||||||
'PATRONI_RESTAPI_VERIFY_CLIENT': 'required',
|
'PATRONI_RESTAPI_VERIFY_CLIENT': 'required',
|
||||||
'PATRONI_CTL_INSECURE': 'on'})
|
'PATRONI_CTL_INSECURE': 'on',
|
||||||
|
'PATRONI_CTL_CERTFILE': context.certfile,
|
||||||
|
'PATRONI_CTL_KEYFILE': context.keyfile})
|
||||||
ctl.update({'cacert': context.certfile, 'certfile': context.certfile, 'keyfile': context.keyfile})
|
ctl.update({'cacert': context.certfile, 'certfile': context.certfile, 'keyfile': context.keyfile})
|
||||||
context.request_executor = PatroniRequest({'ctl': ctl}, True)
|
context.request_executor = PatroniRequest({'ctl': ctl}, True)
|
||||||
context.dcs_ctl = context.pctl.known_dcs[context.pctl.dcs](context)
|
context.dcs_ctl = context.pctl.known_dcs[context.pctl.dcs](context)
|
||||||
@@ -1100,12 +1136,14 @@ def before_feature(context, feature):
|
|||||||
elif feature.name == 'citus':
|
elif feature.name == 'citus':
|
||||||
lib = subprocess.check_output(['pg_config', '--pkglibdir']).decode('utf-8').strip()
|
lib = subprocess.check_output(['pg_config', '--pkglibdir']).decode('utf-8').strip()
|
||||||
if not os.path.exists(os.path.join(lib, 'citus.so')):
|
if not os.path.exists(os.path.join(lib, 'citus.so')):
|
||||||
return feature.skip("Citus extenstion isn't available")
|
return feature.skip("Citus extension isn't available")
|
||||||
|
elif feature.name == 'bootstrap labels' and context.dcs_ctl.name() != 'kubernetes':
|
||||||
|
feature.skip("Tested only on Kubernetes")
|
||||||
context.pctl.create_and_set_output_directory(feature.name)
|
context.pctl.create_and_set_output_directory(feature.name)
|
||||||
|
|
||||||
|
|
||||||
def after_feature(context, feature):
|
def after_feature(context, feature):
|
||||||
""" send SIGCONT to a dcs if neccessary,
|
""" send SIGCONT to a dcs if necessary,
|
||||||
stop all Patronis remove their data directory and cleanup the keys in etcd """
|
stop all Patronis remove their data directory and cleanup the keys in etcd """
|
||||||
context.dcs_ctl.stop_outage()
|
context.dcs_ctl.stop_outage()
|
||||||
context.pctl.stop_all()
|
context.pctl.stop_all()
|
||||||
@@ -1130,10 +1168,19 @@ def after_feature(context, feature):
|
|||||||
|
|
||||||
|
|
||||||
def before_scenario(context, scenario):
|
def before_scenario(context, scenario):
|
||||||
if 'slot-advance' in scenario.effective_tags:
|
for tag in scenario.effective_tags:
|
||||||
for p in context.pctl._processes.values():
|
if tag.startswith('pg') and 6 < len(tag) < 9:
|
||||||
if p._conn and p._conn.server_version < 110000:
|
try:
|
||||||
scenario.skip('pg_replication_slot_advance() is not supported on {0}'.format(p._conn.server_version))
|
ver = int(tag[2:])
|
||||||
break
|
except Exception:
|
||||||
|
ver = 0
|
||||||
|
if not ver:
|
||||||
|
continue
|
||||||
|
for p in context.pctl._processes.values():
|
||||||
|
if p._conn and p._conn.server_version < ver:
|
||||||
|
scenario.skip('not supported on {0}'.format(p._conn.server_version))
|
||||||
|
break
|
||||||
if 'dcs-failsafe' in scenario.effective_tags and not context.dcs_ctl._handle:
|
if 'dcs-failsafe' in scenario.effective_tags and not context.dcs_ctl._handle:
|
||||||
scenario.skip('it is not possible to control state of {0} from tests'.format(context.dcs_ctl.name()))
|
scenario.skip('it is not possible to control state of {0} from tests'.format(context.dcs_ctl.name()))
|
||||||
|
if 'reject-duplicate-name' in scenario.effective_tags and context.dcs_ctl.name() == 'raft':
|
||||||
|
scenario.skip('Flaky test with Raft')
|
||||||
|
|||||||
@@ -1,61 +1,66 @@
|
|||||||
Feature: ignored slots
|
Feature: ignored slots
|
||||||
Scenario: check ignored slots aren't removed on failover/switchover
|
Scenario: check ignored slots aren't removed on failover/switchover
|
||||||
Given I start postgres1
|
Given I start postgres-1
|
||||||
Then postgres1 is a leader after 10 seconds
|
Then postgres-1 is a leader after 10 seconds
|
||||||
And there is a non empty initialize key in DCS after 15 seconds
|
And there is a non empty initialize key in DCS after 15 seconds
|
||||||
When I issue a PATCH request to http://127.0.0.1:8009/config with {"ignore_slots": [{"name": "unmanaged_slot_0", "database": "postgres", "plugin": "test_decoding", "type": "logical"}, {"name": "unmanaged_slot_1", "database": "postgres", "plugin": "test_decoding"}, {"name": "unmanaged_slot_2", "database": "postgres"}, {"name": "unmanaged_slot_3"}], "postgresql": {"parameters": {"wal_level": "logical"}}}
|
When I issue a PATCH request to http://127.0.0.1:8009/config with {"ignore_slots": [{"name": "unmanaged_slot_0", "database": "postgres", "plugin": "test_decoding", "type": "logical"}, {"name": "unmanaged_slot_1", "database": "postgres", "plugin": "test_decoding"}, {"name": "unmanaged_slot_2", "database": "postgres"}, {"name": "unmanaged_slot_3"}], "postgresql": {"parameters": {"wal_level": "logical"}}}
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
And Response on GET http://127.0.0.1:8009/config contains ignore_slots after 10 seconds
|
And Response on GET http://127.0.0.1:8009/config contains ignore_slots after 10 seconds
|
||||||
|
And Response on GET http://127.0.0.1:8009/patroni contains pending_restart after 10 seconds
|
||||||
# Make sure the wal_level has been changed.
|
# Make sure the wal_level has been changed.
|
||||||
When I shut down postgres1
|
When I run patronictl.py restart batman postgres-1 --force
|
||||||
And I start postgres1
|
Then "members/postgres-1" key in DCS has role=primary after 10 seconds
|
||||||
Then postgres1 is a leader after 10 seconds
|
|
||||||
And "members/postgres1" key in DCS has role=master after 3 seconds
|
|
||||||
# Make sure Patroni has finished telling Postgres it should be accepting writes.
|
# Make sure Patroni has finished telling Postgres it should be accepting writes.
|
||||||
And postgres1 role is the primary after 20 seconds
|
And postgres-1 role is the primary after 20 seconds
|
||||||
# 1. Create our test logical replication slot.
|
# 1. Create our test logical replication slot.
|
||||||
# Test that ny subset of attributes in the ignore slots matcher is enough to match a slot
|
# Test that ny subset of attributes in the ignore slots matcher is enough to match a slot
|
||||||
# by using 3 different slots.
|
# by using 3 different slots.
|
||||||
When I create a logical replication slot unmanaged_slot_0 on postgres1 with the test_decoding plugin
|
When I create a logical replication slot unmanaged_slot_0 on postgres-1 with the test_decoding plugin
|
||||||
And I create a logical replication slot unmanaged_slot_1 on postgres1 with the test_decoding plugin
|
And I create a logical replication slot unmanaged_slot_1 on postgres-1 with the test_decoding plugin
|
||||||
And I create a logical replication slot unmanaged_slot_2 on postgres1 with the test_decoding plugin
|
And I create a logical replication slot unmanaged_slot_2 on postgres-1 with the test_decoding plugin
|
||||||
And I create a logical replication slot unmanaged_slot_3 on postgres1 with the test_decoding plugin
|
And I create a logical replication slot unmanaged_slot_3 on postgres-1 with the test_decoding plugin
|
||||||
And I create a logical replication slot dummy_slot on postgres1 with the test_decoding plugin
|
And I create a logical replication slot dummy_slot on postgres-1 with the test_decoding plugin
|
||||||
# It seems like it'd be obvious that these slots exist since we just created them,
|
# It seems like it'd be obvious that these slots exist since we just created them,
|
||||||
# but Patroni can actually end up dropping them almost immediately, so it's helpful
|
# but Patroni can actually end up dropping them almost immediately, so it's helpful
|
||||||
# to verify they exist before we begin testing whether they persist through failover
|
# to verify they exist before we begin testing whether they persist through failover
|
||||||
# cycles.
|
# cycles.
|
||||||
Then postgres1 has a logical replication slot named unmanaged_slot_0 with the test_decoding plugin
|
Then postgres-1 has a logical replication slot named unmanaged_slot_0 with the test_decoding plugin after 2 seconds
|
||||||
And postgres1 has a logical replication slot named unmanaged_slot_1 with the test_decoding plugin
|
And postgres-1 has a logical replication slot named unmanaged_slot_1 with the test_decoding plugin after 2 seconds
|
||||||
And postgres1 has a logical replication slot named unmanaged_slot_2 with the test_decoding plugin
|
And postgres-1 has a logical replication slot named unmanaged_slot_2 with the test_decoding plugin after 2 seconds
|
||||||
And postgres1 has a logical replication slot named unmanaged_slot_3 with the test_decoding plugin
|
And postgres-1 has a logical replication slot named unmanaged_slot_3 with the test_decoding plugin after 2 seconds
|
||||||
|
|
||||||
When I start postgres0
|
When I start postgres-0
|
||||||
Then "members/postgres0" key in DCS has role=replica after 3 seconds
|
Then "members/postgres-0" key in DCS has role=replica after 10 seconds
|
||||||
And postgres0 role is the secondary after 20 seconds
|
And postgres-0 role is the secondary after 20 seconds
|
||||||
# Verify that the replica has advanced beyond the point in the WAL
|
# Verify that the replica has advanced beyond the point in the WAL
|
||||||
# where we created the replication slot so that on the next failover
|
# where we created the replication slot so that on the next failover
|
||||||
# cycle we don't accidentally rewind to before the slot creation.
|
# cycle we don't accidentally rewind to before the slot creation.
|
||||||
And replication works from postgres1 to postgres0 after 20 seconds
|
And replication works from postgres-1 to postgres-0 after 20 seconds
|
||||||
When I shut down postgres1
|
When I shut down postgres-1
|
||||||
Then "members/postgres0" key in DCS has role=master after 3 seconds
|
Then "members/postgres-0" key in DCS has role=primary after 10 seconds
|
||||||
|
|
||||||
# 2. After a failover the server (now a replica) still has the slot.
|
# 2. After a failover the server (now a replica) still has the slot.
|
||||||
When I start postgres1
|
When I start postgres-1
|
||||||
Then postgres1 role is the secondary after 20 seconds
|
Then postgres-1 role is the secondary after 20 seconds
|
||||||
And "members/postgres1" key in DCS has role=replica after 3 seconds
|
And "members/postgres-1" key in DCS has role=replica after 10 seconds
|
||||||
# give Patroni time to sync replication slots
|
# give Patroni time to sync replication slots
|
||||||
And I sleep for 2 seconds
|
And I sleep for 2 seconds
|
||||||
And postgres1 has a logical replication slot named unmanaged_slot_0 with the test_decoding plugin
|
And postgres-1 has a logical replication slot named unmanaged_slot_0 with the test_decoding plugin after 2 seconds
|
||||||
And postgres1 has a logical replication slot named unmanaged_slot_1 with the test_decoding plugin
|
And postgres-1 has a logical replication slot named unmanaged_slot_1 with the test_decoding plugin after 2 seconds
|
||||||
And postgres1 has a logical replication slot named unmanaged_slot_2 with the test_decoding plugin
|
And postgres-1 has a logical replication slot named unmanaged_slot_2 with the test_decoding plugin after 2 seconds
|
||||||
And postgres1 has a logical replication slot named unmanaged_slot_3 with the test_decoding plugin
|
And postgres-1 has a logical replication slot named unmanaged_slot_3 with the test_decoding plugin after 2 seconds
|
||||||
And postgres1 does not have a logical replication slot named dummy_slot
|
And postgres-1 does not have a replication slot named dummy_slot
|
||||||
|
|
||||||
# 3. After a failover the server (now a primary) still has the slot.
|
# 3. After a failover the server (now a primary) still has the slot.
|
||||||
When I shut down postgres0
|
When I shut down postgres-0
|
||||||
Then "members/postgres1" key in DCS has role=master after 3 seconds
|
Then "members/postgres-1" key in DCS has role=primary after 10 seconds
|
||||||
And postgres1 has a logical replication slot named unmanaged_slot_0 with the test_decoding plugin
|
And postgres-1 has a logical replication slot named unmanaged_slot_0 with the test_decoding plugin after 2 seconds
|
||||||
And postgres1 has a logical replication slot named unmanaged_slot_1 with the test_decoding plugin
|
And postgres-1 has a logical replication slot named unmanaged_slot_1 with the test_decoding plugin after 2 seconds
|
||||||
And postgres1 has a logical replication slot named unmanaged_slot_2 with the test_decoding plugin
|
And postgres-1 has a logical replication slot named unmanaged_slot_2 with the test_decoding plugin after 2 seconds
|
||||||
And postgres1 has a logical replication slot named unmanaged_slot_3 with the test_decoding plugin
|
And postgres-1 has a logical replication slot named unmanaged_slot_3 with the test_decoding plugin after 2 seconds
|
||||||
|
|
||||||
|
@pg170000
|
||||||
|
Scenario: check that logical slots with failover are not removed by Patroni
|
||||||
|
Given I create a logical failover slot test17 on postgres-1 with the pgoutput plugin
|
||||||
|
When I run patronictl.py restart batman postgres-1 --force
|
||||||
|
Then postgres-1 has a logical replication slot named test17 with the pgoutput plugin after 2 seconds
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
Feature: nostream node
|
||||||
|
|
||||||
|
Scenario: check nostream node is recovering from archive
|
||||||
|
When I start postgres-0
|
||||||
|
And I configure and start postgres-1 with a tag nostream true
|
||||||
|
Then "members/postgres-1" key in DCS has replication_state=in archive recovery after 10 seconds
|
||||||
|
And replication works from postgres-0 to postgres-1 after 30 seconds
|
||||||
|
|
||||||
|
@pg110000
|
||||||
|
Scenario: check permanent logical replication slots are not copied
|
||||||
|
When I issue a PATCH request to http://127.0.0.1:8008/config with {"postgresql": {"parameters": {"wal_level": "logical"}}, "slots":{"test_logical":{"type":"logical","database":"postgres","plugin":"test_decoding"}}}
|
||||||
|
Then I receive a response code 200
|
||||||
|
When I run patronictl.py restart batman postgres-0 --force
|
||||||
|
Then postgres-0 has a logical replication slot named test_logical with the test_decoding plugin after 10 seconds
|
||||||
|
When I configure and start postgres-2 with a tag replicatefrom postgres-1
|
||||||
|
Then "members/postgres-2" key in DCS has replication_state=streaming after 10 seconds
|
||||||
|
And postgres-1 does not have a replication slot named test_logical
|
||||||
|
And postgres-2 does not have a replication slot named test_logical
|
||||||
|
|
||||||
|
@pg110000
|
||||||
|
Scenario: check that slots are written to the /status key
|
||||||
|
Given "status" key in DCS has postgres_0 in slots
|
||||||
|
And "status" key in DCS has postgres_2 in slots
|
||||||
|
And "status" key in DCS has test_logical in slots
|
||||||
|
And "status" key in DCS has test_logical in slots
|
||||||
|
And "status" key in DCS does not have postgres_1 in slots
|
||||||
@@ -2,12 +2,12 @@ Feature: patroni api
|
|||||||
We should check that patroni correctly responds to valid and not-valid API requests.
|
We should check that patroni correctly responds to valid and not-valid API requests.
|
||||||
|
|
||||||
Scenario: check API requests on a stand-alone server
|
Scenario: check API requests on a stand-alone server
|
||||||
Given I start postgres0
|
Given I start postgres-0
|
||||||
And postgres0 is a leader after 10 seconds
|
And postgres-0 is a leader after 10 seconds
|
||||||
When I issue a GET request to http://127.0.0.1:8008/
|
When I issue a GET request to http://127.0.0.1:8008/
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
And I receive a response state running
|
And I receive a response state running
|
||||||
And I receive a response role master
|
And I receive a response role primary
|
||||||
When I issue a GET request to http://127.0.0.1:8008/standby_leader
|
When I issue a GET request to http://127.0.0.1:8008/standby_leader
|
||||||
Then I receive a response code 503
|
Then I receive a response code 503
|
||||||
When I issue a GET request to http://127.0.0.1:8008/health
|
When I issue a GET request to http://127.0.0.1:8008/health
|
||||||
@@ -17,10 +17,10 @@ Scenario: check API requests on a stand-alone server
|
|||||||
When I issue a POST request to http://127.0.0.1:8008/reinitialize with {"force": true}
|
When I issue a POST request to http://127.0.0.1:8008/reinitialize with {"force": true}
|
||||||
Then I receive a response code 503
|
Then I receive a response code 503
|
||||||
And I receive a response text I am the leader, can not reinitialize
|
And I receive a response text I am the leader, can not reinitialize
|
||||||
When I run patronictl.py switchover batman --master postgres0 --force
|
When I run patronictl.py switchover batman --primary postgres-0 --force
|
||||||
Then I receive a response returncode 1
|
Then I receive a response returncode 1
|
||||||
And I receive a response output "Error: No candidates found to switchover to"
|
And I receive a response output "Error: No candidates found to switchover to"
|
||||||
When I issue a POST request to http://127.0.0.1:8008/switchover with {"leader": "postgres0"}
|
When I issue a POST request to http://127.0.0.1:8008/switchover with {"leader": "postgres-0"}
|
||||||
Then I receive a response code 412
|
Then I receive a response code 412
|
||||||
And I receive a response text switchover is not possible: cluster does not have members except leader
|
And I receive a response text switchover 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
|
When I issue an empty POST request to http://127.0.0.1:8008/failover
|
||||||
@@ -30,70 +30,71 @@ Scenario: check API requests on a stand-alone server
|
|||||||
And I receive a response text "Failover could be performed only to a specific candidate"
|
And I receive a response text "Failover could be performed only to a specific candidate"
|
||||||
|
|
||||||
Scenario: check local configuration reload
|
Scenario: check local configuration reload
|
||||||
Given I add tag new_tag new_value to postgres0 config
|
Given I add tag new_tag new_value to postgres-0 config
|
||||||
And I issue an empty POST request to http://127.0.0.1:8008/reload
|
And I issue an empty POST request to http://127.0.0.1:8008/reload
|
||||||
Then I receive a response code 202
|
Then I receive a response code 202
|
||||||
|
|
||||||
Scenario: check dynamic configuration change via DCS
|
Scenario: check dynamic configuration change via DCS
|
||||||
Given I run patronictl.py edit-config -s 'ttl=10' -p 'max_connections=101' --force batman
|
Given I issue a PATCH request to http://127.0.0.1:8008/config with {"ttl": 20, "postgresql": {"parameters": {"max_connections": "101"}}}
|
||||||
Then I receive a response returncode 0
|
Then I receive a response code 200
|
||||||
And I receive a response output "+ttl: 10"
|
|
||||||
And Response on GET http://127.0.0.1:8008/patroni contains pending_restart after 11 seconds
|
And Response on GET http://127.0.0.1:8008/patroni contains pending_restart after 11 seconds
|
||||||
When I issue a GET request to http://127.0.0.1:8008/config
|
When I issue a GET request to http://127.0.0.1:8008/config
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
And I receive a response ttl 10
|
And I receive a response ttl 20
|
||||||
When I issue a GET request to http://127.0.0.1:8008/patroni
|
When I issue a GET request to http://127.0.0.1:8008/patroni
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
And I receive a response tags {'new_tag': 'new_value'}
|
And I receive a response tags {'new_tag': 'new_value'}
|
||||||
And I sleep for 4 seconds
|
And I sleep for 4 seconds
|
||||||
|
|
||||||
Scenario: check the scheduled restart
|
Scenario: check the scheduled restart
|
||||||
Given I issue a PATCH request to http://127.0.0.1:8008/config with {"postgresql": {"parameters": {"superuser_reserved_connections": "6"}}}
|
Given I run patronictl.py edit-config -p 'superuser_reserved_connections=6' --force batman
|
||||||
Then I receive a response code 200
|
Then I receive a response returncode 0
|
||||||
And Response on GET http://127.0.0.1:8008/patroni contains pending_restart after 5 seconds
|
And I receive a response output "+ superuser_reserved_connections: 6"
|
||||||
|
And Response on GET http://127.0.0.1:8008/patroni contains pending_restart after 5 seconds
|
||||||
Given I issue a scheduled restart at http://127.0.0.1:8008 in 5 seconds with {"role": "replica"}
|
Given I issue a scheduled restart at http://127.0.0.1:8008 in 5 seconds with {"role": "replica"}
|
||||||
Then I receive a response code 202
|
Then I receive a response code 202
|
||||||
And I sleep for 8 seconds
|
And I sleep for 8 seconds
|
||||||
And Response on GET http://127.0.0.1:8008/patroni contains pending_restart after 10 seconds
|
And Response on GET http://127.0.0.1:8008/patroni contains pending_restart after 10 seconds
|
||||||
Given I issue a scheduled restart at http://127.0.0.1:8008 in 5 seconds with {"restart_pending": "True"}
|
Given I issue a scheduled restart at http://127.0.0.1:8008 in 5 seconds with {"restart_pending": "True"}
|
||||||
Then I receive a response code 202
|
Then I receive a response code 202
|
||||||
And Response on GET http://127.0.0.1:8008/patroni does not contain pending_restart after 10 seconds
|
And Response on GET http://127.0.0.1:8008/patroni does not contain pending_restart after 10 seconds
|
||||||
And postgres0 role is the primary after 10 seconds
|
And postgres-0 role is the primary after 10 seconds
|
||||||
|
|
||||||
Scenario: check API requests for the primary-replica pair in the pause mode
|
Scenario: check API requests for the primary-replica pair in the pause mode
|
||||||
Given I start postgres1
|
Given I start postgres-1
|
||||||
Then replication works from postgres0 to postgres1 after 20 seconds
|
Then replication works from postgres-0 to postgres-1 after 20 seconds
|
||||||
When I run patronictl.py pause batman
|
When I run patronictl.py pause batman
|
||||||
Then I receive a response returncode 0
|
Then I receive a response returncode 0
|
||||||
When I kill postmaster on postgres1
|
When I kill postmaster on postgres-1
|
||||||
And I issue a GET request to http://127.0.0.1:8009/replica
|
And I issue a GET request to http://127.0.0.1:8009/replica
|
||||||
Then I receive a response code 503
|
Then I receive a response code 503
|
||||||
When I run patronictl.py restart batman postgres1 --force
|
And "members/postgres-1" key in DCS has state=stopped after 10 seconds
|
||||||
|
When I run patronictl.py restart batman postgres-1 --force
|
||||||
Then I receive a response returncode 0
|
Then I receive a response returncode 0
|
||||||
Then replication works from postgres0 to postgres1 after 20 seconds
|
Then replication works from postgres-0 to postgres-1 after 20 seconds
|
||||||
And I sleep for 2 seconds
|
And I sleep for 2 seconds
|
||||||
When I issue a GET request to http://127.0.0.1:8009/replica
|
When I issue a GET request to http://127.0.0.1:8009/replica
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
And I receive a response state running
|
And I receive a response state running
|
||||||
And I receive a response role replica
|
And I receive a response role replica
|
||||||
When I run patronictl.py reinit batman postgres1 --force
|
When I run patronictl.py reinit batman postgres-1 --force --wait
|
||||||
Then I receive a response returncode 0
|
Then I receive a response returncode 0
|
||||||
And I receive a response output "Success: reinitialize for member postgres1"
|
And I receive a response output "Success: reinitialize for member postgres-1"
|
||||||
When I run patronictl.py restart batman postgres0 --force
|
And postgres-1 role is the secondary after 30 seconds
|
||||||
|
And replication works from postgres-0 to postgres-1 after 20 seconds
|
||||||
|
When I run patronictl.py restart batman postgres-0 --force
|
||||||
Then I receive a response returncode 0
|
Then I receive a response returncode 0
|
||||||
And I receive a response output "Success: restart on member postgres0"
|
And I receive a response output "Success: restart on member postgres-0"
|
||||||
And postgres0 role is the primary after 5 seconds
|
And postgres-0 role is the primary after 5 seconds
|
||||||
When I sleep for 10 seconds
|
|
||||||
Then postgres1 role is the secondary after 15 seconds
|
|
||||||
|
|
||||||
Scenario: check the switchover via the API in the pause mode
|
Scenario: check the switchover via the API in the pause mode
|
||||||
Given I issue a POST request to http://127.0.0.1:8008/switchover with {"leader": "postgres0", "candidate": "postgres1"}
|
Given I issue a POST request to http://127.0.0.1:8008/switchover with {"leader": "postgres-0", "candidate": "postgres-1"}
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
And postgres1 is a leader after 5 seconds
|
And postgres-1 is a leader after 5 seconds
|
||||||
And postgres1 role is the primary after 10 seconds
|
And postgres-1 role is the primary after 10 seconds
|
||||||
And postgres0 role is the secondary after 10 seconds
|
And postgres-0 role is the secondary after 10 seconds
|
||||||
And replication works from postgres1 to postgres0 after 20 seconds
|
And replication works from postgres-1 to postgres-0 after 20 seconds
|
||||||
And "members/postgres0" key in DCS has state=running after 10 seconds
|
And "members/postgres-0" key in DCS has state=running after 10 seconds
|
||||||
When I issue a GET request to http://127.0.0.1:8008/primary
|
When I issue a GET request to http://127.0.0.1:8008/primary
|
||||||
Then I receive a response code 503
|
Then I receive a response code 503
|
||||||
When I issue a GET request to http://127.0.0.1:8008/replica
|
When I issue a GET request to http://127.0.0.1:8008/replica
|
||||||
@@ -104,18 +105,18 @@ Scenario: check the switchover via the API in the pause mode
|
|||||||
Then I receive a response code 503
|
Then I receive a response code 503
|
||||||
|
|
||||||
Scenario: check the scheduled switchover
|
Scenario: check the scheduled switchover
|
||||||
Given I issue a scheduled switchover from postgres1 to postgres0 in 10 seconds
|
Given I issue a scheduled switchover from postgres-1 to postgres-0 in 10 seconds
|
||||||
Then I receive a response returncode 1
|
Then I receive a response returncode 1
|
||||||
And I receive a response output "Can't schedule switchover in the paused state"
|
And I receive a response output "Can't schedule switchover in the paused state"
|
||||||
When I run patronictl.py resume batman
|
When I run patronictl.py resume batman
|
||||||
Then I receive a response returncode 0
|
Then I receive a response returncode 0
|
||||||
Given I issue a scheduled switchover from postgres1 to postgres0 in 10 seconds
|
Given I issue a scheduled switchover from postgres-1 to postgres-0 in 10 seconds
|
||||||
Then I receive a response returncode 0
|
Then I receive a response returncode 0
|
||||||
And postgres0 is a leader after 20 seconds
|
And postgres-0 is a leader after 20 seconds
|
||||||
And postgres0 role is the primary after 10 seconds
|
And postgres-0 role is the primary after 10 seconds
|
||||||
And postgres1 role is the secondary after 10 seconds
|
And postgres-1 role is the secondary after 10 seconds
|
||||||
And replication works from postgres0 to postgres1 after 25 seconds
|
And replication works from postgres-0 to postgres-1 after 25 seconds
|
||||||
And "members/postgres1" key in DCS has state=running after 10 seconds
|
And "members/postgres-1" key in DCS has state=running after 10 seconds
|
||||||
When I issue a GET request to http://127.0.0.1:8008/primary
|
When I issue a GET request to http://127.0.0.1:8008/primary
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
When I issue a GET request to http://127.0.0.1:8008/replica
|
When I issue a GET request to http://127.0.0.1:8008/replica
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
Feature: permanent slots
|
||||||
|
Scenario: check that physical permanent slots are created
|
||||||
|
Given I start postgres-0
|
||||||
|
Then postgres-0 is a leader after 10 seconds
|
||||||
|
And there is a non empty initialize key in DCS after 15 seconds
|
||||||
|
When I issue a PATCH request to http://127.0.0.1:8008/config with {"slots":{"test_physical":0,"postgres_3":0},"postgresql":{"parameters":{"wal_level":"logical"}}}
|
||||||
|
Then I receive a response code 200
|
||||||
|
And Response on GET http://127.0.0.1:8008/config contains slots after 10 seconds
|
||||||
|
When I start postgres-1
|
||||||
|
And I configure and start postgres-2 with a tag nofailover true
|
||||||
|
And I configure and start postgres-3 with a tag replicatefrom postgres-2
|
||||||
|
Then postgres-0 has a physical replication slot named test_physical after 10 seconds
|
||||||
|
And postgres-0 has a physical replication slot named postgres_1 after 10 seconds
|
||||||
|
And postgres-0 has a physical replication slot named postgres_2 after 10 seconds
|
||||||
|
And postgres-2 has a physical replication slot named postgres_3 after 10 seconds
|
||||||
|
And postgres-2 does not have a replication slot named test_physical
|
||||||
|
|
||||||
|
@pg110000
|
||||||
|
Scenario: check that logical permanent slots are created
|
||||||
|
Given I run patronictl.py restart batman postgres-0 --force
|
||||||
|
And I issue a PATCH request to http://127.0.0.1:8008/config with {"slots":{"test_logical":{"type":"logical","database":"postgres","plugin":"test_decoding"}}}
|
||||||
|
Then postgres-0 has a logical replication slot named test_logical with the test_decoding plugin after 10 seconds
|
||||||
|
|
||||||
|
@pg110000
|
||||||
|
Scenario: check that permanent slots are created on replicas
|
||||||
|
Given postgres-1 has a logical replication slot named test_logical with the test_decoding plugin after 10 seconds
|
||||||
|
Then Logical slot test_logical is in sync between postgres-0 and postgres-1 after 10 seconds
|
||||||
|
And Logical slot test_logical is in sync between postgres-0 and postgres-3 after 10 seconds
|
||||||
|
And postgres-1 has a physical replication slot named test_physical after 2 seconds
|
||||||
|
And postgres-2 does not have a replication slot named test_logical
|
||||||
|
And postgres-3 has a physical replication slot named test_physical after 2 seconds
|
||||||
|
|
||||||
|
@pg110000
|
||||||
|
Scenario: check permanent physical slots that match with member names
|
||||||
|
Given postgres-0 has a physical replication slot named postgres_3 after 2 seconds
|
||||||
|
And postgres-1 has a physical replication slot named postgres_0 after 2 seconds
|
||||||
|
And postgres-1 has a physical replication slot named postgres_2 after 2 seconds
|
||||||
|
And postgres-1 has a physical replication slot named postgres_3 after 2 seconds
|
||||||
|
And postgres-2 does not have a replication slot named postgres_0
|
||||||
|
And postgres-2 does not have a replication slot named postgres_1
|
||||||
|
And postgres-2 has a physical replication slot named postgres_3 after 2 seconds
|
||||||
|
And postgres-3 has a physical replication slot named postgres_0 after 2 seconds
|
||||||
|
And postgres-3 has a physical replication slot named postgres_1 after 2 seconds
|
||||||
|
And postgres-3 has a physical replication slot named postgres_2 after 2 seconds
|
||||||
|
|
||||||
|
@pg110000
|
||||||
|
Scenario: check that permanent slots are advanced on replicas
|
||||||
|
Given I add the table replicate_me to postgres-0
|
||||||
|
When I get all changes from logical slot test_logical on postgres-0
|
||||||
|
And I get all changes from physical slot test_physical on postgres-0
|
||||||
|
Then Logical slot test_logical is in sync between postgres-0 and postgres-1 after 10 seconds
|
||||||
|
And Physical slot test_physical is in sync between postgres-0 and postgres-1 after 10 seconds
|
||||||
|
And Logical slot test_logical is in sync between postgres-0 and postgres-3 after 10 seconds
|
||||||
|
And Physical slot test_physical is in sync between postgres-0 and postgres-3 after 10 seconds
|
||||||
|
And Physical slot postgres_1 is in sync between postgres-0 and postgres-3 after 10 seconds
|
||||||
|
And Physical slot postgres_3 is in sync between postgres-2 and postgres-0 after 20 seconds
|
||||||
|
And Physical slot postgres_3 is in sync between postgres-2 and postgres-1 after 10 seconds
|
||||||
|
|
||||||
|
@pg110000
|
||||||
|
Scenario: check that permanent slots and member slots are written to the /status key
|
||||||
|
Given "status" key in DCS has test_physical in slots
|
||||||
|
And "status" key in DCS has postgres_0 in slots
|
||||||
|
And "status" key in DCS has postgres_1 in slots
|
||||||
|
And "status" key in DCS has postgres_2 in slots
|
||||||
|
And "status" key in DCS has postgres_3 in slots
|
||||||
|
|
||||||
|
@pg110000
|
||||||
|
Scenario: check that only non-permanent member slots are written to the retain_slots in /status key
|
||||||
|
Given "status" key in DCS has postgres_0 in retain_slots
|
||||||
|
And "status" key in DCS has postgres_1 in retain_slots
|
||||||
|
And "status" key in DCS has postgres_2 in retain_slots
|
||||||
|
And "status" key in DCS does not have postgres_3 in retain_slots
|
||||||
|
|
||||||
|
Scenario: check permanent physical replication slot after failover
|
||||||
|
Given I shut down postgres-3
|
||||||
|
And I shut down postgres-2
|
||||||
|
And I shut down postgres-0
|
||||||
|
Then postgres-1 has a physical replication slot named test_physical after 10 seconds
|
||||||
|
And postgres-1 has a physical replication slot named postgres_0 after 10 seconds
|
||||||
|
And postgres-1 has a physical replication slot named postgres_3 after 10 seconds
|
||||||
|
When I start postgres-0
|
||||||
|
Then postgres-0 role is the replica after 20 seconds
|
||||||
|
And physical replication slot named postgres_1 on postgres-0 has no xmin value after 10 seconds
|
||||||
|
# postgres_2 and postgres_3 slots are retained, but postgres_2 will still have xmin value :(
|
||||||
|
And postgres-0 has a physical replication slot named postgres_2 after 10 seconds
|
||||||
|
And postgres-0 has a physical replication slot named postgres_3 after 10 seconds
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
Feature: priority replication
|
||||||
|
We should check that we can give nodes priority during failover
|
||||||
|
|
||||||
|
Scenario: check failover priority 0 prevents leaderships
|
||||||
|
Given I configure and start postgres-0 with a tag failover_priority 1
|
||||||
|
And I configure and start postgres-1 with a tag failover_priority 0
|
||||||
|
Then replication works from postgres-0 to postgres-1 after 20 seconds
|
||||||
|
When I shut down postgres-0
|
||||||
|
And there is one of ["following a different leader because I am not allowed to promote"] INFO in the postgres-1 patroni log after 5 seconds
|
||||||
|
Then postgres-1 role is the secondary after 10 seconds
|
||||||
|
When I start postgres-0
|
||||||
|
Then postgres-0 role is the primary after 10 seconds
|
||||||
|
|
||||||
|
Scenario: check higher failover priority is respected
|
||||||
|
Given I configure and start postgres-2 with a tag failover_priority 1
|
||||||
|
And I configure and start postgres-3 with a tag failover_priority 2
|
||||||
|
Then replication works from postgres-0 to postgres-2 after 20 seconds
|
||||||
|
And replication works from postgres-0 to postgres-3 after 20 seconds
|
||||||
|
When I shut down postgres-0
|
||||||
|
Then postgres-3 role is the primary after 10 seconds
|
||||||
|
And there is one of ["postgres-3 has equally tolerable WAL position and priority 2, while this node has priority 1","Wal position of postgres-3 is ahead of my wal position"] INFO in the postgres-2 patroni log after 5 seconds
|
||||||
|
|
||||||
|
Scenario: check conflicting configuration handling
|
||||||
|
When I set nofailover tag in postgres-2 config
|
||||||
|
And I issue an empty POST request to http://127.0.0.1:8010/reload
|
||||||
|
Then I receive a response code 202
|
||||||
|
And there is one of ["Conflicting configuration between nofailover: True and failover_priority: 1. Defaulting to nofailover: True"] WARNING in the postgres-2 patroni log after 5 seconds
|
||||||
|
And "members/postgres-2" key in DCS has tags={'failover_priority': '1', 'nofailover': True} after 10 seconds
|
||||||
|
When I issue a POST request to http://127.0.0.1:8010/failover with {"candidate": "postgres-2"}
|
||||||
|
Then I receive a response code 412
|
||||||
|
And I receive a response text "failover is not possible: no good candidates have been found"
|
||||||
|
When I reset nofailover tag in postgres-1 config
|
||||||
|
And I issue an empty POST request to http://127.0.0.1:8009/reload
|
||||||
|
Then I receive a response code 202
|
||||||
|
And there is one of ["Conflicting configuration between nofailover: False and failover_priority: 0. Defaulting to nofailover: False"] WARNING in the postgres-1 patroni log after 5 seconds
|
||||||
|
And "members/postgres-1" key in DCS has tags={'failover_priority': '0', 'nofailover': False} after 10 seconds
|
||||||
|
And I issue a POST request to http://127.0.0.1:8009/failover with {"candidate": "postgres-1"}
|
||||||
|
Then I receive a response code 200
|
||||||
|
And postgres-1 role is the primary after 10 seconds
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
Feature: synchronous replicas priority
|
||||||
|
We should check that we can give nodes priority for becoming synchronous replicas
|
||||||
|
|
||||||
|
Scenario: check replica with sync_priority=0 does not become a synchronous replica
|
||||||
|
Given I start postgres-0
|
||||||
|
Then postgres-0 is a leader after 10 seconds
|
||||||
|
And there is a non empty initialize key in DCS after 15 seconds
|
||||||
|
When I issue a PATCH request to http://127.0.0.1:8008/config with {"ttl": 20, "synchronous_mode": true}
|
||||||
|
Then I receive a response code 200
|
||||||
|
When I configure and start postgres-1 with a tag sync_priority 0
|
||||||
|
Then sync key in DCS has leader=postgres-0 after 20 seconds
|
||||||
|
And sync key in DCS has sync_standby=None after 5 seconds
|
||||||
|
|
||||||
|
Scenario: check higher synchronous replicas priority is respected
|
||||||
|
Given I configure and start postgres-2 with a tag sync_priority 1
|
||||||
|
And I configure and start postgres-3 with a tag sync_priority 2
|
||||||
|
Then replication works from postgres-0 to postgres-2 after 20 seconds
|
||||||
|
And replication works from postgres-0 to postgres-3 after 20 seconds
|
||||||
|
When I issue a PATCH request to http://127.0.0.1:8008/config with {"synchronous_node_count": 1}
|
||||||
|
Then I receive a response code 200
|
||||||
|
And sync key in DCS has sync_standby=postgres-3 after 10 seconds
|
||||||
|
|
||||||
|
|
||||||
|
Scenario: check conflicting configuration handling
|
||||||
|
When I set nosync tag in postgres-3 config
|
||||||
|
And I issue an empty POST request to http://127.0.0.1:8011/reload
|
||||||
|
Then I receive a response code 202
|
||||||
|
And there is one of ["Conflicting configuration between nosync: True and sync_priority: 2. Defaulting to nosync: True"] WARNING in the postgres-3 patroni log after 5 seconds
|
||||||
|
And "members/postgres-3" key in DCS has tags={'nosync': True, 'sync_priority': '2'} after 10 seconds
|
||||||
|
And "sync" key in DCS has sync_standby=postgres-2 after 10 seconds
|
||||||
|
When I reset nosync tag in postgres-1 config
|
||||||
|
And I issue an empty POST request to http://127.0.0.1:8009/reload
|
||||||
|
Then I receive a response code 202
|
||||||
|
And there is one of ["Conflicting configuration between nosync: False and sync_priority: 0. Defaulting to nosync: False"] WARNING in the postgres-1 patroni log after 5 seconds
|
||||||
|
And "members/postgres-1" key in DCS has tags={'nosync': False, 'sync_priority': '0'} after 10 seconds
|
||||||
|
When I shut down postgres-2
|
||||||
|
And "sync" key in DCS has sync_standby=postgres-1 after 3 seconds
|
||||||
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
Feature: quorum commit
|
||||||
|
Check basic workfrlows when quorum commit is enabled
|
||||||
|
|
||||||
|
Scenario: check enable quorum commit and that the only leader promotes after restart
|
||||||
|
Given I start postgres-0
|
||||||
|
Then postgres-0 is a leader after 10 seconds
|
||||||
|
And there is a non empty initialize key in DCS after 15 seconds
|
||||||
|
When I issue a PATCH request to http://127.0.0.1:8008/config with {"ttl": 20, "synchronous_mode": "quorum"}
|
||||||
|
Then I receive a response code 200
|
||||||
|
And sync key in DCS has leader=postgres-0 after 20 seconds
|
||||||
|
And sync key in DCS has quorum=0 after 2 seconds
|
||||||
|
And synchronous_standby_names on postgres-0 is set to '_empty_str_' after 2 seconds
|
||||||
|
When I shut down postgres-0
|
||||||
|
And sync key in DCS has leader=postgres-0 after 2 seconds
|
||||||
|
When I start postgres-0
|
||||||
|
Then postgres-0 role is the primary after 10 seconds
|
||||||
|
When I issue a PATCH request to http://127.0.0.1:8008/config with {"synchronous_mode_strict": true}
|
||||||
|
Then synchronous_standby_names on postgres-0 is set to 'ANY 1 (*)' after 10 seconds
|
||||||
|
|
||||||
|
Scenario: check failover with one quorum standby
|
||||||
|
Given I start postgres-1
|
||||||
|
Then sync key in DCS has sync_standby=postgres-1 after 10 seconds
|
||||||
|
And synchronous_standby_names on postgres-0 is set to 'ANY 1 ("postgres-1")' after 2 seconds
|
||||||
|
When I shut down postgres-0
|
||||||
|
Then postgres-1 role is the primary after 10 seconds
|
||||||
|
And sync key in DCS has quorum=0 after 10 seconds
|
||||||
|
Then synchronous_standby_names on postgres-1 is set to 'ANY 1 (*)' after 10 seconds
|
||||||
|
When I start postgres-0
|
||||||
|
Then sync key in DCS has leader=postgres-1 after 10 seconds
|
||||||
|
Then sync key in DCS has sync_standby=postgres-0 after 10 seconds
|
||||||
|
And synchronous_standby_names on postgres-1 is set to 'ANY 1 ("postgres-0")' after 2 seconds
|
||||||
|
|
||||||
|
Scenario: check behavior with three nodes and different replication factor
|
||||||
|
Given I start postgres-2
|
||||||
|
Then sync key in DCS has sync_standby=postgres-0,postgres-2 after 10 seconds
|
||||||
|
And sync key in DCS has quorum=1 after 2 seconds
|
||||||
|
And synchronous_standby_names on postgres-1 is set to 'ANY 1 ("postgres-0","postgres-2")' after 2 seconds
|
||||||
|
When I issue a PATCH request to http://127.0.0.1:8009/config with {"synchronous_node_count": 2}
|
||||||
|
Then sync key in DCS has quorum=0 after 10 seconds
|
||||||
|
And synchronous_standby_names on postgres-1 is set to 'ANY 2 ("postgres-0","postgres-2")' after 2 seconds
|
||||||
|
|
||||||
|
Scenario: switch from quorum replication to good old multisync and back
|
||||||
|
Given I issue a PATCH request to http://127.0.0.1:8009/config with {"synchronous_mode": true, "synchronous_node_count": 1}
|
||||||
|
And I shut down postgres-0
|
||||||
|
Then synchronous_standby_names on postgres-1 is set to '"postgres-2"' after 10 seconds
|
||||||
|
And sync key in DCS has sync_standby=postgres-2 after 10 seconds
|
||||||
|
Then sync key in DCS has quorum=0 after 2 seconds
|
||||||
|
When I issue a PATCH request to http://127.0.0.1:8009/config with {"synchronous_mode": "quorum"}
|
||||||
|
And I start postgres-0
|
||||||
|
Then synchronous_standby_names on postgres-1 is set to 'ANY 1 ("postgres-0","postgres-2")' after 10 seconds
|
||||||
|
And sync key in DCS has sync_standby=postgres-0,postgres-2 after 10 seconds
|
||||||
|
Then sync key in DCS has quorum=1 after 2 seconds
|
||||||
|
|
||||||
|
Scenario: REST API and patronictl
|
||||||
|
Given I run patronictl.py list batman
|
||||||
|
Then I receive a response returncode 0
|
||||||
|
And I receive a response output "Quorum Standby"
|
||||||
|
And Status code on GET http://127.0.0.1:8008/quorum is 200 after 3 seconds
|
||||||
|
And Status code on GET http://127.0.0.1:8010/quorum is 200 after 3 seconds
|
||||||
|
|
||||||
|
Scenario: nosync node is removed from voters and synchronous_standby_names
|
||||||
|
Given I add tag nosync true to postgres-2 config
|
||||||
|
When I issue an empty POST request to http://127.0.0.1:8010/reload
|
||||||
|
Then I receive a response code 202
|
||||||
|
And sync key in DCS has quorum=0 after 10 seconds
|
||||||
|
And sync key in DCS has sync_standby=postgres-0 after 10 seconds
|
||||||
|
And synchronous_standby_names on postgres-1 is set to 'ANY 1 ("postgres-0")' after 2 seconds
|
||||||
|
And Status code on GET http://127.0.0.1:8010/quorum is 503 after 10 seconds
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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 postgres-0
|
||||||
|
Then postgres-0 is a leader after 10 seconds
|
||||||
|
And there is a non empty initialize key in DCS after 15 seconds
|
||||||
|
When I start postgres-1
|
||||||
|
And I add the table foo to postgres-0
|
||||||
|
Then table foo is present on postgres-1 after 20 seconds
|
||||||
|
When I kill postmaster on postgres-0
|
||||||
|
Then postgres-0 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 primary
|
||||||
|
And I receive a response timeline 1
|
||||||
|
And "members/postgres-0" key in DCS has state=running after 12 seconds
|
||||||
|
And replication works from postgres-0 to postgres-1 after 15 seconds
|
||||||
|
|
||||||
|
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 postgres-0
|
||||||
|
Then postgres-1 is a leader after 10 seconds
|
||||||
|
And postgres-1 role is the primary after 10 seconds
|
||||||
|
|
||||||
|
Scenario: check crashed primary demotes after failed attempt to start
|
||||||
|
Given I issue a PATCH request to http://127.0.0.1:8009/config with {"master_start_timeout": null}
|
||||||
|
Then I receive a response code 200
|
||||||
|
And postgres-0 role is the replica after 10 seconds
|
||||||
|
When I ensure postgres-1 fails to start after a failure
|
||||||
|
When I kill postmaster on postgres-1
|
||||||
|
Then postgres-0 is a leader after 10 seconds
|
||||||
|
And there is a postgres-1_cb.log with "on_role_change demoted batman" in postgres-1 data directory
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
Feature: standby cluster
|
Feature: standby cluster
|
||||||
Scenario: prepare the cluster with logical slots
|
Scenario: prepare the cluster with logical slots
|
||||||
Given I start postgres1
|
Given I start postgres-1
|
||||||
Then postgres1 is a leader after 10 seconds
|
Then postgres-1 is a leader after 10 seconds
|
||||||
And there is a non empty initialize key in DCS after 15 seconds
|
And there is a non empty initialize key in DCS after 15 seconds
|
||||||
When I issue a PATCH request to http://127.0.0.1:8009/config with {"slots": {"pm_1": {"type": "physical"}}, "postgresql": {"parameters": {"wal_level": "logical"}}}
|
When I issue a PATCH request to http://127.0.0.1:8009/config with {"slots": {"pm_1": {"type": "physical"}}, "postgresql": {"parameters": {"wal_level": "logical"}}}
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
@@ -9,52 +9,58 @@ Feature: standby cluster
|
|||||||
And I sleep for 3 seconds
|
And I sleep for 3 seconds
|
||||||
When I issue a PATCH request to http://127.0.0.1:8009/config with {"slots": {"test_logical": {"type": "logical", "database": "postgres", "plugin": "test_decoding"}}}
|
When I issue a PATCH request to http://127.0.0.1:8009/config with {"slots": {"test_logical": {"type": "logical", "database": "postgres", "plugin": "test_decoding"}}}
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
And I do a backup of postgres1
|
And I do a backup of postgres-1
|
||||||
When I start postgres0
|
When I start postgres-0
|
||||||
Then "members/postgres0" key in DCS has state=running after 10 seconds
|
Then "members/postgres-0" key in DCS has state=running after 10 seconds
|
||||||
And replication works from postgres1 to postgres0 after 15 seconds
|
And replication works from postgres-1 to postgres-0 after 15 seconds
|
||||||
|
And Response on GET http://127.0.0.1:8008/patroni contains replication_state=streaming after 10 seconds
|
||||||
|
And "members/postgres-0" key in DCS has replication_state=streaming after 10 seconds
|
||||||
|
|
||||||
@slot-advance
|
@pg110000
|
||||||
Scenario: check permanent logical slots are synced to the replica
|
Scenario: check permanent logical slots are synced to the replica
|
||||||
Given I run patronictl.py restart batman postgres1 --force
|
Given I run patronictl.py restart batman postgres-1 --force
|
||||||
Then Logical slot test_logical is in sync between postgres0 and postgres1 after 10 seconds
|
Then Logical slot test_logical is in sync between postgres-0 and postgres-1 after 10 seconds
|
||||||
When I add the table replicate_me to postgres1
|
|
||||||
And I get all changes from logical slot test_logical on postgres1
|
|
||||||
Then Logical slot test_logical is in sync between postgres0 and postgres1 after 10 seconds
|
|
||||||
|
|
||||||
Scenario: Detach exiting node from the cluster
|
Scenario: Detach exiting node from the cluster
|
||||||
When I shut down postgres1
|
When I shut down postgres-1
|
||||||
Then postgres0 is a leader after 10 seconds
|
Then postgres-0 is a leader after 10 seconds
|
||||||
And "members/postgres0" key in DCS has role=master after 3 seconds
|
And "members/postgres-0" key in DCS has role=primary after 5 seconds
|
||||||
When I issue a GET request to http://127.0.0.1:8008/
|
When I issue a GET request to http://127.0.0.1:8008/
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
|
|
||||||
Scenario: check replication of a single table in a standby cluster
|
Scenario: check replication of a single table in a standby cluster
|
||||||
Given I start postgres1 in a standby cluster batman1 as a clone of postgres0
|
Given I start postgres-1 in a standby cluster batman1 as a clone of postgres-0
|
||||||
Then postgres1 is a leader of batman1 after 10 seconds
|
Then postgres-1 is a leader of batman1 after 10 seconds
|
||||||
When I add the table foo to postgres0
|
When I add the table foo to postgres-0
|
||||||
Then table foo is present on postgres1 after 20 seconds
|
Then table foo is present on postgres-1 after 20 seconds
|
||||||
|
And Response on GET http://127.0.0.1:8009/patroni contains replication_state=streaming after 10 seconds
|
||||||
And I sleep for 3 seconds
|
And I sleep for 3 seconds
|
||||||
When I issue a GET request to http://127.0.0.1:8009/primary
|
When I issue a GET request to http://127.0.0.1:8009/primary
|
||||||
Then I receive a response code 503
|
Then I receive a response code 503
|
||||||
When I issue a GET request to http://127.0.0.1:8009/standby_leader
|
When I issue a GET request to http://127.0.0.1:8009/standby_leader
|
||||||
Then I receive a response code 200
|
Then I receive a response code 200
|
||||||
And I receive a response role standby_leader
|
And I receive a response role standby_leader
|
||||||
And there is a postgres1_cb.log with "on_role_change standby_leader batman1" in postgres1 data directory
|
And there is a postgres-1_cb.log with "on_role_change standby_leader batman1" in postgres-1 data directory
|
||||||
When I start postgres2 in a cluster batman1
|
When I start postgres-2 in a cluster batman1
|
||||||
Then postgres2 role is the replica after 24 seconds
|
Then postgres-2 role is the replica after 24 seconds
|
||||||
And table foo is present on postgres2 after 20 seconds
|
And postgres-2 is replicating from postgres-1 after 10 seconds
|
||||||
And postgres1 does not have a logical replication slot named test_logical
|
And table foo is present on postgres-2 after 20 seconds
|
||||||
|
And Response on GET http://127.0.0.1:8010/patroni contains replication_state=streaming after 10 seconds
|
||||||
|
And postgres-1 does not have a replication slot named test_logical
|
||||||
|
|
||||||
|
Scenario: check switchover
|
||||||
|
Given I run patronictl.py switchover batman1 --force
|
||||||
|
Then Status code on GET http://127.0.0.1:8010/standby_leader is 200 after 10 seconds
|
||||||
|
And postgres-1 is replicating from postgres-2 after 32 seconds
|
||||||
|
And there is a postgres-2_cb.log with "on_start replica batman1\non_role_change standby_leader batman1" in postgres-2 data directory
|
||||||
|
|
||||||
Scenario: check failover
|
Scenario: check failover
|
||||||
When I kill postgres1
|
When I kill postgres-2
|
||||||
And I kill postmaster on postgres1
|
And I kill postmaster on postgres-2
|
||||||
Then postgres2 is replicating from postgres0 after 32 seconds
|
Then postgres-1 is replicating from postgres-0 after 32 seconds
|
||||||
When I issue a GET request to http://127.0.0.1:8010/primary
|
And Status code on GET http://127.0.0.1:8009/standby_leader is 200 after 10 seconds
|
||||||
|
When I issue a GET request to http://127.0.0.1:8009/primary
|
||||||
Then I receive a response code 503
|
Then I receive a response code 503
|
||||||
And I sleep for 3 seconds
|
|
||||||
When I issue a GET request to http://127.0.0.1:8010/standby_leader
|
|
||||||
Then I receive a response code 200
|
|
||||||
And I receive a response role standby_leader
|
And I receive a response role standby_leader
|
||||||
And replication works from postgres0 to postgres2 after 15 seconds
|
And replication works from postgres-0 to postgres-1 after 15 seconds
|
||||||
And there is a postgres2_cb.log with "on_start replica batman1\non_role_change standby_leader batman1" in postgres2 data directory
|
And there is a postgres-1_cb.log with "on_role_change replica batman1\non_role_change standby_leader batman1" in postgres-1 data directory
|
||||||
|
|||||||
@@ -1,50 +1,88 @@
|
|||||||
import patroni.psycopg as pg
|
import json
|
||||||
|
|
||||||
from behave import step, then
|
|
||||||
from time import sleep, time
|
from time import sleep, time
|
||||||
|
|
||||||
|
import parse
|
||||||
|
|
||||||
@step('I start {name:w}')
|
from behave import register_type, step, then
|
||||||
|
|
||||||
|
import patroni.psycopg as pg
|
||||||
|
|
||||||
|
|
||||||
|
@parse.with_pattern(r'[a-z][a-z0-9_\-]*[a-z0-9]')
|
||||||
|
def parse_name(text):
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
register_type(name=parse_name)
|
||||||
|
|
||||||
|
|
||||||
|
@step('I start {name:name}')
|
||||||
def start_patroni(context, name):
|
def start_patroni(context, name):
|
||||||
return context.pctl.start(name)
|
return context.pctl.start(name)
|
||||||
|
|
||||||
|
|
||||||
@step('I shut down {name:w}')
|
@step('I start duplicate {name:name} on port {port:d}')
|
||||||
|
def start_duplicate_patroni(context, name, port):
|
||||||
|
config = {
|
||||||
|
"name": name,
|
||||||
|
"restapi": {
|
||||||
|
"listen": "127.0.0.1:{0}".format(port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
context.pctl.start('dup-' + name, custom_config=config)
|
||||||
|
assert False, "Process was expected to fail"
|
||||||
|
except AssertionError as e:
|
||||||
|
assert 'is not running after being started' in str(e), \
|
||||||
|
"No error was raised by duplicate start of {0} ".format(name)
|
||||||
|
|
||||||
|
|
||||||
|
@step('I shut down {name:name}')
|
||||||
def stop_patroni(context, name):
|
def stop_patroni(context, name):
|
||||||
return context.pctl.stop(name, timeout=60)
|
return context.pctl.stop(name, timeout=60)
|
||||||
|
|
||||||
|
|
||||||
@step('I kill {name:w}')
|
@step('I kill {name:name}')
|
||||||
def kill_patroni(context, name):
|
def kill_patroni(context, name):
|
||||||
return context.pctl.stop(name, kill=True)
|
return context.pctl.stop(name, kill=True)
|
||||||
|
|
||||||
|
|
||||||
@step('I kill postmaster on {name:w}')
|
@step('I shut down postmaster on {name:name}')
|
||||||
def stop_postgres(context, name):
|
def stop_postgres(context, name):
|
||||||
return context.pctl.stop(name, postgres=True)
|
return context.pctl.stop(name, postgres=True)
|
||||||
|
|
||||||
|
|
||||||
@step('I add the table {table_name:w} to {pg_name:w}')
|
@step('I kill postmaster on {name:name}')
|
||||||
|
def kill_postgres(context, name):
|
||||||
|
return context.pctl.stop(name, kill=True, postgres=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_wal_name(context, pg_name):
|
||||||
|
version = context.pctl.query(pg_name, "SHOW server_version_num").fetchone()[0]
|
||||||
|
return 'xlog' if int(version) / 10000 < 10 else 'wal'
|
||||||
|
|
||||||
|
|
||||||
|
@step('I add the table {table_name:w} to {pg_name:name}')
|
||||||
def add_table(context, table_name, pg_name):
|
def add_table(context, table_name, pg_name):
|
||||||
# parse the configuration file and get the port
|
# parse the configuration file and get the port
|
||||||
try:
|
try:
|
||||||
context.pctl.query(pg_name, "CREATE TABLE public.{0}()".format(table_name))
|
context.pctl.query(pg_name, "CREATE TABLE public.{0}()".format(table_name))
|
||||||
|
context.pctl.query(pg_name, "SELECT pg_switch_{0}()".format(get_wal_name(context, pg_name)))
|
||||||
except pg.Error as e:
|
except pg.Error as e:
|
||||||
assert False, "Error creating table {0} on {1}: {2}".format(table_name, pg_name, e)
|
assert False, "Error creating table {0} on {1}: {2}".format(table_name, pg_name, e)
|
||||||
|
|
||||||
|
|
||||||
@step('I {action:w} wal replay on {pg_name:w}')
|
@step('I {action:w} wal replay on {pg_name:name}')
|
||||||
def toggle_wal_replay(context, action, pg_name):
|
def toggle_wal_replay(context, action, pg_name):
|
||||||
# pause or resume the wal replay process
|
# pause or resume the wal replay process
|
||||||
try:
|
try:
|
||||||
version = context.pctl.query(pg_name, "SHOW server_version_num").fetchone()[0]
|
context.pctl.query(pg_name, "SELECT pg_{0}_replay_{1}()".format(get_wal_name(context, pg_name), action))
|
||||||
wal_name = 'xlog' if int(version)/10000 < 10 else 'wal'
|
|
||||||
context.pctl.query(pg_name, "SELECT pg_{0}_replay_{1}()".format(wal_name, action))
|
|
||||||
except pg.Error as e:
|
except pg.Error as e:
|
||||||
assert False, "Error during {0} wal recovery on {1}: {2}".format(action, pg_name, e)
|
assert False, "Error during {0} wal recovery on {1}: {2}".format(action, pg_name, e)
|
||||||
|
|
||||||
|
|
||||||
@step('I {action:w} table on {pg_name:w}')
|
@step('I {action:w} table on {pg_name:name}')
|
||||||
def crdr_mytest(context, action, pg_name):
|
def crdr_mytest(context, action, pg_name):
|
||||||
try:
|
try:
|
||||||
if (action == "create"):
|
if (action == "create"):
|
||||||
@@ -55,7 +93,7 @@ def crdr_mytest(context, action, pg_name):
|
|||||||
assert False, "Error {0} table mytest on {1}: {2}".format(action, pg_name, e)
|
assert False, "Error {0} table mytest on {1}: {2}".format(action, pg_name, e)
|
||||||
|
|
||||||
|
|
||||||
@step('I load data on {pg_name:w}')
|
@step('I load data on {pg_name:name}')
|
||||||
def initiate_load(context, pg_name):
|
def initiate_load(context, pg_name):
|
||||||
# perform dummy load
|
# perform dummy load
|
||||||
try:
|
try:
|
||||||
@@ -64,7 +102,7 @@ def initiate_load(context, pg_name):
|
|||||||
assert False, "Error loading test data on {0}: {1}".format(pg_name, e)
|
assert False, "Error loading test data on {0}: {1}".format(pg_name, e)
|
||||||
|
|
||||||
|
|
||||||
@then('Table {table_name:w} is present on {pg_name:w} after {max_replication_delay:d} seconds')
|
@then('Table {table_name:w} is present on {pg_name:name} after {max_replication_delay:d} seconds')
|
||||||
def table_is_present_on(context, table_name, pg_name, max_replication_delay):
|
def table_is_present_on(context, table_name, pg_name, max_replication_delay):
|
||||||
max_replication_delay *= context.timeout_multiplier
|
max_replication_delay *= context.timeout_multiplier
|
||||||
for _ in range(int(max_replication_delay)):
|
for _ in range(int(max_replication_delay)):
|
||||||
@@ -72,21 +110,35 @@ def table_is_present_on(context, table_name, pg_name, max_replication_delay):
|
|||||||
break
|
break
|
||||||
sleep(1)
|
sleep(1)
|
||||||
else:
|
else:
|
||||||
assert False,\
|
assert False, \
|
||||||
"Table {0} is not present on {1} after {2} seconds".format(table_name, pg_name, max_replication_delay)
|
"Table {0} is not present on {1} after {2} seconds".format(table_name, pg_name, max_replication_delay)
|
||||||
|
|
||||||
|
|
||||||
@then('{pg_name:w} role is the {pg_role:w} after {max_promotion_timeout:d} seconds')
|
@then('{pg_name:name} role is the {pg_role:w} after {max_promotion_timeout:d} seconds')
|
||||||
def check_role(context, pg_name, pg_role, max_promotion_timeout):
|
def check_role(context, pg_name, pg_role, max_promotion_timeout):
|
||||||
max_promotion_timeout *= context.timeout_multiplier
|
max_promotion_timeout *= context.timeout_multiplier
|
||||||
assert context.pctl.check_role_has_changed_to(pg_name, pg_role, timeout=int(max_promotion_timeout)),\
|
assert context.pctl.check_role_has_changed_to(pg_name, pg_role, timeout=int(max_promotion_timeout)), \
|
||||||
"{0} role didn't change to {1} after {2} seconds".format(pg_name, pg_role, max_promotion_timeout)
|
"{0} role didn't change to {1} after {2} seconds".format(pg_name, pg_role, max_promotion_timeout)
|
||||||
|
|
||||||
|
|
||||||
@step('replication works from {primary:w} to {replica:w} after {time_limit:d} seconds')
|
@step('replication works from {primary:name} to {replica:name} after {time_limit:d} seconds')
|
||||||
@then('replication works from {primary:w} to {replica:w} after {time_limit:d} seconds')
|
@then('replication works from {primary:name} to {replica:name} after {time_limit:d} seconds')
|
||||||
def replication_works(context, primary, replica, time_limit):
|
def replication_works(context, primary, replica, time_limit):
|
||||||
context.execute_steps(u"""
|
context.execute_steps(u"""
|
||||||
When I add the table test_{0} to {1}
|
When I add the table test_{0} to {1}
|
||||||
Then table test_{0} is present on {2} after {3} seconds
|
Then table test_{0} is present on {2} after {3} seconds
|
||||||
""".format(int(time()), primary, replica, time_limit))
|
""".format(str(time()).replace('.', '_').replace(',', '_'), primary, replica, time_limit))
|
||||||
|
|
||||||
|
|
||||||
|
@step('there is one of {message_list} {level:w} in the {node} patroni log after {timeout:d} seconds')
|
||||||
|
def check_patroni_log(context, message_list, level, node, timeout):
|
||||||
|
timeout *= context.timeout_multiplier
|
||||||
|
message_list = json.loads(message_list)
|
||||||
|
|
||||||
|
for _ in range(int(timeout)):
|
||||||
|
messages_of_level = context.pctl.read_patroni_log(node, level)
|
||||||
|
if any(any(message in line for line in messages_of_level) for message in message_list):
|
||||||
|
break
|
||||||
|
sleep(1)
|
||||||
|
else:
|
||||||
|
assert False, f"There were none of {message_list} {level} in the {node} patroni log after {timeout} seconds"
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
from behave import step, then
|
||||||
|
|
||||||
|
|
||||||
|
@step('I start {name:name} in a cluster {cluster_name:w} as a long-running clone of {name2:name}')
|
||||||
|
def start_cluster_clone(context, name, cluster_name, name2):
|
||||||
|
context.pctl.clone(name2, cluster_name, name, True)
|
||||||
|
|
||||||
|
|
||||||
|
@step('I start {name:name} in cluster {cluster_name:w} using long-running backup_restore')
|
||||||
|
def start_patroni(context, name, cluster_name):
|
||||||
|
return context.pctl.start(name, custom_config={
|
||||||
|
"scope": cluster_name,
|
||||||
|
"postgresql": {
|
||||||
|
'create_replica_methods': ['backup_restore'],
|
||||||
|
"backup_restore": context.pctl.backup_restore_config(long_running=True),
|
||||||
|
'authentication': {
|
||||||
|
'superuser': {'password': 'patroni1'},
|
||||||
|
'replication': {'password': 'rep-pass1'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, max_wait_limit=-1)
|
||||||
|
|
||||||
|
|
||||||
|
@then('{name:name} is labeled with "{label:w}"')
|
||||||
|
def pod_labeled(context, name, label):
|
||||||
|
assert label in context.dcs_ctl.pod_labels(name), f'pod {name} is not labeled with {label}'
|
||||||
|
|
||||||
|
|
||||||
|
@then('{name:name} is not labeled with "{label:w}"')
|
||||||
|
def pod_not_labeled(context, name, label):
|
||||||
|
assert label not in context.dcs_ctl.pod_labels(name), f'pod {name} is still labeled with {label}'
|
||||||
@@ -4,18 +4,18 @@ import time
|
|||||||
from behave import step, then
|
from behave import step, then
|
||||||
|
|
||||||
|
|
||||||
@step('I configure and start {name:w} with a tag {tag_name:w} {tag_value:w}')
|
@step('I configure and start {name:name} with a tag {tag_name:w} {tag_value}')
|
||||||
def start_patroni_with_a_name_value_tag(context, name, tag_name, tag_value):
|
def start_patroni_with_a_name_value_tag(context, name, tag_name, tag_value):
|
||||||
return context.pctl.start(name, custom_config={'tags': {tag_name: tag_value}})
|
return context.pctl.start(name, custom_config={'tags': {tag_name: tag_value}})
|
||||||
|
|
||||||
|
|
||||||
@then('There is a {label} with "{content}" in {name:w} data directory')
|
@then('There is a {label} with "{content}" in {name:name} data directory')
|
||||||
def check_label(context, label, content, name):
|
def check_label(context, label, content, name):
|
||||||
value = (context.pctl.read_label(name, label) or '').replace('\n', '\\n')
|
value = (context.pctl.read_label(name, label) or '').replace('\n', '\\n')
|
||||||
assert content in value, "\"{0}\" in {1} doesn't contain {2}".format(value, label, content)
|
assert content in value, "\"{0}\" in {1} doesn't contain {2}".format(value, label, content)
|
||||||
|
|
||||||
|
|
||||||
@step('I create label with "{content:w}" in {name:w} data directory')
|
@step('I create label with "{content}" in {name:name} data directory')
|
||||||
def write_label(context, content, name):
|
def write_label(context, content, name):
|
||||||
context.pctl.write_label(name, content)
|
context.pctl.write_label(name, content)
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ def check_member(context, name, key, value, time_limit):
|
|||||||
while time.time() < max_time:
|
while time.time() < max_time:
|
||||||
try:
|
try:
|
||||||
response = json.loads(context.dcs_ctl.query(name))
|
response = json.loads(context.dcs_ctl.query(name))
|
||||||
dcs_value = response.get(key)
|
dcs_value = str(response.get(key))
|
||||||
if dcs_value == value:
|
if dcs_value == value:
|
||||||
return
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
+44
-25
@@ -1,17 +1,18 @@
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from behave import step, then
|
|
||||||
from dateutil import tz
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from threading import Thread, Event
|
from threading import Event, Thread
|
||||||
|
|
||||||
|
from behave import step, then
|
||||||
|
from dateutil import tz
|
||||||
|
|
||||||
tzutc = tz.tzutc()
|
tzutc = tz.tzutc()
|
||||||
|
|
||||||
|
|
||||||
@step('{name:w} is a leader in a group {group:d} after {time_limit:d} seconds')
|
@step('{name:name} is a leader in a group {group:d} after {time_limit:d} seconds')
|
||||||
@then('{name:w} is a leader in a group {group:d} after {time_limit:d} seconds')
|
@then('{name:name} is a leader in a group {group:d} after {time_limit:d} seconds')
|
||||||
def is_a_group_leader(context, name, group, time_limit):
|
def is_a_group_leader(context, name, group, time_limit):
|
||||||
time_limit *= context.timeout_multiplier
|
time_limit *= context.timeout_multiplier
|
||||||
max_time = time.time() + int(time_limit)
|
max_time = time.time() + int(time_limit)
|
||||||
@@ -35,30 +36,42 @@ def check_group_member(context, name, group, key, value, time_limit):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
assert False, ("{0} in a group {1} does not have {2}={3} (found {4}) in dcs" +
|
assert False, ("{0} in a group {1} does not have {2}={3} (found {4}) in dcs"
|
||||||
" after {5} seconds").format(name, group, key, value, response, time_limit)
|
" after {5} seconds").format(name, group, key, value, response, time_limit)
|
||||||
|
|
||||||
|
|
||||||
@step('I start {name:w} in citus group {group:d}')
|
@step('I start {name:name} in citus group {group:d}')
|
||||||
def start_citus(context, name, group):
|
def start_citus(context, name, group):
|
||||||
return context.pctl.start(name, custom_config={"citus": {"database": "postgres", "group": int(group)}})
|
return context.pctl.start(name, custom_config={"citus": {"database": "postgres", "group": int(group)}})
|
||||||
|
|
||||||
|
|
||||||
@step('{name1:w} is registered in the coordinator {name2:w} as the worker in group {group:d}')
|
@step('{name1:name} is registered in the {name2:name} as the {role:w} in group {group:d} after {time_limit:d} seconds')
|
||||||
def check_registration(context, name1, name2, group):
|
def check_registration(context, name1, name2, role, group, time_limit):
|
||||||
|
time_limit *= context.timeout_multiplier
|
||||||
|
max_time = time.time() + int(time_limit)
|
||||||
|
|
||||||
worker_port = int(context.pctl.query(name1, "SHOW port").fetchone()[0])
|
worker_port = int(context.pctl.query(name1, "SHOW port").fetchone()[0])
|
||||||
r = context.pctl.query(name2, "SELECT nodeport FROM pg_catalog.pg_dist_node WHERE groupid = {0}".format(group))
|
|
||||||
assert worker_port == r.fetchone()[0],\
|
while time.time() < max_time:
|
||||||
"Worker {0} is not registered in pg_dist_node on the coordinator {1}".format(name1, name2)
|
try:
|
||||||
|
cur = context.pctl.query(name2, "SELECT nodeport, noderole"
|
||||||
|
" FROM pg_catalog.pg_dist_node WHERE groupid = {0}".format(group))
|
||||||
|
mapping = {r[0]: r[1] for r in cur}
|
||||||
|
if mapping.get(worker_port) == role:
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(1)
|
||||||
|
assert False, "Node {0} is not registered in pg_dist_node on the node {1}".format(name1, name2)
|
||||||
|
|
||||||
|
|
||||||
@step('I create a distributed table on {name:w}')
|
@step('I create a distributed table on {name:name}')
|
||||||
def create_distributed_table(context, name):
|
def create_distributed_table(context, name):
|
||||||
context.pctl.query(name, 'CREATE TABLE public.d(id int not null)')
|
context.pctl.query(name, 'CREATE TABLE public.d(id int not null)')
|
||||||
context.pctl.query(name, "SELECT create_distributed_table('public.d', 'id')")
|
context.pctl.query(name, "SELECT create_distributed_table('public.d', 'id')")
|
||||||
|
|
||||||
|
|
||||||
@step('I cleanup a distributed table on {name:w}')
|
@step('I cleanup a distributed table on {name:name}')
|
||||||
def cleanup_distributed_table(context, name):
|
def cleanup_distributed_table(context, name):
|
||||||
context.pctl.query(name, 'TRUNCATE public.d')
|
context.pctl.query(name, 'TRUNCATE public.d')
|
||||||
|
|
||||||
@@ -74,7 +87,7 @@ def insert_thread(query_func, context):
|
|||||||
context.thread_stop_event.wait(0.01)
|
context.thread_stop_event.wait(0.01)
|
||||||
|
|
||||||
|
|
||||||
@step('I start a thread inserting data on {name:w}')
|
@step('I start a thread inserting data on {name:name}')
|
||||||
def start_insert_thread(context, name):
|
def start_insert_thread(context, name):
|
||||||
context.thread_stop_event = Event()
|
context.thread_stop_event = Event()
|
||||||
context.insert_counter = 0
|
context.insert_counter = 0
|
||||||
@@ -93,25 +106,31 @@ def thread_is_alive(context):
|
|||||||
@step("I stop a thread")
|
@step("I stop a thread")
|
||||||
def stop_insert_thread(context):
|
def stop_insert_thread(context):
|
||||||
context.thread_stop_event.set()
|
context.thread_stop_event.set()
|
||||||
context.thread.join(1*context.timeout_multiplier)
|
context.thread.join(1 * context.timeout_multiplier)
|
||||||
assert not context.thread.is_alive(), "Thread is still alive"
|
assert not context.thread.is_alive(), "Thread is still alive"
|
||||||
|
|
||||||
|
|
||||||
@step("a distributed table on {name:w} has expected rows")
|
@step("a distributed table on {name:name} has expected rows")
|
||||||
def count_rows(context, name):
|
def count_rows(context, name):
|
||||||
rows = context.pctl.query(name, "SELECT COUNT(*) FROM public.d").fetchone()[0]
|
rows = context.pctl.query(name, "SELECT COUNT(*) FROM public.d").fetchone()[0]
|
||||||
assert rows == context.insert_counter, "Distributed table doesn't have expected amount of rows"
|
assert rows == context.insert_counter, "Distributed table doesn't have expected amount of rows"
|
||||||
|
|
||||||
|
|
||||||
@step("There is a transaction in progress on {name:w} changing pg_dist_node")
|
@step("there is a transaction in progress on {name:name} changing pg_dist_node after {time_limit:d} seconds")
|
||||||
def check_transaction(context, name):
|
def check_transaction(context, name, time_limit):
|
||||||
cur = context.pctl.query(name, "SELECT xact_start FROM pg_stat_activity WHERE pid <> pg_backend_pid()"
|
time_limit *= context.timeout_multiplier
|
||||||
" AND state = 'idle in transaction' AND query ~ 'citus_update_node'")
|
max_time = time.time() + int(time_limit)
|
||||||
assert cur.rowcount == 1, "There is no idle in transaction updating pg_dist_node"
|
while time.time() < max_time:
|
||||||
context.xact_start = cur.fetchone()[0]
|
cur = context.pctl.query(name, "SELECT xact_start FROM pg_stat_activity WHERE pid <> pg_backend_pid()"
|
||||||
|
" AND state = 'idle in transaction' AND query ~ 'citus_update_node'")
|
||||||
|
if cur.rowcount == 1:
|
||||||
|
context.xact_start = cur.fetchone()[0]
|
||||||
|
return
|
||||||
|
time.sleep(1)
|
||||||
|
assert False, f"There is no idle in transaction on {name} updating pg_dist_node after {time_limit} seconds"
|
||||||
|
|
||||||
|
|
||||||
@step("a transaction finishes in {timeout:d} seconds")
|
@step("a transaction finishes in {timeout:d} seconds")
|
||||||
def check_transaction_timeout(context, timeout):
|
def check_transaction_timeout(context, timeout):
|
||||||
assert (datetime.now(tzutc) - context.xact_start).seconds > timeout,\
|
assert (datetime.now(tzutc) - context.xact_start).seconds >= timeout, \
|
||||||
"a transaction finished earlier than in {0} seconds".format(timeout)
|
"a transaction finished earlier than in {0} seconds".format(timeout)
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ import time
|
|||||||
from behave import step, then
|
from behave import step, then
|
||||||
|
|
||||||
|
|
||||||
@step('I start {name:w} in a cluster {cluster_name:w} as a clone of {name2:w}')
|
@step('I start {name:name} in a cluster {cluster_name:w} as a clone of {name2:name}')
|
||||||
def start_cluster_clone(context, name, cluster_name, name2):
|
def start_cluster_clone(context, name, cluster_name, name2):
|
||||||
context.pctl.clone(name2, cluster_name, name)
|
context.pctl.clone(name2, cluster_name, name)
|
||||||
|
|
||||||
|
|
||||||
@step('I start {name:w} in a cluster {cluster_name:w} from backup')
|
@step('I start {name:name} in a cluster {cluster_name:w} from backup')
|
||||||
def start_cluster_from_backup(context, name, cluster_name):
|
def start_cluster_from_backup(context, name, cluster_name):
|
||||||
context.pctl.bootstrap_from_backup(name, cluster_name)
|
context.pctl.bootstrap_from_backup(name, cluster_name)
|
||||||
|
|
||||||
|
|
||||||
@then('{name:w} is a leader of {cluster_name:w} after {time_limit:d} seconds')
|
@then('{name:name} is a leader of {cluster_name:w} after {time_limit:d} seconds')
|
||||||
def is_a_leader(context, name, cluster_name, time_limit):
|
def is_a_leader(context, name, cluster_name, time_limit):
|
||||||
time_limit *= context.timeout_multiplier
|
time_limit *= context.timeout_multiplier
|
||||||
max_time = time.time() + int(time_limit)
|
max_time = time.time() + int(time_limit)
|
||||||
@@ -22,6 +22,6 @@ def is_a_leader(context, name, cluster_name, time_limit):
|
|||||||
assert time.time() < max_time, "{0} is not a leader in dcs after {1} seconds".format(name, time_limit)
|
assert time.time() < max_time, "{0} is not a leader in dcs after {1} seconds".format(name, time_limit)
|
||||||
|
|
||||||
|
|
||||||
@step('I do a backup of {name:w}')
|
@step('I do a backup of {name:name}')
|
||||||
def do_backup(context, name):
|
def do_backup(context, name):
|
||||||
context.pctl.backup(name)
|
context.pctl.backup(name)
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ def stop_dcs_outage(context):
|
|||||||
context.dcs_ctl.stop_outage()
|
context.dcs_ctl.stop_outage()
|
||||||
|
|
||||||
|
|
||||||
@step('I start {name:w} in a cluster {cluster_name:w} from backup with no_leader')
|
@step('I start {name:name} in a cluster {cluster_name:w} from backup with no_leader')
|
||||||
def start_cluster_from_backup_no_leader(context, name, cluster_name):
|
def start_cluster_from_backup_no_leader(context, name, cluster_name):
|
||||||
context.pctl.bootstrap_from_backup_no_leader(name, cluster_name)
|
context.pctl.bootstrap_from_backup_no_leader(name, cluster_name)
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import json
|
import json
|
||||||
import parse
|
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import parse
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from behave import register_type, step, then
|
from behave import register_type, step, then
|
||||||
from dateutil import tz
|
from dateutil import tz
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
tzutc = tz.tzutc()
|
tzutc = tz.tzutc()
|
||||||
|
|
||||||
@@ -26,8 +28,8 @@ register_type(url=parse_url)
|
|||||||
# just rely on the database availability, since there is
|
# just rely on the database availability, since there is
|
||||||
# a short gap between the time PostgreSQL becomes available
|
# a short gap between the time PostgreSQL becomes available
|
||||||
# and Patroni assuming the leader role.
|
# and Patroni assuming the leader role.
|
||||||
@step('{name:w} is a leader after {time_limit:d} seconds')
|
@step('{name:name} is a leader after {time_limit:d} seconds')
|
||||||
@then('{name:w} is a leader after {time_limit:d} seconds')
|
@then('{name:name} is a leader after {time_limit:d} seconds')
|
||||||
def is_a_leader(context, name, time_limit):
|
def is_a_leader(context, name, time_limit):
|
||||||
time_limit *= context.timeout_multiplier
|
time_limit *= context.timeout_multiplier
|
||||||
max_time = time.time() + int(time_limit)
|
max_time = time.time() + int(time_limit)
|
||||||
@@ -95,10 +97,10 @@ def do_run(context, cmd):
|
|||||||
context.response = response.decode('utf-8').strip()
|
context.response = response.decode('utf-8').strip()
|
||||||
|
|
||||||
|
|
||||||
@then('I receive a response {component:w} {data}')
|
@then('I receive a response {component:name} {data}')
|
||||||
def check_response(context, component, data):
|
def check_response(context, component, data):
|
||||||
if component == 'code':
|
if component == 'code':
|
||||||
assert context.status_code == int(data),\
|
assert context.status_code == int(data), \
|
||||||
"status code {0} != {1}, response: {2}".format(context.status_code, data, context.response)
|
"status code {0} != {1}, response: {2}".format(context.status_code, data, context.response)
|
||||||
elif component == 'returncode':
|
elif component == 'returncode':
|
||||||
assert context.status_code == int(data), "return code {0} != {1}, {2}".format(context.status_code,
|
assert context.status_code == int(data), "return code {0} != {1}, {2}".format(context.status_code,
|
||||||
@@ -114,10 +116,10 @@ def check_response(context, component, data):
|
|||||||
assert str(context.response[component]) == str(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 switchover from {from_host:w} to {to_host:w} in {in_seconds:d} seconds')
|
@step('I issue a scheduled switchover from {from_host:name} to {to_host:name} in {in_seconds:d} seconds')
|
||||||
def scheduled_switchover(context, from_host, to_host, in_seconds):
|
def scheduled_switchover(context, from_host, to_host, in_seconds):
|
||||||
context.execute_steps(u"""
|
context.execute_steps(u"""
|
||||||
Given I run patronictl.py switchover batman --master {0} --candidate {1} --scheduled "{2}" --force
|
Given I run patronictl.py switchover batman --primary {0} --candidate {1} --scheduled "{2}" --force
|
||||||
""".format(from_host, to_host, datetime.now(tzutc) + timedelta(seconds=int(in_seconds))))
|
""".format(from_host, to_host, datetime.now(tzutc) + timedelta(seconds=int(in_seconds))))
|
||||||
|
|
||||||
|
|
||||||
@@ -128,7 +130,13 @@ def scheduled_restart(context, url, in_seconds, data):
|
|||||||
context.execute_steps(u"""Given I issue a POST request to {0}/restart with {1}""".format(url, json.dumps(data)))
|
context.execute_steps(u"""Given I issue a POST request to {0}/restart with {1}""".format(url, json.dumps(data)))
|
||||||
|
|
||||||
|
|
||||||
@step('I add tag {tag:w} {value:w} to {pg_name:w} config')
|
@step('I {action:w} {tag:w} tag in {pg_name:name} config')
|
||||||
|
def add_bool_tag_to_config(context, action, tag, pg_name):
|
||||||
|
value = action == 'set'
|
||||||
|
context.pctl.add_tag_to_config(pg_name, tag, value)
|
||||||
|
|
||||||
|
|
||||||
|
@step('I add tag {tag:w} {value:w} to {pg_name:name} config')
|
||||||
def add_tag_to_config(context, tag, value, pg_name):
|
def add_tag_to_config(context, tag, value, pg_name):
|
||||||
context.pctl.add_tag_to_config(pg_name, tag, value)
|
context.pctl.add_tag_to_config(pg_name, tag, value)
|
||||||
|
|
||||||
@@ -152,13 +160,28 @@ def check_http_response(context, url, value, timeout, negate=False):
|
|||||||
if context.certfile:
|
if context.certfile:
|
||||||
url = url.replace('http://', 'https://')
|
url = url.replace('http://', 'https://')
|
||||||
timeout *= context.timeout_multiplier
|
timeout *= context.timeout_multiplier
|
||||||
|
if '=' in value:
|
||||||
|
key, val = value.split('=', 1)
|
||||||
|
else:
|
||||||
|
key, val = value, None
|
||||||
for _ in range(int(timeout)):
|
for _ in range(int(timeout)):
|
||||||
r = context.request_executor.request('GET', url)
|
r = context.request_executor.request('GET', url)
|
||||||
if (value in r.data.decode('utf-8')) != negate:
|
data = r.data.decode('utf-8')
|
||||||
|
if val is not None:
|
||||||
|
try:
|
||||||
|
data = json.loads(data)
|
||||||
|
if negate:
|
||||||
|
if key not in data or data[key] != val:
|
||||||
|
break
|
||||||
|
elif key in data and data[key] == val:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif (value in r.data.decode('utf-8')) != negate:
|
||||||
break
|
break
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
else:
|
else:
|
||||||
assert False,\
|
assert False, \
|
||||||
"Value {0} is {1} present in response after {2} seconds".format(value, "not" if not negate else "", timeout)
|
"Value {0} is {1} present in response after {2} seconds".format(value, "not" if not negate else "", timeout)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import json
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
|
from behave import step, then
|
||||||
|
|
||||||
|
|
||||||
|
@step('sync key in DCS has {key:w}={value} after {time_limit:d} seconds')
|
||||||
|
def check_sync(context, key, value, time_limit):
|
||||||
|
time_limit *= context.timeout_multiplier
|
||||||
|
max_time = time.time() + int(time_limit)
|
||||||
|
dcs_value = None
|
||||||
|
while time.time() < max_time:
|
||||||
|
try:
|
||||||
|
response = json.loads(context.dcs_ctl.query('sync'))
|
||||||
|
dcs_value = response.get(key)
|
||||||
|
if key == 'sync_standby' and set((dcs_value or '').split(',')) == set(value.split(',')):
|
||||||
|
return
|
||||||
|
elif str(dcs_value) == value:
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(1)
|
||||||
|
assert False, "sync does not have {0}={1} (found {2}) in dcs after {3} seconds".format(key, value,
|
||||||
|
dcs_value, time_limit)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_synchronous_standby_names(value):
|
||||||
|
if '(' in value:
|
||||||
|
m = re.match(r'.*(\d+) \(([^)]+)\)', value)
|
||||||
|
expected_value = set(m.group(2).split())
|
||||||
|
expected_num = m.group(1)
|
||||||
|
else:
|
||||||
|
expected_value = set([value])
|
||||||
|
expected_num = '1'
|
||||||
|
return expected_num, expected_value
|
||||||
|
|
||||||
|
|
||||||
|
@then("synchronous_standby_names on {name:2} is set to '{value}' after {time_limit:d} seconds")
|
||||||
|
def check_synchronous_standby_names(context, name, value, time_limit):
|
||||||
|
time_limit *= context.timeout_multiplier
|
||||||
|
max_time = time.time() + int(time_limit)
|
||||||
|
|
||||||
|
if value == '_empty_str_':
|
||||||
|
value = ''
|
||||||
|
|
||||||
|
expected_num, expected_value = _parse_synchronous_standby_names(value)
|
||||||
|
|
||||||
|
ssn = None
|
||||||
|
while time.time() < max_time:
|
||||||
|
try:
|
||||||
|
ssn = context.pctl.query(name, "SHOW synchronous_standby_names").fetchone()[0]
|
||||||
|
db_num, db_value = _parse_synchronous_standby_names(ssn)
|
||||||
|
if expected_value == db_value and expected_num == db_num:
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(1)
|
||||||
|
assert False, "synchronous_standby_names is not set to '{0}' (found '{1}') after {2} seconds".format(value, ssn,
|
||||||
|
time_limit)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from behave import step
|
||||||
|
|
||||||
|
|
||||||
|
@step('I ensure {name:name} fails to start after a failure')
|
||||||
|
def spoil_autoconf(context, name):
|
||||||
|
with open(os.path.join(context.pctl._processes[name]._data_dir, 'postgresql.auto.conf'), 'w') as f:
|
||||||
|
f.write('foo=bar')
|
||||||
+90
-25
@@ -1,35 +1,44 @@
|
|||||||
|
import json
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from behave import step, then
|
from behave import step, then
|
||||||
|
|
||||||
import patroni.psycopg as pg
|
import patroni.psycopg as pg
|
||||||
|
|
||||||
|
|
||||||
@step('I create a logical replication slot {slot_name} on {pg_name:w} with the {plugin:w} plugin')
|
@step('I create a logical {slot_type} slot {slot_name} on {pg_name:name} with the {plugin:w} plugin')
|
||||||
def create_logical_replication_slot(context, slot_name, pg_name, plugin):
|
def create_logical_replication_slot(context, slot_type, slot_name, pg_name, plugin):
|
||||||
|
failover = ', failover=>true' if slot_type == 'failover' else ''
|
||||||
try:
|
try:
|
||||||
output = context.pctl.query(pg_name, ("SELECT pg_create_logical_replication_slot('{0}', '{1}'),"
|
context.pctl.query(pg_name, f"SELECT pg_create_logical_replication_slot('{slot_name}', '{plugin}'{failover})")
|
||||||
" current_database()").format(slot_name, plugin))
|
|
||||||
print(output.fetchone())
|
|
||||||
except pg.Error as e:
|
except pg.Error as e:
|
||||||
print(e)
|
assert False, "Error creating slot {0} on {1} with plugin {2}: {3}".format(slot_name, pg_name, plugin, e)
|
||||||
assert False, "Error creating slot {0} on {1} with plugin {2}".format(slot_name, pg_name, plugin)
|
|
||||||
|
|
||||||
|
|
||||||
@then('{pg_name:w} has a logical replication slot named {slot_name} with the {plugin:w} plugin')
|
@step('{pg_name:name} has a logical replication slot named {slot_name}'
|
||||||
def has_logical_replication_slot(context, pg_name, slot_name, plugin):
|
' with the {plugin:w} plugin after {time_limit:d} seconds')
|
||||||
try:
|
@then('{pg_name:name} has a logical replication slot named {slot_name}'
|
||||||
row = context.pctl.query(pg_name, ("SELECT slot_type, plugin FROM pg_replication_slots"
|
' with the {plugin:w} plugin after {time_limit:d} seconds')
|
||||||
" WHERE slot_name = '{0}'").format(slot_name)).fetchone()
|
def has_logical_replication_slot(context, pg_name, slot_name, plugin, time_limit):
|
||||||
assert row, "Couldn't find replication slot named {0}".format(slot_name)
|
time_limit *= context.timeout_multiplier
|
||||||
assert row[0] == "logical", "Found replication slot named {0} but wasn't a logical slot".format(slot_name)
|
max_time = time.time() + int(time_limit)
|
||||||
assert row[1] == plugin, ("Found replication slot named {0} but was using plugin "
|
while time.time() < max_time:
|
||||||
"{1} rather than {2}").format(slot_name, row[1], plugin)
|
try:
|
||||||
except pg.Error:
|
row = context.pctl.query(pg_name, ("SELECT slot_type, plugin FROM pg_replication_slots"
|
||||||
assert False, "Error looking for slot {0} on {1} with plugin {2}".format(slot_name, pg_name, plugin)
|
f" WHERE slot_name = '{slot_name}'")).fetchone()
|
||||||
|
if row:
|
||||||
|
assert row[0] == "logical", f"Replication slot {slot_name} isn't a logical but {row[0]}"
|
||||||
|
assert row[1] == plugin, f"Replication slot {slot_name} using plugin {row[1]} rather than {plugin}"
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(1)
|
||||||
|
assert False, f"Error looking for slot {slot_name} on {pg_name} with plugin {plugin}"
|
||||||
|
|
||||||
|
|
||||||
@then('{pg_name:w} does not have a logical replication slot named {slot_name}')
|
@step('{pg_name:name} does not have a replication slot named {slot_name:w}')
|
||||||
def does_not_have_logical_replication_slot(context, pg_name, slot_name):
|
@then('{pg_name:name} does not have a replication slot named {slot_name:w}')
|
||||||
|
def does_not_have_replication_slot(context, pg_name, slot_name):
|
||||||
try:
|
try:
|
||||||
row = context.pctl.query(pg_name, ("SELECT 1 FROM pg_replication_slots"
|
row = context.pctl.query(pg_name, ("SELECT 1 FROM pg_replication_slots"
|
||||||
" WHERE slot_name = '{0}'").format(slot_name)).fetchone()
|
" WHERE slot_name = '{0}'").format(slot_name)).fetchone()
|
||||||
@@ -38,13 +47,15 @@ def does_not_have_logical_replication_slot(context, pg_name, slot_name):
|
|||||||
assert False, "Error looking for slot {0} on {1}".format(slot_name, pg_name)
|
assert False, "Error looking for slot {0} on {1}".format(slot_name, pg_name)
|
||||||
|
|
||||||
|
|
||||||
@step('Logical slot {slot_name:w} is in sync between {pg_name1:w} and {pg_name2:w} after {time_limit:d} seconds')
|
@step('{slot_type:w} slot {slot_name:w} is in sync between '
|
||||||
def logical_slots_in_sync(context, slot_name, pg_name1, pg_name2, time_limit):
|
'{pg_name1:name} and {pg_name2:name} after {time_limit:d} seconds')
|
||||||
|
def slots_in_sync(context, slot_type, slot_name, pg_name1, pg_name2, time_limit):
|
||||||
time_limit *= context.timeout_multiplier
|
time_limit *= context.timeout_multiplier
|
||||||
max_time = time.time() + int(time_limit)
|
max_time = time.time() + int(time_limit)
|
||||||
|
column = 'confirmed_flush_lsn' if slot_type.lower() == 'logical' else 'restart_lsn'
|
||||||
|
query = f"SELECT {column} FROM pg_replication_slots WHERE slot_name = '{slot_name}'"
|
||||||
while time.time() < max_time:
|
while time.time() < max_time:
|
||||||
try:
|
try:
|
||||||
query = "SELECT confirmed_flush_lsn FROM pg_replication_slots WHERE slot_name = '{0}'".format(slot_name)
|
|
||||||
slot1 = context.pctl.query(pg_name1, query).fetchone()
|
slot1 = context.pctl.query(pg_name1, query).fetchone()
|
||||||
slot2 = context.pctl.query(pg_name2, query).fetchone()
|
slot2 = context.pctl.query(pg_name2, query).fetchone()
|
||||||
if slot1[0] == slot2[0]:
|
if slot1[0] == slot2[0]:
|
||||||
@@ -52,9 +63,63 @@ def logical_slots_in_sync(context, slot_name, pg_name1, pg_name2, time_limit):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
assert False, "Logical slot {0} is not in sync between {1} and {2}".format(slot_name, pg_name1, pg_name2)
|
assert False, \
|
||||||
|
f"{slot_type} slot {slot_name} is not in sync between {pg_name1} and {pg_name2} after {time_limit} seconds"
|
||||||
|
|
||||||
|
|
||||||
@step('I get all changes from logical slot {slot_name:w} on {pg_name:w}')
|
@step('I get all changes from logical slot {slot_name:w} on {pg_name:name}')
|
||||||
def logical_slot_get_changes(context, slot_name, pg_name):
|
def logical_slot_get_changes(context, slot_name, pg_name):
|
||||||
context.pctl.query(pg_name, "SELECT * FROM pg_logical_slot_get_changes('{0}', NULL, NULL)".format(slot_name))
|
context.pctl.query(pg_name, "SELECT * FROM pg_logical_slot_get_changes('{0}', NULL, NULL)".format(slot_name))
|
||||||
|
|
||||||
|
|
||||||
|
@step('I get all changes from physical slot {slot_name:w} on {pg_name:name}')
|
||||||
|
def physical_slot_get_changes(context, slot_name, pg_name):
|
||||||
|
context.pctl.query(pg_name, f"SELECT * FROM pg_replication_slot_advance('{slot_name}', pg_current_wal_lsn())")
|
||||||
|
|
||||||
|
|
||||||
|
@step('{pg_name:name} has a physical replication slot named {slot_name} after {time_limit:d} seconds')
|
||||||
|
def has_physical_replication_slot(context, pg_name, slot_name, time_limit):
|
||||||
|
time_limit *= context.timeout_multiplier
|
||||||
|
max_time = time.time() + int(time_limit)
|
||||||
|
query = f"SELECT * FROM pg_catalog.pg_replication_slots WHERE slot_type = 'physical' AND slot_name = '{slot_name}'"
|
||||||
|
while time.time() < max_time:
|
||||||
|
try:
|
||||||
|
row = context.pctl.query(pg_name, query).fetchone()
|
||||||
|
if row:
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(1)
|
||||||
|
assert False, f"Physical slot {slot_name} doesn't exist after {time_limit} seconds"
|
||||||
|
|
||||||
|
|
||||||
|
@step('physical replication slot named {slot_name} on {pg_name:name} has no xmin value after {time_limit:d} seconds')
|
||||||
|
def physical_slot_no_xmin(context, pg_name, slot_name, time_limit):
|
||||||
|
time_limit *= context.timeout_multiplier
|
||||||
|
max_time = time.time() + int(time_limit)
|
||||||
|
query = "SELECT xmin FROM pg_catalog.pg_replication_slots WHERE slot_type = 'physical'" +\
|
||||||
|
f" AND slot_name = '{slot_name}'"
|
||||||
|
exists = False
|
||||||
|
while time.time() < max_time:
|
||||||
|
try:
|
||||||
|
row = context.pctl.query(pg_name, query).fetchone()
|
||||||
|
exists = bool(row)
|
||||||
|
if exists and row[0] is None:
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(1)
|
||||||
|
assert False, f"Physical slot {slot_name} doesn't exist after {time_limit} seconds" if not exists \
|
||||||
|
else f"Physical slot {slot_name} has xmin value after {time_limit} seconds"
|
||||||
|
|
||||||
|
|
||||||
|
@step('"{name}" key in DCS has {subkey} in {key:w}')
|
||||||
|
def dcs_key_contains(context, name, subkey, key):
|
||||||
|
response = json.loads(context.dcs_ctl.query(name))
|
||||||
|
assert key in response and subkey in response[key], f"{name} key in DCS doesn't have {subkey} in {key}"
|
||||||
|
|
||||||
|
|
||||||
|
@step('"{name}" key in DCS does not have {subkey} in {key:w}')
|
||||||
|
def dcs_key_does_not_contain(context, name, subkey, key):
|
||||||
|
response = json.loads(context.dcs_ctl.query(name))
|
||||||
|
assert key not in response or subkey not in response[key], f"{name} key in DCS has {subkey} in {key}"
|
||||||
|
|||||||
@@ -9,20 +9,18 @@ def callbacks(context, name):
|
|||||||
for c in ('on_start', 'on_stop', 'on_restart', 'on_role_change')}
|
for c in ('on_start', 'on_stop', 'on_restart', 'on_role_change')}
|
||||||
|
|
||||||
|
|
||||||
@step('I start {name:w} in a cluster {cluster_name:w}')
|
@step('I start {name:name} in a cluster {cluster_name:w}')
|
||||||
def start_patroni(context, name, cluster_name):
|
def start_patroni(context, name, cluster_name):
|
||||||
return context.pctl.start(name, custom_config={
|
return context.pctl.start(name, custom_config={
|
||||||
"scope": cluster_name,
|
"scope": cluster_name,
|
||||||
"postgresql": {
|
"postgresql": {
|
||||||
"callbacks": callbacks(context, name),
|
"callbacks": callbacks(context, name),
|
||||||
"backup_restore": {
|
"backup_restore": context.pctl.backup_restore_config()
|
||||||
"command": (context.pctl.PYTHON + " features/backup_restore.py --sourcedir=" +
|
|
||||||
os.path.join(context.pctl.patroni_path, 'data', 'basebackup').replace('\\', '/'))}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@step('I start {name:w} in a standby cluster {cluster_name:w} as a clone of {name2:w}')
|
@step('I start {name:name} in a standby cluster {cluster_name:w} as a clone of {name2:name}')
|
||||||
def start_patroni_standby_cluster(context, name, cluster_name, name2):
|
def start_patroni_standby_cluster(context, name, cluster_name, name2):
|
||||||
# we need to remove patroni.dynamic.json in order to "bootstrap" standby cluster with existing PGDATA
|
# we need to remove patroni.dynamic.json in order to "bootstrap" standby cluster with existing PGDATA
|
||||||
os.unlink(os.path.join(context.pctl._processes[name]._data_dir, 'patroni.dynamic.json'))
|
os.unlink(os.path.join(context.pctl._processes[name]._data_dir, 'patroni.dynamic.json'))
|
||||||
@@ -34,6 +32,7 @@ def start_patroni_standby_cluster(context, name, cluster_name, name2):
|
|||||||
"ttl": 20,
|
"ttl": 20,
|
||||||
"loop_wait": 2,
|
"loop_wait": 2,
|
||||||
"retry_timeout": 5,
|
"retry_timeout": 5,
|
||||||
|
"synchronous_mode": True, # should be completely ignored
|
||||||
"standby_cluster": {
|
"standby_cluster": {
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": port,
|
"port": port,
|
||||||
@@ -50,7 +49,7 @@ def start_patroni_standby_cluster(context, name, cluster_name, name2):
|
|||||||
return context.pctl.start(name)
|
return context.pctl.start(name)
|
||||||
|
|
||||||
|
|
||||||
@step('{pg_name1:w} is replicating from {pg_name2:w} after {timeout:d} seconds')
|
@step('{pg_name1:name} is replicating from {pg_name2:name} after {timeout:d} seconds')
|
||||||
def check_replication_status(context, pg_name1, pg_name2, timeout):
|
def check_replication_status(context, pg_name1, pg_name2, timeout):
|
||||||
bound_time = time.time() + timeout * context.timeout_multiplier
|
bound_time = time.time() + timeout * context.timeout_multiplier
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from behave import step, then
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from behave import step, then
|
||||||
|
|
||||||
|
|
||||||
def polling_loop(timeout, interval=1):
|
def polling_loop(timeout, interval=1):
|
||||||
"""Returns an iterator that returns values until timeout has passed. Timeout is measured from start of iteration."""
|
"""Returns an iterator that returns values until timeout has passed. Timeout is measured from start of iteration."""
|
||||||
@@ -13,12 +14,12 @@ def polling_loop(timeout, interval=1):
|
|||||||
time.sleep(interval)
|
time.sleep(interval)
|
||||||
|
|
||||||
|
|
||||||
@step('I start {name:w} with watchdog')
|
@step('I start {name:name} with watchdog')
|
||||||
def start_patroni_with_watchdog(context, name):
|
def start_patroni_with_watchdog(context, name):
|
||||||
return context.pctl.start(name, custom_config={'watchdog': True, 'bootstrap': {'dcs': {'ttl': 20}}})
|
return context.pctl.start(name, custom_config={'watchdog': True, 'bootstrap': {'dcs': {'ttl': 20}}})
|
||||||
|
|
||||||
|
|
||||||
@step('{name:w} watchdog has been pinged after {timeout:d} seconds')
|
@step('{name:name} watchdog has been pinged after {timeout:d} seconds')
|
||||||
def watchdog_was_pinged(context, name, timeout):
|
def watchdog_was_pinged(context, name, timeout):
|
||||||
for _ in polling_loop(timeout):
|
for _ in polling_loop(timeout):
|
||||||
if context.pctl.get_watchdog(name).was_pinged:
|
if context.pctl.get_watchdog(name).was_pinged:
|
||||||
@@ -26,22 +27,22 @@ def watchdog_was_pinged(context, name, timeout):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@then('{name:w} watchdog has been closed')
|
@then('{name:name} watchdog has been closed')
|
||||||
def watchdog_was_closed(context, name):
|
def watchdog_was_closed(context, name):
|
||||||
assert context.pctl.get_watchdog(name).was_closed
|
assert context.pctl.get_watchdog(name).was_closed
|
||||||
|
|
||||||
|
|
||||||
@step('{name:w} watchdog has a {timeout:d} second timeout')
|
@step('{name:name} watchdog has a {timeout:d} second timeout')
|
||||||
def watchdog_has_timeout(context, name, timeout):
|
def watchdog_has_timeout(context, name, timeout):
|
||||||
assert context.pctl.get_watchdog(name).timeout == timeout
|
assert context.pctl.get_watchdog(name).timeout == timeout
|
||||||
|
|
||||||
|
|
||||||
@step('I reset {name:w} watchdog state')
|
@step('I reset {name:name} watchdog state')
|
||||||
def watchdog_reset_pinged(context, name):
|
def watchdog_reset_pinged(context, name):
|
||||||
context.pctl.get_watchdog(name).reset()
|
context.pctl.get_watchdog(name).reset()
|
||||||
|
|
||||||
|
|
||||||
@then('{name:w} watchdog is triggered after {timeout:d} seconds')
|
@then('{name:name} watchdog is triggered after {timeout:d} seconds')
|
||||||
def watchdog_was_triggered(context, name, timeout):
|
def watchdog_was_triggered(context, name, timeout):
|
||||||
for _ in polling_loop(timeout):
|
for _ in polling_loop(timeout):
|
||||||
if context.pctl.get_watchdog(name).was_triggered:
|
if context.pctl.get_watchdog(name).was_triggered:
|
||||||
@@ -49,6 +50,6 @@ def watchdog_was_triggered(context, name, timeout):
|
|||||||
assert False
|
assert False
|
||||||
|
|
||||||
|
|
||||||
@step('{name:w} hangs for {timeout:d} seconds')
|
@step('{name:name} hangs for {timeout:d} seconds')
|
||||||
def patroni_hang(context, name, timeout):
|
def patroni_hang(context, name, timeout):
|
||||||
return context.pctl.patroni_hang(name, timeout)
|
return context.pctl.patroni_hang(name, timeout)
|
||||||
|
|||||||
+16
-16
@@ -2,38 +2,38 @@ Feature: watchdog
|
|||||||
Verify that watchdog gets pinged and triggered under appropriate circumstances.
|
Verify that watchdog gets pinged and triggered under appropriate circumstances.
|
||||||
|
|
||||||
Scenario: watchdog is opened and pinged
|
Scenario: watchdog is opened and pinged
|
||||||
Given I start postgres0 with watchdog
|
Given I start postgres-0 with watchdog
|
||||||
Then postgres0 is a leader after 10 seconds
|
Then postgres-0 is a leader after 10 seconds
|
||||||
And postgres0 role is the primary after 10 seconds
|
And postgres-0 role is the primary after 10 seconds
|
||||||
And postgres0 watchdog has been pinged after 10 seconds
|
And postgres-0 watchdog has been pinged after 10 seconds
|
||||||
And postgres0 watchdog has a 15 second timeout
|
And postgres-0 watchdog has a 15 second timeout
|
||||||
|
|
||||||
Scenario: watchdog is reconfigured after global ttl changed
|
Scenario: watchdog is reconfigured after global ttl changed
|
||||||
Given I run patronictl.py edit-config batman -s ttl=30 --force
|
Given I run patronictl.py edit-config batman -s ttl=30 --force
|
||||||
Then I receive a response returncode 0
|
Then I receive a response returncode 0
|
||||||
And I receive a response output "+ttl: 30"
|
And I receive a response output "+ttl: 30"
|
||||||
When I sleep for 4 seconds
|
When I sleep for 4 seconds
|
||||||
Then postgres0 watchdog has a 25 second timeout
|
Then postgres-0 watchdog has a 25 second timeout
|
||||||
|
|
||||||
Scenario: watchdog is disabled during pause
|
Scenario: watchdog is disabled during pause
|
||||||
Given I run patronictl.py pause batman
|
Given I run patronictl.py pause batman
|
||||||
Then I receive a response returncode 0
|
Then I receive a response returncode 0
|
||||||
When I sleep for 2 seconds
|
When I sleep for 2 seconds
|
||||||
Then postgres0 watchdog has been closed
|
Then postgres-0 watchdog has been closed
|
||||||
|
|
||||||
Scenario: watchdog is opened and pinged after resume
|
Scenario: watchdog is opened and pinged after resume
|
||||||
Given I reset postgres0 watchdog state
|
Given I reset postgres-0 watchdog state
|
||||||
And I run patronictl.py resume batman
|
And I run patronictl.py resume batman
|
||||||
Then I receive a response returncode 0
|
Then I receive a response returncode 0
|
||||||
And postgres0 watchdog has been pinged after 10 seconds
|
And postgres-0 watchdog has been pinged after 10 seconds
|
||||||
|
|
||||||
Scenario: watchdog is disabled when shutting down
|
Scenario: watchdog is disabled when shutting down
|
||||||
Given I shut down postgres0
|
Given I shut down postgres-0
|
||||||
Then postgres0 watchdog has been closed
|
Then postgres-0 watchdog has been closed
|
||||||
|
|
||||||
Scenario: watchdog is triggered if patroni stops responding
|
Scenario: watchdog is triggered if patroni stops responding
|
||||||
Given I reset postgres0 watchdog state
|
Given I reset postgres-0 watchdog state
|
||||||
And I start postgres0 with watchdog
|
And I start postgres-0 with watchdog
|
||||||
Then postgres0 role is the primary after 10 seconds
|
Then postgres-0 role is the primary after 10 seconds
|
||||||
When postgres0 hangs for 30 seconds
|
When postgres-0 hangs for 30 seconds
|
||||||
Then postgres0 watchdog is triggered after 30 seconds
|
Then postgres-0 watchdog is triggered after 30 seconds
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM postgres:15
|
FROM postgres:16
|
||||||
LABEL maintainer="Alexander Kukushkin <[email protected]>"
|
LABEL maintainer="Alexander Kukushkin <[email protected]>"
|
||||||
|
|
||||||
RUN export DEBIAN_FRONTEND=noninteractive \
|
RUN export DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -9,8 +9,8 @@ RUN export DEBIAN_FRONTEND=noninteractive \
|
|||||||
| xargs apt-get install -y vim-tiny curl jq locales git python3-pip python3-wheel \
|
| xargs apt-get install -y vim-tiny curl jq locales git python3-pip python3-wheel \
|
||||||
## Make sure we have a en_US.UTF-8 locale available
|
## Make sure we have a en_US.UTF-8 locale available
|
||||||
&& localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 \
|
&& localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 \
|
||||||
&& pip3 install setuptools \
|
&& pip3 install --break-system-packages setuptools \
|
||||||
&& pip3 install 'git+https://github.com/zalando/patroni.git#egg=patroni[kubernetes]' \
|
&& pip3 install --break-system-packages 'git+https://github.com/patroni/patroni.git#egg=patroni[kubernetes]' \
|
||||||
&& PGHOME=/home/postgres \
|
&& PGHOME=/home/postgres \
|
||||||
&& mkdir -p $PGHOME \
|
&& mkdir -p $PGHOME \
|
||||||
&& chown postgres $PGHOME \
|
&& chown postgres $PGHOME \
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM postgres:15
|
FROM postgres:16
|
||||||
LABEL maintainer="Alexander Kukushkin <[email protected]>"
|
LABEL maintainer="Alexander Kukushkin <[email protected]>"
|
||||||
|
|
||||||
RUN export DEBIAN_FRONTEND=noninteractive \
|
RUN export DEBIAN_FRONTEND=noninteractive \
|
||||||
@@ -7,13 +7,27 @@ RUN export DEBIAN_FRONTEND=noninteractive \
|
|||||||
&& apt-get upgrade -y \
|
&& apt-get upgrade -y \
|
||||||
&& apt-cache depends patroni | sed -n -e 's/.* Depends: \(python3-.\+\)$/\1/p' \
|
&& apt-cache depends patroni | sed -n -e 's/.* Depends: \(python3-.\+\)$/\1/p' \
|
||||||
| grep -Ev '^python3-(sphinx|etcd|consul|kazoo|kubernetes)' \
|
| grep -Ev '^python3-(sphinx|etcd|consul|kazoo|kubernetes)' \
|
||||||
| xargs apt-get install -y busybox vim-tiny curl jq less locales git python3-pip python3-wheel \
|
| xargs apt-get install -y busybox vim-tiny curl jq less locales git python3-pip python3-wheel lsb-release \
|
||||||
## Make sure we have a en_US.UTF-8 locale available
|
## Make sure we have a en_US.UTF-8 locale available
|
||||||
&& localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 \
|
&& localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 \
|
||||||
&& curl https://install.citusdata.com/community/deb.sh | bash \
|
&& if [ $(dpkg --print-architecture) = 'arm64' ]; then \
|
||||||
&& apt-get -y install postgresql-15-citus-11.2 \
|
apt-get install -y postgresql-server-dev-16 \
|
||||||
&& pip3 install setuptools \
|
gcc make autoconf \
|
||||||
&& pip3 install 'git+https://github.com/zalando/patroni.git#egg=patroni[kubernetes]' \
|
libc6-dev flex libcurl4-gnutls-dev \
|
||||||
|
libicu-dev libkrb5-dev liblz4-dev \
|
||||||
|
libpam0g-dev libreadline-dev libselinux1-dev\
|
||||||
|
libssl-dev libxslt1-dev libzstd-dev uuid-dev \
|
||||||
|
&& git clone -b "main" https://github.com/citusdata/citus.git \
|
||||||
|
&& MAKEFLAGS="-j $(grep -c ^processor /proc/cpuinfo)" \
|
||||||
|
&& cd citus && ./configure && make install && cd ../ && rm -rf /citus; \
|
||||||
|
else \
|
||||||
|
echo "deb [signed-by=/etc/apt/trusted.gpg.d/citusdata_community.gpg] https://packagecloud.io/citusdata/community/debian/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/citusdata_community.list \
|
||||||
|
&& curl -sL https://packagecloud.io/citusdata/community/gpgkey | gpg --dearmor > /etc/apt/trusted.gpg.d/citusdata_community.gpg \
|
||||||
|
&& apt-get update -y \
|
||||||
|
&& apt-get -y install postgresql-16-citus-12.1; \
|
||||||
|
fi \
|
||||||
|
&& pip3 install --break-system-packages setuptools \
|
||||||
|
&& pip3 install --break-system-packages 'git+https://github.com/patroni/patroni.git#egg=patroni[kubernetes]' \
|
||||||
&& PGHOME=/home/postgres \
|
&& PGHOME=/home/postgres \
|
||||||
&& mkdir -p $PGHOME \
|
&& mkdir -p $PGHOME \
|
||||||
&& chown postgres $PGHOME \
|
&& chown postgres $PGHOME \
|
||||||
@@ -24,6 +38,9 @@ RUN export DEBIAN_FRONTEND=noninteractive \
|
|||||||
&& chmod 664 /etc/passwd \
|
&& chmod 664 /etc/passwd \
|
||||||
# Clean up
|
# Clean up
|
||||||
&& apt-get remove -y git python3-pip python3-wheel \
|
&& apt-get remove -y git python3-pip python3-wheel \
|
||||||
|
postgresql-server-dev-16 gcc make autoconf \
|
||||||
|
libc6-dev flex libicu-dev libkrb5-dev liblz4-dev \
|
||||||
|
libpam0g-dev libreadline-dev libselinux1-dev libssl-dev libxslt1-dev libzstd-dev uuid-dev \
|
||||||
&& apt-get autoremove -y \
|
&& apt-get autoremove -y \
|
||||||
&& apt-get clean -y \
|
&& apt-get clean -y \
|
||||||
&& rm -rf /var/lib/apt/lists/* /root/.cache
|
&& rm -rf /var/lib/apt/lists/* /root/.cache
|
||||||
@@ -32,7 +49,9 @@ ADD entrypoint.sh /
|
|||||||
ENV PGSSLMODE=verify-ca PGSSLKEY=/etc/ssl/private/ssl-cert-snakeoil.key PGSSLCERT=/etc/ssl/certs/ssl-cert-snakeoil.pem PGSSLROOTCERT=/etc/ssl/certs/ssl-cert-snakeoil.pem
|
ENV PGSSLMODE=verify-ca PGSSLKEY=/etc/ssl/private/ssl-cert-snakeoil.key PGSSLCERT=/etc/ssl/certs/ssl-cert-snakeoil.pem PGSSLROOTCERT=/etc/ssl/certs/ssl-cert-snakeoil.pem
|
||||||
|
|
||||||
RUN sed -i 's/^postgresql:/&\n basebackup:\n checkpoint: fast/' /entrypoint.sh \
|
RUN sed -i 's/^postgresql:/&\n basebackup:\n checkpoint: fast/' /entrypoint.sh \
|
||||||
&& sed -i "s|^ postgresql:|&\n pg_hba:\n - local all all trust\n - hostssl replication all all md5 clientcert=$PGSSLMODE\n - hostssl all all all md5 clientcert=$PGSSLMODE\n parameters:\n max_connections: 100\n shared_buffers: 16MB\n ssl: 'on'\n ssl_ca_file: $PGSSLROOTCERT\n ssl_cert_file: $PGSSLCERT\n ssl_key_file: $PGSSLKEY\n citus.node_conninfo: 'sslrootcert=$PGSSLROOTCERT sslkey=$PGSSLKEY sslcert=$PGSSLCERT sslmode=$PGSSLMODE'|" /entrypoint.sh \
|
&& sed -i "s|^ postgresql:|&\n parameters:\n max_connections: 100\n shared_buffers: 16MB\n ssl: 'on'\n ssl_ca_file: $PGSSLROOTCERT\n ssl_cert_file: $PGSSLCERT\n ssl_key_file: $PGSSLKEY\n citus.node_conninfo: 'sslrootcert=$PGSSLROOTCERT sslkey=$PGSSLKEY sslcert=$PGSSLCERT sslmode=$PGSSLMODE'|" /entrypoint.sh \
|
||||||
|
&& sed -i 's/^ pg_hba:/&\n - local all all trust/' /entrypoint.sh \
|
||||||
|
&& sed -i "s/^\(.*\) \(.*\) \(.*\) \(.*\) \(.*\) md5.*$/\1 hostssl \3 \4 all md5 clientcert=$PGSSLMODE/" /entrypoint.sh \
|
||||||
&& sed -i "s#^ \(superuser\|replication\):#&\n sslmode: $PGSSLMODE\n sslkey: $PGSSLKEY\n sslcert: $PGSSLCERT\n sslrootcert: $PGSSLROOTCERT#" /entrypoint.sh
|
&& sed -i "s#^ \(superuser\|replication\):#&\n sslmode: $PGSSLMODE\n sslkey: $PGSSLKEY\n sslcert: $PGSSLCERT\n sslrootcert: $PGSSLROOTCERT#" /entrypoint.sh
|
||||||
|
|
||||||
EXPOSE 5432 8008
|
EXPOSE 5432 8008
|
||||||
|
|||||||
+28
-24
@@ -23,7 +23,7 @@ Example session:
|
|||||||
|
|
||||||
$ docker build -t patroni .
|
$ docker build -t patroni .
|
||||||
Sending build context to Docker daemon 138.8kB
|
Sending build context to Docker daemon 138.8kB
|
||||||
Step 1/9 : FROM postgres:15
|
Step 1/9 : FROM postgres:16
|
||||||
...
|
...
|
||||||
Successfully built e9bfe69c5d2b
|
Successfully built e9bfe69c5d2b
|
||||||
Successfully tagged patroni:latest
|
Successfully tagged patroni:latest
|
||||||
@@ -46,7 +46,7 @@ Example session:
|
|||||||
|
|
||||||
$ kubectl get pods -L role
|
$ kubectl get pods -L role
|
||||||
NAME READY STATUS RESTARTS AGE ROLE
|
NAME READY STATUS RESTARTS AGE ROLE
|
||||||
patronidemo-0 1/1 Running 0 34s master
|
patronidemo-0 1/1 Running 0 34s primary
|
||||||
patronidemo-1 1/1 Running 0 30s replica
|
patronidemo-1 1/1 Running 0 30s replica
|
||||||
patronidemo-2 1/1 Running 0 26s replica
|
patronidemo-2 1/1 Running 0 26s replica
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ Example session:
|
|||||||
|
|
||||||
demo@localhost:~/git/patroni/kubernetes$ docker build -f Dockerfile.citus -t patroni-citus-k8s .
|
demo@localhost:~/git/patroni/kubernetes$ docker build -f Dockerfile.citus -t patroni-citus-k8s .
|
||||||
Sending build context to Docker daemon 138.8kB
|
Sending build context to Docker daemon 138.8kB
|
||||||
Step 1/11 : FROM postgres:15
|
Step 1/11 : FROM postgres:16
|
||||||
...
|
...
|
||||||
Successfully built 8cd73e325028
|
Successfully built 8cd73e325028
|
||||||
Successfully tagged patroni-citus-k8s:latest
|
Successfully tagged patroni-citus-k8s:latest
|
||||||
@@ -119,36 +119,40 @@ Example session:
|
|||||||
|
|
||||||
$ kubectl get pods -l cluster-name=citusdemo -L role
|
$ kubectl get pods -l cluster-name=citusdemo -L role
|
||||||
NAME READY STATUS RESTARTS AGE ROLE
|
NAME READY STATUS RESTARTS AGE ROLE
|
||||||
citusdemo-0-0 1/1 Running 0 105s master
|
citusdemo-0-0 1/1 Running 0 105s primary
|
||||||
citusdemo-0-1 1/1 Running 0 101s replica
|
citusdemo-0-1 1/1 Running 0 101s replica
|
||||||
citusdemo-0-2 1/1 Running 0 96s replica
|
citusdemo-0-2 1/1 Running 0 96s replica
|
||||||
citusdemo-1-0 1/1 Running 0 105s master
|
citusdemo-1-0 1/1 Running 0 105s primary
|
||||||
citusdemo-1-1 1/1 Running 0 101s replica
|
citusdemo-1-1 1/1 Running 0 101s replica
|
||||||
citusdemo-2-0 1/1 Running 0 105s master
|
citusdemo-2-0 1/1 Running 0 105s primary
|
||||||
citusdemo-2-1 1/1 Running 0 101s replica
|
citusdemo-2-1 1/1 Running 0 101s replica
|
||||||
|
|
||||||
$ kubectl exec -ti citusdemo-0-0 -- bash
|
$ kubectl exec -ti citusdemo-0-0 -- bash
|
||||||
postgres@citusdemo-0-0:~$ patronictl list
|
postgres@citusdemo-0-0:~$ patronictl list
|
||||||
+ Citus cluster: citusdemo -----------+--------------+---------+----+-----------+
|
+ Citus cluster: citusdemo -----------+----------------+---------+----+-----------+
|
||||||
| Group | Member | Host | Role | State | TL | Lag in MB |
|
| Group | Member | Host | Role | State | TL | Lag in MB |
|
||||||
+-------+---------------+-------------+--------------+---------+----+-----------+
|
+-------+---------------+-------------+----------------+---------+----+-----------+
|
||||||
| 0 | citusdemo-0-0 | 10.244.0.10 | Leader | running | 1 | |
|
| 0 | citusdemo-0-0 | 10.244.0.10 | Leader | running | 1 | |
|
||||||
| 0 | citusdemo-0-1 | 10.244.0.12 | Replica | running | 1 | 0 |
|
| 0 | citusdemo-0-1 | 10.244.0.12 | Replica | running | 1 | 0 |
|
||||||
| 0 | citusdemo-0-2 | 10.244.0.14 | Sync Standby | running | 1 | 0 |
|
| 0 | citusdemo-0-2 | 10.244.0.14 | Quorum Standby | running | 1 | 0 |
|
||||||
| 1 | citusdemo-1-0 | 10.244.0.8 | Leader | running | 1 | |
|
| 1 | citusdemo-1-0 | 10.244.0.8 | Leader | running | 1 | |
|
||||||
| 1 | citusdemo-1-1 | 10.244.0.11 | Sync Standby | running | 1 | 0 |
|
| 1 | citusdemo-1-1 | 10.244.0.11 | Quorum Standby | running | 1 | 0 |
|
||||||
| 2 | citusdemo-2-0 | 10.244.0.9 | Leader | running | 1 | |
|
| 2 | citusdemo-2-0 | 10.244.0.9 | Leader | running | 1 | |
|
||||||
| 2 | citusdemo-2-1 | 10.244.0.13 | Sync Standby | running | 1 | 0 |
|
| 2 | citusdemo-2-1 | 10.244.0.13 | Quorum Standby | running | 1 | 0 |
|
||||||
+-------+---------------+-------------+--------------+---------+----+-----------+
|
+-------+---------------+-------------+----------------+---------+----+-----------+
|
||||||
|
|
||||||
postgres@citusdemo-0-0:~$ psql citus
|
postgres@citusdemo-0-0:~$ psql citus
|
||||||
psql (15.1 (Debian 15.1-1.pgdg110+1))
|
psql (16.4 (Debian 16.4-1.pgdg120+1))
|
||||||
Type "help" for help.
|
Type "help" for help.
|
||||||
|
|
||||||
citus=# table pg_dist_node;
|
citus=# table pg_dist_node;
|
||||||
nodeid | groupid | nodename | nodeport | noderack | hasmetadata | isactive | noderole | nodecluster | metadatasynced | shouldhaveshards
|
nodeid | groupid | nodename | nodeport | noderack | hasmetadata | isactive | noderole | nodecluster | metadatasynced | shouldhaveshards
|
||||||
--------+---------+-------------+----------+----------+-------------+----------+----------+-------------+----------------+------------------
|
--------+---------+-------------+----------+----------+-------------+----------+-----------+-------------+----------------+------------------
|
||||||
1 | 0 | 10.244.0.10 | 5432 | default | t | t | primary | default | t | f
|
1 | 0 | 10.244.0.10 | 5432 | default | t | t | primary | default | t | f
|
||||||
2 | 1 | 10.244.0.8 | 5432 | default | t | t | primary | default | t | t
|
2 | 1 | 10.244.0.8 | 5432 | default | t | t | primary | default | t | t
|
||||||
3 | 2 | 10.244.0.9 | 5432 | default | t | t | primary | default | t | t
|
3 | 2 | 10.244.0.9 | 5432 | default | t | t | primary | default | t | t
|
||||||
(3 rows)
|
4 | 0 | 10.244.0.14 | 5432 | default | t | t | secondary | default | t | f
|
||||||
|
5 | 0 | 10.244.0.12 | 5432 | default | t | t | secondary | default | t | f
|
||||||
|
6 | 1 | 10.244.0.11 | 5432 | default | t | t | secondary | default | t | t
|
||||||
|
7 | 2 | 10.244.0.13 | 5432 | default | t | t | secondary | default | t | t
|
||||||
|
(7 rows)
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ spec:
|
|||||||
serviceAccountName: citusdemo
|
serviceAccountName: citusdemo
|
||||||
containers:
|
containers:
|
||||||
- name: *cluster_name
|
- name: *cluster_name
|
||||||
image: patroni-citus-k8s # docker build -f Dockerfile.citus -t patroni-citus-k8s .
|
image: harbor.optimcloud.com/library/optim/patroni-citus-k8s # docker build -f Dockerfile.citus -t patroni-citus-k8s .
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
@@ -169,7 +169,7 @@ spec:
|
|||||||
serviceAccountName: citusdemo
|
serviceAccountName: citusdemo
|
||||||
containers:
|
containers:
|
||||||
- name: *cluster_name
|
- name: *cluster_name
|
||||||
image: patroni-citus-k8s # docker build -f Dockerfile.citus -t patroni-citus-k8s .
|
image: harbor.optimcloud.com/library/optim/patroni-citus-k8s # docker build -f Dockerfile.citus -t patroni-citus-k8s .
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
@@ -279,7 +279,7 @@ spec:
|
|||||||
serviceAccountName: citusdemo
|
serviceAccountName: citusdemo
|
||||||
containers:
|
containers:
|
||||||
- name: *cluster_name
|
- name: *cluster_name
|
||||||
image: patroni-citus-k8s # docker build -f Dockerfile.citus -t patroni-citus-k8s .
|
image: harbor.optimcloud.com/library/optim/patroni-citus-k8s # docker build -f Dockerfile.citus -t patroni-citus-k8s .
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
@@ -458,7 +458,7 @@ metadata:
|
|||||||
application: patroni
|
application: patroni
|
||||||
cluster-name: citusdemo
|
cluster-name: citusdemo
|
||||||
citus-type: worker
|
citus-type: worker
|
||||||
role: master
|
role: primary
|
||||||
spec:
|
spec:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
selector:
|
selector:
|
||||||
|
|||||||
@@ -12,15 +12,16 @@ bootstrap:
|
|||||||
dcs:
|
dcs:
|
||||||
postgresql:
|
postgresql:
|
||||||
use_pg_rewind: true
|
use_pg_rewind: true
|
||||||
|
pg_hba:
|
||||||
|
- host all all 0.0.0.0/0 md5
|
||||||
|
- host replication ${PATRONI_REPLICATION_USERNAME} ${PATRONI_KUBERNETES_POD_IP}/16 md5
|
||||||
|
- host replication ${PATRONI_REPLICATION_USERNAME} 127.0.0.1/32 md5
|
||||||
initdb:
|
initdb:
|
||||||
- auth-host: md5
|
- auth-host: md5
|
||||||
- auth-local: trust
|
- auth-local: trust
|
||||||
- encoding: UTF8
|
- encoding: UTF8
|
||||||
- locale: en_US.UTF-8
|
- locale: en_US.UTF-8
|
||||||
- data-checksums
|
- data-checksums
|
||||||
pg_hba:
|
|
||||||
- host all all 0.0.0.0/0 md5
|
|
||||||
- host replication ${PATRONI_REPLICATION_USERNAME} ${PATRONI_KUBERNETES_POD_IP}/16 md5
|
|
||||||
restapi:
|
restapi:
|
||||||
connect_address: '${PATRONI_KUBERNETES_POD_IP}:8008'
|
connect_address: '${PATRONI_KUBERNETES_POD_IP}:8008'
|
||||||
postgresql:
|
postgresql:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Note: If deploying as a template for multiple users, the following commands shou
|
|||||||
|
|
||||||
```
|
```
|
||||||
oc import-image postgres:10 --confirm -n openshift
|
oc import-image postgres:10 --confirm -n openshift
|
||||||
oc new-build https://github.com/zalando/patroni --context-dir=kubernetes -n openshift
|
oc new-build https://github.com/patroni/patroni --context-dir=kubernetes -n openshift
|
||||||
```
|
```
|
||||||
|
|
||||||
## Deploy the Image
|
## Deploy the Image
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ objects:
|
|||||||
labels:
|
labels:
|
||||||
application: ${APPLICATION_NAME}
|
application: ${APPLICATION_NAME}
|
||||||
cluster-name: ${PATRONI_CLUSTER_NAME}
|
cluster-name: ${PATRONI_CLUSTER_NAME}
|
||||||
name: ${PATRONI_MASTER_SERVICE_NAME}
|
name: ${PATRONI_PRIMARY_SERVICE_NAME}
|
||||||
spec:
|
spec:
|
||||||
ports:
|
ports:
|
||||||
- port: 5432
|
- port: 5432
|
||||||
@@ -45,7 +45,7 @@ objects:
|
|||||||
selector:
|
selector:
|
||||||
application: ${APPLICATION_NAME}
|
application: ${APPLICATION_NAME}
|
||||||
cluster-name: ${PATRONI_CLUSTER_NAME}
|
cluster-name: ${PATRONI_CLUSTER_NAME}
|
||||||
role: master
|
role: primary
|
||||||
sessionAffinity: None
|
sessionAffinity: None
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
status:
|
status:
|
||||||
@@ -289,12 +289,12 @@ parameters:
|
|||||||
displayName: Cluster Name
|
displayName: Cluster Name
|
||||||
name: PATRONI_CLUSTER_NAME
|
name: PATRONI_CLUSTER_NAME
|
||||||
value: patroni-ephemeral
|
value: patroni-ephemeral
|
||||||
- description: The name of the OpenShift Service exposed for the patroni-ephemeral-master container.
|
- description: The name of the OpenShift Service exposed for the patroni-ephemeral-primary container.
|
||||||
displayName: Master service name.
|
displayName: Primary service name.
|
||||||
name: PATRONI_MASTER_SERVICE_NAME
|
name: PATRONI_PRIMARY_SERVICE_NAME
|
||||||
value: patroni-ephemeral-master
|
value: patroni-ephemeral-primary
|
||||||
- description: The name of the OpenShift Service exposed for the patroni-ephemeral-replica containers.
|
- description: The name of the OpenShift Service exposed for the patroni-ephemeral-replica containers.
|
||||||
displayName: Replica service name.
|
displayName: Replica service name.
|
||||||
name: PATRONI_REPLICA_SERVICE_NAME
|
name: PATRONI_REPLICA_SERVICE_NAME
|
||||||
value: patroni-ephemeral-replica
|
value: patroni-ephemeral-replica
|
||||||
- description: Maximum amount of memory the container can use.
|
- description: Maximum amount of memory the container can use.
|
||||||
@@ -310,7 +310,7 @@ parameters:
|
|||||||
name: PATRONI_SUPERUSER_USERNAME
|
name: PATRONI_SUPERUSER_USERNAME
|
||||||
value: postgres
|
value: postgres
|
||||||
- description: Password of the superuser account for initialization.
|
- description: Password of the superuser account for initialization.
|
||||||
displayName: Superuser Passsword
|
displayName: Superuser Password
|
||||||
name: PATRONI_SUPERUSER_PASSWORD
|
name: PATRONI_SUPERUSER_PASSWORD
|
||||||
value: postgres
|
value: postgres
|
||||||
- description: Username of the replication account for initialization.
|
- description: Username of the replication account for initialization.
|
||||||
@@ -318,10 +318,10 @@ parameters:
|
|||||||
name: PATRONI_REPLICATION_USERNAME
|
name: PATRONI_REPLICATION_USERNAME
|
||||||
value: postgres
|
value: postgres
|
||||||
- description: Password of the replication account for initialization.
|
- description: Password of the replication account for initialization.
|
||||||
displayName: Repication Passsword
|
displayName: Repication Password
|
||||||
name: PATRONI_REPLICATION_PASSWORD
|
name: PATRONI_REPLICATION_PASSWORD
|
||||||
value: postgres
|
value: postgres
|
||||||
- description: Service account name used for pods and rolebindings to form a cluster in the project.
|
- description: Service account name used for pods and rolebindings to form a cluster in the project.
|
||||||
displayName: Service Account
|
displayName: Service Account
|
||||||
name: SERVICE_ACCOUNT
|
name: SERVICE_ACCOUNT
|
||||||
value: patroniocp
|
value: patroniocp
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ objects:
|
|||||||
labels:
|
labels:
|
||||||
application: ${APPLICATION_NAME}
|
application: ${APPLICATION_NAME}
|
||||||
cluster-name: ${PATRONI_CLUSTER_NAME}
|
cluster-name: ${PATRONI_CLUSTER_NAME}
|
||||||
name: ${PATRONI_MASTER_SERVICE_NAME}
|
name: ${PATRONI_PRIMARY_SERVICE_NAME}
|
||||||
spec:
|
spec:
|
||||||
ports:
|
ports:
|
||||||
- port: 5432
|
- port: 5432
|
||||||
@@ -43,7 +43,7 @@ objects:
|
|||||||
selector:
|
selector:
|
||||||
application: ${APPLICATION_NAME}
|
application: ${APPLICATION_NAME}
|
||||||
cluster-name: ${PATRONI_CLUSTER_NAME}
|
cluster-name: ${PATRONI_CLUSTER_NAME}
|
||||||
role: master
|
role: primary
|
||||||
sessionAffinity: None
|
sessionAffinity: None
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
status:
|
status:
|
||||||
@@ -107,7 +107,7 @@ objects:
|
|||||||
initContainers:
|
initContainers:
|
||||||
- command:
|
- command:
|
||||||
- sh
|
- sh
|
||||||
- -c
|
- -c
|
||||||
- "mkdir -p /home/postgres/pgdata/pgroot/data && chmod 0700 /home/postgres/pgdata/pgroot/data"
|
- "mkdir -p /home/postgres/pgdata/pgroot/data && chmod 0700 /home/postgres/pgdata/pgroot/data"
|
||||||
image: docker-registry.default.svc:5000/${NAMESPACE}/patroni:latest
|
image: docker-registry.default.svc:5000/${NAMESPACE}/patroni:latest
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
@@ -196,7 +196,7 @@ objects:
|
|||||||
terminationGracePeriodSeconds: 0
|
terminationGracePeriodSeconds: 0
|
||||||
volumes:
|
volumes:
|
||||||
- name: ${APPLICATION_NAME}
|
- name: ${APPLICATION_NAME}
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: ${APPLICATION_NAME}
|
claimName: ${APPLICATION_NAME}
|
||||||
volumeClaimTemplates:
|
volumeClaimTemplates:
|
||||||
- metadata:
|
- metadata:
|
||||||
@@ -313,12 +313,12 @@ parameters:
|
|||||||
displayName: Cluster Name
|
displayName: Cluster Name
|
||||||
name: PATRONI_CLUSTER_NAME
|
name: PATRONI_CLUSTER_NAME
|
||||||
value: patroni-persistent
|
value: patroni-persistent
|
||||||
- description: The name of the OpenShift Service exposed for the patroni-persistent-master container.
|
- description: The name of the OpenShift Service exposed for the patroni-persistent-primary container.
|
||||||
displayName: Master service name.
|
displayName: Primary service name.
|
||||||
name: PATRONI_MASTER_SERVICE_NAME
|
name: PATRONI_PRIMARY_SERVICE_NAME
|
||||||
value: patroni-persistent-master
|
value: patroni-persistent-primary
|
||||||
- description: The name of the OpenShift Service exposed for the patroni-persistent-replica containers.
|
- description: The name of the OpenShift Service exposed for the patroni-persistent-replica containers.
|
||||||
displayName: Replica service name.
|
displayName: Replica service name.
|
||||||
name: PATRONI_REPLICA_SERVICE_NAME
|
name: PATRONI_REPLICA_SERVICE_NAME
|
||||||
value: patroni-persistent-replica
|
value: patroni-persistent-replica
|
||||||
- description: Maximum amount of memory the container can use.
|
- description: Maximum amount of memory the container can use.
|
||||||
@@ -334,7 +334,7 @@ parameters:
|
|||||||
name: PATRONI_SUPERUSER_USERNAME
|
name: PATRONI_SUPERUSER_USERNAME
|
||||||
value: postgres
|
value: postgres
|
||||||
- description: Password of the superuser account for initialization.
|
- description: Password of the superuser account for initialization.
|
||||||
displayName: Superuser Passsword
|
displayName: Superuser Password
|
||||||
name: PATRONI_SUPERUSER_PASSWORD
|
name: PATRONI_SUPERUSER_PASSWORD
|
||||||
value: postgres
|
value: postgres
|
||||||
- description: Username of the replication account for initialization.
|
- description: Username of the replication account for initialization.
|
||||||
@@ -342,14 +342,14 @@ parameters:
|
|||||||
name: PATRONI_REPLICATION_USERNAME
|
name: PATRONI_REPLICATION_USERNAME
|
||||||
value: postgres
|
value: postgres
|
||||||
- description: Password of the replication account for initialization.
|
- description: Password of the replication account for initialization.
|
||||||
displayName: Repication Passsword
|
displayName: Repication Password
|
||||||
name: PATRONI_REPLICATION_PASSWORD
|
name: PATRONI_REPLICATION_PASSWORD
|
||||||
value: postgres
|
value: postgres
|
||||||
- description: Service account name used for pods and rolebindings to form a cluster in the project.
|
- description: Service account name used for pods and rolebindings to form a cluster in the project.
|
||||||
displayName: Service Account
|
displayName: Service Account
|
||||||
name: SERVICE_ACCOUNT
|
name: SERVICE_ACCOUNT
|
||||||
value: patroni-persistent
|
value: patroni-persistent
|
||||||
- description: The size of the persistent volume to create.
|
- description: The size of the persistent volume to create.
|
||||||
displayName: Persistent Volume Size
|
displayName: Persistent Volume Size
|
||||||
name: PVC_SIZE
|
name: PVC_SIZE
|
||||||
value: 5Gi
|
value: 5Gi
|
||||||
|
|||||||
+2
-2
@@ -15,9 +15,9 @@ pipeline {
|
|||||||
script {
|
script {
|
||||||
openshift.withCluster() {
|
openshift.withCluster() {
|
||||||
openshift.withProject() {
|
openshift.withProject() {
|
||||||
def pgbench = openshift.newApp( "https://github.com/stewartshea/docker-pgbench/", "--name=pgbench", "-e PGPASSWORD=postgres", "-e PGUSER=postgres", "-e PGHOST=patroni-persistent-master", "-e PGDATABASE=postgres", "-e TEST_CLIENT_COUNT=20", "-e TEST_DURATION=120" )
|
def pgbench = openshift.newApp( "https://github.com/stewartshea/docker-pgbench/", "--name=pgbench", "-e PGPASSWORD=postgres", "-e PGUSER=postgres", "-e PGHOST=patroni-persistent-primary", "-e PGDATABASE=postgres", "-e TEST_CLIENT_COUNT=20", "-e TEST_DURATION=120" )
|
||||||
def pgbenchdc = openshift.selector( "dc", "pgbench" )
|
def pgbenchdc = openshift.selector( "dc", "pgbench" )
|
||||||
timeout(5) {
|
timeout(5) {
|
||||||
pgbenchdc.rollout().status()
|
pgbenchdc.rollout().status()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -13,10 +13,17 @@ def hiddenimports():
|
|||||||
sys.path.pop(0)
|
sys.path.pop(0)
|
||||||
|
|
||||||
|
|
||||||
|
def resources():
|
||||||
|
import os
|
||||||
|
res_dir = 'patroni/postgresql/available_parameters/'
|
||||||
|
exts = set(f.split('.')[-1] for f in os.listdir(res_dir))
|
||||||
|
return [(res_dir + '*.' + e, res_dir) for e in exts if e.lower() in {'yml', 'yaml'}]
|
||||||
|
|
||||||
|
|
||||||
a = Analysis(['patroni/__main__.py'],
|
a = Analysis(['patroni/__main__.py'],
|
||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=None,
|
binaries=None,
|
||||||
datas=None,
|
datas=resources(),
|
||||||
hiddenimports=hiddenimports(),
|
hiddenimports=hiddenimports(),
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
runtime_hooks=[],
|
runtime_hooks=[],
|
||||||
|
|||||||
+34
-27
@@ -1,41 +1,48 @@
|
|||||||
import sys
|
"""Define general variables and functions for :mod:`patroni`.
|
||||||
|
|
||||||
|
:var PATRONI_ENV_PREFIX: prefix for Patroni related configuration environment variables.
|
||||||
|
:var KUBERNETES_ENV_PREFIX: prefix for Kubernetes related configuration environment variables.
|
||||||
|
:var MIN_PSYCOPG2: minimum version of :mod:`psycopg2` required by Patroni to work.
|
||||||
|
:var MIN_PSYCOPG3: minimum version of :mod:`psycopg` required by Patroni to work.
|
||||||
|
"""
|
||||||
|
from typing import Iterator, Tuple
|
||||||
|
|
||||||
PATRONI_ENV_PREFIX = 'PATRONI_'
|
PATRONI_ENV_PREFIX = 'PATRONI_'
|
||||||
KUBERNETES_ENV_PREFIX = 'KUBERNETES_'
|
KUBERNETES_ENV_PREFIX = 'KUBERNETES_'
|
||||||
MIN_PSYCOPG2 = (2, 5, 4)
|
MIN_PSYCOPG2 = (2, 5, 4)
|
||||||
|
MIN_PSYCOPG3 = (3, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
def fatal(string, *args):
|
def parse_version(version: str) -> Tuple[int, ...]:
|
||||||
sys.stderr.write('FATAL: ' + string.format(*args) + '\n')
|
"""Convert *version* from human-readable format to tuple of integers.
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Designed for easy comparison of software versions in Python.
|
||||||
|
|
||||||
def parse_version(version):
|
:param version: human-readable software version, e.g. ``2.5.4.dev1 (dt dec pq3 ext lo64)``.
|
||||||
def _parse_version(version):
|
|
||||||
|
:returns: tuple of *version* parts, each part as an integer.
|
||||||
|
|
||||||
|
:Example:
|
||||||
|
|
||||||
|
>>> parse_version('2.5.4.dev1 (dt dec pq3 ext lo64)')
|
||||||
|
(2, 5, 4)
|
||||||
|
"""
|
||||||
|
def _parse_version(version: str) -> Iterator[int]:
|
||||||
|
"""Yield each part of a human-readable version string as an integer.
|
||||||
|
|
||||||
|
:param version: human-readable software version, e.g. ``2.5.4.dev1``.
|
||||||
|
|
||||||
|
:yields: each part of *version* as an integer.
|
||||||
|
|
||||||
|
:Example:
|
||||||
|
|
||||||
|
>>> tuple(_parse_version('2.5.4.dev1'))
|
||||||
|
(2, 5, 4)
|
||||||
|
"""
|
||||||
for e in version.split('.'):
|
for e in version.split('.'):
|
||||||
try:
|
try:
|
||||||
yield int(e)
|
yield int(e)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
break
|
break
|
||||||
return tuple(_parse_version(version.split(' ')[0]))
|
return tuple(_parse_version(version.split(' ')[0]))
|
||||||
|
|
||||||
|
|
||||||
# We pass MIN_PSYCOPG2 and parse_version as arguments to simplify usage of check_psycopg from the setup.py
|
|
||||||
def check_psycopg(_min_psycopg2=MIN_PSYCOPG2, _parse_version=parse_version):
|
|
||||||
min_psycopg2_str = '.'.join(map(str, _min_psycopg2))
|
|
||||||
|
|
||||||
try:
|
|
||||||
from psycopg2 import __version__
|
|
||||||
if _parse_version(__version__) >= _min_psycopg2:
|
|
||||||
return
|
|
||||||
version_str = __version__.split(' ')[0]
|
|
||||||
except ImportError:
|
|
||||||
version_str = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
from psycopg import __version__
|
|
||||||
except ImportError:
|
|
||||||
error = 'Patroni requires psycopg2>={0}, psycopg2-binary, or psycopg>=3.0'.format(min_psycopg2_str)
|
|
||||||
if version_str:
|
|
||||||
error += ', but only psycopg2=={0} is available'.format(version_str)
|
|
||||||
fatal(error)
|
|
||||||
|
|||||||
+298
-54
@@ -1,16 +1,55 @@
|
|||||||
|
"""Patroni main entry point.
|
||||||
|
|
||||||
|
Implement ``patroni`` main daemon and expose its entry point.
|
||||||
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from patroni.daemon import AbstractPatroniDaemon, abstract_main
|
from argparse import Namespace
|
||||||
|
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
from patroni import global_config, MIN_PSYCOPG2, MIN_PSYCOPG3, parse_version
|
||||||
|
from patroni.daemon import abstract_main, AbstractPatroniDaemon, get_base_arg_parser
|
||||||
|
from patroni.tags import Tags
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
from .config import Config
|
||||||
|
from .dcs import Cluster
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Patroni(AbstractPatroniDaemon):
|
class Patroni(AbstractPatroniDaemon, Tags):
|
||||||
|
"""Implement ``patroni`` command daemon.
|
||||||
|
|
||||||
def __init__(self, config):
|
:ivar version: Patroni version.
|
||||||
|
:ivar dcs: DCS object.
|
||||||
|
:ivar watchdog: watchdog handler, if configured to use watchdog.
|
||||||
|
:ivar postgresql: managed Postgres instance.
|
||||||
|
:ivar api: REST API server instance of this node.
|
||||||
|
:ivar request: wrapper for performing HTTP requests.
|
||||||
|
:ivar ha: HA handler.
|
||||||
|
:ivar next_run: time when to run the next HA loop cycle.
|
||||||
|
:ivar scheduled_restart: when a restart has been scheduled to occur, if any. In that case, should contain two keys:
|
||||||
|
* ``schedule``: timestamp when restart should occur;
|
||||||
|
* ``postmaster_start_time``: timestamp when Postgres was last started.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config: 'Config') -> None:
|
||||||
|
"""Create a :class:`Patroni` instance with the given *config*.
|
||||||
|
|
||||||
|
Get a connection to the DCS, configure watchdog (if required), set up Patroni interface with Postgres, configure
|
||||||
|
the HA loop and bring the REST API up.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Expected to be instantiated and run through :func:`~patroni.daemon.abstract_main`.
|
||||||
|
|
||||||
|
:param config: Patroni configuration.
|
||||||
|
"""
|
||||||
from patroni.api import RestApiServer
|
from patroni.api import RestApiServer
|
||||||
from patroni.dcs import get_dcs
|
from patroni.dcs import get_dcs
|
||||||
from patroni.ha import Ha
|
from patroni.ha import Ha
|
||||||
@@ -23,53 +62,108 @@ class Patroni(AbstractPatroniDaemon):
|
|||||||
|
|
||||||
self.version = __version__
|
self.version = __version__
|
||||||
self.dcs = get_dcs(self.config)
|
self.dcs = get_dcs(self.config)
|
||||||
self.watchdog = Watchdog(self.config)
|
|
||||||
self.load_dynamic_configuration()
|
|
||||||
|
|
||||||
self.postgresql = Postgresql(self.config['postgresql'])
|
|
||||||
self.api = RestApiServer(self, self.config['restapi'])
|
|
||||||
self.request = PatroniRequest(self.config, True)
|
self.request = PatroniRequest(self.config, True)
|
||||||
|
|
||||||
|
cluster = self.ensure_dcs_access()
|
||||||
|
self.ensure_unique_name(cluster)
|
||||||
|
|
||||||
|
self.watchdog = Watchdog(self.config)
|
||||||
|
self.apply_dynamic_configuration(cluster)
|
||||||
|
|
||||||
|
# Initialize global config
|
||||||
|
global_config.update(None, self.config.dynamic_configuration)
|
||||||
|
|
||||||
|
self.postgresql = Postgresql(self.config['postgresql'], self.dcs.mpp)
|
||||||
|
self.api = RestApiServer(self, self.config['restapi'])
|
||||||
self.ha = Ha(self)
|
self.ha = Ha(self)
|
||||||
|
|
||||||
self.tags = self.get_tags()
|
self._tags = self._get_tags()
|
||||||
self.next_run = time.time()
|
self.next_run = time.time()
|
||||||
self.scheduled_restart = {}
|
self.scheduled_restart: Dict[str, Any] = {}
|
||||||
|
|
||||||
def load_dynamic_configuration(self):
|
def ensure_dcs_access(self, sleep_time: int = 5) -> 'Cluster':
|
||||||
|
"""Continuously attempt to retrieve cluster from DCS with delay.
|
||||||
|
|
||||||
|
:param sleep_time: seconds to wait between retry attempts after dcs connection raise :exc:`DCSError`.
|
||||||
|
|
||||||
|
:returns: a PostgreSQL or MPP implementation of :class:`Cluster`.
|
||||||
|
"""
|
||||||
from patroni.exceptions import DCSError
|
from patroni.exceptions import DCSError
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
cluster = self.dcs.get_cluster()
|
return self.dcs.get_cluster()
|
||||||
if cluster and cluster.config and cluster.config.data:
|
|
||||||
if self.config.set_dynamic_configuration(cluster.config):
|
|
||||||
self.dcs.reload_config(self.config)
|
|
||||||
self.watchdog.reload_config(self.config)
|
|
||||||
elif not self.config.dynamic_configuration and 'bootstrap' in self.config:
|
|
||||||
if self.config.set_dynamic_configuration(self.config['bootstrap']['dcs']):
|
|
||||||
self.dcs.reload_config(self.config)
|
|
||||||
self.watchdog.reload_config(self.config)
|
|
||||||
break
|
|
||||||
except DCSError:
|
except DCSError:
|
||||||
logger.warning('Can not get cluster from dcs')
|
logger.warning('Can not get cluster from dcs')
|
||||||
time.sleep(5)
|
time.sleep(sleep_time)
|
||||||
|
|
||||||
def get_tags(self):
|
def apply_dynamic_configuration(self, cluster: 'Cluster') -> None:
|
||||||
return {tag: value for tag, value in self.config.get('tags', {}).items()
|
"""Apply Patroni dynamic configuration.
|
||||||
if tag not in ('clonefrom', 'nofailover', 'noloadbalance', 'nosync') or value}
|
|
||||||
|
|
||||||
@property
|
Apply dynamic configuration from the DCS, if `/config` key is available in the DCS, otherwise fall back to
|
||||||
def nofailover(self):
|
``bootstrap.dcs`` section from the configuration file.
|
||||||
return bool(self.tags.get('nofailover', False))
|
|
||||||
|
|
||||||
@property
|
.. note::
|
||||||
def nosync(self):
|
This method is called only once, at the time when Patroni is started.
|
||||||
return bool(self.tags.get('nosync', False))
|
|
||||||
|
|
||||||
def reload_config(self, sighup=False, local=False):
|
:param cluster: a PostgreSQL or MPP implementation of :class:`Cluster`.
|
||||||
|
"""
|
||||||
|
if cluster and cluster.config and cluster.config.data:
|
||||||
|
if self.config.set_dynamic_configuration(cluster.config):
|
||||||
|
self.dcs.reload_config(self.config)
|
||||||
|
self.watchdog.reload_config(self.config)
|
||||||
|
elif not self.config.dynamic_configuration and 'bootstrap' in self.config:
|
||||||
|
if self.config.set_dynamic_configuration(self.config['bootstrap']['dcs']):
|
||||||
|
self.dcs.reload_config(self.config)
|
||||||
|
self.watchdog.reload_config(self.config)
|
||||||
|
|
||||||
|
def ensure_unique_name(self, cluster: 'Cluster') -> None:
|
||||||
|
"""A helper method to prevent splitbrain from operator naming error.
|
||||||
|
|
||||||
|
:param cluster: a PostgreSQL or MPP implementation of :class:`Cluster`.
|
||||||
|
"""
|
||||||
|
from patroni.dcs import Member
|
||||||
|
|
||||||
|
if not cluster:
|
||||||
|
return
|
||||||
|
member = cluster.get_member(self.config['name'], False)
|
||||||
|
if not isinstance(member, Member):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
# Silence annoying WARNING: Retrying (...) messages when Patroni is quickly restarted.
|
||||||
|
# At this moment we don't have custom log levels configured and hence shouldn't lose anything useful.
|
||||||
|
self.logger.update_loggers({'urllib3.connectionpool': 'ERROR'})
|
||||||
|
_ = self.request(member, endpoint="/liveness", timeout=3)
|
||||||
|
logger.fatal("Can't start; there is already a node named '%s' running", self.config['name'])
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception:
|
||||||
|
self.logger.update_loggers({})
|
||||||
|
|
||||||
|
def _get_tags(self) -> Dict[str, Any]:
|
||||||
|
"""Get tags configured for this node, if any.
|
||||||
|
|
||||||
|
:returns: a dictionary of tags set for this node.
|
||||||
|
"""
|
||||||
|
return self._filter_tags(self.config.get('tags', {}))
|
||||||
|
|
||||||
|
def reload_config(self, sighup: bool = False, local: Optional[bool] = False) -> None:
|
||||||
|
"""Apply new configuration values for ``patroni`` daemon.
|
||||||
|
|
||||||
|
Reload:
|
||||||
|
* Cached tags;
|
||||||
|
* Request wrapper configuration;
|
||||||
|
* REST API configuration;
|
||||||
|
* Watchdog configuration;
|
||||||
|
* Postgres configuration;
|
||||||
|
* DCS configuration.
|
||||||
|
|
||||||
|
:param sighup: if it is related to a SIGHUP signal.
|
||||||
|
:param local: if there has been changes to the local configuration file.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
super(Patroni, self).reload_config(sighup, local)
|
super(Patroni, self).reload_config(sighup, local)
|
||||||
if local:
|
if local:
|
||||||
self.tags = self.get_tags()
|
self._tags = self._get_tags()
|
||||||
self.request.reload_config(self.config)
|
self.request.reload_config(self.config)
|
||||||
if local or sighup and self.api.reload_local_certificate():
|
if local or sighup and self.api.reload_local_certificate():
|
||||||
self.api.reload_config(self.config['restapi'])
|
self.api.reload_config(self.config['restapi'])
|
||||||
@@ -80,14 +174,16 @@ class Patroni(AbstractPatroniDaemon):
|
|||||||
logger.exception('Failed to reload config_file=%s', self.config.config_file)
|
logger.exception('Failed to reload config_file=%s', self.config.config_file)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def replicatefrom(self):
|
def tags(self) -> Dict[str, Any]:
|
||||||
return self.tags.get('replicatefrom')
|
"""Tags configured for this node, if any."""
|
||||||
|
return self._tags
|
||||||
|
|
||||||
@property
|
def schedule_next_run(self) -> None:
|
||||||
def noloadbalance(self):
|
"""Schedule the next run of the ``patroni`` daemon main loop.
|
||||||
return bool(self.tags.get('noloadbalance', False))
|
|
||||||
|
|
||||||
def schedule_next_run(self):
|
Next run is scheduled based on previous run plus value of ``loop_wait`` configuration from DCS. If that has
|
||||||
|
already been exceeded, run the next cycle immediately.
|
||||||
|
"""
|
||||||
self.next_run += self.dcs.loop_wait
|
self.next_run += self.dcs.loop_wait
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
nap_time = self.next_run - current_time
|
nap_time = self.next_run - current_time
|
||||||
@@ -100,24 +196,40 @@ class Patroni(AbstractPatroniDaemon):
|
|||||||
elif self.ha.watch(nap_time):
|
elif self.ha.watch(nap_time):
|
||||||
self.next_run = time.time()
|
self.next_run = time.time()
|
||||||
|
|
||||||
def run(self):
|
def run(self) -> None:
|
||||||
|
"""Run ``patroni`` daemon process main loop.
|
||||||
|
|
||||||
|
Start the REST API and keep running HA cycles every ``loop_wait`` seconds.
|
||||||
|
"""
|
||||||
self.api.start()
|
self.api.start()
|
||||||
self.next_run = time.time()
|
self.next_run = time.time()
|
||||||
super(Patroni, self).run()
|
super(Patroni, self).run()
|
||||||
|
|
||||||
def _run_cycle(self):
|
def _run_cycle(self) -> None:
|
||||||
|
"""Run a cycle of the ``patroni`` daemon main loop.
|
||||||
|
|
||||||
|
Run an HA cycle and schedule the next cycle run. If any dynamic configuration change request is detected, apply
|
||||||
|
the change and cache the new dynamic configuration values in ``patroni.dynamic.json`` file under Postgres data
|
||||||
|
directory.
|
||||||
|
"""
|
||||||
|
from patroni.postgresql.misc import PostgresqlRole
|
||||||
|
|
||||||
logger.info(self.ha.run_cycle())
|
logger.info(self.ha.run_cycle())
|
||||||
|
|
||||||
if self.dcs.cluster and self.dcs.cluster.config and self.dcs.cluster.config.data \
|
if self.dcs.cluster and self.dcs.cluster.config and self.dcs.cluster.config.data \
|
||||||
and self.config.set_dynamic_configuration(self.dcs.cluster.config):
|
and self.config.set_dynamic_configuration(self.dcs.cluster.config):
|
||||||
self.reload_config()
|
self.reload_config()
|
||||||
|
|
||||||
if self.postgresql.role != 'uninitialized':
|
if self.postgresql.role != PostgresqlRole.UNINITIALIZED:
|
||||||
self.config.save_cache()
|
self.config.save_cache()
|
||||||
|
|
||||||
self.schedule_next_run()
|
self.schedule_next_run()
|
||||||
|
|
||||||
def _shutdown(self):
|
def _shutdown(self) -> None:
|
||||||
|
"""Perform shutdown of ``patroni`` daemon process.
|
||||||
|
|
||||||
|
Shut down the REST API and the HA handler.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
self.api.shutdown()
|
self.api.shutdown()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -128,27 +240,154 @@ class Patroni(AbstractPatroniDaemon):
|
|||||||
logger.exception('Exception during Ha.shutdown')
|
logger.exception('Exception during Ha.shutdown')
|
||||||
|
|
||||||
|
|
||||||
def patroni_main():
|
def patroni_main(configfile: str) -> None:
|
||||||
|
"""Configure and start ``patroni`` main daemon process.
|
||||||
|
|
||||||
|
:param configfile: path to Patroni configuration file.
|
||||||
|
"""
|
||||||
|
abstract_main(Patroni, configfile)
|
||||||
|
|
||||||
|
|
||||||
|
def process_arguments() -> Namespace:
|
||||||
|
"""Process command-line arguments.
|
||||||
|
|
||||||
|
Create a basic command-line parser through :func:`~patroni.daemon.get_base_arg_parser`, extend its capabilities by
|
||||||
|
adding these flags and parse command-line arguments.:
|
||||||
|
|
||||||
|
* ``--validate-config`` -- used to validate the Patroni configuration file
|
||||||
|
* ``--generate-config`` -- used to generate Patroni configuration from a running PostgreSQL instance
|
||||||
|
* ``--generate-sample-config`` -- used to generate a sample Patroni configuration
|
||||||
|
* ``--ignore-listen-port`` | ``-i`` -- used to ignore ``listen`` ports already in use.
|
||||||
|
Can be used only with ``--validate-config``
|
||||||
|
* ``--print`` | ``-p`` -- used to print out local configuration (incl. environment configuration overrides).
|
||||||
|
Can be used only with ``--validate-config``
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
If running with ``--generate-config``, ``--generate-sample-config`` or ``--validate-flag`` will exit
|
||||||
|
after generating or validating configuration.
|
||||||
|
|
||||||
|
:returns: parsed arguments, if not running with ``--validate-config`` flag.
|
||||||
|
"""
|
||||||
|
from patroni.config_generator import generate_config
|
||||||
|
|
||||||
|
parser = get_base_arg_parser()
|
||||||
|
group = parser.add_mutually_exclusive_group()
|
||||||
|
group.add_argument('--validate-config', action='store_true', help='Run config validator and exit')
|
||||||
|
group.add_argument('--generate-sample-config', action='store_true',
|
||||||
|
help='Generate a sample Patroni yaml configuration file')
|
||||||
|
group.add_argument('--generate-config', action='store_true',
|
||||||
|
help='Generate a Patroni yaml configuration file for a running instance')
|
||||||
|
parser.add_argument('--dsn', help='Optional DSN string of the instance to be used as a source \
|
||||||
|
for config generation. Superuser connection is required.')
|
||||||
|
parser.add_argument('--ignore-listen-port', '-i', action='store_true',
|
||||||
|
help='Ignore `listen` ports already in use.\
|
||||||
|
Can only be used with --validate-config')
|
||||||
|
parser.add_argument('--print', '-p', action='store_true',
|
||||||
|
help='Print out local configuration (incl. environment configuration overrides).\
|
||||||
|
Can only be used with --validate-config')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.generate_sample_config:
|
||||||
|
generate_config(args.configfile, True, None)
|
||||||
|
sys.exit(0)
|
||||||
|
elif args.generate_config:
|
||||||
|
generate_config(args.configfile, False, args.dsn)
|
||||||
|
sys.exit(0)
|
||||||
|
elif args.validate_config:
|
||||||
|
from patroni.config import Config, ConfigParseError
|
||||||
|
from patroni.validator import populate_validate_params, schema
|
||||||
|
|
||||||
|
populate_validate_params(ignore_listen_port=args.ignore_listen_port)
|
||||||
|
|
||||||
|
try:
|
||||||
|
config = Config(args.configfile, validator=schema)
|
||||||
|
except ConfigParseError as e:
|
||||||
|
sys.exit(e.value)
|
||||||
|
|
||||||
|
if args.print:
|
||||||
|
import yaml
|
||||||
|
yaml.safe_dump(config.local_configuration, sys.stdout, default_flow_style=False, allow_unicode=True)
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def check_psycopg() -> None:
|
||||||
|
"""Ensure at least one among :mod:`psycopg2` or :mod:`psycopg` libraries are available in the environment.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Patroni chooses :mod:`psycopg2` over :mod:`psycopg`, if possible.
|
||||||
|
|
||||||
|
If nothing meeting the requirements is found, then exit with a fatal message.
|
||||||
|
"""
|
||||||
|
min_psycopg2_str = '.'.join(map(str, MIN_PSYCOPG2))
|
||||||
|
min_psycopg3_str = '.'.join(map(str, MIN_PSYCOPG3))
|
||||||
|
|
||||||
|
available_versions: List[str] = []
|
||||||
|
|
||||||
|
# try psycopg2
|
||||||
|
try:
|
||||||
|
from psycopg2 import __version__
|
||||||
|
if parse_version(__version__) >= MIN_PSYCOPG2:
|
||||||
|
return
|
||||||
|
available_versions.append('psycopg2=={0}'.format(__version__.split(' ')[0]))
|
||||||
|
except ImportError:
|
||||||
|
logger.debug('psycopg2 module is not available')
|
||||||
|
|
||||||
|
# try psycopg3
|
||||||
|
try:
|
||||||
|
from psycopg import __version__
|
||||||
|
if parse_version(__version__) >= MIN_PSYCOPG3:
|
||||||
|
return
|
||||||
|
available_versions.append('psycopg=={0}'.format(__version__.split(' ')[0]))
|
||||||
|
except ImportError:
|
||||||
|
logger.debug('psycopg module is not available')
|
||||||
|
|
||||||
|
error = f'FATAL: Patroni requires psycopg2>={min_psycopg2_str}, psycopg2-binary, or psycopg>={min_psycopg3_str}'
|
||||||
|
if available_versions:
|
||||||
|
error += ', but only {0} {1} available'.format(
|
||||||
|
' and '.join(available_versions),
|
||||||
|
'is' if len(available_versions) == 1 else 'are')
|
||||||
|
sys.exit(error)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Main entrypoint of :mod:`patroni.__main__`.
|
||||||
|
|
||||||
|
Process command-line arguments, ensure :mod:`psycopg2` (or :mod:`psycopg`) attendee the pre-requisites and start
|
||||||
|
``patroni`` daemon process.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
If running through a Docker container, make the main process take care of init process duties and run
|
||||||
|
``patroni`` daemon as another process. In that case relevant signals received by the main process and forwarded
|
||||||
|
to ``patroni`` daemon process.
|
||||||
|
"""
|
||||||
from multiprocessing import freeze_support
|
from multiprocessing import freeze_support
|
||||||
from patroni.validator import schema
|
|
||||||
|
|
||||||
|
# Executables created by PyInstaller are frozen, thus we need to enable frozen support for
|
||||||
|
# :mod:`multiprocessing` to avoid :class:`RuntimeError` exceptions.
|
||||||
freeze_support()
|
freeze_support()
|
||||||
abstract_main(Patroni, schema)
|
|
||||||
|
|
||||||
|
check_psycopg()
|
||||||
|
|
||||||
|
args = process_arguments()
|
||||||
|
|
||||||
def main():
|
|
||||||
if os.getpid() != 1:
|
if os.getpid() != 1:
|
||||||
from patroni import check_psycopg
|
return patroni_main(args.configfile)
|
||||||
|
|
||||||
check_psycopg()
|
|
||||||
return patroni_main()
|
|
||||||
|
|
||||||
# Patroni started with PID=1, it looks like we are in the container
|
# Patroni started with PID=1, it looks like we are in the container
|
||||||
|
from types import FrameType
|
||||||
pid = 0
|
pid = 0
|
||||||
|
|
||||||
# Looks like we are in a docker, so we will act like init
|
# Looks like we are in a docker, so we will act like init
|
||||||
def sigchld_handler(signo, stack_frame):
|
def sigchld_handler(signo: int, stack_frame: Optional[FrameType]) -> None:
|
||||||
|
"""Handle ``SIGCHLD`` received by main process from ``patroni`` daemon when the daemon terminates.
|
||||||
|
|
||||||
|
:param signo: signal number.
|
||||||
|
:param stack_frame: current stack frame.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
|
# log exit code of all children processes, and break loop when there is none left
|
||||||
while True:
|
while True:
|
||||||
ret = os.waitpid(-1, os.WNOHANG)
|
ret = os.waitpid(-1, os.WNOHANG)
|
||||||
if ret == (0, 0):
|
if ret == (0, 0):
|
||||||
@@ -158,7 +397,12 @@ def main():
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def passtochild(signo, stack_frame):
|
def passtochild(signo: int, stack_frame: Optional[FrameType]) -> None:
|
||||||
|
"""Forward a signal *signo* from main process to child process.
|
||||||
|
|
||||||
|
:param signo: signal number.
|
||||||
|
:param stack_frame: current stack frame.
|
||||||
|
"""
|
||||||
if pid:
|
if pid:
|
||||||
os.kill(pid, signo)
|
os.kill(pid, signo)
|
||||||
|
|
||||||
@@ -173,7 +417,7 @@ def main():
|
|||||||
signal.signal(signal.SIGTERM, passtochild)
|
signal.signal(signal.SIGTERM, passtochild)
|
||||||
|
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
patroni = multiprocessing.Process(target=patroni_main)
|
patroni = multiprocessing.Process(target=patroni_main, args=(args.configfile,))
|
||||||
patroni.start()
|
patroni.start()
|
||||||
pid = patroni.pid
|
pid = patroni.pid
|
||||||
patroni.join()
|
patroni.join()
|
||||||
|
|||||||
+1110
-235
File diff suppressed because it is too large
Load Diff
+125
-27
@@ -1,5 +1,11 @@
|
|||||||
|
"""Implement facilities for executing asynchronous tasks."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from threading import Event, Lock, RLock, Thread
|
from threading import Event, Lock, RLock, Thread
|
||||||
|
from types import TracebackType
|
||||||
|
from typing import Any, Callable, Optional, Tuple, Type
|
||||||
|
|
||||||
|
from .postgresql.cancellable import CancellableSubprocess
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -8,65 +14,110 @@ class CriticalTask(object):
|
|||||||
"""Represents a critical task in a background process that we either need to cancel or get the result of.
|
"""Represents a critical task in a background process that we either need to cancel or get the result of.
|
||||||
|
|
||||||
Fields of this object may be accessed only when holding a lock on it. To perform the critical task the background
|
Fields of this object may be accessed only when holding a lock on it. To perform the critical task the background
|
||||||
thread must, while holding lock on this object, check `is_cancelled` flag, run the task and mark the task as
|
thread must, while holding lock on this object, check ``is_cancelled`` flag, run the task and mark the task as
|
||||||
complete using `complete()`.
|
complete using :func:`complete`.
|
||||||
|
|
||||||
The main thread must hold async lock to prevent the task from completing, hold lock on critical task object,
|
The main thread must hold async lock to prevent the task from completing, hold lock on critical task object,
|
||||||
call cancel. If the task has completed `cancel()` will return False and `result` field will contain the result of
|
call :func:`cancel`. If the task has completed :func:`cancel` will return ``False`` and ``result`` field will
|
||||||
the task. When cancel returns True it is guaranteed that the background task will notice the `is_cancelled` flag.
|
contain the result of the task. When :func:`cancel` returns ``True`` it is guaranteed that the background task will
|
||||||
|
notice the ``is_cancelled`` flag.
|
||||||
|
|
||||||
|
:ivar is_cancelled: if the critical task has been cancelled.
|
||||||
|
:ivar result: contains the result of the task, if it has already been completed.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Create a new instance of :class:`CriticalTask`.
|
||||||
|
|
||||||
|
Instantiate the lock and the task control attributes.
|
||||||
|
"""
|
||||||
self._lock = Lock()
|
self._lock = Lock()
|
||||||
self.is_cancelled = False
|
self.is_cancelled = False
|
||||||
self.result = None
|
self.result = None
|
||||||
|
|
||||||
def reset(self):
|
def reset(self) -> None:
|
||||||
"""Must be called every time the background task is finished.
|
"""Must be called every time the background task is finished.
|
||||||
|
|
||||||
Must be called from async thread. Caller must hold lock on async executor when calling."""
|
.. note::
|
||||||
|
Must be called from async thread. Caller must hold lock on async executor when calling.
|
||||||
|
"""
|
||||||
self.is_cancelled = False
|
self.is_cancelled = False
|
||||||
self.result = None
|
self.result = None
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self) -> bool:
|
||||||
"""Tries to cancel the task, returns True if the task has already run.
|
"""Tries to cancel the task.
|
||||||
|
|
||||||
Caller must hold lock on async executor and the task when calling."""
|
.. note::
|
||||||
|
Caller must hold lock on async executor and the task when calling.
|
||||||
|
|
||||||
|
:returns: ``False`` if the task has already run, or ``True`` it has been cancelled.
|
||||||
|
"""
|
||||||
if self.result is not None:
|
if self.result is not None:
|
||||||
return False
|
return False
|
||||||
self.is_cancelled = True
|
self.is_cancelled = True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def complete(self, result):
|
def complete(self, result: Any) -> None:
|
||||||
"""Mark task as completed along with a result.
|
"""Mark task as completed along with a *result*.
|
||||||
|
|
||||||
Must be called from async thread. Caller must hold lock on task when calling."""
|
.. note::
|
||||||
|
Must be called from async thread. Caller must hold lock on task when calling.
|
||||||
|
"""
|
||||||
self.result = result
|
self.result = result
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self) -> 'CriticalTask':
|
||||||
|
"""Acquire the object lock when entering the context manager."""
|
||||||
self._lock.acquire()
|
self._lock.acquire()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __exit__(self, exc_type: Optional[Type[BaseException]],
|
||||||
|
exc_val: Optional[BaseException], exc_tb: Optional[TracebackType]) -> None:
|
||||||
|
"""Release the object lock when exiting the context manager."""
|
||||||
self._lock.release()
|
self._lock.release()
|
||||||
|
|
||||||
|
|
||||||
class AsyncExecutor(object):
|
class AsyncExecutor(object):
|
||||||
|
"""Asynchronous executor of (long) tasks.
|
||||||
|
|
||||||
def __init__(self, cancellable, ha_wakeup):
|
:ivar critical_task: a :class:`CriticalTask` instance to handle execution of critical background tasks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, cancellable: CancellableSubprocess, ha_wakeup: Callable[..., None]) -> None:
|
||||||
|
"""Create a new instance of :class:`AsyncExecutor`.
|
||||||
|
|
||||||
|
Configure the given *cancellable* and *ha_wakeup*, initializes the control attributes, and instantiate the lock
|
||||||
|
and event objects that are used to access attributes and manage communication between threads.
|
||||||
|
|
||||||
|
:param cancellable: a subprocess that supports being cancelled.
|
||||||
|
:param ha_wakeup: function to wake up the HA loop.
|
||||||
|
"""
|
||||||
self._cancellable = cancellable
|
self._cancellable = cancellable
|
||||||
self._ha_wakeup = ha_wakeup
|
self._ha_wakeup = ha_wakeup
|
||||||
self._thread_lock = RLock()
|
self._thread_lock = RLock()
|
||||||
self._scheduled_action = None
|
self._scheduled_action: Optional[str] = None
|
||||||
self._scheduled_action_lock = RLock()
|
self._scheduled_action_lock = RLock()
|
||||||
self._is_cancelled = False
|
self._is_cancelled = False
|
||||||
self._finish_event = Event()
|
self._finish_event = Event()
|
||||||
self.critical_task = CriticalTask()
|
self.critical_task = CriticalTask()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def busy(self):
|
def busy(self) -> bool:
|
||||||
|
"""``True`` if there is an action scheduled to occur, else ``False``."""
|
||||||
return self.scheduled_action is not None
|
return self.scheduled_action is not None
|
||||||
|
|
||||||
def schedule(self, action):
|
def schedule(self, action: str) -> Optional[str]:
|
||||||
|
"""Schedule *action* to be executed.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Must be called before executing a task.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
*action* can only be scheduled if there is no other action currently scheduled.
|
||||||
|
|
||||||
|
:param action: action to be executed.
|
||||||
|
|
||||||
|
:returns: ``None`` if *action* has been successfully scheduled, or the previously scheduled action, if any.
|
||||||
|
"""
|
||||||
with self._scheduled_action_lock:
|
with self._scheduled_action_lock:
|
||||||
if self._scheduled_action is not None:
|
if self._scheduled_action is not None:
|
||||||
return self._scheduled_action
|
return self._scheduled_action
|
||||||
@@ -76,15 +127,33 @@ class AsyncExecutor(object):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scheduled_action(self):
|
def scheduled_action(self) -> Optional[str]:
|
||||||
|
"""The currently scheduled action, if any, else ``None``."""
|
||||||
with self._scheduled_action_lock:
|
with self._scheduled_action_lock:
|
||||||
return self._scheduled_action
|
return self._scheduled_action
|
||||||
|
|
||||||
def reset_scheduled_action(self):
|
def reset_scheduled_action(self) -> None:
|
||||||
|
"""Unschedule a previously scheduled action, if any.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Must be called once the scheduled task finishes or is cancelled.
|
||||||
|
"""
|
||||||
with self._scheduled_action_lock:
|
with self._scheduled_action_lock:
|
||||||
self._scheduled_action = None
|
self._scheduled_action = None
|
||||||
|
|
||||||
def run(self, func, args=()):
|
def run(self, func: Callable[..., Any], args: Tuple[Any, ...] = ()) -> Optional[Any]:
|
||||||
|
"""Run *func* with *args*.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Expected to be executed through a thread.
|
||||||
|
|
||||||
|
:param func: function to be run. If it returns anything other than ``None``, HA loop will be woken up at the end
|
||||||
|
of :func:`run` execution.
|
||||||
|
:param args: arguments to be passed to *func*.
|
||||||
|
|
||||||
|
:returns: ``None`` if *func* execution has been cancelled or faced any exception, otherwise the result of
|
||||||
|
*func*.
|
||||||
|
"""
|
||||||
wakeup = False
|
wakeup = False
|
||||||
try:
|
try:
|
||||||
with self:
|
with self:
|
||||||
@@ -107,16 +176,37 @@ class AsyncExecutor(object):
|
|||||||
if wakeup is not None:
|
if wakeup is not None:
|
||||||
self._ha_wakeup()
|
self._ha_wakeup()
|
||||||
|
|
||||||
def run_async(self, func, args=()):
|
def run_async(self, func: Callable[..., Any], args: Tuple[Any, ...] = ()) -> None:
|
||||||
|
"""Start an async thread that runs *func* with *args*.
|
||||||
|
|
||||||
|
:param func: function to be run. Will be passed through args to :class:`~threading.Thread` with a target of
|
||||||
|
:func:`run`.
|
||||||
|
:param args: arguments to be passed along to :class:`~threading.Thread` with *func*.
|
||||||
|
|
||||||
|
"""
|
||||||
Thread(target=self.run, args=(func, args)).start()
|
Thread(target=self.run, args=(func, args)).start()
|
||||||
|
|
||||||
def try_run_async(self, action, func, args=()):
|
def try_run_async(self, action: str, func: Callable[..., Any], args: Tuple[Any, ...] = ()) -> Optional[str]:
|
||||||
|
"""Try to run an async task, if none is currently being executed.
|
||||||
|
|
||||||
|
:param action: name of the task to be executed.
|
||||||
|
:param func: actual function that performs the task *action*.
|
||||||
|
:param args: arguments to be passed to *func*.
|
||||||
|
|
||||||
|
:returns: ``None`` if *func* was scheduled successfully, otherwise an error message informing of an already
|
||||||
|
ongoing task.
|
||||||
|
"""
|
||||||
prev = self.schedule(action)
|
prev = self.schedule(action)
|
||||||
if prev is None:
|
if prev is None:
|
||||||
return self.run_async(func, args)
|
return self.run_async(func, args)
|
||||||
return 'Failed to run {0}, {1} is already in progress'.format(action, prev)
|
return 'Failed to run {0}, {1} is already in progress'.format(action, prev)
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self) -> None:
|
||||||
|
"""Request cancellation of a scheduled async task, if any.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Wait until task is cancelled before returning control to caller.
|
||||||
|
"""
|
||||||
with self:
|
with self:
|
||||||
with self._scheduled_action_lock:
|
with self._scheduled_action_lock:
|
||||||
if self._scheduled_action is None:
|
if self._scheduled_action is None:
|
||||||
@@ -130,8 +220,16 @@ class AsyncExecutor(object):
|
|||||||
with self:
|
with self:
|
||||||
self.reset_scheduled_action()
|
self.reset_scheduled_action()
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self) -> 'AsyncExecutor':
|
||||||
|
"""Acquire the thread lock when entering the context manager."""
|
||||||
self._thread_lock.acquire()
|
self._thread_lock.acquire()
|
||||||
|
return self
|
||||||
|
|
||||||
def __exit__(self, *args):
|
def __exit__(self, exc_type: Optional[Type[BaseException]],
|
||||||
|
exc_val: Optional[BaseException], exc_tb: Optional[TracebackType]) -> None:
|
||||||
|
"""Release the thread lock when exiting the context manager.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
The arguments are not used, but we need them to match the expected method signature.
|
||||||
|
"""
|
||||||
self._thread_lock.release()
|
self._thread_lock.release()
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
"""Patroni custom object types somewhat like :mod:`collections` module.
|
||||||
|
|
||||||
|
Provides a case insensitive :class:`dict` and :class:`set` object types, and `EMPTY_DICT` frozen dictionary object.
|
||||||
|
"""
|
||||||
|
from collections import OrderedDict
|
||||||
|
from copy import deepcopy
|
||||||
|
from typing import Any, Collection, Dict, Iterator, KeysView, Mapping, MutableMapping, MutableSet, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class CaseInsensitiveSet(MutableSet[str]):
|
||||||
|
"""A case-insensitive :class:`set`-like object.
|
||||||
|
|
||||||
|
Implements all methods and operations of :class:`~typing.MutableSet`. All values are expected to be strings.
|
||||||
|
The structure remembers the case of the last value set, however, contains testing is case insensitive.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, values: Optional[Collection[str]] = None) -> None:
|
||||||
|
"""Create a new instance of :class:`CaseInsensitiveSet` with the given *values*.
|
||||||
|
|
||||||
|
:param values: values to be added to the set.
|
||||||
|
"""
|
||||||
|
self._values: Dict[str, str] = {}
|
||||||
|
for v in values or ():
|
||||||
|
self.add(v)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
"""Get a string representation of the set.
|
||||||
|
|
||||||
|
Provide a helpful way of recreating the set.
|
||||||
|
|
||||||
|
:returns: representation of the set, showing its values.
|
||||||
|
|
||||||
|
:Example:
|
||||||
|
|
||||||
|
>>> repr(CaseInsensitiveSet(('1', 'test', 'Test', 'TESt', 'test2'))) # doctest: +ELLIPSIS
|
||||||
|
"<CaseInsensitiveSet('1', 'TESt', 'test2') at ..."
|
||||||
|
"""
|
||||||
|
return '<{0}{1} at {2:x}>'.format(type(self).__name__, tuple(self._values.values()), id(self))
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Get set values for printing.
|
||||||
|
|
||||||
|
:returns: set of values in string format.
|
||||||
|
|
||||||
|
:Example:
|
||||||
|
|
||||||
|
>>> str(CaseInsensitiveSet(('1', 'test', 'Test', 'TESt', 'test2'))) # doctest: +SKIP
|
||||||
|
"{'TESt', 'test2', '1'}"
|
||||||
|
"""
|
||||||
|
return str(set(self._values.values()))
|
||||||
|
|
||||||
|
def __contains__(self, value: object) -> bool:
|
||||||
|
"""Check if set contains *value*.
|
||||||
|
|
||||||
|
The check is performed case-insensitively.
|
||||||
|
|
||||||
|
:param value: value to be checked.
|
||||||
|
|
||||||
|
:returns: ``True`` if *value* is already in the set, ``False`` otherwise.
|
||||||
|
"""
|
||||||
|
return isinstance(value, str) and value.lower() in self._values
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[str]:
|
||||||
|
"""Iterate over the values in this set.
|
||||||
|
|
||||||
|
:yields: values from set.
|
||||||
|
"""
|
||||||
|
return iter(self._values.values())
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
"""Get the length of this set.
|
||||||
|
|
||||||
|
:returns: number of values in the set.
|
||||||
|
|
||||||
|
:Example:
|
||||||
|
|
||||||
|
>>> len(CaseInsensitiveSet(('1', 'test', 'Test', 'TESt', 'test2')))
|
||||||
|
3
|
||||||
|
"""
|
||||||
|
return len(self._values)
|
||||||
|
|
||||||
|
def add(self, value: str) -> None:
|
||||||
|
"""Add *value* to this set.
|
||||||
|
|
||||||
|
Search is performed case-insensitively. If *value* is already in the set, overwrite it with *value*, so we
|
||||||
|
"remember" the last case of *value*.
|
||||||
|
|
||||||
|
:param value: value to be added to the set.
|
||||||
|
"""
|
||||||
|
self._values[value.lower()] = value
|
||||||
|
|
||||||
|
def discard(self, value: str) -> None:
|
||||||
|
"""Remove *value* from this set.
|
||||||
|
|
||||||
|
Search is performed case-insensitively. If *value* is not present in the set, no exception is raised.
|
||||||
|
|
||||||
|
:param value: value to be removed from the set.
|
||||||
|
"""
|
||||||
|
self._values.pop(value.lower(), None)
|
||||||
|
|
||||||
|
def issubset(self, other: 'CaseInsensitiveSet') -> bool:
|
||||||
|
"""Check if this set is a subset of *other*.
|
||||||
|
|
||||||
|
:param other: another set to be compared with this set.
|
||||||
|
:returns: ``True`` if this set is a subset of *other*, else ``False``.
|
||||||
|
"""
|
||||||
|
return self <= other
|
||||||
|
|
||||||
|
|
||||||
|
class CaseInsensitiveDict(MutableMapping[str, Any]):
|
||||||
|
"""A case-insensitive :class:`dict`-like object.
|
||||||
|
|
||||||
|
Implements all methods and operations of :class:`~typing.MutableMapping` as well as :class:`dict`'s
|
||||||
|
:func:`~dict.copy`. All keys are expected to be strings. The structure remembers the case of the last key to be set,
|
||||||
|
and :func:`iter`, :func:`dict.keys`, :func:`dict.items`, :func:`dict.iterkeys`, and :func:`dict.iteritems` will
|
||||||
|
contain case-sensitive keys. However, querying and contains testing is case insensitive.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, data: Optional[Dict[str, Any]] = None) -> None:
|
||||||
|
"""Create a new instance of :class:`CaseInsensitiveDict` with the given *data*.
|
||||||
|
|
||||||
|
:param data: initial dictionary to create a :class:`CaseInsensitiveDict` from.
|
||||||
|
"""
|
||||||
|
self._values: OrderedDict[str, Any] = OrderedDict()
|
||||||
|
self.update(data or {})
|
||||||
|
|
||||||
|
def __setitem__(self, key: str, value: Any) -> None:
|
||||||
|
"""Assign *value* to *key* in this dict.
|
||||||
|
|
||||||
|
*key* is searched/stored case-insensitively in the dict. The corresponding value in the dict is a tuple of:
|
||||||
|
* original *key*;
|
||||||
|
* *value*.
|
||||||
|
|
||||||
|
:param key: key to be created or updated in the dict.
|
||||||
|
:param value: value for *key*.
|
||||||
|
"""
|
||||||
|
self._values[key.lower()] = (key, value)
|
||||||
|
|
||||||
|
def __getitem__(self, key: str) -> Any:
|
||||||
|
"""Get the value corresponding to *key*.
|
||||||
|
|
||||||
|
*key* is searched case-insensitively in the dict.
|
||||||
|
|
||||||
|
.. note:
|
||||||
|
If *key* is not present in the dict, :class:`KeyError` will be triggered.
|
||||||
|
|
||||||
|
:param key: key to be searched in the dict.
|
||||||
|
|
||||||
|
:returns: value corresponding to *key*.
|
||||||
|
"""
|
||||||
|
return self._values[key.lower()][1]
|
||||||
|
|
||||||
|
def __delitem__(self, key: str) -> None:
|
||||||
|
"""Remove *key* from this dict.
|
||||||
|
|
||||||
|
*key* is searched case-insensitively in the dict.
|
||||||
|
|
||||||
|
.. note:
|
||||||
|
If *key* is not present in the dict, :class:`KeyError` will be triggered.
|
||||||
|
|
||||||
|
:param key: key to be removed from the dict.
|
||||||
|
"""
|
||||||
|
del self._values[key.lower()]
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[str]:
|
||||||
|
"""Iterate over keys of this dict.
|
||||||
|
|
||||||
|
:yields: each key present in the dict. Yields each key with its last case that has been stored.
|
||||||
|
"""
|
||||||
|
return iter(key for key, _ in self._values.values())
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
"""Get the length of this dict.
|
||||||
|
|
||||||
|
:returns: number of keys in the dict.
|
||||||
|
|
||||||
|
:Example:
|
||||||
|
|
||||||
|
>>> len(CaseInsensitiveDict({'a': 'b', 'A': 'B', 'c': 'd'}))
|
||||||
|
2
|
||||||
|
"""
|
||||||
|
return len(self._values)
|
||||||
|
|
||||||
|
def copy(self) -> 'CaseInsensitiveDict':
|
||||||
|
"""Create a copy of this dict.
|
||||||
|
|
||||||
|
:return: a new dict object with the same keys and values of this dict.
|
||||||
|
"""
|
||||||
|
return CaseInsensitiveDict({v[0]: v[1] for v in self._values.values()})
|
||||||
|
|
||||||
|
def keys(self) -> KeysView[str]:
|
||||||
|
"""Return a new view of the dict's keys.
|
||||||
|
|
||||||
|
:returns: a set-like object providing a view on the dict's keys
|
||||||
|
"""
|
||||||
|
return self._values.keys()
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
"""Get a string representation of the dict.
|
||||||
|
|
||||||
|
Provide a helpful way of recreating the dict.
|
||||||
|
|
||||||
|
:returns: representation of the dict, showing its keys and values.
|
||||||
|
|
||||||
|
:Example:
|
||||||
|
|
||||||
|
>>> repr(CaseInsensitiveDict({'a': 'b', 'A': 'B', 'c': 'd'})) # doctest: +ELLIPSIS
|
||||||
|
"<CaseInsensitiveDict{'A': 'B', 'c': 'd'} at ..."
|
||||||
|
"""
|
||||||
|
return '<{0}{1} at {2:x}>'.format(type(self).__name__, dict(self.items()), id(self))
|
||||||
|
|
||||||
|
|
||||||
|
class _FrozenDict(Mapping[str, Any]):
|
||||||
|
"""Frozen dictionary object."""
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""Create a new instance of :class:`_FrozenDict` with given data."""
|
||||||
|
self.__values: Dict[str, Any] = dict(*args, **kwargs)
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[str]:
|
||||||
|
"""Iterate over keys of this dict.
|
||||||
|
|
||||||
|
:yields: each key present in the dict. Yields each key with its last case that has been stored.
|
||||||
|
"""
|
||||||
|
return iter(self.__values)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
"""Get the length of this dict.
|
||||||
|
|
||||||
|
:returns: number of keys in the dict.
|
||||||
|
|
||||||
|
:Example:
|
||||||
|
|
||||||
|
>>> len(_FrozenDict())
|
||||||
|
0
|
||||||
|
"""
|
||||||
|
return len(self.__values)
|
||||||
|
|
||||||
|
def __getitem__(self, key: str) -> Any:
|
||||||
|
"""Get the value corresponding to *key*.
|
||||||
|
|
||||||
|
:returns: value corresponding to *key*.
|
||||||
|
"""
|
||||||
|
return self.__values[key]
|
||||||
|
|
||||||
|
def copy(self) -> Dict[str, Any]:
|
||||||
|
"""Create a copy of this dict.
|
||||||
|
|
||||||
|
:return: a new dict object with the same keys and values of this dict.
|
||||||
|
"""
|
||||||
|
return deepcopy(self.__values)
|
||||||
|
|
||||||
|
|
||||||
|
EMPTY_DICT = _FrozenDict()
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user