Skip to content

Commit a9dcb44

Browse files
author
Commitfest Bot
committed
[CF 5396] v6 - Add Option to Check All Addresses For Matching target_session_attr
This branch was automatically generated by a robot using patches from an email thread registered at: https://commitfest.postgresql.org/patch/5396 The branch will be overwritten each time a new patch version is posted to the thread, and also periodically to check for bitrot caused by changes on the master branch. Patch(es): https://www.postgresql.org/message-id/CAKK5BkEuY-iEDMcxDUDZM6N0sv8_ov6QPPPd5MQOiU8Wn_9HXw@mail.gmail.com Author(s): Andrew Jackson
2 parents 5d6eac8 + 3515114 commit a9dcb44

File tree

5 files changed

+306
-10
lines changed

5 files changed

+306
-10
lines changed

doc/src/sgml/libpq.sgml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2555,6 +2555,39 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
25552555
</listitem>
25562556
</varlistentry>
25572557

2558+
<varlistentry id="libpq-connect-check-all-addrs" xreflabel="check_all_addrs">
2559+
<term><literal>check_all_addrs</literal></term>
2560+
<listitem>
2561+
<para>
2562+
Controls whether or not all addresses within a hostname are checked when trying to make a connection
2563+
when attempting to find a connection with a matching <xref linkend="libpq-connect-target-session-attrs"/>.
2564+
2565+
There are two modes:
2566+
<variablelist>
2567+
<varlistentry>
2568+
<term><literal>0</literal> (default)</term>
2569+
<listitem>
2570+
<para>
2571+
If a successful connection is made and that connection is found to have a
2572+
mismatching <xref linkend="libpq-connect-target-session-attrs"/> do not check
2573+
any additional addresses and move onto the next host if one was provided.
2574+
</para>
2575+
</listitem>
2576+
</varlistentry>
2577+
<varlistentry>
2578+
<term><literal>1</literal></term>
2579+
<listitem>
2580+
<para>
2581+
If a successful connection is made and that connection is found to have a
2582+
mismatching <xref linkend="libpq-connect-target-session-attrs"/> proceed
2583+
to check any additional addresses.
2584+
</para>
2585+
</listitem>
2586+
</varlistentry>
2587+
</variablelist>
2588+
</para>
2589+
</listitem>
2590+
</varlistentry>
25582591
</variablelist>
25592592
</para>
25602593
</sect2>

src/interfaces/libpq/fe-connect.c

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
389389

390390
{"scram_server_key", NULL, NULL, NULL, "SCRAM-Server-Key", "D", SCRAM_MAX_KEY_LEN * 2,
391391
offsetof(struct pg_conn, scram_server_key)},
392+
{"check_all_addrs", "PGCHECKALLADDRS",
393+
DefaultLoadBalanceHosts, NULL,
394+
"Check-All-Addrs", "", 1,
395+
offsetof(struct pg_conn, check_all_addrs)},
392396

393397
/* OAuth v2 */
394398
{"oauth_issuer", NULL, NULL, NULL,
@@ -4434,11 +4438,11 @@ PQconnectPoll(PGconn *conn)
44344438
conn->status = CONNECTION_OK;
44354439
sendTerminateConn(conn);
44364440

4437-
/*
4438-
* Try next host if any, but we don't want to consider
4439-
* additional addresses for this host.
4440-
*/
4441-
conn->try_next_host = true;
4441+
if (strcmp(conn->check_all_addrs, "1") == 0)
4442+
conn->try_next_addr = true;
4443+
else
4444+
conn->try_next_host = true;
4445+
44424446
goto keep_going;
44434447
}
44444448
}
@@ -4489,11 +4493,11 @@ PQconnectPoll(PGconn *conn)
44894493
conn->status = CONNECTION_OK;
44904494
sendTerminateConn(conn);
44914495

