Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 8bf58c0

Browse files
committed
Re-establish postgres_fdw connections after server or user mapping changes.
Previously, postgres_fdw would keep on using an existing connection even if the user did ALTER SERVER or ALTER USER MAPPING commands that should affect connection parameters. Teach it to watch for catcache invals on these catalogs and re-establish connections when the relevant catalog entries change. Per bug #14738 from Michal Lis. In passing, clean up some rather crufty decisions in commit ae9bfc5 about where fields of ConnCacheEntry should be reset. We now reset all the fields whenever we open a new connection. Kyotaro Horiguchi, reviewed by Ashutosh Bapat and myself. Back-patch to 9.3 where postgres_fdw appeared. Discussion: https://postgr.es/m/20170710113917.7727.10247@wrigleys.postgresql.org
1 parent 7e1fb4c commit 8bf58c0

File tree

3 files changed

+157
-18
lines changed

3 files changed

+157
-18
lines changed

contrib/postgres_fdw/connection.c

+100-18
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include "pgstat.h"
2323
#include "storage/latch.h"
2424
#include "utils/hsearch.h"
25+
#include "utils/inval.h"
2526
#include "utils/memutils.h"
2627
#include "utils/syscache.h"
2728

@@ -48,11 +49,15 @@ typedef struct ConnCacheEntry
4849
{
4950
ConnCacheKey key; /* hash key (must be first) */
5051
PGconn *conn; /* connection to foreign server, or NULL */
52+
/* Remaining fields are invalid when conn is NULL: */
5153
int xact_depth; /* 0 = no xact open, 1 = main xact open, 2 =
5254
* one level of subxact open, etc */
5355
bool have_prep_stmt; /* have we prepared any stmts in this xact? */
5456
bool have_error; /* have any subxacts aborted in this xact? */
5557
bool changing_xact_state; /* xact state change in process */
58+
bool invalidated; /* true if reconnect is pending */
59+
uint32 server_hashvalue; /* hash value of foreign server OID */
60+
uint32 mapping_hashvalue; /* hash value of user mapping OID */
5661
} ConnCacheEntry;
5762

