1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 receiverSubmodule = {
7 options = {
8 postgresqlPackage = mkOption {
9 type = types.package;
10 example = literalExpression "pkgs.postgresql_11";
11 description = lib.mdDoc ''
12 PostgreSQL package to use.
13 '';
14 };
15
16 directory = mkOption {
17 type = types.path;
18 example = literalExpression "/mnt/pg_wal/main/";
19 description = lib.mdDoc ''
20 Directory to write the output to.
21 '';
22 };
23
24 statusInterval = mkOption {
25 type = types.int;
26 default = 10;
27 description = lib.mdDoc ''
28 Specifies the number of seconds between status packets sent back to the server.
29 This allows for easier monitoring of the progress from server.
30 A value of zero disables the periodic status updates completely,
31 although an update will still be sent when requested by the server, to avoid timeout disconnect.
32 '';
33 };
34
35 slot = mkOption {
36 type = types.str;
37 default = "";
38 example = "some_slot_name";
39 description = lib.mdDoc ''
40 Require {command}`pg_receivewal` to use an existing replication slot (see
41 [Section 26.2.6 of the PostgreSQL manual](https://www.postgresql.org/docs/current/warm-standby.html#STREAMING-REPLICATION-SLOTS)).
42 When this option is used, {command}`pg_receivewal` will report a flush position to the server,
43 indicating when each segment has been synchronized to disk so that the server can remove that segment if it is not otherwise needed.
44
45 When the replication client of {command}`pg_receivewal` is configured on the server as a synchronous standby,
46 then using a replication slot will report the flush position to the server, but only when a WAL file is closed.
47 Therefore, that configuration will cause transactions on the primary to wait for a long time and effectively not work satisfactorily.
48 The option {option}`synchronous` must be specified in addition to make this work correctly.
49 '';
50 };
51
52 synchronous = mkOption {
53 type = types.bool;
54 default = false;
55 description = lib.mdDoc ''
56 Flush the WAL data to disk immediately after it has been received.
57 Also send a status packet back to the server immediately after flushing, regardless of {option}`statusInterval`.
58
59 This option should be specified if the replication client of {command}`pg_receivewal` is configured on the server as a synchronous standby,
60 to ensure that timely feedback is sent to the server.
61 '';
62 };
63
64 compress = mkOption {
65 type = types.ints.between 0 9;
66 default = 0;
67 description = lib.mdDoc ''
68 Enables gzip compression of write-ahead logs, and specifies the compression level
69 (`0` through `9`, `0` being no compression and `9` being best compression).
70 The suffix `.gz` will automatically be added to all filenames.
71
72 This option requires PostgreSQL >= 10.
73 '';
74 };
75
76 connection = mkOption {
77 type = types.str;
78 example = "postgresql://user@somehost";
79 description = lib.mdDoc ''
80 Specifies parameters used to connect to the server, as a connection string.
81 See [Section 34.1.1 of the PostgreSQL manual](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING) for more information.
82
83 Because {command}`pg_receivewal` doesn't connect to any particular database in the cluster,
84 database name in the connection string will be ignored.
85 '';
86 };
87
88 extraArgs = mkOption {
89 type = with types; listOf str;
90 default = [ ];
91 example = literalExpression ''
92 [
93 "--no-sync"
94 ]
95 '';
96 description = lib.mdDoc ''
97 A list of extra arguments to pass to the {command}`pg_receivewal` command.
98 '';
99 };
100
101 environment = mkOption {
102 type = with types; attrsOf str;
103 default = { };
104 example = literalExpression ''
105 {
106 PGPASSFILE = "/private/passfile";
107 PGSSLMODE = "require";
108 }
109 '';
110 description = lib.mdDoc ''
111 Environment variables passed to the service.
112 Usable parameters are listed in [Section 34.14 of the PostgreSQL manual](https://www.postgresql.org/docs/current/libpq-envars.html).
113 '';
114 };
115 };
116 };
117
118in {
119 options = {
120 services.postgresqlWalReceiver = {
121 receivers = mkOption {
122 type = with types; attrsOf (submodule receiverSubmodule);
123 default = { };
124 example = literalExpression ''
125 {
126 main = {
127 postgresqlPackage = pkgs.postgresql_11;
128 directory = /mnt/pg_wal/main/;
129 slot = "main_wal_receiver";
130 connection = "postgresql://user@somehost";
131 };
132 }
133 '';
134 description = lib.mdDoc ''
135 PostgreSQL WAL receivers.
136 Stream write-ahead logs from a PostgreSQL server using {command}`pg_receivewal` (formerly {command}`pg_receivexlog`).
137 See [the man page](https://www.postgresql.org/docs/current/app-pgreceivewal.html) for more information.
138 '';
139 };
140 };
141 };
142
143 config = let
144 receivers = config.services.postgresqlWalReceiver.receivers;
145 in mkIf (receivers != { }) {
146 users = {
147 users.postgres = {
148 uid = config.ids.uids.postgres;
149 group = "postgres";
150 description = "PostgreSQL server user";
151 };
152
153 groups.postgres = {
154 gid = config.ids.gids.postgres;
155 };
156 };
157
158 assertions = concatLists (attrsets.mapAttrsToList (name: config: [
159 {
160 assertion = config.compress > 0 -> versionAtLeast config.postgresqlPackage.version "10";
161 message = "Invalid configuration for WAL receiver \"${name}\": compress requires PostgreSQL version >= 10.";
162 }
163 ]) receivers);
164
165 systemd.tmpfiles.rules = mapAttrsToList (name: config: ''
166 d ${escapeShellArg config.directory} 0750 postgres postgres - -
167 '') receivers;
168
169 systemd.services = with attrsets; mapAttrs' (name: config: nameValuePair "postgresql-wal-receiver-${name}" {
170 description = "PostgreSQL WAL receiver (${name})";
171 wantedBy = [ "multi-user.target" ];
172 startLimitIntervalSec = 0; # retry forever, useful in case of network disruption
173
174 serviceConfig = {
175 User = "postgres";
176 Group = "postgres";
177 KillSignal = "SIGINT";
178 Restart = "always";
179 RestartSec = 60;
180 };
181
182 inherit (config) environment;
183
184 script = let
185 receiverCommand = postgresqlPackage:
186 if (versionAtLeast postgresqlPackage.version "10")
187 then "${postgresqlPackage}/bin/pg_receivewal"
188 else "${postgresqlPackage}/bin/pg_receivexlog";
189 in ''
190 ${receiverCommand config.postgresqlPackage} \
191 --no-password \
192 --directory=${escapeShellArg config.directory} \
193 --status-interval=${toString config.statusInterval} \
194 --dbname=${escapeShellArg config.connection} \
195 ${optionalString (config.compress > 0) "--compress=${toString config.compress}"} \
196 ${optionalString (config.slot != "") "--slot=${escapeShellArg config.slot}"} \
197 ${optionalString config.synchronous "--synchronous"} \
198 ${concatStringsSep " " config.extraArgs}
199 '';
200 }) receivers;
201 };
202
203 meta.maintainers = with maintainers; [ pacien ];
204}