4492-
/*
4493-
* Try next host if any, but we don't want to consider
4494-
* additional addresses for this host.
4495-
*/
4496-
conn->try_next_host = true;
4496+
if (strcmp(conn->check_all_addrs, "1") == 0)
4497+
conn->try_next_addr = true;
4498+
else
4499+
conn->try_next_host = true;
4500+
44974501
goto keep_going;
44984502
}
44994503
}
@@ -5119,6 +5123,7 @@ freePGconn(PGconn *conn)
51195123
free(conn->oauth_client_id);
51205124
free(conn->oauth_client_secret);
51215125
free(conn->oauth_scope);
5126+
free(conn->check_all_addrs);
51225127
termPQExpBuffer(&conn->errorMessage);
51235128
termPQExpBuffer(&conn->workBuffer);
51245129

src/interfaces/libpq/libpq-int.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,7 @@ struct pg_conn
427427
char *scram_client_key; /* base64-encoded SCRAM client key */
428428
char *scram_server_key; /* base64-encoded SCRAM server key */
429429
char *sslkeylogfile; /* where should the client write ssl keylogs */
430+
char *check_all_addrs; /* whether to check all ips within a host or terminate on failure */
430431

431432
bool cancelRequest; /* true if this connection is used to send a
432433
* cancel request, instead of being a normal
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
2+
# Copyright (c) 2023-2025, PostgreSQL Global Development Group
3+
use strict;
4+
use warnings FATAL => 'all';
5+
use Config;
6+
use PostgreSQL::Test::Utils;
7+
use PostgreSQL::Test::Cluster;
8+
use Test::More;
9+
10+
if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bload_balance\b/)
11+
{
12+
plan skip_all =>
13+
'Potentially unsafe test load_balance not enabled in PG_TEST_EXTRA';
14+
}
15+
16+
# Cluster setup which is shared for testing both load balancing methods
17+
my $can_bind_to_127_0_0_2 =
18+
$Config{osname} eq 'linux' || $PostgreSQL::Test::Utils::windows_os;
19+
20+
# Checks for the requirements for testing load balancing method 2
21+
if (!$can_bind_to_127_0_0_2)
22+
{
23+
plan skip_all => 'load_balance test only supported on Linux and Windows';
24+
}
25+
26+
my $hosts_path;
27+
if ($windows_os)
28+
{
29+
$hosts_path = 'c:\Windows\System32\Drivers\etc\hosts';
30+
}
31+
else
32+
{
33+
$hosts_path = '/etc/hosts';
34+
}
35+
36+
my $hosts_content = PostgreSQL::Test::Utils::slurp_file($hosts_path);
37+
38+
my $hosts_count = () =
39+
$hosts_content =~ /127\.0\.0\.[1-3] pg-loadbalancetest/g;
40+
if ($hosts_count != 3)
41+
{
42+
# Host file is not prepared for this test
43+
plan skip_all => "hosts file was not prepared for DNS load balance test";
44+
}
45+
46+
$PostgreSQL::Test::Cluster::use_tcp = 1;
47+
$PostgreSQL::Test::Cluster::test_pghost = '127.0.0.1';
48+
my $port = PostgreSQL::Test::Cluster::get_free_port();
49+
50+
my $node_primary1 = PostgreSQL::Test::Cluster->new('primary1', port => $port);
51+
$node_primary1->init(has_archiving => 1, allows_streaming => 1);
52+
53+
# Start it
54+
$node_primary1->start;
55+
56+
# Take backup from which all operations will be run
57+
$node_primary1->backup('my_backup');
58+
59+
my $node_standby = PostgreSQL::Test::Cluster->new('standby', port => $port, own_host => 1);
60+
$node_standby->init_from_backup($node_primary1, 'my_backup',
61+
has_restoring => 1);
62+
$node_standby->start();
63+
64+
my $node_primary2 = PostgreSQL::Test::Cluster->new('node1', port => $port, own_host => 1);
65+
$node_primary2 ->init();
66+
$node_primary2 ->start();
67+
68+
# target_session_attrs=primary should always choose the first one.
69+
$node_primary1->connect_ok(
70+
"host=pg-loadbalancetest port=$port target_session_attrs=primary check_all_addrs=1",
71+
"target_session_attrs=primary connects to the first node",
72+
sql => "SELECT 'connect1'",
73+
log_like => [qr/statement: SELECT 'connect1'/]);
74+
$node_primary1->connect_ok(
75+
"host=pg-loadbalancetest port=$port target_session_attrs=read-write check_all_addrs=1",
76+
"target_session_attrs=read-write connects to the first node",
77+
sql => "SELECT 'connect1'",
78+
log_like => [qr/statement: SELECT 'connect1'/]);
79+
$node_primary1->connect_ok(
80+
"host=pg-loadbalancetest port=$port target_session_attrs=any check_all_addrs=1",
81+
"target_session_attrs=any connects to the first node",
82+
sql => "SELECT 'connect1'",
83+
log_like => [qr/statement: SELECT 'connect1'/]);
84+
$node_standby->connect_ok(
85+
"host=pg-loadbalancetest port=$port target_session_attrs=standby check_all_addrs=1",
86+
"target_session_attrs=standby connects to the third node",
87+
sql => "SELECT 'connect1'",
88+
log_like => [qr/statement: SELECT 'connect1'/]);
89+
$node_standby->connect_ok(
90+
"host=pg-loadbalancetest port=$port target_session_attrs=read-only check_all_addrs=1",
91+
"target_session_attrs=read-only connects to the third node",
92+
sql => "SELECT 'connect1'",
93+
log_like => [qr/statement: SELECT 'connect1'/]);
94+
95+
96+
$node_primary1->stop();
97+
98+
# target_session_attrs=primary should always choose the first one.
99+
$node_primary2->connect_ok(
100+
"host=pg-loadbalancetest port=$port target_session_attrs=primary check_all_addrs=1",
101+
"target_session_attrs=primary connects to the first node",
102+
sql => "SELECT 'connect1'",
103+
log_like => [qr/statement: SELECT 'connect1'/]);
104+
$node_primary2->connect_ok(
105+
"host=pg-loadbalancetest port=$port target_session_attrs=read-write check_all_addrs=1",
106+
"target_session_attrs=read-write connects to the first node",
107+
sql => "SELECT 'connect1'",
108+
log_like => [qr/statement: SELECT 'connect1'/]);
109+
$node_standby->connect_ok(
110+
"host=pg-loadbalancetest port=$port target_session_attrs=any check_all_addrs=1",
111+
"target_session_attrs=any connects to the first node",
112+
sql => "SELECT 'connect1'",
113+
log_like => [qr/statement: SELECT 'connect1'/]);
114+
$node_standby->connect_ok(
115+
"host=pg-loadbalancetest port=$port target_session_attrs=standby check_all_addrs=1",
116+
"target_session_attrs=standby connects to the third node",
117+
sql => "SELECT 'connect1'",
118+
log_like => [qr/statement: SELECT 'connect1'/]);
119+
$node_standby->connect_ok(
120+
"host=pg-loadbalancetest port=$port target_session_attrs=read-only check_all_addrs=1",
121+
"target_session_attrs=read-only connects to the third node",
122+
sql => "SELECT 'connect1'",
123+
log_like => [qr/statement: SELECT 'connect1'/]);
124+
125+
$node_primary2->stop();
126+
$node_standby->stop();
127+
128+
129+
done_testing();
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Copyright (c) 2023-2025, PostgreSQL Global Development Group
2+
use strict;
3+
use warnings FATAL => 'all';
4+
use Config;
5+
use PostgreSQL::Test::Utils;
6+
use PostgreSQL::Test::Cluster;
7+
use Test::More;
8+
9+
if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bload_balance\b/)
10+
{
11+
plan skip_all =>
12+
'Potentially unsafe test load_balance not enabled in PG_TEST_EXTRA';
13+
}
14+
15+
my $can_bind_to_127_0_0_2 =
16+
$Config{osname} eq 'linux' || $PostgreSQL::Test::Utils::windows_os;
17+
18+
# Checks for the requirements for testing load balancing method 2
19+
if (!$can_bind_to_127_0_0_2)
20+
{
21+
plan skip_all => 'load_balance test only supported on Linux and Windows';
22+
}
23+
24+
my $hosts_path;
25+
if ($windows_os)
26+
{
27+
$hosts_path = 'c:\Windows\System32\Drivers\etc\hosts';
28+
}
29+
else
30+
{
31+
$hosts_path = '/etc/hosts';
32+
}
33+
34+
my $hosts_content = PostgreSQL::Test::Utils::slurp_file($hosts_path);
35+
36+
my $hosts_count = () =
37+
$hosts_content =~ /127\.0\.0\.[1-3] pg-loadbalancetest/g;
38+
if ($hosts_count != 3)
39+
{
40+
# Host file is not prepared for this test
41+
plan skip_all => "hosts file was not prepared for DNS load balance test";
42+
}
43+
44+
$PostgreSQL::Test::Cluster::use_tcp = 1;
45+
$PostgreSQL::Test::Cluster::test_pghost = '127.0.0.1';
46+
47+
my $port = PostgreSQL::Test::Cluster::get_free_port();
48+
local $Test::Builder::Level = $Test::Builder::Level + 1;
49+
my $node_primary1 = PostgreSQL::Test::Cluster->new("primary1", port => $port);
50+
$node_primary1->init(has_archiving => 1, allows_streaming => 1);
51+
52+
# Start it
53+
$node_primary1->start();
54+
55+
# Take backup from which all operations will be run
56+
$node_primary1->backup("my_backup");
57+
58+
my $node_standby = PostgreSQL::Test::Cluster->new("standby", port => $port, own_host => 1);
59+
$node_standby->init_from_backup($node_primary1, "my_backup",
60+
has_restoring => 1);
61+
$node_standby->start();
62+
63+
my $node_primary2 = PostgreSQL::Test::Cluster->new("node1", port => $port, own_host => 1);
64+
$node_primary2->init();
65+
$node_primary2->start();
66+
sub test_target_session_attr {
67+
my $target_session_attrs = shift;
68+
my $test_num = shift;
69+
my $primary1_expect_traffic = shift;
70+
my $standby_expeect_traffic = shift;
71+
my $primary2_expect_traffic = shift;
72+
# Statistically the following loop with load_balance_hosts=random will almost
73+
# certainly connect at least once to each of the nodes. The chance of that not
74+
# happening is so small that it's negligible: (2/3)^50 = 1.56832855e-9
75+
foreach my $i (1 .. 50)
76+
{
77+
$node_primary1->connect_ok(
78+
"host=pg-loadbalancetest port=$port load_balance_hosts=random target_session_attrs=${target_session_attrs} check_all_addrs=1",
79+
"repeated connections with random load balancing",
80+
sql => "SELECT 'connect${test_num}'");
81+
}
82+
my $node_primary1_occurrences = () =
83+
$node_primary1->log_content() =~ /statement: SELECT 'connect${test_num}'/g;
84+
my $node_standby_occurrences = () =
85+
$node_standby->log_content() =~ /statement: SELECT 'connect${test_num}'/g;
86+
my $node_primary2_occurrences = () =
87+
$node_primary2->log_content() =~ /statement: SELECT 'connect${test_num}'/g;
88+
89+
my $total_occurrences =
90+
$node_primary1_occurrences + $node_standby_occurrences + $node_primary2_occurrences;
91+
92+
if ($primary1_expect_traffic == 1) {
93+
ok($node_primary1_occurrences > 0, "received at least one connection on node1");
94+
}else{
95+
ok($node_primary1_occurrences == 0, "received at least one connection on node1");
96+
}
97+
if ($standby_expeect_traffic == 1) {
98+
ok($node_standby_occurrences > 0, "received at least one connection on node1");
99+
}else{
100+
ok($node_standby_occurrences == 0, "received at least one connection on node1");
101+
}
102+
103+
if ($primary2_expect_traffic == 1) {
104+
ok($node_primary2_occurrences > 0, "received at least one connection on node1");
105+
}else{
106+
ok($node_primary2_occurrences == 0, "received at least one connection on node1");
107+
}
108+
109+
ok($total_occurrences == 50, "received 50 connections across all nodes");
110+
}
111+
112+
test_target_session_attr('any',
113+
1, 1, 1, 1);
114+
test_target_session_attr('read-only',
115+
2, 0, 1, 0);
116+
test_target_session_attr('read-write',
117+
3, 1, 0, 1);
118+
test_target_session_attr('primary',
119+
4, 1, 0, 1);
120+
test_target_session_attr('standby',
121+
5, 0, 1, 0);
122+
123+
124+
$node_primary1->stop();
125+
$node_primary2->stop();
126+
$node_standby->stop();
127+
128+
done_testing();

0 commit comments

Comments
 (0)