summary refs log tree commit diff
path: root/nixos/modules/services/backup/postgresql-wal-receiver.nix
blob: 32643adfdaeacb743ba8466de685ce6207d0b602 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
{ config, lib, pkgs, ... }:

with lib;

let
  receiverSubmodule = {
    options = {
      postgresqlPackage = mkOption {
        type = types.package;
        example = literalExpression "pkgs.postgresql_11";
        description = ''
          PostgreSQL package to use.
        '';
      };

      directory = mkOption {
        type = types.path;
        example = literalExpression "/mnt/pg_wal/main/";
        description = ''
          Directory to write the output to.
        '';
      };

      statusInterval = mkOption {
        type = types.int;
        default = 10;
        description = ''
          Specifies the number of seconds between status packets sent back to the server.
          This allows for easier monitoring of the progress from server.
          A value of zero disables the periodic status updates completely,
          although an update will still be sent when requested by the server, to avoid timeout disconnect.
        '';
      };

      slot = mkOption {
        type = types.str;
        default = "";
        example = "some_slot_name";
        description = ''
          Require <command>pg_receivewal</command> to use an existing replication slot (see
          <link xlink:href="https://www.postgresql.org/docs/current/warm-standby.html#STREAMING-REPLICATION-SLOTS">Section 26.2.6 of the PostgreSQL manual</link>).
          When this option is used, <command>pg_receivewal</command> will report a flush position to the server,
          indicating when each segment has been synchronized to disk so that the server can remove that segment if it is not otherwise needed.

          When the replication client of <command>pg_receivewal</command> is configured on the server as a synchronous standby,
          then using a replication slot will report the flush position to the server, but only when a WAL file is closed.
          Therefore, that configuration will cause transactions on the primary to wait for a long time and effectively not work satisfactorily.
          The option <option>synchronous</option> must be specified in addition to make this work correctly.
        '';
      };

      synchronous = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Flush the WAL data to disk immediately after it has been received.
          Also send a status packet back to the server immediately after flushing, regardless of <option>statusInterval</option>.

          This option should be specified if the replication client of <command>pg_receivewal</command> is configured on the server as a synchronous standby,
          to ensure that timely feedback is sent to the server.
        '';
      };

      compress = mkOption {
        type = types.ints.between 0 9;
        default = 0;
        description = ''
          Enables gzip compression of write-ahead logs, and specifies the compression level
          (<literal>0</literal> through <literal>9</literal>, <literal>0</literal> being no compression and <literal>9</literal> being best compression).
          The suffix <literal>.gz</literal> will automatically be added to all filenames.

          This option requires PostgreSQL >= 10.
        '';
      };

      connection = mkOption {
        type = types.str;
        example = "postgresql://user@somehost";
        description = ''
          Specifies parameters used to connect to the server, as a connection string.
          See <link xlink:href="https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING">Section 34.1.1 of the PostgreSQL manual</link> for more information.

          Because <command>pg_receivewal</command> doesn't connect to any particular database in the cluster,
          database name in the connection string will be ignored.
        '';
      };

      extraArgs = mkOption {
        type = with types; listOf str;
        default = [ ];
        example = literalExpression ''
          [
            "--no-sync"
          ]
        '';
        description = ''
          A list of extra arguments to pass to the <command>pg_receivewal</command> command.
        '';
      };

      environment = mkOption {
        type = with types; attrsOf str;
        default = { };
        example = literalExpression ''
          {
            PGPASSFILE = "/private/passfile";
            PGSSLMODE = "require";
          }
        '';
        description = ''
          Environment variables passed to the service.
          Usable parameters are listed in <link xlink:href="https://www.postgresql.org/docs/current/libpq-envars.html">Section 34.14 of the PostgreSQL manual</link>.
        '';
      };
    };
  };

in {
  options = {
    services.postgresqlWalReceiver = {
      receivers = mkOption {
        type = with types; attrsOf (submodule receiverSubmodule);
        default = { };
        example = literalExpression ''
          {
            main = {
              postgresqlPackage = pkgs.postgresql_11;
              directory = /mnt/pg_wal/main/;
              slot = "main_wal_receiver";
              connection = "postgresql://user@somehost";
            };
          }
        '';
        description = ''
          PostgreSQL WAL receivers.
          Stream write-ahead logs from a PostgreSQL server using <command>pg_receivewal</command> (formerly <command>pg_receivexlog</command>).
          See <link xlink:href="https://www.postgresql.org/docs/current/app-pgreceivewal.html">the man page</link> for more information.
        '';
      };
    };
  };

  config = let
    receivers = config.services.postgresqlWalReceiver.receivers;
  in mkIf (receivers != { }) {
    users = {
      users.postgres = {
        uid = config.ids.uids.postgres;
        group = "postgres";
        description = "PostgreSQL server user";
      };

      groups.postgres = {
        gid = config.ids.gids.postgres;
      };
    };

    assertions = concatLists (attrsets.mapAttrsToList (name: config: [
      {
        assertion = config.compress > 0 -> versionAtLeast config.postgresqlPackage.version "10";
        message = "Invalid configuration for WAL receiver \"${name}\": compress requires PostgreSQL version >= 10.";
      }
    ]) receivers);

    systemd.tmpfiles.rules = mapAttrsToList (name: config: ''
      d ${escapeShellArg config.directory} 0750 postgres postgres - -
    '') receivers;

    systemd.services = with attrsets; mapAttrs' (name: config: nameValuePair "postgresql-wal-receiver-${name}" {
      description = "PostgreSQL WAL receiver (${name})";
      wantedBy = [ "multi-user.target" ];
      startLimitIntervalSec = 0; # retry forever, useful in case of network disruption

      serviceConfig = {
        User = "postgres";
        Group = "postgres";
        KillSignal = "SIGINT";
        Restart = "always";
        RestartSec = 60;
      };

      inherit (config) environment;

      script = let
        receiverCommand = postgresqlPackage:
         if (versionAtLeast postgresqlPackage.version "10")
           then "${postgresqlPackage}/bin/pg_receivewal"
           else "${postgresqlPackage}/bin/pg_receivexlog";
      in ''
        ${receiverCommand config.postgresqlPackage} \
          --no-password \
          --directory=${escapeShellArg config.directory} \
          --status-interval=${toString config.statusInterval} \
          --dbname=${escapeShellArg config.connection} \
          ${optionalString (config.compress > 0) "--compress=${toString config.compress}"} \
          ${optionalString (config.slot != "") "--slot=${escapeShellArg config.slot}"} \
          ${optionalString config.synchronous "--synchronous"} \
          ${concatStringsSep " " config.extraArgs}
      '';
    }) receivers;
  };

  meta.maintainers = with maintainers; [ pacien ];
}