5863
/*
@@ -69,6 +74,7 @@ static bool xact_got_connection = false;
6974

7075
/* prototypes of private functions */
7176
static PGconn *connect_pg_server(ForeignServer *server, UserMapping *user);
77+
static void disconnect_pg_server(ConnCacheEntry *entry);
7278
static void check_conn_params(const char **keywords, const char **values);
7379
static void configure_remote_session(PGconn *conn);
7480
static void do_sql_command(PGconn *conn, const char *sql);
@@ -78,6 +84,7 @@ static void pgfdw_subxact_callback(SubXactEvent event,
7884
SubTransactionId mySubid,
7985
SubTransactionId parentSubid,
8086
void *arg);
87+
static void pgfdw_inval_callback(Datum arg, int cacheid, uint32 hashvalue);
8188
static void pgfdw_reject_incomplete_xact_state_change(ConnCacheEntry *entry);
8289
static bool pgfdw_cancel_query(PGconn *conn);
8390
static bool pgfdw_exec_cleanup_query(PGconn *conn, const char *query,
@@ -95,13 +102,6 @@ static bool pgfdw_get_cleanup_result(PGconn *conn, TimestampTz endtime,
95102
* will_prep_stmt must be true if caller intends to create any prepared
96103
* statements. Since those don't go away automatically at transaction end
97104
* (not even on error), we need this flag to cue manual cleanup.
98-
*
99-
* XXX Note that caching connections theoretically requires a mechanism to
100-
* detect change of FDW objects to invalidate already established connections.
101-
* We could manage that by watching for invalidation events on the relevant
102-
* syscaches. For the moment, though, it's not clear that this would really
103-
* be useful and not mere pedantry. We could not flush any active connections
104-
* mid-transaction anyway.
105105
*/
106106
PGconn *
107107
GetConnection(UserMapping *user, bool will_prep_stmt)
@@ -130,6 +130,10 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
130130
*/
131131
RegisterXactCallback(pgfdw_xact_callback, NULL);
132132
RegisterSubXactCallback(pgfdw_subxact_callback, NULL);
133+
CacheRegisterSyscacheCallback(FOREIGNSERVEROID,
134+
pgfdw_inval_callback, (Datum) 0);
135+
CacheRegisterSyscacheCallback(USERMAPPINGOID,
136+
pgfdw_inval_callback, (Datum) 0);
133137
}
134138

135139
/* Set flag that we did GetConnection during the current transaction */
@@ -144,17 +148,27 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
144148
entry = hash_search(ConnectionHash, &key, HASH_ENTER, &found);
145149
if (!found)
146150
{
147-
/* initialize new hashtable entry (key is already filled in) */
151+
/*
152+
* We need only clear "conn" here; remaining fields will be filled
153+
* later when "conn" is set.
154+
*/
148155
entry->conn = NULL;
149-
entry->xact_depth = 0;
150-
entry->have_prep_stmt = false;
151-
entry->have_error = false;
152-
entry->changing_xact_state = false;
153156
}
154157

155158
/* Reject further use of connections which failed abort cleanup. */
156159
pgfdw_reject_incomplete_xact_state_change(entry);
157160

161+
/*
162+
* If the connection needs to be remade due to invalidation, disconnect as
163+
* soon as we're out of all transactions.
164+
*/
165+
if (entry->conn != NULL && entry->invalidated && entry->xact_depth == 0)
166+
{
167+
elog(DEBUG3, "closing connection %p for option changes to take effect",
168+
entry->conn);
169+
disconnect_pg_server(entry);
170+
}
171+
158172
/*
159173
* We don't check the health of cached connection here, because it would
160174
* require some overhead. Broken connection will be detected when the
@@ -164,15 +178,26 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
164178
/*
165179
* If cache entry doesn't have a connection, we have to establish a new
166180
* connection. (If connect_pg_server throws an error, the cache entry
167-
* will be left in a valid empty state.)
181+
* will remain in a valid empty state, ie conn == NULL.)
168182
*/
169183
if (entry->conn == NULL)
170184
{
171185
ForeignServer *server = GetForeignServer(user->serverid);
172186

173-
entry->xact_depth = 0; /* just to be sure */
187+
/* Reset all transient state fields, to be sure all are clean */
188+
entry->xact_depth = 0;
174189
entry->have_prep_stmt = false;
175190
entry->have_error = false;
191+
entry->changing_xact_state = false;
192+
entry->invalidated = false;
193+
entry->server_hashvalue =
194+
GetSysCacheHashValue1(FOREIGNSERVEROID,
195+
ObjectIdGetDatum(server->serverid));
196+
entry->mapping_hashvalue =
197+
GetSysCacheHashValue1(USERMAPPINGOID,
198+
ObjectIdGetDatum(user->umid));
199+
200+
/* Now try to make the connection */
176201
entry->conn = connect_pg_server(server, user);
177202

178203
elog(DEBUG3, "new postgres_fdw connection %p for server \"%s\" (user mapping oid %u, userid %u)",
@@ -276,6 +301,19 @@ connect_pg_server(ForeignServer *server, UserMapping *user)
276301
return conn;
277302
}
278303

304+
/*
305+
* Disconnect any open connection for a connection cache entry.
306+
*/
307+
static void
308+
disconnect_pg_server(ConnCacheEntry *entry)
309+
{
310+
if (entry->conn != NULL)
311+
{
312+
PQfinish(entry->conn);
313+
entry->conn = NULL;
314+
}
315+
}
316+
279317
/*
280318
* For non-superusers, insist that the connstr specify a password. This
281319
* prevents a password from being picked up from .pgpass, a service file,
@@ -777,9 +815,7 @@ pgfdw_xact_callback(XactEvent event, void *arg)
777815
entry->changing_xact_state)
778816
{
779817
elog(DEBUG3, "discarding connection %p", entry->conn);
780-
PQfinish(entry->conn);
781-
entry->conn = NULL;
782-
entry->changing_xact_state = false;
818+
disconnect_pg_server(entry);
783819
}
784820
}
785821

@@ -896,6 +932,47 @@ pgfdw_subxact_callback(SubXactEvent event, SubTransactionId mySubid,
896932
}
897933
}
898934

935+
/*
936+
* Connection invalidation callback function
937+
*
938+
* After a change to a pg_foreign_server or pg_user_mapping catalog entry,
939+
* mark connections depending on that entry as needing to be remade.
940+
* We can't immediately destroy them, since they might be in the midst of
941+
* a transaction, but we'll remake them at the next opportunity.
942+
*
943+
* Although most cache invalidation callbacks blow away all the related stuff
944+
* regardless of the given hashvalue, connections are expensive enough that
945+
* it's worth trying to avoid that.
946+
*
947+
* NB: We could avoid unnecessary disconnection more strictly by examining
948+
* individual option values, but it seems too much effort for the gain.
949+
*/
950+
static void
951+
pgfdw_inval_callback(Datum arg, int cacheid, uint32 hashvalue)
952+
{
953+
HASH_SEQ_STATUS scan;
954+
ConnCacheEntry *entry;
955+
956+
Assert(cacheid == FOREIGNSERVEROID || cacheid == USERMAPPINGOID);
957+
958+
/* ConnectionHash must exist already, if we're registered */
959+
hash_seq_init(&scan, ConnectionHash);
960+
while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
961+
{
962+
/* Ignore invalid entries */
963+
if (entry->conn == NULL)
964+
continue;
965+
966+
/* hashvalue == 0 means a cache reset, must clear all state */
967+
if (hashvalue == 0 ||
968+
(cacheid == FOREIGNSERVEROID &&
969+
entry->server_hashvalue == hashvalue) ||
970+
(cacheid == USERMAPPINGOID &&
971+
entry->mapping_hashvalue == hashvalue))
972+
entry->invalidated = true;
973+
}
974+
}
975+
899976
/*
900977
* Raise an error if the given connection cache entry is marked as being
901978
* in the middle of an xact state change. This should be called at which no
@@ -913,9 +990,14 @@ pgfdw_reject_incomplete_xact_state_change(ConnCacheEntry *entry)
913990
Form_pg_user_mapping umform;
914991
ForeignServer *server;
915992

916-
if (!entry->changing_xact_state)
993+
/* nothing to do for inactive entries and entries of sane state */
994+
if (entry->conn == NULL || !entry->changing_xact_state)
917995
return;
918996

997+
/* make sure this entry is inactive */
998+
disconnect_pg_server(entry);
999+
1000+
/* find server name to be shown in the message below */
9191001
tup = SearchSysCache1(USERMAPPINGOID,
9201002
ObjectIdGetDatum(entry->key));
9211003
if (!HeapTupleIsValid(tup))

contrib/postgres_fdw/expected/postgres_fdw.out

+37
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,43 @@ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (column_name 'C 1');
191191
public | ft_pg_type | loopback | (schema_name 'pg_catalog', table_name 'pg_type') |
192192
(6 rows)
193193

194+
-- Test that alteration of server options causes reconnection
195+
SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work
196+
c3 | c4
197+
-------+------------------------------
198+
00001 | Fri Jan 02 00:00:00 1970 PST
199+
(1 row)
200+
201+
ALTER SERVER loopback OPTIONS (SET dbname 'no such database');
202+
SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should fail
203+
ERROR: could not connect to server "loopback"
204+
DETAIL: FATAL: database "no such database" does not exist
205+
DO $d$
206+
BEGIN
207+
EXECUTE $$ALTER SERVER loopback
208+
OPTIONS (SET dbname '$$||current_database()||$$')$$;
209+
END;
210+
$d$;
211+
SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work again
212+
c3 | c4
213+
-------+------------------------------
214+
00001 | Fri Jan 02 00:00:00 1970 PST
215+
(1 row)
216+
217+
-- Test that alteration of user mapping options causes reconnection
218+
ALTER USER MAPPING FOR CURRENT_USER SERVER loopback
219+
OPTIONS (ADD user 'no such user');
220+
SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should fail
221+
ERROR: could not connect to server "loopback"
222+
DETAIL: FATAL: role "no such user" does not exist
223+
ALTER USER MAPPING FOR CURRENT_USER SERVER loopback
224+
OPTIONS (DROP user);
225+
SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work again
226+
c3 | c4
227+
-------+------------------------------
228+
00001 | Fri Jan 02 00:00:00 1970 PST
229+
(1 row)
230+
194231
-- Now we should be able to run ANALYZE.
195232
-- To exercise multiple code paths, we use local stats on ft1
196233
-- and remote-estimate mode on ft2.

contrib/postgres_fdw/sql/postgres_fdw.sql

+20
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,26 @@ ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (column_name 'C 1');
195195
ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (column_name 'C 1');
196196
\det+
197197

198+
-- Test that alteration of server options causes reconnection
199+
SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work
200+
ALTER SERVER loopback OPTIONS (SET dbname 'no such database');
201+
SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should fail
202+
DO $d$
203+
BEGIN
204+
EXECUTE $$ALTER SERVER loopback
205+
OPTIONS (SET dbname '$$||current_database()||$$')$$;
206+
END;
207+
$d$;
208+
SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work again
209+
210+
-- Test that alteration of user mapping options causes reconnection
211+
ALTER USER MAPPING FOR CURRENT_USER SERVER loopback
212+
OPTIONS (ADD user 'no such user');
213+
SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should fail
214+
ALTER USER MAPPING FOR CURRENT_USER SERVER loopback
215+
OPTIONS (DROP user);
216+
SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work again
217+
198218
-- Now we should be able to run ANALYZE.
199219
-- To exercise multiple code paths, we use local stats on ft1
200220
-- and remote-estimate mode on ft2.

0 commit comments

Comments
 (0)