activerecord-wait_for_lsn 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cbd7e6e0f5eb7df644152b7ca91788239c5fbbc650ccd1fc6e17666cf70c0f85
4
+ data.tar.gz: f7d5f9438d176acf4b937a1786b570cd1253092ee8de6aacb7e392dabdf4e77f
5
+ SHA512:
6
+ metadata.gz: 706c03870f25a5f9ea1642d2825b5eacc1a83cc617460e201f47f4b200db63aa8ef218cf4e2611d57048f235cd4c67e7786bb4082fa63aea37e090c51f09fdf6
7
+ data.tar.gz: 56f9d278efbd92800270b8bc35e1c2034e56567276fe3033f4a763d2a2faa0e7bd265b3e500fbe2106fcd865d70213ab812a2201dbb4c07aa24f9e985a985617
data/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-09-04
4
+
5
+ First release.
6
+
7
+ - Keep the replica connection leased for the whole request. `WAIT FOR LSN`
8
+ and every read that follows now run on the same connection, so the guarantee
9
+ holds behind a TCP load balancer or a libpq multi-host `host=a,b` where the
10
+ pool's connections point at different standbys. Before, the read could pick
11
+ another pooled connection and land on a standby that had not replayed the
12
+ LSN yet.
13
+ - Add `database` (the `database.yml` entry name) to the `read_from_replica`,
14
+ `read_from_primary` and `wait_for_lsn` event payloads.
15
+ - Add an opt-in Yabeda integration: `require "active_record/wait_for_lsn/yabeda"`
16
+ and `ActiveRecord::WaitForLSN::Yabeda.collect_replication_lag!` fill
17
+ `wait_for_lsn_replication_lag_seconds` and `wait_for_lsn_replication_lag_bytes`
18
+ per standby from `pg_stat_replication` on every export.
19
+ - Drop the `mode` option. The wait always uses `standby_replay`, the only mode
20
+ that guarantees a following `SELECT` on the replica sees the write.
21
+ `standby_write` and `standby_flush` only confirm WAL arrival, so a read could
22
+ still miss a fresh row or return a stale one. `primary_flush` is primary-only
23
+ and never applied to the replica wait.
24
+ - Rename `timeout` to `wait_timeout` and lower the default from `500ms` to
25
+ `20ms`.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 izhanov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # activerecord-wait_for_lsn
2
+
3
+ Read-your-writes on Rails read replicas via PostgreSQL 19
4
+ [`WAIT FOR LSN`](https://www.postgresql.org/docs/19/sql-wait-for.html).
5
+
6
+ After a write the resolver stores the primary's `pg_current_wal_lsn()` in the
7
+ session. Before a read it runs `WAIT FOR LSN` on the replica. If the replica has
8
+ caught up, the read goes to the replica, otherwise to the primary.
9
+
10
+ ## Usage
11
+
12
+ ```ruby
13
+ # Gemfile
14
+ gem "activerecord-wait_for_lsn"
15
+ ```
16
+
17
+ ```ruby
18
+ # config/initializers/multi_db.rb
19
+ Rails.application.configure do
20
+ config.active_record.database_selector = { wait_timeout: "50ms" } # default 20ms
21
+ config.active_record.database_resolver = ActiveRecord::WaitForLSN::Resolver
22
+ config.active_record.database_resolver_context = ActiveRecord::WaitForLSN::Session
23
+ end
24
+ ```
25
+
26
+ Requires PostgreSQL 19, a streaming standby configured as `replica: true`, and
27
+ Rails 7.2+.
28
+
29
+ ## Options
30
+
31
+ Passed through the `database_selector` hash.
32
+
33
+ | Option | Default | Description |
34
+ |------------|--------------------|-------------|
35
+ | `wait_timeout` | `"20ms"` | How long to wait for the replica before reading from the primary. Any PostgreSQL interval literal: `"200ms"`, `"1s"`. |
36
+ | `no_throw` | `true` | Return a status (`success`, `timeout`, `not in recovery`) instead of raising on timeout. |
37
+
38
+ ## Several replicas
39
+
40
+ Rails has one reading role per class, so several standbys sit behind one
41
+ `replica` entry: a TCP load balancer (HAProxy, PgBouncer in session mode) or
42
+ libpq's own multi-host connection string. With PostgreSQL 16+ libpq the entry
43
+ can spread connections itself:
44
+
45
+ ```yaml
46
+ replica:
47
+ <<: *default
48
+ host: standby1,standby2,standby3
49
+ port: "5432,5432,5432" # quoted, or YAML reads it as one number
50
+ load_balance_hosts: random
51
+ replica: true
52
+ ```
53
+
54
+ Every request leases one replica connection: the `WAIT FOR LSN` and all reads
55
+ of the request run over that connection, hence on the same standby, whichever
56
+ one the pool handed out. Transaction-pooling proxies that spread the statements
57
+ of one client connection over several servers break the guarantee.
58
+
59
+ Note that `load_balance_hosts` balances connections, not requests: libpq picks
60
+ a host when a connection is opened, and the pool then reuses its most recently
61
+ returned connections first. Under light load a couple of pooled connections
62
+ serve most requests, so the standbys they landed on see most of the reads.
63
+ For an even spread per request put a round-robin TCP balancer in front of the
64
+ standbys.
65
+
66
+ Which standby is behind and by how much is visible per standby through the
67
+ [lag metrics](#metrics), which come from `pg_stat_replication` on the primary
68
+ and cover every streaming standby, whether or not it takes reads.
69
+
70
+ ## Wait mode
71
+
72
+ The wait always uses `MODE 'standby_replay'`: the LSN must be applied on the
73
+ standby, so a following `SELECT` sees the write. The other modes
74
+ (`standby_write`, `standby_flush`) only confirm the WAL has arrived on the
75
+ standby, not that it is visible, so they are not exposed.
76
+
77
+ ## Instrumentation
78
+
79
+ Each wait emits `database_selector.active_record.wait_for_lsn` with `lsn`,
80
+ `lsn_status` and `database` (the `database.yml` entry of the standby) in the
81
+ payload. On `success` the event duration is how long the replica took to
82
+ replay your last write. `database_selector.active_record.read_from_replica`
83
+ and `database_selector.active_record.read_from_primary` fire around every read,
84
+ also with `database` in the payload, so per-replica counters are one
85
+ subscriber away:
86
+
87
+ ```ruby
88
+ ActiveSupport::Notifications.subscribe("database_selector.active_record.wait_for_lsn") do |event|
89
+ Rails.logger.info "WAIT FOR LSN on #{event.payload[:database]}: #{event.payload[:lsn_status]} in #{event.duration.round}ms"
90
+ end
91
+ ```
92
+
93
+ ## Metrics
94
+
95
+ An opt-in [Yabeda](https://github.com/yabeda-rb/yabeda) integration exports
96
+ replication lag per standby. Add Yabeda and an exporter to the Gemfile:
97
+
98
+ ```ruby
99
+ gem "yabeda"
100
+ gem "yabeda-prometheus" # or yabeda-datadog, yabeda-statsd, ...
101
+ ```
102
+
103
+ Then require the integration from an initializer and opt in:
104
+
105
+ ```ruby
106
+ # config/initializers/multi_db.rb
107
+ require "active_record/wait_for_lsn/yabeda"
108
+ ActiveRecord::WaitForLSN::Yabeda.collect_replication_lag!
109
+ ```
110
+
111
+ Before every export (each Prometheus scrape) it runs one query against the
112
+ primary and sets two gauges per streaming standby, replay stage only, since
113
+ that is the point at which a `SELECT` on the standby sees the write:
114
+
115
+ | Metric | Tags | Description |
116
+ |--------|------|-------------|
117
+ | `wait_for_lsn_replication_lag_seconds` | `standby` | `replay_lag` from `pg_stat_replication`. |
118
+ | `wait_for_lsn_replication_lag_bytes` | `standby` | `pg_current_wal_lsn() - replay_lsn`. |
119
+
120
+ `standby` is the `application_name` the standby sets in `primary_conninfo`,
121
+ which falls back to its `cluster_name`.
122
+
123
+ Why `pg_stat_replication` and not the wait itself: the duration of the
124
+ `wait_for_lsn` event is how long a request waited for its own write, capped by
125
+ `wait_timeout`. A replica that caught up before the next request arrives shows
126
+ a wait of zero. The real lag is measured by PostgreSQL from standby feedback.
127
+ Per-request counters (reads by role, wait statuses) are yours to build from the
128
+ events above if you need them.
129
+
130
+ Requirements and caveats:
131
+
132
+ - The application's database role needs `pg_read_all_stats` (or superuser),
133
+ otherwise the lag columns are `NULL` for every row:
134
+ ```sql
135
+ GRANT pg_read_all_stats TO app;
136
+ ```
137
+ - PostgreSQL reports `NULL` once a standby is fully caught up and the primary
138
+ is idle. Both gauges record it as `0`.
139
+ - The query runs on the writing connection with `prevent_writes: true` and
140
+ costs one round trip per scrape, not per request.
141
+
142
+ ```promql
143
+ # Replay lag per standby, the number DBAs see in pg_stat_replication
144
+ wait_for_lsn_replication_lag_seconds
145
+
146
+ # Alert when a standby falls more than a second behind
147
+ max(wait_for_lsn_replication_lag_seconds) > 1
148
+ ```
149
+
150
+ ## License
151
+
152
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module WaitForLSN
5
+ module ReadQueryPatch
6
+ WAIT_FOR = /\A(?:\s|\()*WAIT\s+FOR\b/i
7
+
8
+ def write_query?(sql)
9
+ return false if WAIT_FOR.match?(sql)
10
+
11
+ super
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module WaitForLSN
5
+ class Resolver < ::ActiveRecord::Middleware::DatabaseSelector::Resolver
6
+ DEFAULT_LSN_TIMEOUT = "20ms"
7
+ DEFAULT_LSN_NO_THROW = true
8
+ LSN_MODE = "standby_replay"
9
+ LSN_OUTPUT_SUCCESS = "success"
10
+
11
+ attr_reader :wait_timeout, :no_throw
12
+
13
+ def initialize(context, options = {})
14
+ super
15
+ @wait_timeout = options.fetch(:wait_timeout, DEFAULT_LSN_TIMEOUT)
16
+ @no_throw = options.fetch(:no_throw, DEFAULT_LSN_NO_THROW)
17
+ end
18
+
19
+ def read(&blk)
20
+ super
21
+ ensure
22
+ release_replica_connection
23
+ end
24
+
25
+ private
26
+
27
+ def write_to_primary
28
+ ActiveRecord::Base.connected_to(role: ActiveRecord.writing_role, prevent_writes: false) do
29
+ instrumenter.instrument("database_selector.active_record.write_to_primary") do
30
+ yield
31
+ ensure
32
+ context.update_last_write_lsn(current_write_lsn)
33
+ end
34
+ end
35
+ end
36
+
37
+ def read_from_primary(&blk)
38
+ ActiveRecord::Base.connected_to(role: ActiveRecord.writing_role, prevent_writes: true) do
39
+ instrumenter.instrument("database_selector.active_record.read_from_primary", database: database_name, &blk)
40
+ end
41
+ end
42
+
43
+ def read_from_replica(&blk)
44
+ connected_to_replica(prevent_writes: true) do
45
+ instrumenter.instrument("database_selector.active_record.read_from_replica", database: database_name, &blk)
46
+ end
47
+ end
48
+
49
+ def read_from_primary?
50
+ lsn = context.last_write_lsn
51
+ return false unless lsn
52
+ return false if wait_for_lsn(lsn) == LSN_OUTPUT_SUCCESS
53
+
54
+ release_replica_connection
55
+ true
56
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotDefined
57
+ release_replica_connection
58
+ true
59
+ end
60
+
61
+ def wait_for_lsn(lsn)
62
+ connected_to_replica do
63
+ conn = ActiveRecord::Base.connection_pool.lease_connection
64
+ query = "WAIT FOR LSN #{conn.quote(lsn)} WITH (#{wait_for_lsn_options})"
65
+ payload = { lsn: lsn, database: database_name }
66
+ instrumenter.instrument("database_selector.active_record.wait_for_lsn", payload) do |p|
67
+ p[:lsn_status] = conn.select_value(query)
68
+ end
69
+ end
70
+ end
71
+
72
+ def release_replica_connection
73
+ connected_to_replica do
74
+ ActiveRecord::Base.connection_pool.release_connection
75
+ end
76
+ rescue ActiveRecord::ConnectionNotDefined
77
+ nil
78
+ end
79
+
80
+ def connected_to_replica(prevent_writes: false, &blk)
81
+ ActiveRecord::Base.connected_to(role: ActiveRecord.reading_role, prevent_writes: prevent_writes, &blk)
82
+ end
83
+
84
+ def database_name
85
+ ActiveRecord::Base.connection_pool.db_config.name
86
+ rescue ActiveRecord::ConnectionNotDefined
87
+ nil
88
+ end
89
+
90
+ def current_write_lsn
91
+ ActiveRecord::Base.connection_pool.with_connection do |conn|
92
+ conn.select_value("SELECT pg_current_wal_lsn();")
93
+ end
94
+ end
95
+
96
+ def wait_for_lsn_options
97
+ options = { timeout: wait_timeout, mode: LSN_MODE, no_throw: no_throw }
98
+ options.filter_map do |(opt, val)|
99
+ next unless val
100
+
101
+ opt == :no_throw ? opt.to_s.upcase : "#{opt.upcase} '#{val}'"
102
+ end.join(", ")
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module WaitForLSN
5
+ class Session < ::ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
6
+ def last_write_lsn
7
+ session[:last_write_lsn]
8
+ end
9
+
10
+ def update_last_write_lsn(new_lsn)
11
+ session[:last_write_lsn] = new_lsn
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module WaitForLSN
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yabeda"
4
+
5
+ module ActiveRecord
6
+ module WaitForLSN
7
+ # Opt-in Yabeda metrics. Not loaded automatically; require it from an initializer:
8
+ #
9
+ # require "active_record/wait_for_lsn/yabeda"
10
+ # ActiveRecord::WaitForLSN::Yabeda.collect_replication_lag!
11
+ #
12
+ # Registers the +wait_for_lsn+ group with replication lag gauges per standby.
13
+ # They are filled before every export by querying +pg_stat_replication+ on the
14
+ # primary, which needs +pg_read_all_stats+. Metrics are recorded once
15
+ # +Yabeda.configure!+ has run, which Yabeda's own Railtie does after the app
16
+ # is initialized.
17
+ module Yabeda
18
+ # Replay stage only: the point at which a SELECT on the standby sees the write.
19
+ REPLICATION_LAG_SQL = <<~SQL
20
+ SELECT application_name,
21
+ pg_current_wal_lsn() - replay_lsn AS replay_lag_bytes,
22
+ EXTRACT(EPOCH FROM replay_lag) AS replay_lag_seconds
23
+ FROM pg_stat_replication
24
+ WHERE state = 'streaming'
25
+ SQL
26
+
27
+ ::Yabeda.configure do
28
+ group :wait_for_lsn do
29
+ gauge :replication_lag,
30
+ comment: "Replay lag reported by pg_stat_replication on the primary",
31
+ unit: :seconds,
32
+ tags: [:standby]
33
+
34
+ gauge :replication_lag_bytes,
35
+ comment: "WAL bytes the standby has yet to replay, from pg_stat_replication",
36
+ tags: [:standby]
37
+ end
38
+ end
39
+
40
+ class << self
41
+ # Queries pg_stat_replication on the primary before every export (each
42
+ # Prometheus scrape) and sets the replication lag gauges per standby.
43
+ def collect_replication_lag!
44
+ ::Yabeda.collect { record_replication_lag }
45
+ end
46
+
47
+ def record_replication_lag
48
+ return unless ::Yabeda.already_configured?
49
+
50
+ replication_lag_rows.each do |row|
51
+ tags = { standby: row["application_name"] }
52
+ # NULL means the standby is idle and fully caught up.
53
+ ::Yabeda.wait_for_lsn.replication_lag.set(tags, row["replay_lag_seconds"].to_f)
54
+ ::Yabeda.wait_for_lsn.replication_lag_bytes.set(tags, row["replay_lag_bytes"].to_i)
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def replication_lag_rows
61
+ ActiveRecord::Base.connected_to(role: ActiveRecord.writing_role, prevent_writes: true) do
62
+ ActiveRecord::Base.connection_pool.with_connection do |conn|
63
+ conn.select_all(REPLICATION_LAG_SQL).to_a
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "zeitwerk"
5
+
6
+ require_relative "wait_for_lsn/version"
7
+
8
+ loader = Zeitwerk::Loader.for_gem_extension(ActiveRecord)
9
+ loader.inflector.inflect("wait_for_lsn" => "WaitForLSN")
10
+ loader.ignore("#{__dir__}/wait_for_lsn/version.rb")
11
+ loader.ignore("#{__dir__}/wait_for_lsn/yabeda.rb") # opt-in, required explicitly
12
+ loader.setup
13
+
14
+ module ActiveRecord
15
+ # Read-your-writes for Rails replicas via PostgreSQL 19 +WAIT FOR LSN+.
16
+ #
17
+ # config.active_record.database_selector = { wait_timeout: "50ms" }
18
+ # config.active_record.database_resolver = ActiveRecord::WaitForLSN::Resolver
19
+ # config.active_record.database_resolver_context = ActiveRecord::WaitForLSN::Session
20
+ module WaitForLSN
21
+ end
22
+ end
23
+
24
+ # Rails classifies SQL on replica connections by its first keyword and treats
25
+ # anything unknown as a write. Teach the PostgreSQL adapter that WAIT FOR is read-only.
26
+ ActiveSupport.on_load(:active_record_postgresqladapter) do
27
+ prepend ActiveRecord::WaitForLSN::ReadQueryPatch
28
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/wait_for_lsn"
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-wait_for_lsn
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Aibek Izhanov
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activerecord
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.2'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.2'
26
+ - !ruby/object:Gem::Dependency
27
+ name: zeitwerk
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.6'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.6'
40
+ description: |-
41
+ A DatabaseSelector resolver that stores the primary WAL LSN after each write and runs
42
+ WAIT FOR LSN on the replica before reading, falling back to the primary on timeout.
43
+ email:
44
+ - aibek.izhanov@evilmartians.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - CHANGELOG.md
50
+ - LICENSE.txt
51
+ - README.md
52
+ - Rakefile
53
+ - lib/active_record/wait_for_lsn.rb
54
+ - lib/active_record/wait_for_lsn/read_query_patch.rb
55
+ - lib/active_record/wait_for_lsn/resolver.rb
56
+ - lib/active_record/wait_for_lsn/session.rb
57
+ - lib/active_record/wait_for_lsn/version.rb
58
+ - lib/active_record/wait_for_lsn/yabeda.rb
59
+ - lib/activerecord-wait_for_lsn.rb
60
+ homepage: https://github.com/izhanov/activerecord-wait_for_lsn
61
+ licenses:
62
+ - MIT
63
+ metadata:
64
+ homepage_uri: https://github.com/izhanov/activerecord-wait_for_lsn
65
+ source_code_uri: https://github.com/izhanov/activerecord-wait_for_lsn
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 3.2.0
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubygems_version: 3.6.9
81
+ specification_version: 4
82
+ summary: Read-your-writes on Rails replicas using PostgreSQL 19 WAIT FOR LSN
83
+ test_files: []