Retry Cached Remote Connections for postgres_fdw in case remote backend gets killed/goes away
Hi,
Currently with the postgres_fdw remote connections cached in the local
backend, the queries that use the cached connections from local
backend will not check whether the remote backend is killed or gone
away, and it goes tries to submit the query and fails if the remote
backend is killed.
This problem was found during the discussions made in [1]/messages/by-id/CAExHW5t21B_XPQy_hownm1Qq=hMrgOhX+8gDj3YEKFvpk=VRgw@mail.gmail.com.
One way, we could solve the above problem is that, upon firing the new
foreign query from local backend using the cached connection,
(assuming the remote backend that was cached in the local backed got
killed for some reasons), instead of failing the query in the local
backend, upon detecting error from the remote backend, we could just
delete the cached old entry and try getting another connection to
remote backend, cache it and proceed to submit the query. This has to
happen only at the beginning of remote xact.
This way, instead of the query getting failed, the query succeeds if
the local backend is able to get a new remote backend connection.
Attaching the patch that implements the above retry mechanism.
The way I tested the patch:
1. select * from foreign_tbl; /*from local backend - this results in a
remote connection being cached in the postgres_fdw connection cache
and a remote backend is opened.*/
2. (intentionally) kill the remote backend, just to simulate the scenario.
3. select * from foreign_tbl; /*from local backend - without patch
this throws error "ERROR: server closed the connection unexpectedly".
with patch - try to use the cached connection at the beginning of
remote xact, upon receiving error from remote postgres server, instead
of aborting the query, delete the cached entry, try to get a new
connection, if it gets, caches it and uses that for executing the
query, query succeeds.
I couldn't think of adding a test case to the existing postgres_fdw
regression test suite with an automated scenario of the remote backend
getting killed.
I would like to thank Ashutosh Bapat (ashutosh.bapat.oss@gmail.com)
for the suggestion to fix this and the review of my initial patch
attached in [2]/messages/by-id/CALj2ACXp6DQ3iLGx5g+LgVtGwC4F6K9WzKQJpyR4FfdydQzC_g@mail.gmail.com. I tried to address the review comments provided on my
initial patch [3]/messages/by-id/CAExHW5u3Gyv6Q1BEr6zMg0t+59e8c4KMfKVrV3Z=4UKKjJ19nQ@mail.gmail.com.
For, one of the Ashutosh's review comments from [3]/messages/by-id/CAExHW5u3Gyv6Q1BEr6zMg0t+59e8c4KMfKVrV3Z=4UKKjJ19nQ@mail.gmail.com "the fact that the
same connection may be used by multiple plan nodes", I tried to have
few use cases where there exist joins on two foreign tables on the
same remote server, in a single query, so essentially, the same
connection was used for multiple plan nodes. In this case we avoid
retrying for the second GetConnection() request for the second foreign
table, with the check entry->xact_depth <= 0 , xact_depth after the
first GetConnection() and the first remote query will become 1 and we
don't hit the retry logic and seems like we are safe here. Please add
If I'm missing something here.
Request the community to consider the patch for further review if the
overall idea seems beneficial.
[1]: /messages/by-id/CAExHW5t21B_XPQy_hownm1Qq=hMrgOhX+8gDj3YEKFvpk=VRgw@mail.gmail.com
[2]: /messages/by-id/CALj2ACXp6DQ3iLGx5g+LgVtGwC4F6K9WzKQJpyR4FfdydQzC_g@mail.gmail.com
[3]: /messages/by-id/CAExHW5u3Gyv6Q1BEr6zMg0t+59e8c4KMfKVrV3Z=4UKKjJ19nQ@mail.gmail.com
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
Attachments:
v1-Retry-Cached-Remote-Connections-For-postgres_fdw.patchapplication/octet-stream; name=v1-Retry-Cached-Remote-Connections-For-postgres_fdw.patchDownload
From 5dba42e231986d4c0f0f826ab8ddbcd6861524a6 Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddy@enterprisedb.com>
Date: Wed, 8 Jul 2020 17:24:03 +0530
Subject: [PATCH v2] Retry Cached Remote Connections For postgres_fdw
Remote connections are cached for postgres_fdw in the local backend.
There are high chances that the remote backend can no logner be
avaiable i.e. it can get killed, the subsequent foreign queries
from local backend/session that uses cached connection fails as
the remote backend woule have become unavailable/killed. So,
this patch, solves this problem,
1. local backend/session uses cached connection, but recieves error
2. upon receiving the first error, it deletes the cached entry
3. try to get new connection at the start of begin remote xact
---
contrib/postgres_fdw/connection.c | 125 +++++++++++++++++++++++++++++-
1 file changed, 121 insertions(+), 4 deletions(-)
diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c
index 52d1fe3563..e2d4fe0569 100644
--- a/contrib/postgres_fdw/connection.c
+++ b/contrib/postgres_fdw/connection.c
@@ -45,6 +45,19 @@
*/
typedef Oid ConnCacheKey;
+typedef enum ConnectionRetryType
+{
+ /* initial value for all cache entries */
+ CONN_RETRY_NONE,
+ /* indicates that the caller is ready to retry connection */
+ CONN_RETRY_READY,
+ /*
+ * indicates to the caller that the connection may have
+ * broken, it's okay to retry to get a new connection.
+ */
+ CONN_RETRY_DO
+}ConnectionRetryType;
+
typedef struct ConnCacheEntry
{
ConnCacheKey key; /* hash key (must be first) */
@@ -58,8 +71,15 @@ typedef struct ConnCacheEntry
bool invalidated; /* true if reconnect is pending */
uint32 server_hashvalue; /* hash value of foreign server OID */
uint32 mapping_hashvalue; /* hash value of user mapping OID */
+ /* conn retry status, default is CONN_RETRY_NONE for all entries */
+ uint8 conn_retry;
} ConnCacheEntry;
+#define IsRemoteXactBegin(sql) \
+ ((strcmp(sql, "START TRANSACTION ISOLATION LEVEL SERIALIZABLE") == 0 || \
+ strcmp(sql, "START TRANSACTION ISOLATION LEVEL REPEATABLE READ") == 0) ? \
+ 1 : 0)
+
/*
* Connection cache (initialized on first use)
*/
@@ -109,6 +129,7 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
bool found;
ConnCacheEntry *entry;
ConnCacheKey key;
+ bool dobeginxact = true;
/* First time through, initialize connection cache hashtable */
if (ConnectionHash == NULL)
@@ -170,10 +191,53 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
}
/*
- * We don't check the health of cached connection here, because it would
- * require some overhead. Broken connection will be detected when the
- * connection is actually used.
+ * We check the health of cached connection here, while the remote xact is
+ * goting to begin. Broken connection will be detected and if so, clear the
+ * existing conn, get a new connection and resubmit the remote xact begin.
+ * We retry the broken connections only once during each begin remote xact
+ * call, still the broken connection exist after that then, the query fails.
*/
+ if (entry->conn != NULL &&
+ entry->xact_depth <= 0 &&
+ entry->conn_retry == CONN_RETRY_NONE)
+ {
+ /*
+ * This is before begin remote xact, so safe to do retry, hence indicate
+ * that in the cached entry.
+ */
+ entry->conn_retry = CONN_RETRY_READY;
+ begin_remote_xact(entry);
+
+ /*
+ * The previously set RETRY_READY status gets changed to RETRY_DO, if there
+ * exists a broken connection, if so, then clear the entry.
+ */
+ if (entry->conn_retry == CONN_RETRY_DO)
+ {
+ elog(DEBUG3, "cached connection %p is broken, hence closing it", entry->conn);
+ /*
+ * It is okay to call disconnect here thought the connection is broken.
+ * No termninate message will actually be sent to remote backend by
+ * sendTerminateConn() as the connection status will be CONNECTION_BAD.
+ * So disconnect will just do the clearing and resetting of parameters
+ * in entry data structure.
+ */
+ disconnect_pg_server(entry);
+ }
+ else if (entry->conn_retry == CONN_RETRY_READY)
+ {
+ elog(DEBUG3, "cached connection %p is healthy, hence using it", entry->conn);
+ /*
+ * Connection is not broken, so remote xact being command is executed
+ * successfully. Mark that here, to avoid further begin remote xact call
+ * in this funciton.
+ */
+ dobeginxact = false;
+ }
+
+ /* Reset the conn_retry to default value. */
+ entry->conn_retry = CONN_RETRY_NONE;
+ }
/*
* If cache entry doesn't have a connection, we have to establish a new
@@ -199,6 +263,7 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
/* Now try to make the connection */
entry->conn = connect_pg_server(server, user);
+ entry->conn_retry = CONN_RETRY_NONE;
elog(DEBUG3, "new postgres_fdw connection %p for server \"%s\" (user mapping oid %u, userid %u)",
entry->conn, server->servername, user->umid, user->userid);
@@ -207,7 +272,8 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
/*
* Start a new transaction or subtransaction if needed.
*/
- begin_remote_xact(entry);
+ if (dobeginxact)
+ begin_remote_xact(entry);
/* Remember if caller will prepare statements */
entry->have_prep_stmt |= will_prep_stmt;
@@ -457,6 +523,17 @@ do_sql_command(PGconn *conn, const char *sql)
if (!PQsendQuery(conn, sql))
pgfdw_report_error(ERROR, NULL, conn, false, sql);
res = pgfdw_get_result(conn, sql);
+
+ /*
+ * If this function is called from begin_remote_xact and returned res is null
+ * from pgfdw_get_result, which means that the remote connection is broken, so
+ * just return from here, the cached entry's retry status would have been set
+ * appropriately to indicate the broken connection.
+ */
+ if (res == NULL &&
+ IsRemoteXactBegin(sql))
+ return;
+
if (PQresultStatus(res) != PGRES_COMMAND_OK)
pgfdw_report_error(ERROR, res, conn, true, sql);
PQclear(res);
@@ -591,6 +668,7 @@ PGresult *
pgfdw_get_result(PGconn *conn, const char *query)
{
PGresult *volatile last_res = NULL;
+ bool retry = false;
/* In what follows, do not leak any PGresults on an error. */
PG_TRY();
@@ -617,10 +695,49 @@ pgfdw_get_result(PGconn *conn, const char *query)
if (wc & WL_SOCKET_READABLE)
{
if (!PQconsumeInput(conn))
+ {
+ /*
+ * Do this on error and for only begin remote xact
+ * commands.
+ */
+ if (IsRemoteXactBegin(query) &&
+ ConnectionHash != NULL)
+ {
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+
+ hash_seq_init(&scan, ConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ if (entry->conn == conn &&
+ entry->conn_retry == CONN_RETRY_READY)
+ {
+ /* got the required connection from the cache. */
+ entry->conn_retry = CONN_RETRY_DO;
+ hash_seq_term(&scan);
+ retry = true;
+ break;
+ }
+ }
+ }
+
+ /* if retry to be done, don't report error. */
+ if (retry == true)
+ break;
+
pgfdw_report_error(ERROR, NULL, conn, false, query);
+ }
}
}
+ if (retry == true)
+ {
+ /* clear if there is any previous res. */
+ PQclear(last_res);
+ last_res = NULL;
+ break;
+ }
+
res = PQgetResult(conn);
if (res == NULL)
break; /* query is complete */
--
2.25.1
Hi,
On Wed, Jul 8, 2020 at 9:40 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:
One way, we could solve the above problem is that, upon firing the new
foreign query from local backend using the cached connection,
(assuming the remote backend that was cached in the local backed got
killed for some reasons), instead of failing the query in the local
backend, upon detecting error from the remote backend, we could just
delete the cached old entry and try getting another connection to
remote backend, cache it and proceed to submit the query. This has to
happen only at the beginning of remote xact.
+1.
I think this is a very useful feature.
In an environment with connection pooling for local, if a remote
server has a failover or switchover,
this feature would prevent unexpected errors of local queries after
recovery of the remote server.
I haven't looked at the code in detail yet, some comments here.
1. To keep the previous behavior (and performance), how about allowing
the user to specify
whether or not to retry as a GUC parameter or in the FOREIGN SERVER OPTION?
2. How about logging a LOG message when retry was success to let us know
the retry feature worked or how often the retries worked ?
I couldn't think of adding a test case to the existing postgres_fdw
regression test suite with an automated scenario of the remote backend
getting killed.
Couldn't you confirm this by adding a test case like the following?
===================================================
BEGIN;
-- Generate a connection to remote
SELECT * FROM ft1 LIMIT 1;
-- retrieve pid of postgres_fdw and kill it
-- could use the other unique identifier (not postgres_fdw but
fdw_retry_check, etc ) for application name
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
backend_type = 'client backend' AND application_name = 'postgres_fdw'
-- COMMIT, so next query will should success if connection-retry works
COMMIT;
SELECT * FROM ft1 LIMIT 1;
===================================================
Best regards,
--
Tatsuhito Kasahara
kasahara.tatsuhito _at_ gmail.com
Thanks for the comments. Attaching the v2 patch.
One way, we could solve the above problem is that, upon firing the new
foreign query from local backend using the cached connection,
(assuming the remote backend that was cached in the local backed got
killed for some reasons), instead of failing the query in the local
backend, upon detecting error from the remote backend, we could just
delete the cached old entry and try getting another connection to
remote backend, cache it and proceed to submit the query. This has to
happen only at the beginning of remote xact.+1.
I think this is a very useful feature.
In an environment with connection pooling for local, if a remote
server has a failover or switchover,
this feature would prevent unexpected errors of local queries after
recovery of the remote server.
Thanks for backing this feature.
I haven't looked at the code in detail yet, some comments here.
Thanks for the comments. Please feel free to review more of the
attached v2 patch.
1. To keep the previous behavior (and performance), how about allowing
the user to specify
whether or not to retry as a GUC parameter or in the FOREIGN SERVER
OPTION?
Do we actually need this? We don't encounter much performance with this
connection retry, as
we just do it at the beginning of the remote xact i.e. only once per a
remote session, if we are
able to establish it's well and good otherwise, the query is bound to fail.
If at all, we need one (if there exists a strong reason to have the
option), then the question is
GUC or the SERVER OPTION?
There's a similar discussion going on having GUC at the core level vs
SERVER OPTION for
postgres_fdw in [1]/messages/by-id/CALj2ACVvrp5=AVp2PupEm+nAC8S4buqR3fJMmaCoc7ftT0aD2A@mail.gmail.com With Regards, Bharath Rupireddy. EnterpriseDB: http://www.enterprisedb.com.
2. How about logging a LOG message when retry was success to let us know
the retry feature worked or how often the retries worked ?
In the v1 patch I added the logging messages, but in v2 patch
"postgres_fdw connection retry is successful" is added. Please note that
all the
new logs are added at level "DEBUG3" as all the existing logs are also at
the same
level.
I couldn't think of adding a test case to the existing postgres_fdw
regression test suite with an automated scenario of the remote backend
getting killed.Couldn't you confirm this by adding a test case like the following?
===================================================
BEGIN;
-- Generate a connection to remote
SELECT * FROM ft1 LIMIT 1;-- retrieve pid of postgres_fdw and kill it
-- could use the other unique identifier (not postgres_fdw but
fdw_retry_check, etc ) for application name
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
backend_type = 'client backend' AND application_name = 'postgres_fdw'-- COMMIT, so next query will should success if connection-retry works
COMMIT;
SELECT * FROM ft1 LIMIT 1;
===================================================
Yes, this way it works. Thanks for the suggestion. I added the test
case to the postgres_fdw regression test suite. v2 patch has these
changes also.
[1]: /messages/by-id/CALj2ACVvrp5=AVp2PupEm+nAC8S4buqR3fJMmaCoc7ftT0aD2A@mail.gmail.com With Regards, Bharath Rupireddy. EnterpriseDB: http://www.enterprisedb.com
/messages/by-id/CALj2ACVvrp5=AVp2PupEm+nAC8S4buqR3fJMmaCoc7ftT0aD2A@mail.gmail.com
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
Attachments:
v2-Retry-Cached-Remote-Connections-For-postgres_fdw.patchapplication/x-patch; name=v2-Retry-Cached-Remote-Connections-For-postgres_fdw.patchDownload
From b3725acc916e0daa2797b9d159fc6cc5cdcf43c6 Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddy@enterprisedb.com>
Date: Sat, 11 Jul 2020 14:57:58 +0530
Subject: [PATCH v2] Retry Cached Remote Connections For postgres_fdw
Remote connections are cached for postgres_fdw in the local backend.
There are high chances that the remote backend can no logner be
avaiable i.e. it can get killed, the subsequent foreign queries
from local backend/session that uses cached connection fails as
the remote backend woule have become unavailable/killed. So,
this patch, solves this problem,
1. local backend/session uses cached connection, but recieves error
2. upon receiving the first error, it deletes the cached entry
3. try to get new connection at the start of begin remote xact
---
contrib/postgres_fdw/connection.c | 130 +++++++++++++++++-
.../postgres_fdw/expected/postgres_fdw.out | 30 ++++
contrib/postgres_fdw/sql/postgres_fdw.sql | 19 +++
3 files changed, 175 insertions(+), 4 deletions(-)
diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c
index 52d1fe3563..e3658dd704 100644
--- a/contrib/postgres_fdw/connection.c
+++ b/contrib/postgres_fdw/connection.c
@@ -45,6 +45,19 @@
*/
typedef Oid ConnCacheKey;
+typedef enum ConnectionRetryType
+{
+ /* initial value for all cache entries */
+ CONN_RETRY_NONE,
+ /* indicates that the caller is ready to retry connection */
+ CONN_RETRY_READY,
+ /*
+ * indicates to the caller that the connection may have
+ * broken, it's okay to retry to get a new connection.
+ */
+ CONN_RETRY_DO
+}ConnectionRetryType;
+
typedef struct ConnCacheEntry
{
ConnCacheKey key; /* hash key (must be first) */
@@ -58,8 +71,15 @@ typedef struct ConnCacheEntry
bool invalidated; /* true if reconnect is pending */
uint32 server_hashvalue; /* hash value of foreign server OID */
uint32 mapping_hashvalue; /* hash value of user mapping OID */
+ /* conn retry status, default is CONN_RETRY_NONE for all entries */
+ uint8 conn_retry;
} ConnCacheEntry;
+#define IsRemoteXactBegin(sql) \
+ ((strcmp(sql, "START TRANSACTION ISOLATION LEVEL SERIALIZABLE") == 0 || \
+ strcmp(sql, "START TRANSACTION ISOLATION LEVEL REPEATABLE READ") == 0) ? \
+ 1 : 0)
+
/*
* Connection cache (initialized on first use)
*/
@@ -109,6 +129,8 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
bool found;
ConnCacheEntry *entry;
ConnCacheKey key;
+ bool dobeginxact = true;
+ bool doretry = false;
/* First time through, initialize connection cache hashtable */
if (ConnectionHash == NULL)
@@ -170,10 +192,54 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
}
/*
- * We don't check the health of cached connection here, because it would
- * require some overhead. Broken connection will be detected when the
- * connection is actually used.
+ * We check the health of cached connection here, while the remote xact is
+ * goting to begin. Broken connection will be detected and if so, clear the
+ * existing conn, get a new connection and resubmit the remote xact begin.
+ * We retry the broken connections only once during each begin remote xact
+ * call, still the broken connection exist after that then, the query fails.
*/
+ if (entry->conn != NULL &&
+ entry->xact_depth <= 0 &&
+ entry->conn_retry == CONN_RETRY_NONE)
+ {
+ /*
+ * This is before begin remote xact, so safe to do retry, hence indicate
+ * that in the cached entry.
+ */
+ entry->conn_retry = CONN_RETRY_READY;
+ begin_remote_xact(entry);
+
+ /*
+ * The previously set RETRY_READY status gets changed to RETRY_DO, if there
+ * exists a broken connection, if so, then clear the entry.
+ */
+ if (entry->conn_retry == CONN_RETRY_DO)
+ {
+ elog(DEBUG3, "postgres_fdw cached connection %p is broken, hence closing it", entry->conn);
+ /*
+ * It is okay to call disconnect here thought the connection is broken.
+ * No termninate message will actually be sent to remote backend by
+ * sendTerminateConn() as the connection status will be CONNECTION_BAD.
+ * So disconnect will just do the clearing and resetting of parameters
+ * in entry data structure.
+ */
+ disconnect_pg_server(entry);
+ doretry = true;
+ }
+ else if (entry->conn_retry == CONN_RETRY_READY)
+ {
+ elog(DEBUG3, "postgres_fdw cached connection %p is healthy, hence using it", entry->conn);
+ /*
+ * Connection is not broken, so remote xact being command is executed
+ * successfully. Mark that here, to avoid further begin remote xact call
+ * in this funciton.
+ */
+ dobeginxact = false;
+ }
+
+ /* Reset the conn_retry to default value. */
+ entry->conn_retry = CONN_RETRY_NONE;
+ }
/*
* If cache entry doesn't have a connection, we have to establish a new
@@ -199,15 +265,20 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
/* Now try to make the connection */
entry->conn = connect_pg_server(server, user);
+ entry->conn_retry = CONN_RETRY_NONE;
elog(DEBUG3, "new postgres_fdw connection %p for server \"%s\" (user mapping oid %u, userid %u)",
entry->conn, server->servername, user->umid, user->userid);
+
+ if (doretry)
+ elog(DEBUG3, "postgres_fdw connection retry is successful");
}
/*
* Start a new transaction or subtransaction if needed.
*/
- begin_remote_xact(entry);
+ if (dobeginxact)
+ begin_remote_xact(entry);
/* Remember if caller will prepare statements */
entry->have_prep_stmt |= will_prep_stmt;
@@ -457,6 +528,17 @@ do_sql_command(PGconn *conn, const char *sql)
if (!PQsendQuery(conn, sql))
pgfdw_report_error(ERROR, NULL, conn, false, sql);
res = pgfdw_get_result(conn, sql);
+
+ /*
+ * If this function is called from begin_remote_xact and returned res is null
+ * from pgfdw_get_result, which means that the remote connection is broken, so
+ * just return from here, the cached entry's retry status would have been set
+ * appropriately to indicate the broken connection.
+ */
+ if (res == NULL &&
+ IsRemoteXactBegin(sql))
+ return;
+
if (PQresultStatus(res) != PGRES_COMMAND_OK)
pgfdw_report_error(ERROR, res, conn, true, sql);
PQclear(res);
@@ -591,6 +673,7 @@ PGresult *
pgfdw_get_result(PGconn *conn, const char *query)
{
PGresult *volatile last_res = NULL;
+ bool retry = false;
/* In what follows, do not leak any PGresults on an error. */
PG_TRY();
@@ -617,10 +700,49 @@ pgfdw_get_result(PGconn *conn, const char *query)
if (wc & WL_SOCKET_READABLE)
{
if (!PQconsumeInput(conn))
+ {
+ /*
+ * Do this on error and for only begin remote xact
+ * commands.
+ */
+ if (IsRemoteXactBegin(query) &&
+ ConnectionHash != NULL)
+ {
+ HASH_SEQ_STATUS scan;
+ ConnCacheEntry *entry;
+
+ hash_seq_init(&scan, ConnectionHash);
+ while ((entry = (ConnCacheEntry *) hash_seq_search(&scan)))
+ {
+ if (entry->conn == conn &&
+ entry->conn_retry == CONN_RETRY_READY)
+ {
+ /* got the required connection from the cache. */
+ entry->conn_retry = CONN_RETRY_DO;
+ hash_seq_term(&scan);
+ retry = true;
+ break;
+ }
+ }
+ }
+
+ /* if retry to be done, don't report error. */
+ if (retry == true)
+ break;
+
pgfdw_report_error(ERROR, NULL, conn, false, query);
+ }
}
}
+ if (retry == true)
+ {
+ /* clear if there is any previous res. */
+ PQclear(last_res);
+ last_res = NULL;
+ break;
+ }
+
res = PQgetResult(conn);
if (res == NULL)
break; /* query is complete */
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 82fc1290ef..a813c4918f 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8974,3 +8974,33 @@ PREPARE TRANSACTION 'fdw_tpc';
ERROR: cannot PREPARE a transaction that has operated on postgres_fdw foreign tables
ROLLBACK;
WARNING: there is no transaction in progress
+-- Retry cached connections at the beginning of the remote xact
+-- in case remote backend is killed.
+-- Let's use a different application name for remote connection,
+-- so that this test will not kill other backends wrongly.
+ALTER SERVER loopback OPTIONS (application_name 'fdw_retry_check');
+-- Generate a connection to remote. Local backend will cache it.
+SELECT * FROM ft1 LIMIT 1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+------+----+----+----+----+----+------------+----
+ 1111 | 2 | | | | | ft1 |
+(1 row)
+
+-- Retrieve pid of remote backend with application name fdw_retry_check
+-- and kill it intentionally here. Note that, local backend still has
+-- the remote connection/backend info in it's cache.
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+ pg_terminate_backend
+----------------------
+ t
+(1 row)
+
+-- Next query using the same foreign server should succeed if connection
+-- retry works.
+SELECT * FROM ft1 LIMIT 1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+------+----+----+----+----+----+------------+----
+ 1111 | 2 | | | | | ft1 |
+(1 row)
+
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..9552625d2e 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2634,3 +2634,22 @@ SELECT count(*) FROM ft1;
-- error here
PREPARE TRANSACTION 'fdw_tpc';
ROLLBACK;
+
+-- Retry cached connections at the beginning of the remote xact
+-- in case remote backend is killed.
+-- Let's use a different application name for remote connection,
+-- so that this test will not kill other backends wrongly.
+ALTER SERVER loopback OPTIONS (application_name 'fdw_retry_check');
+
+-- Generate a connection to remote. Local backend will cache it.
+SELECT * FROM ft1 LIMIT 1;
+
+-- Retrieve pid of remote backend with application name fdw_retry_check
+-- and kill it intentionally here. Note that, local backend still has
+-- the remote connection/backend info in it's cache.
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+
+-- Next query using the same foreign server should succeed if connection
+-- retry works.
+SELECT * FROM ft1 LIMIT 1;
--
2.25.1
On Wed, Jul 8, 2020 at 6:10 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:
I couldn't think of adding a test case to the existing postgres_fdw
regression test suite with an automated scenario of the remote backend
getting killed.
You could get a backend's PID using PQbackendPID and then kill it by
calling pg_terminate_backend() to kill the remote backend to automate
scenario of remote backend being killed.
I would like to thank Ashutosh Bapat (ashutosh.bapat.oss@gmail.com)
for the suggestion to fix this and the review of my initial patch
attached in [2]. I tried to address the review comments provided on my
initial patch [3].For, one of the Ashutosh's review comments from [3] "the fact that the
same connection may be used by multiple plan nodes", I tried to have
few use cases where there exist joins on two foreign tables on the
same remote server, in a single query, so essentially, the same
connection was used for multiple plan nodes. In this case we avoid
retrying for the second GetConnection() request for the second foreign
table, with the check entry->xact_depth <= 0 , xact_depth after the
first GetConnection() and the first remote query will become 1 and we
don't hit the retry logic and seems like we are safe here. Please add
If I'm missing something here.Request the community to consider the patch for further review if the
overall idea seems beneficial.
I think this idea will be generally useful if your work on dropping
stale connection uses idle_connection_timeout or something like that
on the remote server.
About the patch. It seems we could just catch the error from
begin_remote_xact() in GetConnection() and retry connection if the
error is "bad connection". Retrying using PQreset() might be better
than calling PQConnect* always.
[1] /messages/by-id/CAExHW5t21B_XPQy_hownm1Qq=hMrgOhX+8gDj3YEKFvpk=VRgw@mail.gmail.com
[2] /messages/by-id/CALj2ACXp6DQ3iLGx5g+LgVtGwC4F6K9WzKQJpyR4FfdydQzC_g@mail.gmail.com
[3] /messages/by-id/CAExHW5u3Gyv6Q1BEr6zMg0t+59e8c4KMfKVrV3Z=4UKKjJ19nQ@mail.gmail.comWith Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
--
Best Wishes,
Ashutosh Bapat
You could get a backend's PID using PQbackendPID and then kill it by
calling pg_terminate_backend() to kill the remote backend to automate
scenario of remote backend being killed.
I already added the test case in v2 patch itself(added one more test
case in v3 patch), using the similar approach.
For, one of the Ashutosh's review comments from [3] "the fact that the
same connection may be used by multiple plan nodes", I tried to have
few use cases where there exist joins on two foreign tables on the
same remote server, in a single query, so essentially, the same
connection was used for multiple plan nodes. In this case we avoid
retrying for the second GetConnection() request for the second foreign
table, with the check entry->xact_depth <= 0 , xact_depth after the
first GetConnection() and the first remote query will become 1 and we
don't hit the retry logic and seems like we are safe here. Please add
If I'm missing something here.Request the community to consider the patch for further review if the
overall idea seems beneficial.I think this idea will be generally useful if your work on dropping
stale connection uses idle_connection_timeout or something like that
on the remote server.
Assuming we use idle_connection_timeout or some other means(as it is
not yet finalized, I will try to respond in that mail chain) to drop
stale/idle connections from the local backend, I think we have two
options 1) deleting that cached entry from the connection cache
entirely using disconnect_pg_server() and hash table remove. This
frees up some space and we don't have to deal with the connection
invalidations and don't have to bother on resetting cached entry's
other parameters such as xact_depth, have_prep_stmt etc. 2) or we
could just drop the connections using disconnect_pg_server(), retain
the hash entry, reset other parameters, and deal with the
invalidations. This is like, we maintain unnecessary info in the
cached entry, where we actually don't have a connection at all and
keep holding some space for cached entry.
IMO, if we go with option 1, then it will be good.
Anyways, this connection retry feature will not have any dependency on
the idle_connection_timeout or dropping stale connection feature,
because there can be many reasons where remote backends go away/get
killed.
If I'm not sidetracking - if we use something like
idle_session_timeout [1]/messages/by-id/763A0689-F189-459E-946F-F0EC4458980B@hotmail.com on the remote server, this retry feature will
be very useful.
About the patch. It seems we could just catch the error from
begin_remote_xact() in GetConnection() and retry connection if the
error is "bad connection". Retrying using PQreset() might be better
than calling PQConnect* always.
Thanks for the comment, it made life easier. Added the patch with the
changes. Please take a look at the v3 patch and let me know if still
something needs to be done.
[1]: /messages/by-id/763A0689-F189-459E-946F-F0EC4458980B@hotmail.com
/messages/by-id/763A0689-F189-459E-946F-F0EC4458980B@hotmail.com
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
Attachments:
v3-Retry-Cached-Remote-Connections-For-postgres_fdw.patchapplication/octet-stream; name=v3-Retry-Cached-Remote-Connections-For-postgres_fdw.patchDownload
From 5be13fbfd31ab5af9265ac9cb3adcf0589bf2a8e Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddy@enterprisedb.com>
Date: Tue, 14 Jul 2020 07:23:14 +0530
Subject: [PATCH v3] Retry Cached Remote Connections For postgres_fdw
Remote connections are cached for postgres_fdw in the local backend.
There are high chances that the remote backend can no logner be
avaiable i.e. it can get killed, the subsequent foreign queries
from local backend/session that uses cached connection fails as
the remote backend woule have become unavailable/killed. So,
this patch, solves this problem,
1. local backend/session uses cached connection, but recieves error
2. upon receiving the first error, it deletes the cached entry
3. try to get new connection at the start of begin remote xact
---
contrib/postgres_fdw/connection.c | 65 ++++++++++++++++---
.../postgres_fdw/expected/postgres_fdw.out | 55 ++++++++++++++++
contrib/postgres_fdw/sql/postgres_fdw.sql | 31 +++++++++
3 files changed, 143 insertions(+), 8 deletions(-)
diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c
index 52d1fe3563..b7ae8dbbac 100644
--- a/contrib/postgres_fdw/connection.c
+++ b/contrib/postgres_fdw/connection.c
@@ -169,12 +169,6 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
disconnect_pg_server(entry);
}
- /*
- * We don't check the health of cached connection here, because it would
- * require some overhead. Broken connection will be detected when the
- * connection is actually used.
- */
-
/*
* If cache entry doesn't have a connection, we have to establish a new
* connection. (If connect_pg_server throws an error, the cache entry
@@ -205,9 +199,64 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
}
/*
- * Start a new transaction or subtransaction if needed.
+ * We check the health of cached connection here, while the remote xact is
+ * going to begin. Broken connection will be detected and if so, get a
+ * new connection and resubmit the remote xact begin. We retry the broken
+ * connections only once during each begin remote xact call, still the broken
+ * connection exist after that then, the query fails.
*/
- begin_remote_xact(entry);
+ PG_TRY();
+ {
+ /* Start a new transaction or subtransaction if needed. */
+ begin_remote_xact(entry);
+ }
+ PG_CATCH();
+ {
+ if (entry->conn &&
+ PQstatus(entry->conn) == CONNECTION_BAD &&
+ entry->xact_depth <= 0)
+ {
+ /* server object will be used for log messages. */
+ ForeignServer *server = GetForeignServer(user->serverid);
+
+ /*
+ * xact_depth <=0, which means we are retrying only at the beginning
+ * of remote xact. This is sensible, since we can't retry in the
+ * middle of a remote xact.
+ */
+ elog(DEBUG3, "postgres_fdw connection %p to the server \"%s\" is broken. retrying to connect",
+ entry->conn, server->servername);
+
+ PQreset(entry->conn);
+
+ if (entry->conn && PQstatus(entry->conn) == CONNECTION_OK)
+ elog(DEBUG3, "postgres_fdw connection retry to the server \"%s\" is successful",
+ server->servername);
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION),
+ errmsg("could not connect to server \"%s\"",
+ server->servername),
+ errdetail_internal("%s", pchomp(PQerrorMessage(entry->conn)))));
+
+ /* Retry starting a new transaction. */
+ begin_remote_xact(entry);
+ }
+ else
+ {
+ /*
+ * We are here, due to either some error other than CONNECTION_BAD
+ * occurred or connection may have broken during start of a
+ * subtransacation. Just, clear off any result, try rethrowing the
+ * error, so that it will be caught appropriately.
+ */
+ PGresult *res = NULL;
+ res = PQgetResult(entry->conn);
+ PQclear(res);
+ PG_RE_THROW();
+ }
+ }
+ PG_END_TRY();
/* Remember if caller will prepare statements */
entry->have_prep_stmt |= will_prep_stmt;
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..46511a340e 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8974,3 +8974,58 @@ PREPARE TRANSACTION 'fdw_tpc';
ERROR: cannot PREPARE a transaction that has operated on postgres_fdw foreign tables
ROLLBACK;
WARNING: there is no transaction in progress
+-- Retry cached connections at the beginning of the remote xact
+-- in case remote backend is killed.
+-- Let's use a different application name for remote connection,
+-- so that this test will not kill other backends wrongly.
+ALTER SERVER loopback OPTIONS (application_name 'fdw_retry_check');
+-- Generate a connection to remote. Local backend will cache it.
+SELECT * FROM ft1 LIMIT 1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+------+----+----+----+----+----+------------+----
+ 1111 | 2 | | | | | ft1 |
+(1 row)
+
+-- Retrieve pid of remote backend with application name fdw_retry_check
+-- and kill it intentionally here. Note that, local backend still has
+-- the remote connection/backend info in it's cache.
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+ pg_terminate_backend
+----------------------
+ t
+(1 row)
+
+-- Next query using the same foreign server should succeed if connection
+-- retry works.
+SELECT * FROM ft1 LIMIT 1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+------+----+----+----+----+----+------------+----
+ 1111 | 2 | | | | | ft1 |
+(1 row)
+
+-- Subtransaction - remote backend is killed in the middle of a remote
+-- xact, and we don't do retry connection, hence the subsequent query
+-- using the same foreign server should fail.
+BEGIN;
+DECLARE c CURSOR FOR SELECT * FROM ft1 LIMIT 1;
+FETCH c;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+------+----+----+----+----+----+------------+----
+ 1111 | 2 | | | | | ft1 |
+(1 row)
+
+SAVEPOINT s;
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+ pg_terminate_backend
+----------------------
+ t
+(1 row)
+
+SELECT * FROM ft1 LIMIT 1; -- should fail
+ERROR: server closed the connection unexpectedly
+ This probably means the server terminated abnormally
+ before or while processing the request.
+CONTEXT: remote SQL command: SAVEPOINT s2
+COMMIT;
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..6f647450e7 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2634,3 +2634,34 @@ SELECT count(*) FROM ft1;
-- error here
PREPARE TRANSACTION 'fdw_tpc';
ROLLBACK;
+
+-- Retry cached connections at the beginning of the remote xact
+-- in case remote backend is killed.
+-- Let's use a different application name for remote connection,
+-- so that this test will not kill other backends wrongly.
+ALTER SERVER loopback OPTIONS (application_name 'fdw_retry_check');
+
+-- Generate a connection to remote. Local backend will cache it.
+SELECT * FROM ft1 LIMIT 1;
+
+-- Retrieve pid of remote backend with application name fdw_retry_check
+-- and kill it intentionally here. Note that, local backend still has
+-- the remote connection/backend info in it's cache.
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+
+-- Next query using the same foreign server should succeed if connection
+-- retry works.
+SELECT * FROM ft1 LIMIT 1;
+
+-- Subtransaction - remote backend is killed in the middle of a remote
+-- xact, and we don't do retry connection, hence the subsequent query
+-- using the same foreign server should fail.
+BEGIN;
+DECLARE c CURSOR FOR SELECT * FROM ft1 LIMIT 1;
+FETCH c;
+SAVEPOINT s;
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+SELECT * FROM ft1 LIMIT 1; -- should fail
+COMMIT;
--
2.25.1
Has this been added to CF, possibly next CF?
On Tue, Jul 14, 2020 at 7:27 AM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:
You could get a backend's PID using PQbackendPID and then kill it by
calling pg_terminate_backend() to kill the remote backend to automate
scenario of remote backend being killed.I already added the test case in v2 patch itself(added one more test
case in v3 patch), using the similar approach.For, one of the Ashutosh's review comments from [3] "the fact that the
same connection may be used by multiple plan nodes", I tried to have
few use cases where there exist joins on two foreign tables on the
same remote server, in a single query, so essentially, the same
connection was used for multiple plan nodes. In this case we avoid
retrying for the second GetConnection() request for the second foreign
table, with the check entry->xact_depth <= 0 , xact_depth after the
first GetConnection() and the first remote query will become 1 and we
don't hit the retry logic and seems like we are safe here. Please add
If I'm missing something here.Request the community to consider the patch for further review if the
overall idea seems beneficial.I think this idea will be generally useful if your work on dropping
stale connection uses idle_connection_timeout or something like that
on the remote server.Assuming we use idle_connection_timeout or some other means(as it is
not yet finalized, I will try to respond in that mail chain) to drop
stale/idle connections from the local backend, I think we have two
options 1) deleting that cached entry from the connection cache
entirely using disconnect_pg_server() and hash table remove. This
frees up some space and we don't have to deal with the connection
invalidations and don't have to bother on resetting cached entry's
other parameters such as xact_depth, have_prep_stmt etc. 2) or we
could just drop the connections using disconnect_pg_server(), retain
the hash entry, reset other parameters, and deal with the
invalidations. This is like, we maintain unnecessary info in the
cached entry, where we actually don't have a connection at all and
keep holding some space for cached entry.IMO, if we go with option 1, then it will be good.
Anyways, this connection retry feature will not have any dependency on
the idle_connection_timeout or dropping stale connection feature,
because there can be many reasons where remote backends go away/get
killed.If I'm not sidetracking - if we use something like
idle_session_timeout [1] on the remote server, this retry feature will
be very useful.About the patch. It seems we could just catch the error from
begin_remote_xact() in GetConnection() and retry connection if the
error is "bad connection". Retrying using PQreset() might be better
than calling PQConnect* always.Thanks for the comment, it made life easier. Added the patch with the
changes. Please take a look at the v3 patch and let me know if still
something needs to be done.[1] - /messages/by-id/763A0689-F189-459E-946F-F0EC4458980B@hotmail.com
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
--
Best Wishes,
Ashutosh Bapat
On Tue, Jul 14, 2020 at 6:13 PM Ashutosh Bapat
<ashutosh.bapat.oss@gmail.com> wrote:
Has this been added to CF, possibly next CF?
I have not added yet. Request to get it done in this CF, as the final
patch for review(v3 patch) is ready and shared. We can target it to
the next CF if there are major issues with the patch.
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
On Tue, Jul 14, 2020 at 6:13 PM Ashutosh Bapat
<ashutosh.bapat.oss@gmail.com> wrote:Has this been added to CF, possibly next CF?
I have not added yet. Request to get it done in this CF, as the final
patch for review(v3 patch) is ready and shared. We can target it to
the next CF if there are major issues with the patch.
I added this feature to the next CF - https://commitfest.postgresql.org/29/2651/
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
On 2020/07/17 21:02, Bharath Rupireddy wrote:
On Tue, Jul 14, 2020 at 6:13 PM Ashutosh Bapat
<ashutosh.bapat.oss@gmail.com> wrote:Has this been added to CF, possibly next CF?
I have not added yet. Request to get it done in this CF, as the final
patch for review(v3 patch) is ready and shared. We can target it to
the next CF if there are major issues with the patch.I added this feature to the next CF - https://commitfest.postgresql.org/29/2651/
Thanks for the patch! Here are some comments from me.
+ PQreset(entry->conn);
Isn't using PQreset() to reconnect to the foreign server unsafe?
When new connection is established, some SET commands seem
to need to be issued like configure_remote_session() does.
But PQreset() doesn't do that at all.
Originally when GetConnection() establishes new connection,
it resets all transient state fields, to be sure all are clean (as the
comment says). With the patch, even when reconnecting
the remote server, shouldn't we do the same, for safe?
+ PGresult *res = NULL;
+ res = PQgetResult(entry->conn);
+ PQclear(res);
Are these really necessary? I was just thinking that's not because
pgfdw_get_result() and pgfdw_report_error() seem to do that
already in do_sql_command().
+ /* Start a new transaction or subtransaction if needed. */
+ begin_remote_xact(entry);
Even when there is no cached connection and new connection is made,
then if begin_remote_xact() reports an error, another new connection
tries to be made again. This behavior looks a bit strange.
Regards,
--
Fujii Masao
Advanced Computing Technology Center
Research and Development Headquarters
NTT DATA CORPORATION
Thanks for the review comments. I will post a new patch soon
addressing all the comments.
On Tue, Sep 15, 2020 at 2:49 PM Fujii Masao <masao.fujii@oss.nttdata.com> wrote:
+ PQreset(entry->conn);
Isn't using PQreset() to reconnect to the foreign server unsafe?
When new connection is established, some SET commands seem
to need to be issued like configure_remote_session() does.
But PQreset() doesn't do that at all.
This is a good catch. Thanks, I missed this point. Indeed we need to
set the session params. We can do this in two ways: 1. use
configure_remote_session() after PQreset(). 2. use connect_pg_server()
instead of PQreset() and configure_remote_session(). One problem I see
with the 2nd way is that we will be doing the checks that are being
performed in connect_pg_server() twice, which we would have done for
the first time before retrying. The required parameters such as
keywords, values, are still in entry->conn structure from the first
attempt, which can safely be used by PQreset(). So, I will go with the
1st way. Thoughts?
Originally when GetConnection() establishes new connection,
it resets all transient state fields, to be sure all are clean (as the
comment says). With the patch, even when reconnecting
the remote server, shouldn't we do the same, for safe?
I guess there is no need for us to reset all the transient state
before we do begin_remote_xact() in the 2nd attempt. We retry the
connection only when entry->xact_depth <= 0 i.e. beginning of the
remote txn and the begin_remote_xact() doesn't modify any transient
state if entry->xact_depth <= 0, except for entry->changing_xact_state
= true; all other transient state is intact in entry structure. In the
error case, we will not reach the code after do_sql_command in
begin_remote_xact(). If needed, we can only set
entry->changing_xact_state to false which is set to true before
do_sql_command().
entry->changing_xact_state = true;
do_sql_command(entry->conn, sql);
entry->xact_depth = 1;
entry->changing_xact_state = false;
+ PGresult *res = NULL; + res = PQgetResult(entry->conn); + PQclear(res); Are these really necessary? I was just thinking that's not because pgfdw_get_result() and pgfdw_report_error() seem to do that already in do_sql_command().
If an error occurs in the first attempt, we return from
pgfdw_get_result()'s if (!PQconsumeInput(conn)) to the catch block we
added and pgfdw_report_error() will never get called. And the below
part of the code is reached only in scenarios as mentioned in the
comments. Removing this might have problems if we receive errors other
than CONNECTION_BAD or for subtxns. We could clear if any result and
just rethrow the error upstream. I think no problem having this code
here.
else
{
/*
* We are here, due to either some error other than CONNECTION_BAD
* occurred or connection may have broken during start of a
* subtransacation. Just, clear off any result, try rethrowing the
* error, so that it will be caught appropriately.
*/
PGresult *res = NULL;
res = PQgetResult(entry->conn);
PQclear(res);
PG_RE_THROW();
}
+ /* Start a new transaction or subtransaction if needed. */ + begin_remote_xact(entry);Even when there is no cached connection and new connection is made,
then if begin_remote_xact() reports an error, another new connection
tries to be made again. This behavior looks a bit strange.
When there is no cached connection, we try to acquire one, if we
can't, then no error will be thrown to the user, just we retry one
more time. If we get in the 2nd attempt, fine, if not, we will throw
the error to the user. Assume in the 1st attempt the remote server is
unreachable, we may hope to connect in the 2nd attempt. I think there
is no problem here.
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
On 2020/09/17 15:44, Bharath Rupireddy wrote:
Thanks for the review comments. I will post a new patch soon
addressing all the comments.
Thanks a lot!
On Tue, Sep 15, 2020 at 2:49 PM Fujii Masao <masao.fujii@oss.nttdata.com> wrote:
+ PQreset(entry->conn);
Isn't using PQreset() to reconnect to the foreign server unsafe?
When new connection is established, some SET commands seem
to need to be issued like configure_remote_session() does.
But PQreset() doesn't do that at all.This is a good catch. Thanks, I missed this point. Indeed we need to
set the session params. We can do this in two ways: 1. use
configure_remote_session() after PQreset(). 2. use connect_pg_server()
instead of PQreset() and configure_remote_session(). One problem I see
with the 2nd way is that we will be doing the checks that are being
performed in connect_pg_server() twice, which we would have done for
the first time before retrying. The required parameters such as
keywords, values, are still in entry->conn structure from the first
attempt, which can safely be used by PQreset(). So, I will go with the
1st way. Thoughts?
In 1st way, you may also need to call ReleaseExternalFD() when new connection fails
to be made, as connect_pg_server() does. Also we need to check that
non-superuser has used password to make new connection,
as connect_pg_server() does? I'm concerned about the case where
pg_hba.conf is changed accidentally so that no password is necessary
at the remote server and the existing connection is terminated. In this case,
if we connect to the local server as non-superuser, connection to
the remote server should fail because the remote server doesn't
require password. But with your patch, we can successfully reconnect
to the remote server.
Therefore I like 2nd option. Also maybe disconnect_ps_server() needs to
be called before that. I'm not sure how much useful 1st option is.
Originally when GetConnection() establishes new connection,
it resets all transient state fields, to be sure all are clean (as the
comment says). With the patch, even when reconnecting
the remote server, shouldn't we do the same, for safe?I guess there is no need for us to reset all the transient state
before we do begin_remote_xact() in the 2nd attempt. We retry the
connection only when entry->xact_depth <= 0 i.e. beginning of the
remote txn and the begin_remote_xact() doesn't modify any transient
state if entry->xact_depth <= 0, except for entry->changing_xact_state
= true; all other transient state is intact in entry structure. In the
error case, we will not reach the code after do_sql_command in
begin_remote_xact(). If needed, we can only set
entry->changing_xact_state to false which is set to true before
do_sql_command().
What if 2nd attempt happens with have_prep_stmt=true? I'm not sure
if this case really happens, though. But if that can, it's strange to start
new connection with have_prep_stmt=true even when the caller of
GetConnection() doesn't intend to create any prepared statements.
I think it's safer to do 2nd attempt in the same way as 1st one. Maybe
we can simplify the code by making them into common code block
or function.
entry->changing_xact_state = true;
do_sql_command(entry->conn, sql);
entry->xact_depth = 1;
entry->changing_xact_state = false;+ PGresult *res = NULL; + res = PQgetResult(entry->conn); + PQclear(res); Are these really necessary? I was just thinking that's not because pgfdw_get_result() and pgfdw_report_error() seem to do that already in do_sql_command().If an error occurs in the first attempt, we return from
pgfdw_get_result()'s if (!PQconsumeInput(conn)) to the catch block we
added and pgfdw_report_error() will never get called. And the below
part of the code is reached only in scenarios as mentioned in the
comments. Removing this might have problems if we receive errors other
than CONNECTION_BAD or for subtxns. We could clear if any result and
just rethrow the error upstream. I think no problem having this code
here.
But the orignal code works without this?
Or you mean that the original code has the bug?
else
{
/*
* We are here, due to either some error other than CONNECTION_BAD
* occurred or connection may have broken during start of a
* subtransacation. Just, clear off any result, try rethrowing the
* error, so that it will be caught appropriately.
*/
PGresult *res = NULL;
res = PQgetResult(entry->conn);
PQclear(res);
PG_RE_THROW();
}+ /* Start a new transaction or subtransaction if needed. */ + begin_remote_xact(entry);Even when there is no cached connection and new connection is made,
then if begin_remote_xact() reports an error, another new connection
tries to be made again. This behavior looks a bit strange.When there is no cached connection, we try to acquire one, if we
can't, then no error will be thrown to the user, just we retry one
more time. If we get in the 2nd attempt, fine, if not, we will throw
the error to the user. Assume in the 1st attempt the remote server is
unreachable, we may hope to connect in the 2nd attempt. I think there
is no problem here.With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
--
Fujii Masao
Advanced Computing Technology Center
Research and Development Headquarters
NTT DATA CORPORATION
On Thu, Sep 17, 2020 at 10:20 PM Fujii Masao <masao.fujii@oss.nttdata.com>
wrote:
In 1st way, you may also need to call ReleaseExternalFD() when new
connection fails
to be made, as connect_pg_server() does. Also we need to check that
non-superuser has used password to make new connection,
as connect_pg_server() does? I'm concerned about the case where
pg_hba.conf is changed accidentally so that no password is necessary
at the remote server and the existing connection is terminated. In this
case,
if we connect to the local server as non-superuser, connection to
the remote server should fail because the remote server doesn't
require password. But with your patch, we can successfully reconnect
to the remote server.Therefore I like 2nd option. Also maybe disconnect_ps_server() needs to
be called before that. I'm not sure how much useful 1st option is.
Thanks. Above points look sensible. +1 for the 2nd option i.e. instead of
PQreset(entry->conn);, let's try to disconnect_pg_server() and
connect_pg_server().
What if 2nd attempt happens with have_prep_stmt=true? I'm not sure
if this case really happens, though. But if that can, it's strange to
start
new connection with have_prep_stmt=true even when the caller of
GetConnection() doesn't intend to create any prepared statements.I think it's safer to do 2nd attempt in the same way as 1st one. Maybe
we can simplify the code by making them into common code block
or function.
I don't think the have_prep_stmt will be set by the time we make 2nd
attempt because entry->have_prep_stmt |= will_prep_stmt; gets hit only
after we are successful in either 1st attempt or 2nd attempt. I think we
don't need to set all transient state. No other transient state except
changing_xact_state changes from 1st attempt to 2nd attempt(see below), so
let's set only entry->changing_xact_state to false before 2nd attempt.
1st attempt:
(gdb) p *entry
$3 = {key = 16389, conn = 0x55a896199990, xact_depth = 0, have_prep_stmt =
false,
have_error = false, changing_xact_state = false, invalidated = false,
server_hashvalue = 3905865521, mapping_hashvalue = 2617776010}
2nd attempt i.e. in retry block:
(gdb) p *entry
$4 = {key = 16389, conn = 0x55a896199990, xact_depth = 0, have_prep_stmt =
false,
have_error = false, changing_xact_state = true, invalidated = false,
server_hashvalue = 3905865521, mapping_hashvalue = 2617776010}
If an error occurs in the first attempt, we return from
pgfdw_get_result()'s if (!PQconsumeInput(conn)) to the catch block we
added and pgfdw_report_error() will never get called. And the below
part of the code is reached only in scenarios as mentioned in the
comments. Removing this might have problems if we receive errors other
than CONNECTION_BAD or for subtxns. We could clear if any result and
just rethrow the error upstream. I think no problem having this code
here.But the orignal code works without this?
Or you mean that the original code has the bug?
There's no bug in the original code. Sorry, I was wrong in saying
pgfdw_report_error() will never get called with this patch. Indeed it gets
called even when 1's attempt connection is failed. Since we added an extra
try-catch block, we will not be throwing the error to the user, instead we
make a 2nd attempt from the catch block.
I'm okay to remove below part of the code
+ PGresult *res = NULL; + res = PQgetResult(entry->conn); + PQclear(res); Are these really necessary? I was just thinking that's not because pgfdw_get_result() and pgfdw_report_error() seem to do that already in do_sql_command().
Please let me know if okay with the above agreed points, I will work on the
new patch.
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
On 2020/09/21 12:44, Bharath Rupireddy wrote:
On Thu, Sep 17, 2020 at 10:20 PM Fujii Masao <masao.fujii@oss.nttdata.com <mailto:masao.fujii@oss.nttdata.com>> wrote:
In 1st way, you may also need to call ReleaseExternalFD() when new connection fails
to be made, as connect_pg_server() does. Also we need to check that
non-superuser has used password to make new connection,
as connect_pg_server() does? I'm concerned about the case where
pg_hba.conf is changed accidentally so that no password is necessary
at the remote server and the existing connection is terminated. In this case,
if we connect to the local server as non-superuser, connection to
the remote server should fail because the remote server doesn't
require password. But with your patch, we can successfully reconnect
to the remote server.Therefore I like 2nd option. Also maybe disconnect_ps_server() needs to
be called before that. I'm not sure how much useful 1st option is.Thanks. Above points look sensible. +1 for the 2nd option i.e. instead of PQreset(entry->conn);, let's try to disconnect_pg_server() and connect_pg_server().
What if 2nd attempt happens with have_prep_stmt=true? I'm not sure
if this case really happens, though. But if that can, it's strange to start
new connection with have_prep_stmt=true even when the caller of
GetConnection() doesn't intend to create any prepared statements.I think it's safer to do 2nd attempt in the same way as 1st one. Maybe
we can simplify the code by making them into common code block
or function.I don't think the have_prep_stmt will be set by the time we make 2nd attempt because entry->have_prep_stmt |= will_prep_stmt; gets hit only after we are successful in either 1st attempt or 2nd attempt. I think we don't need to set all transient state. No other transient state except changing_xact_state changes from 1st attempt to 2nd attempt(see below), so let's set only entry->changing_xact_state to false before 2nd attempt.
1st attempt:
(gdb) p *entry
$3 = {key = 16389, conn = 0x55a896199990, xact_depth = 0, have_prep_stmt = false,
have_error = false, changing_xact_state = false, invalidated = false,
server_hashvalue = 3905865521, mapping_hashvalue = 2617776010}2nd attempt i.e. in retry block:
(gdb) p *entry
$4 = {key = 16389, conn = 0x55a896199990, xact_depth = 0, have_prep_stmt = false,
have_error = false, changing_xact_state = true, invalidated = false,
server_hashvalue = 3905865521, mapping_hashvalue = 2617776010}If an error occurs in the first attempt, we return from
pgfdw_get_result()'s if (!PQconsumeInput(conn)) to the catch block we
added and pgfdw_report_error() will never get called. And the below
part of the code is reached only in scenarios as mentioned in the
comments. Removing this might have problems if we receive errors other
than CONNECTION_BAD or for subtxns. We could clear if any result and
just rethrow the error upstream. I think no problem having this code
here.But the orignal code works without this?
Or you mean that the original code has the bug?There's no bug in the original code. Sorry, I was wrong in saying pgfdw_report_error() will never get called with this patch. Indeed it gets called even when 1's attempt connection is failed. Since we added an extra try-catch block, we will not be throwing the error to the user, instead we make a 2nd attempt from the catch block.
I'm okay to remove below part of the code
+ PGresult *res = NULL; + res = PQgetResult(entry->conn); + PQclear(res); Are these really necessary? I was just thinking that's not because pgfdw_get_result() and pgfdw_report_error() seem to do that already in do_sql_command().Please let me know if okay with the above agreed points, I will work on the new patch.
Yes, please work on the patch! Thanks! I may revisit the above points later, though ;)
Regards,
--
Fujii Masao
Advanced Computing Technology Center
Research and Development Headquarters
NTT DATA CORPORATION
On Wed, Sep 23, 2020 at 8:19 PM Fujii Masao <masao.fujii@oss.nttdata.com> wrote:
Please let me know if okay with the above agreed points, I will work on the new patch.
Yes, please work on the patch! Thanks! I may revisit the above points later, though ;)
Thanks, attaching v4 patch. Please consider this for further review.
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
Attachments:
v4-Retry-Cached-Remote-Connections-For-postgres_fdw.patchapplication/octet-stream; name=v4-Retry-Cached-Remote-Connections-For-postgres_fdw.patchDownload
From cc911b79ed1bbcf1a330977b69cc4fe291c2a3a0 Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddy@enterprisedb.com>
Date: Fri, 25 Sep 2020 10:11:59 +0530
Subject: [PATCH] Retry Cached Remote Connections For postgres_fdw
Remote connections are cached for postgres_fdw in the local
backend. There are high chances that the remote backend may
not be available i.e. it can get killed, the subsequent foreign
queries from local backend/session that use cached connection
will fail as the remote backend would have become unavailable.
This patch proposes a retry mechanism. Local backend/session
uses cached connection and if it receives connection bad error,
it deletes the cached entry and retry to get a new connection.
This retry happens only at the start of remote xact.
---
contrib/postgres_fdw/connection.c | 62 ++++++++++++++++---
.../postgres_fdw/expected/postgres_fdw.out | 55 ++++++++++++++++
contrib/postgres_fdw/sql/postgres_fdw.sql | 31 ++++++++++
3 files changed, 140 insertions(+), 8 deletions(-)
diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c
index 08daf26fdf..ab8d505d64 100644
--- a/contrib/postgres_fdw/connection.c
+++ b/contrib/postgres_fdw/connection.c
@@ -170,12 +170,6 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
disconnect_pg_server(entry);
}
- /*
- * We don't check the health of cached connection here, because it would
- * require some overhead. Broken connection will be detected when the
- * connection is actually used.
- */
-
/*
* If cache entry doesn't have a connection, we have to establish a new
* connection. (If connect_pg_server throws an error, the cache entry
@@ -206,9 +200,61 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
}
/*
- * Start a new transaction or subtransaction if needed.
+ * We check the health of the cached connection here, while the remote
+ * xact is going to begin. Broken connection will be detected and the
+ * connection is retried. We do this only once during each begin remote
+ * xact call, if the connection is still broken after the retry attempt,
+ * then the query will fail.
*/
- begin_remote_xact(entry);
+ PG_TRY();
+ {
+ /* Start a new transaction or subtransaction if needed. */
+ begin_remote_xact(entry);
+ }
+ PG_CATCH();
+ {
+ if (entry->conn &&
+ PQstatus(entry->conn) == CONNECTION_BAD &&
+ entry->xact_depth <= 0)
+ {
+ /* server object will be used for log messages. */
+ ForeignServer *server = GetForeignServer(user->serverid);
+
+ /*
+ * xact_depth <=0, which means we are retrying only at
+ * the beginning of remote xact. This is sensible,
+ * since we can't retry in the middle of a remote xact.
+ */
+ elog(DEBUG3, "postgres_fdw connection %p to the server \"%s\" is broken, retrying to connect",
+ entry->conn, server->servername);
+
+ disconnect_pg_server(entry);
+
+ /*
+ * Reset the transient state fields that may have been
+ * modified in the first attempt.
+ */
+ entry->changing_xact_state = false;
+
+ entry->conn = connect_pg_server(server, user);
+
+ if (entry->conn && PQstatus(entry->conn) == CONNECTION_OK)
+ elog(DEBUG3, "postgres_fdw connection retry to the server \"%s\" is successful",
+ server->servername);
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION),
+ errmsg("could not connect to server \"%s\"",
+ server->servername),
+ errdetail_internal("%s", pchomp(PQerrorMessage(entry->conn)))));
+
+ /* Retry starting a new transaction. */
+ begin_remote_xact(entry);
+ }
+ else
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
/* Remember if caller will prepare statements */
entry->have_prep_stmt |= will_prep_stmt;
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 10e23d02ed..d72b11a3ac 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8987,3 +8987,58 @@ PREPARE TRANSACTION 'fdw_tpc';
ERROR: cannot PREPARE a transaction that has operated on postgres_fdw foreign tables
ROLLBACK;
WARNING: there is no transaction in progress
+-- Retry cached connections at the beginning of the remote xact
+-- in case remote backend is killed.
+-- Let's use a different application name for remote connection,
+-- so that this test will not kill other backends wrongly.
+ALTER SERVER loopback OPTIONS (application_name 'fdw_retry_check');
+-- Generate a connection to remote. Local backend will cache it.
+SELECT * FROM ft1 LIMIT 1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+------+----+----+----+----+----+------------+----
+ 1111 | 2 | | | | | ft1 |
+(1 row)
+
+-- Retrieve pid of remote backend with application name fdw_retry_check
+-- and kill it intentionally here. Note that, local backend still has
+-- the remote connection/backend info in it's cache.
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+ pg_terminate_backend
+----------------------
+ t
+(1 row)
+
+-- Next query using the same foreign server should succeed if connection
+-- retry works.
+SELECT * FROM ft1 LIMIT 1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+------+----+----+----+----+----+------------+----
+ 1111 | 2 | | | | | ft1 |
+(1 row)
+
+-- Subtransaction - remote backend is killed in the middle of a remote
+-- xact, and we don't do retry connection, hence the subsequent query
+-- using the same foreign server should fail.
+BEGIN;
+DECLARE c CURSOR FOR SELECT * FROM ft1 LIMIT 1;
+FETCH c;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+------+----+----+----+----+----+------------+----
+ 1111 | 2 | | | | | ft1 |
+(1 row)
+
+SAVEPOINT s;
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+ pg_terminate_backend
+----------------------
+ t
+(1 row)
+
+SELECT * FROM ft1 LIMIT 1; -- should fail
+ERROR: server closed the connection unexpectedly
+ This probably means the server terminated abnormally
+ before or while processing the request.
+CONTEXT: remote SQL command: SAVEPOINT s2
+COMMIT;
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 78156d10b4..7a1b4c78bd 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2653,3 +2653,34 @@ SELECT count(*) FROM ft1;
-- error here
PREPARE TRANSACTION 'fdw_tpc';
ROLLBACK;
+
+-- Retry cached connections at the beginning of the remote xact
+-- in case remote backend is killed.
+-- Let's use a different application name for remote connection,
+-- so that this test will not kill other backends wrongly.
+ALTER SERVER loopback OPTIONS (application_name 'fdw_retry_check');
+
+-- Generate a connection to remote. Local backend will cache it.
+SELECT * FROM ft1 LIMIT 1;
+
+-- Retrieve pid of remote backend with application name fdw_retry_check
+-- and kill it intentionally here. Note that, local backend still has
+-- the remote connection/backend info in it's cache.
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+
+-- Next query using the same foreign server should succeed if connection
+-- retry works.
+SELECT * FROM ft1 LIMIT 1;
+
+-- Subtransaction - remote backend is killed in the middle of a remote
+-- xact, and we don't do retry connection, hence the subsequent query
+-- using the same foreign server should fail.
+BEGIN;
+DECLARE c CURSOR FOR SELECT * FROM ft1 LIMIT 1;
+FETCH c;
+SAVEPOINT s;
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+SELECT * FROM ft1 LIMIT 1; -- should fail
+COMMIT;
--
2.25.1
On 2020/09/25 13:56, Bharath Rupireddy wrote:
On Wed, Sep 23, 2020 at 8:19 PM Fujii Masao <masao.fujii@oss.nttdata.com> wrote:
Please let me know if okay with the above agreed points, I will work on the new patch.
Yes, please work on the patch! Thanks! I may revisit the above points later, though ;)
Thanks, attaching v4 patch. Please consider this for further review.
Thanks for updating the patch!
In the orignal code, disconnect_pg_server() is called when invalidation
occurs and connect_pg_server() is called when no valid connection exists.
I think that we can simplify the code by merging the connection-retry
code into them, like the attached very WIP patch (based on yours) does.
Basically I'd like to avoid duplicating disconnect_pg_server(),
connect_pg_server() and begin_remote_xact() if possible. Thought?
Your patch adds several codes into PG_CATCH() section, but it's better
to keep that section simple enough (as the source comment for
PG_CATCH() explains). So what about moving some codes out of PG_CATCH()
section?
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION),
+ errmsg("could not connect to server \"%s\"",
+ server->servername),
+ errdetail_internal("%s", pchomp(PQerrorMessage(entry->conn)))));
The above is not necessary? If this error occurs, connect_pg_server()
reports an error, before the above code is reached. Right?
Regards,
--
Fujii Masao
Advanced Computing Technology Center
Research and Development Headquarters
NTT DATA CORPORATION
Attachments:
v4-Retry-Cached-Remote-Connections-For-postgres_fdw_fujii.patchtext/plain; charset=UTF-8; name=v4-Retry-Cached-Remote-Connections-For-postgres_fdw_fujii.patch; x-mac-creator=0; x-mac-type=0Download
diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c
index 08daf26fdf..9f76261d99 100644
--- a/contrib/postgres_fdw/connection.c
+++ b/contrib/postgres_fdw/connection.c
@@ -108,6 +108,7 @@ PGconn *
GetConnection(UserMapping *user, bool will_prep_stmt)
{
bool found;
+ bool need_reset_conn = false;
ConnCacheEntry *entry;
ConnCacheKey key;
@@ -159,23 +160,25 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
/* Reject further use of connections which failed abort cleanup. */
pgfdw_reject_incomplete_xact_state_change(entry);
+reset:
/*
* If the connection needs to be remade due to invalidation, disconnect as
- * soon as we're out of all transactions.
+ * soon as we're out of all transactions. Also if previous attempt to start
+ * new remote transaction failed on the cached connection, disconnect
+ * it to reestablish new connection.
*/
- if (entry->conn != NULL && entry->invalidated && entry->xact_depth == 0)
+ if ((entry->conn != NULL && entry->invalidated && entry->xact_depth == 0) ||
+ need_reset_conn)
{
- elog(DEBUG3, "closing connection %p for option changes to take effect",
- entry->conn);
+ if (need_reset_conn)
+ elog(DEBUG3, "closing connection %p to reestablish connection",
+ entry->conn);
+ else
+ elog(DEBUG3, "closing connection %p for option changes to take effect",
+ entry->conn);
disconnect_pg_server(entry);
}
- /*
- * We don't check the health of cached connection here, because it would
- * require some overhead. Broken connection will be detected when the
- * connection is actually used.
- */
-
/*
* If cache entry doesn't have a connection, we have to establish a new
* connection. (If connect_pg_server throws an error, the cache entry
@@ -206,9 +209,31 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
}
/*
- * Start a new transaction or subtransaction if needed.
+ * We check the health of the cached connection here, while the remote
+ * xact is going to begin. Broken connection will be detected and the
+ * connection is retried. We do this only once during each begin remote
+ * xact call, if the connection is still broken after the retry attempt,
+ * then the query will fail.
*/
- begin_remote_xact(entry);
+ PG_TRY();
+ {
+ /* Start a new transaction or subtransaction if needed. */
+ begin_remote_xact(entry);
+ need_reset_conn = false;
+ }
+ PG_CATCH();
+ {
+ if (!entry->conn ||
+ PQstatus(entry->conn) != CONNECTION_BAD ||
+ entry->xact_depth > 0 ||
+ need_reset_conn)
+ PG_RE_THROW();
+ need_reset_conn = true;
+ }
+ PG_END_TRY();
+
+ if (need_reset_conn)
+ goto reset;
/* Remember if caller will prepare statements */
entry->have_prep_stmt |= will_prep_stmt;
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 10e23d02ed..d72b11a3ac 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8987,3 +8987,58 @@ PREPARE TRANSACTION 'fdw_tpc';
ERROR: cannot PREPARE a transaction that has operated on postgres_fdw foreign tables
ROLLBACK;
WARNING: there is no transaction in progress
+-- Retry cached connections at the beginning of the remote xact
+-- in case remote backend is killed.
+-- Let's use a different application name for remote connection,
+-- so that this test will not kill other backends wrongly.
+ALTER SERVER loopback OPTIONS (application_name 'fdw_retry_check');
+-- Generate a connection to remote. Local backend will cache it.
+SELECT * FROM ft1 LIMIT 1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+------+----+----+----+----+----+------------+----
+ 1111 | 2 | | | | | ft1 |
+(1 row)
+
+-- Retrieve pid of remote backend with application name fdw_retry_check
+-- and kill it intentionally here. Note that, local backend still has
+-- the remote connection/backend info in it's cache.
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+ pg_terminate_backend
+----------------------
+ t
+(1 row)
+
+-- Next query using the same foreign server should succeed if connection
+-- retry works.
+SELECT * FROM ft1 LIMIT 1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+------+----+----+----+----+----+------------+----
+ 1111 | 2 | | | | | ft1 |
+(1 row)
+
+-- Subtransaction - remote backend is killed in the middle of a remote
+-- xact, and we don't do retry connection, hence the subsequent query
+-- using the same foreign server should fail.
+BEGIN;
+DECLARE c CURSOR FOR SELECT * FROM ft1 LIMIT 1;
+FETCH c;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+------+----+----+----+----+----+------------+----
+ 1111 | 2 | | | | | ft1 |
+(1 row)
+
+SAVEPOINT s;
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+ pg_terminate_backend
+----------------------
+ t
+(1 row)
+
+SELECT * FROM ft1 LIMIT 1; -- should fail
+ERROR: server closed the connection unexpectedly
+ This probably means the server terminated abnormally
+ before or while processing the request.
+CONTEXT: remote SQL command: SAVEPOINT s2
+COMMIT;
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 78156d10b4..7a1b4c78bd 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2653,3 +2653,34 @@ SELECT count(*) FROM ft1;
-- error here
PREPARE TRANSACTION 'fdw_tpc';
ROLLBACK;
+
+-- Retry cached connections at the beginning of the remote xact
+-- in case remote backend is killed.
+-- Let's use a different application name for remote connection,
+-- so that this test will not kill other backends wrongly.
+ALTER SERVER loopback OPTIONS (application_name 'fdw_retry_check');
+
+-- Generate a connection to remote. Local backend will cache it.
+SELECT * FROM ft1 LIMIT 1;
+
+-- Retrieve pid of remote backend with application name fdw_retry_check
+-- and kill it intentionally here. Note that, local backend still has
+-- the remote connection/backend info in it's cache.
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+
+-- Next query using the same foreign server should succeed if connection
+-- retry works.
+SELECT * FROM ft1 LIMIT 1;
+
+-- Subtransaction - remote backend is killed in the middle of a remote
+-- xact, and we don't do retry connection, hence the subsequent query
+-- using the same foreign server should fail.
+BEGIN;
+DECLARE c CURSOR FOR SELECT * FROM ft1 LIMIT 1;
+FETCH c;
+SAVEPOINT s;
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+SELECT * FROM ft1 LIMIT 1; -- should fail
+COMMIT;
On Fri, Sep 25, 2020 at 3:21 PM Fujii Masao <masao.fujii@oss.nttdata.com>
wrote:
I think that we can simplify the code by merging the connection-retry
code into them, like the attached very WIP patch (based on yours) does.
+1.
+ else + ereport(ERROR, +
(errcode(ERRCODE_SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION),
+ errmsg("could not connect to
server \"%s\"",
+
server->servername),
+ errdetail_internal("%s",
pchomp(PQerrorMessage(entry->conn)))));
The above is not necessary? If this error occurs, connect_pg_server()
reports an error, before the above code is reached. Right?
Removed.
Thanks for the comments.
I think we need to have a volatile qualifier for need_reset_conn, because
of the sigsetjmp.
Instead of "need_reset_conn", "retry_conn" would be more meaningful and
also instead of goto label name "reset;", "retry:".
I changed "closing connection %p to reestablish connection" to "closing
connection %p to reestablish a new one"
I also adjusted the comments to be under the 80char limit.
I feel, when we are about to close an existing connection and reestablish a
new connection, it will be good to have a debug3 message saying that we
"could not connect to postgres_fdw connection %p"[1]This would tell the user that we are not able to connect to the connection. postgres=# SELECT * FROM foreign_tbl; DEBUG: starting remote transaction on connection 0x55ab0e416830 DEBUG: could not connect to postgres_fdw connection 0x55ab0e416830 DEBUG: closing connection 0x55ab0e416830 to reestablish a new one DEBUG: new postgres_fdw connection 0x55ab0e416830 for server "foreign_server" (user mapping oid 16407, userid 10) DEBUG: starting remote transaction on connection 0x55ab0e416830 DEBUG: closing remote transaction on connection 0x55ab0e416830 a1 | b1 -----+----- 100 | 200.
Attaching v5 patch that has the above changes. Both make check and make
check-world regression tests passes. Please review.
[1]: This would tell the user that we are not able to connect to the connection. postgres=# SELECT * FROM foreign_tbl; DEBUG: starting remote transaction on connection 0x55ab0e416830 DEBUG: could not connect to postgres_fdw connection 0x55ab0e416830 DEBUG: closing connection 0x55ab0e416830 to reestablish a new one DEBUG: new postgres_fdw connection 0x55ab0e416830 for server "foreign_server" (user mapping oid 16407, userid 10) DEBUG: starting remote transaction on connection 0x55ab0e416830 DEBUG: closing remote transaction on connection 0x55ab0e416830 a1 | b1 -----+----- 100 | 200
connection.
postgres=# SELECT * FROM foreign_tbl;
DEBUG: starting remote transaction on connection 0x55ab0e416830
DEBUG: could not connect to postgres_fdw connection 0x55ab0e416830
DEBUG: closing connection 0x55ab0e416830 to reestablish a new one
DEBUG: new postgres_fdw connection 0x55ab0e416830 for server
"foreign_server" (user mapping oid 16407, userid 10)
DEBUG: starting remote transaction on connection 0x55ab0e416830
DEBUG: closing remote transaction on connection 0x55ab0e416830
a1 | b1
-----+-----
100 | 200
Without the above message, it would look like we are starting remote txn,
and closing connection without any reason.
postgres=# SELECT * FROM foreign_tbl;
DEBUG: starting remote transaction on connection 0x55ab0e4c0d50
DEBUG: closing connection 0x55ab0e4c0d50 to reestablish a new one
DEBUG: new postgres_fdw connection 0x55ab0e4c0d50 for server
"foreign_server" (user mapping oid 16389, userid 10)
DEBUG: starting remote transaction on connection 0x55ab0e4c0d50
DEBUG: closing remote transaction on connection 0x55ab0e4c0d50
a1 | b1
-----+-----
100 | 200
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
Attachments:
v5-Retry-Cached-Remote-Connections-For-postgres_fdw.patchapplication/octet-stream; name=v5-Retry-Cached-Remote-Connections-For-postgres_fdw.patchDownload
From 0df20a1cf6d084f8bf3d8768d29893416fd3c83e Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddy@enterprisedb.com>
Date: Fri, 25 Sep 2020 17:30:35 +0530
Subject: [PATCH v5] Retry Cached Remote Connections For postgres_fdw
Remote connections are cached for postgres_fdw in the local
backend. There are high chances that the remote backend may
not be available i.e. it can get killed, the subsequent foreign
queries from local backend/session that use cached connection
will fail as the remote backend would have become unavailable.
This patch proposes a retry mechanism. Local backend/session
uses cached connection and if it receives connection bad error,
it deletes the cached entry and retry to get a new connection.
This retry happens only at the start of remote xact.
---
contrib/postgres_fdw/connection.c | 54 +++++++++++++-----
.../postgres_fdw/expected/postgres_fdw.out | 55 +++++++++++++++++++
contrib/postgres_fdw/sql/postgres_fdw.sql | 31 +++++++++++
3 files changed, 127 insertions(+), 13 deletions(-)
diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c
index 08daf26fdf..435cf656f7 100644
--- a/contrib/postgres_fdw/connection.c
+++ b/contrib/postgres_fdw/connection.c
@@ -108,6 +108,7 @@ PGconn *
GetConnection(UserMapping *user, bool will_prep_stmt)
{
bool found;
+ volatile bool retry_conn = false;
ConnCacheEntry *entry;
ConnCacheKey key;
@@ -159,23 +160,25 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
/* Reject further use of connections which failed abort cleanup. */
pgfdw_reject_incomplete_xact_state_change(entry);
+retry:
/*
- * If the connection needs to be remade due to invalidation, disconnect as
- * soon as we're out of all transactions.
+ * If the connection needs to be remade due to invalidation, disconnect
+ * as soon as we're out of all transactions. Also, if previous attempt
+ * to start new remote transaction failed on the cached connection,
+ * disconnect it to retry a new connection.
*/
- if (entry->conn != NULL && entry->invalidated && entry->xact_depth == 0)
+ if ((entry->conn != NULL && entry->invalidated &&
+ entry->xact_depth == 0) || retry_conn)
{
- elog(DEBUG3, "closing connection %p for option changes to take effect",
- entry->conn);
+ if (retry_conn)
+ elog(DEBUG3, "closing connection %p to reestablish a new one",
+ entry->conn);
+ else
+ elog(DEBUG3, "closing connection %p for option changes to take effect",
+ entry->conn);
disconnect_pg_server(entry);
}
- /*
- * We don't check the health of cached connection here, because it would
- * require some overhead. Broken connection will be detected when the
- * connection is actually used.
- */
-
/*
* If cache entry doesn't have a connection, we have to establish a new
* connection. (If connect_pg_server throws an error, the cache entry
@@ -206,9 +209,34 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
}
/*
- * Start a new transaction or subtransaction if needed.
+ * We check the health of the cached connection here, while the remote
+ * xact is going to begin. Broken connection will be detected and the
+ * connection is retried. We do this only once during each begin remote
+ * xact call, if the connection is still broken after the retry attempt,
+ * then the query will fail.
*/
- begin_remote_xact(entry);
+ PG_TRY();
+ {
+ /* Start a new transaction or subtransaction if needed. */
+ begin_remote_xact(entry);
+ retry_conn = false;
+ }
+ PG_CATCH();
+ {
+ if (!entry->conn ||
+ PQstatus(entry->conn) != CONNECTION_BAD ||
+ entry->xact_depth > 0 ||
+ retry_conn)
+ PG_RE_THROW();
+ retry_conn = true;
+ }
+ PG_END_TRY();
+
+ if (retry_conn)
+ {
+ elog(DEBUG3, "could not connect to postgres_fdw connection %p", entry->conn);
+ goto retry;
+ }
/* Remember if caller will prepare statements */
entry->have_prep_stmt |= will_prep_stmt;
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 10e23d02ed..d72b11a3ac 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8987,3 +8987,58 @@ PREPARE TRANSACTION 'fdw_tpc';
ERROR: cannot PREPARE a transaction that has operated on postgres_fdw foreign tables
ROLLBACK;
WARNING: there is no transaction in progress
+-- Retry cached connections at the beginning of the remote xact
+-- in case remote backend is killed.
+-- Let's use a different application name for remote connection,
+-- so that this test will not kill other backends wrongly.
+ALTER SERVER loopback OPTIONS (application_name 'fdw_retry_check');
+-- Generate a connection to remote. Local backend will cache it.
+SELECT * FROM ft1 LIMIT 1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+------+----+----+----+----+----+------------+----
+ 1111 | 2 | | | | | ft1 |
+(1 row)
+
+-- Retrieve pid of remote backend with application name fdw_retry_check
+-- and kill it intentionally here. Note that, local backend still has
+-- the remote connection/backend info in it's cache.
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+ pg_terminate_backend
+----------------------
+ t
+(1 row)
+
+-- Next query using the same foreign server should succeed if connection
+-- retry works.
+SELECT * FROM ft1 LIMIT 1;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+------+----+----+----+----+----+------------+----
+ 1111 | 2 | | | | | ft1 |
+(1 row)
+
+-- Subtransaction - remote backend is killed in the middle of a remote
+-- xact, and we don't do retry connection, hence the subsequent query
+-- using the same foreign server should fail.
+BEGIN;
+DECLARE c CURSOR FOR SELECT * FROM ft1 LIMIT 1;
+FETCH c;
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+------+----+----+----+----+----+------------+----
+ 1111 | 2 | | | | | ft1 |
+(1 row)
+
+SAVEPOINT s;
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+ pg_terminate_backend
+----------------------
+ t
+(1 row)
+
+SELECT * FROM ft1 LIMIT 1; -- should fail
+ERROR: server closed the connection unexpectedly
+ This probably means the server terminated abnormally
+ before or while processing the request.
+CONTEXT: remote SQL command: SAVEPOINT s2
+COMMIT;
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 78156d10b4..7a1b4c78bd 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2653,3 +2653,34 @@ SELECT count(*) FROM ft1;
-- error here
PREPARE TRANSACTION 'fdw_tpc';
ROLLBACK;
+
+-- Retry cached connections at the beginning of the remote xact
+-- in case remote backend is killed.
+-- Let's use a different application name for remote connection,
+-- so that this test will not kill other backends wrongly.
+ALTER SERVER loopback OPTIONS (application_name 'fdw_retry_check');
+
+-- Generate a connection to remote. Local backend will cache it.
+SELECT * FROM ft1 LIMIT 1;
+
+-- Retrieve pid of remote backend with application name fdw_retry_check
+-- and kill it intentionally here. Note that, local backend still has
+-- the remote connection/backend info in it's cache.
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+
+-- Next query using the same foreign server should succeed if connection
+-- retry works.
+SELECT * FROM ft1 LIMIT 1;
+
+-- Subtransaction - remote backend is killed in the middle of a remote
+-- xact, and we don't do retry connection, hence the subsequent query
+-- using the same foreign server should fail.
+BEGIN;
+DECLARE c CURSOR FOR SELECT * FROM ft1 LIMIT 1;
+FETCH c;
+SAVEPOINT s;
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+SELECT * FROM ft1 LIMIT 1; -- should fail
+COMMIT;
--
2.25.1
On 2020/09/25 21:19, Bharath Rupireddy wrote:
On Fri, Sep 25, 2020 at 3:21 PM Fujii Masao <masao.fujii@oss.nttdata.com <mailto:masao.fujii@oss.nttdata.com>> wrote:
I think that we can simplify the code by merging the connection-retry
code into them, like the attached very WIP patch (based on yours) does.+1.
+ else + ereport(ERROR, + (errcode(ERRCODE_SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION), + errmsg("could not connect to server \"%s\"", + server->servername), + errdetail_internal("%s", pchomp(PQerrorMessage(entry->conn)))));The above is not necessary? If this error occurs, connect_pg_server()
reports an error, before the above code is reached. Right?Removed.
Thanks for the comments.
I think we need to have a volatile qualifier for need_reset_conn, because of the sigsetjmp.
Yes.
Instead of "need_reset_conn", "retry_conn" would be more meaningful and also instead of goto label name "reset;", "retry:".
Sounds good.
I changed "closing connection %p to reestablish connection" to "closing connection %p to reestablish a new one"
OK.
I also adjusted the comments to be under the 80char limit.
I feel, when we are about to close an existing connection and reestablish a new connection, it will be good to have a debug3 message saying that we "could not connect to postgres_fdw connection %p"[1].
+1 to add debug3 message there. But this message doesn't seem to
match with what the error actually happened. What about something like
"could not start remote transaction on connection %p", instead?
Also maybe it's better to append PQerrorMessage(entry->conn)?
Attaching v5 patch that has the above changes. Both make check and make check-world regression tests passes. Please review.
Thanks for updating the patch!
+-- Generate a connection to remote. Local backend will cache it.
+SELECT * FROM ft1 LIMIT 1;
The result of this query would not be stable. Probably you need to,
for example, add ORDER BY or replace * with 1, etc.
+-- Retrieve pid of remote backend with application name fdw_retry_check
+-- and kill it intentionally here. Note that, local backend still has
+-- the remote connection/backend info in it's cache.
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
Isn't this test fragile because there is no gurantee that the target backend
has exited just after calling pg_terminate_backend()?
Regards,
--
Fujii Masao
Advanced Computing Technology Center
Research and Development Headquarters
NTT DATA CORPORATION
Thanks for the comments.
On Tue, Sep 29, 2020 at 7:30 PM Fujii Masao <masao.fujii@oss.nttdata.com> wrote:
+1 to add debug3 message there. But this message doesn't seem to
match with what the error actually happened. What about something like
"could not start remote transaction on connection %p", instead?
Looks better. Changed.
Also maybe it's better to append PQerrorMessage(entry->conn)?
Added. Now the log looks like [1]DEBUG: starting remote transaction on connection 0x55cd393a66e0 DEBUG: could not start remote transaction on connection 0x55cd393a66e0 DETAIL: server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. DEBUG: closing connection 0x55cd393a66e0 to reestablish a new one DEBUG: new postgres_fdw connection 0x55cd393a66e0 for server "foreign_server" (user mapping oid 16398, userid 10) DEBUG: starting remote transaction on connection 0x55cd393a66e0 DEBUG: closing remote transaction on connection 0x55cd393a66e0.
+-- Generate a connection to remote. Local backend will cache it. +SELECT * FROM ft1 LIMIT 1;The result of this query would not be stable. Probably you need to,
for example, add ORDER BY or replace * with 1, etc.
Changed to SELECT 1 FROM ft1 LIMIT 1;
+-- Retrieve pid of remote backend with application name fdw_retry_check +-- and kill it intentionally here. Note that, local backend still has +-- the remote connection/backend info in it's cache. +SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE +backend_type = 'client backend' AND application_name = 'fdw_retry_check';Isn't this test fragile because there is no gurantee that the target backend
has exited just after calling pg_terminate_backend()?
I think this is okay, because pg_terminate_backend() sends SIGTERM to
the backend, and upon receiving SIGTERM the signal handler die() will
be called and since there is no query being executed on the backend by
the time SIGTERM is received, it will exit immediately. Thoughts?
pqsignal(SIGTERM, die); /* cancel current query and exit */
And also, pg_terminate_backend() returns true in case the backend is
killed successfully, otherwise it returns false. PG_RETURN_BOOL(r
== SIGNAL_BACKEND_SUCCESS);
Attaching v6 patch, please review it further.
[1]: DEBUG: starting remote transaction on connection 0x55cd393a66e0 DEBUG: could not start remote transaction on connection 0x55cd393a66e0 DETAIL: server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. DEBUG: closing connection 0x55cd393a66e0 to reestablish a new one DEBUG: new postgres_fdw connection 0x55cd393a66e0 for server "foreign_server" (user mapping oid 16398, userid 10) DEBUG: starting remote transaction on connection 0x55cd393a66e0 DEBUG: closing remote transaction on connection 0x55cd393a66e0
DEBUG: starting remote transaction on connection 0x55cd393a66e0
DEBUG: could not start remote transaction on connection 0x55cd393a66e0
DETAIL: server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
DEBUG: closing connection 0x55cd393a66e0 to reestablish a new one
DEBUG: new postgres_fdw connection 0x55cd393a66e0 for server
"foreign_server" (user mapping oid 16398, userid 10)
DEBUG: starting remote transaction on connection 0x55cd393a66e0
DEBUG: closing remote transaction on connection 0x55cd393a66e0
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
Attachments:
v6-Retry-Cached-Remote-Connections-For-postgres_fdw.patchapplication/octet-stream; name=v6-Retry-Cached-Remote-Connections-For-postgres_fdw.patchDownload
From 6c5545c4deb1783a09ecb869a9b189add325099e Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddy@enterprisedb.com>
Date: Tue, 29 Sep 2020 21:16:30 +0530
Subject: [PATCH v6] Retry Cached Remote Connections For postgres_fdw
Remote connections are cached for postgres_fdw in the local
backend. There are high chances that the remote backend may
not be available i.e. it can get killed, the subsequent foreign
queries from local backend/session that use cached connection
will fail as the remote backend would have become unavailable.
This patch proposes a retry mechanism. Local backend/session
uses cached connection and if it receives connection bad error,
it deletes the cached entry and retry to get a new connection.
This retry happens only at the start of remote xact.
---
contrib/postgres_fdw/connection.c | 57 ++++++++++++++-----
.../postgres_fdw/expected/postgres_fdw.out | 55 ++++++++++++++++++
contrib/postgres_fdw/sql/postgres_fdw.sql | 31 ++++++++++
3 files changed, 130 insertions(+), 13 deletions(-)
diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c
index 08daf26fdf..16eca788ac 100644
--- a/contrib/postgres_fdw/connection.c
+++ b/contrib/postgres_fdw/connection.c
@@ -108,6 +108,7 @@ PGconn *
GetConnection(UserMapping *user, bool will_prep_stmt)
{
bool found;
+ volatile bool retry_conn = false;
ConnCacheEntry *entry;
ConnCacheKey key;
@@ -159,23 +160,25 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
/* Reject further use of connections which failed abort cleanup. */
pgfdw_reject_incomplete_xact_state_change(entry);
+retry:
/*
- * If the connection needs to be remade due to invalidation, disconnect as
- * soon as we're out of all transactions.
+ * If the connection needs to be remade due to invalidation, disconnect
+ * as soon as we're out of all transactions. Also, if previous attempt
+ * to start new remote transaction failed on the cached connection,
+ * disconnect it to retry a new connection.
*/
- if (entry->conn != NULL && entry->invalidated && entry->xact_depth == 0)
+ if ((entry->conn != NULL && entry->invalidated &&
+ entry->xact_depth == 0) || retry_conn)
{
- elog(DEBUG3, "closing connection %p for option changes to take effect",
- entry->conn);
+ if (retry_conn)
+ elog(DEBUG3, "closing connection %p to reestablish a new one",
+ entry->conn);
+ else
+ elog(DEBUG3, "closing connection %p for option changes to take effect",
+ entry->conn);
disconnect_pg_server(entry);
}
- /*
- * We don't check the health of cached connection here, because it would
- * require some overhead. Broken connection will be detected when the
- * connection is actually used.
- */
-
/*
* If cache entry doesn't have a connection, we have to establish a new
* connection. (If connect_pg_server throws an error, the cache entry
@@ -206,9 +209,37 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
}
/*
- * Start a new transaction or subtransaction if needed.
+ * We check the health of the cached connection here, while the remote
+ * xact is going to begin. Broken connection will be detected and the
+ * connection is retried. We do this only once during each begin remote
+ * xact call, if the connection is still broken after the retry attempt,
+ * then the query will fail.
*/
- begin_remote_xact(entry);
+ PG_TRY();
+ {
+ /* Start a new transaction or subtransaction if needed. */
+ begin_remote_xact(entry);
+ retry_conn = false;
+ }
+ PG_CATCH();
+ {
+ if (!entry->conn ||
+ PQstatus(entry->conn) != CONNECTION_BAD ||
+ entry->xact_depth > 0 ||
+ retry_conn)
+ PG_RE_THROW();
+ retry_conn = true;
+ }
+ PG_END_TRY();
+
+ if (retry_conn)
+ {
+ ereport(DEBUG3,
+ (errmsg("could not start remote transaction on connection %p",
+ entry->conn)),
+ errdetail_internal("%s", pchomp(PQerrorMessage(entry->conn))));
+ goto retry;
+ }
/* Remember if caller will prepare statements */
entry->have_prep_stmt |= will_prep_stmt;
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 10e23d02ed..e16a5b7e04 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8987,3 +8987,58 @@ PREPARE TRANSACTION 'fdw_tpc';
ERROR: cannot PREPARE a transaction that has operated on postgres_fdw foreign tables
ROLLBACK;
WARNING: there is no transaction in progress
+-- Retry cached connections at the beginning of the remote xact
+-- in case remote backend is killed.
+-- Let's use a different application name for remote connection,
+-- so that this test will not kill other backends wrongly.
+ALTER SERVER loopback OPTIONS (application_name 'fdw_retry_check');
+-- Generate a connection to remote. Local backend will cache it.
+SELECT 1 FROM ft1 LIMIT 1;
+ ?column?
+----------
+ 1
+(1 row)
+
+-- Retrieve pid of remote backend with application name fdw_retry_check
+-- and kill it intentionally here. Note that, local backend still has
+-- the remote connection/backend info in it's cache.
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+ pg_terminate_backend
+----------------------
+ t
+(1 row)
+
+-- Next query using the same foreign server should succeed if connection
+-- retry works.
+SELECT 1 FROM ft1 LIMIT 1;
+ ?column?
+----------
+ 1
+(1 row)
+
+-- Subtransaction - remote backend is killed in the middle of a remote
+-- xact, and we don't do retry connection, hence the subsequent query
+-- using the same foreign server should fail.
+BEGIN;
+DECLARE c CURSOR FOR SELECT 1 FROM ft1 LIMIT 1;
+FETCH c;
+ ?column?
+----------
+ 1
+(1 row)
+
+SAVEPOINT s;
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+ pg_terminate_backend
+----------------------
+ t
+(1 row)
+
+SELECT 1 FROM ft1 LIMIT 1; -- should fail
+ERROR: server closed the connection unexpectedly
+ This probably means the server terminated abnormally
+ before or while processing the request.
+CONTEXT: remote SQL command: SAVEPOINT s2
+COMMIT;
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 78156d10b4..db18d88323 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2653,3 +2653,34 @@ SELECT count(*) FROM ft1;
-- error here
PREPARE TRANSACTION 'fdw_tpc';
ROLLBACK;
+
+-- Retry cached connections at the beginning of the remote xact
+-- in case remote backend is killed.
+-- Let's use a different application name for remote connection,
+-- so that this test will not kill other backends wrongly.
+ALTER SERVER loopback OPTIONS (application_name 'fdw_retry_check');
+
+-- Generate a connection to remote. Local backend will cache it.
+SELECT 1 FROM ft1 LIMIT 1;
+
+-- Retrieve pid of remote backend with application name fdw_retry_check
+-- and kill it intentionally here. Note that, local backend still has
+-- the remote connection/backend info in it's cache.
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+
+-- Next query using the same foreign server should succeed if connection
+-- retry works.
+SELECT 1 FROM ft1 LIMIT 1;
+
+-- Subtransaction - remote backend is killed in the middle of a remote
+-- xact, and we don't do retry connection, hence the subsequent query
+-- using the same foreign server should fail.
+BEGIN;
+DECLARE c CURSOR FOR SELECT 1 FROM ft1 LIMIT 1;
+FETCH c;
+SAVEPOINT s;
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+SELECT 1 FROM ft1 LIMIT 1; -- should fail
+COMMIT;
--
2.25.1
On 2020/09/30 0:50, Bharath Rupireddy wrote:
Thanks for the comments.
On Tue, Sep 29, 2020 at 7:30 PM Fujii Masao <masao.fujii@oss.nttdata.com> wrote:
+1 to add debug3 message there. But this message doesn't seem to
match with what the error actually happened. What about something like
"could not start remote transaction on connection %p", instead?Looks better. Changed.
Also maybe it's better to append PQerrorMessage(entry->conn)?
Added. Now the log looks like [1].
+-- Generate a connection to remote. Local backend will cache it. +SELECT * FROM ft1 LIMIT 1;The result of this query would not be stable. Probably you need to,
for example, add ORDER BY or replace * with 1, etc.Changed to SELECT 1 FROM ft1 LIMIT 1;
+-- Retrieve pid of remote backend with application name fdw_retry_check +-- and kill it intentionally here. Note that, local backend still has +-- the remote connection/backend info in it's cache. +SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE +backend_type = 'client backend' AND application_name = 'fdw_retry_check';Isn't this test fragile because there is no gurantee that the target backend
has exited just after calling pg_terminate_backend()?I think this is okay, because pg_terminate_backend() sends SIGTERM to
the backend, and upon receiving SIGTERM the signal handler die() will
be called and since there is no query being executed on the backend by
the time SIGTERM is received, it will exit immediately. Thoughts?
Yeah, basically you're right. But that backend *can* still be running
when the subsequent test query starts. I'm wondering if wait_pid()
(please see regress.c and sql/dblink.sql) should be used to ensure
the target backend disappeared.
Regards,
--
Fujii Masao
Advanced Computing Technology Center
Research and Development Headquarters
NTT DATA CORPORATION
On Tue, Sep 29, 2020 at 10:01 PM Fujii Masao
<masao.fujii@oss.nttdata.com> wrote:
I think this is okay, because pg_terminate_backend() sends SIGTERM to
the backend, and upon receiving SIGTERM the signal handler die() will
be called and since there is no query being executed on the backend by
the time SIGTERM is received, it will exit immediately. Thoughts?Yeah, basically you're right. But that backend *can* still be running
when the subsequent test query starts. I'm wondering if wait_pid()
(please see regress.c and sql/dblink.sql) should be used to ensure
the target backend disappeared.
I think wait_pid() is not a generic function, and I'm unable to use
that inside postgres_fdw.sql. I think I need to recreate that function
for postgres_fdw.sql. For dblink, it's being created as part of
paths.source. Could you help me in doing so?
And another way, if we don't want to use wait_pid() is to have a
plpgsql stored procedure, that in a loop keeps on checking for the
backed pid from pg_stat_activity, exit when pid is 0. and then proceed
to issue the next foreign table query. Thoughts?
mypid = -1;
while (mypid != 0)
SELECT pid INTO mypid FROM pg_stat_activity WHERE backend_type =
'client backend' AND application_name = 'fdw_retry_check';
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
On 2020/09/30 21:02, Bharath Rupireddy wrote:
On Tue, Sep 29, 2020 at 10:01 PM Fujii Masao
<masao.fujii@oss.nttdata.com> wrote:I think this is okay, because pg_terminate_backend() sends SIGTERM to
the backend, and upon receiving SIGTERM the signal handler die() will
be called and since there is no query being executed on the backend by
the time SIGTERM is received, it will exit immediately. Thoughts?Yeah, basically you're right. But that backend *can* still be running
when the subsequent test query starts. I'm wondering if wait_pid()
(please see regress.c and sql/dblink.sql) should be used to ensure
the target backend disappeared.I think wait_pid() is not a generic function, and I'm unable to use
that inside postgres_fdw.sql. I think I need to recreate that function
for postgres_fdw.sql. For dblink, it's being created as part of
paths.source. Could you help me in doing so?And another way, if we don't want to use wait_pid() is to have a
plpgsql stored procedure, that in a loop keeps on checking for the
backed pid from pg_stat_activity, exit when pid is 0. and then proceed
to issue the next foreign table query. Thoughts?
+1 for this approach! We can use plpgsql or DO command.
mypid = -1;
while (mypid != 0)
SELECT pid INTO mypid FROM pg_stat_activity WHERE backend_type =
'client backend' AND application_name = 'fdw_retry_check';
Or we can just wait for the number of processes with
appname='fdw_retry_check' to be zero rather than checking the pid.
Regards,
--
Fujii Masao
Advanced Computing Technology Center
Research and Development Headquarters
NTT DATA CORPORATION
On Wed, Sep 30, 2020 at 11:32 PM Fujii Masao
<masao.fujii@oss.nttdata.com> wrote:
And another way, if we don't want to use wait_pid() is to have a
plpgsql stored procedure, that in a loop keeps on checking for the
backed pid from pg_stat_activity, exit when pid is 0. and then proceed
to issue the next foreign table query. Thoughts?+1 for this approach! We can use plpgsql or DO command.
Used plpgsql procedure as we have to use the procedure 2 times.
mypid = -1;
while (mypid != 0)
SELECT pid INTO mypid FROM pg_stat_activity WHERE backend_type =
'client backend' AND application_name = 'fdw_retry_check';Or we can just wait for the number of processes with
appname='fdw_retry_check' to be zero rather than checking the pid.
Done.
Attaching v7 patch with above changes. Please review it.
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
Attachments:
v7-Retry-Cached-Remote-Connections-For-postgres_fdw.patchapplication/x-patch; name=v7-Retry-Cached-Remote-Connections-For-postgres_fdw.patchDownload
From e2a85116116e79c76a7b0fcfc1a25eff57627ed4 Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddy@enterprisedb.com>
Date: Thu, 1 Oct 2020 15:16:26 +0530
Subject: [PATCH v7] Retry Cached Remote Connections For postgres_fdw
Remote connections are cached for postgres_fdw in the local
backend. There are high chances that the remote backend may
not be available i.e. it can get killed, the subsequent foreign
queries from local backend/session that use cached connection
will fail as the remote backend would have become unavailable.
This patch proposes a retry mechanism. Local backend/session
uses cached connection and if it receives connection bad error,
it deletes the cached entry and retry to get a new connection.
This retry happens only at the start of remote xact.
---
contrib/postgres_fdw/connection.c | 55 +++++++++---
.../postgres_fdw/expected/postgres_fdw.out | 85 +++++++++++++++++++
contrib/postgres_fdw/sql/postgres_fdw.sql | 64 ++++++++++++++
3 files changed, 192 insertions(+), 12 deletions(-)
diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c
index 08daf26fdf..441124963f 100644
--- a/contrib/postgres_fdw/connection.c
+++ b/contrib/postgres_fdw/connection.c
@@ -108,6 +108,7 @@ PGconn *
GetConnection(UserMapping *user, bool will_prep_stmt)
{
bool found;
+ volatile bool retry_conn = false;
ConnCacheEntry *entry;
ConnCacheKey key;
@@ -159,23 +160,25 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
/* Reject further use of connections which failed abort cleanup. */
pgfdw_reject_incomplete_xact_state_change(entry);
+retry:
/*
* If the connection needs to be remade due to invalidation, disconnect as
- * soon as we're out of all transactions.
+ * soon as we're out of all transactions. Also, if previous attempt to
+ * start new remote transaction failed on the cached connection, disconnect
+ * it to retry a new connection.
*/
- if (entry->conn != NULL && entry->invalidated && entry->xact_depth == 0)
+ if ((entry->conn != NULL && entry->invalidated &&
+ entry->xact_depth == 0) || retry_conn)
{
- elog(DEBUG3, "closing connection %p for option changes to take effect",
- entry->conn);
+ if (retry_conn)
+ elog(DEBUG3, "closing connection %p to reestablish a new one",
+ entry->conn);
+ else
+ elog(DEBUG3, "closing connection %p for option changes to take effect",
+ entry->conn);
disconnect_pg_server(entry);
}
- /*
- * We don't check the health of cached connection here, because it would
- * require some overhead. Broken connection will be detected when the
- * connection is actually used.
- */
-
/*
* If cache entry doesn't have a connection, we have to establish a new
* connection. (If connect_pg_server throws an error, the cache entry
@@ -206,9 +209,37 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
}
/*
- * Start a new transaction or subtransaction if needed.
+ * We check the health of the cached connection here, while the remote xact
+ * is going to begin. Broken connection will be detected and the
+ * connection is retried. We do this only once during each begin remote
+ * xact call, if the connection is still broken after the retry attempt,
+ * then the query will fail.
*/
- begin_remote_xact(entry);
+ PG_TRY();
+ {
+ /* Start a new transaction or subtransaction if needed. */
+ begin_remote_xact(entry);
+ retry_conn = false;
+ }
+ PG_CATCH();
+ {
+ if (!entry->conn ||
+ PQstatus(entry->conn) != CONNECTION_BAD ||
+ entry->xact_depth > 0 ||
+ retry_conn)
+ PG_RE_THROW();
+ retry_conn = true;
+ }
+ PG_END_TRY();
+
+ if (retry_conn)
+ {
+ ereport(DEBUG3,
+ (errmsg("could not start remote transaction on connection %p",
+ entry->conn)),
+ errdetail_internal("%s", pchomp(PQerrorMessage(entry->conn))));
+ goto retry;
+ }
/* Remember if caller will prepare statements */
entry->have_prep_stmt |= will_prep_stmt;
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 10e23d02ed..09d0578d05 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8987,3 +8987,88 @@ PREPARE TRANSACTION 'fdw_tpc';
ERROR: cannot PREPARE a transaction that has operated on postgres_fdw foreign tables
ROLLBACK;
WARNING: there is no transaction in progress
+-- Connection retry feature
+-- Retry cached connections at the beginning of the remote xact in case remote
+-- backend is killed. Let's use a different application name for remote
+-- connection, so that this test will not kill other backends wrongly.
+ALTER SERVER loopback OPTIONS (application_name 'fdw_retry_check');
+-- This procedure waits until the terminated backend has really gone away. We
+-- also had to check for wait event type and wait event of the terminated
+-- backend's row from pg_stat_activity. Otherwise, the usage of this procedure
+-- in an explicit txn block(BEGIN - END) after terminating the backend would
+-- still have an entry with wait event type wait event null, as we have not yet
+-- committed the txn. The entry of the terminated backend from pg_stat_activity
+-- would be removed only after the txn commit.
+CREATE OR REPLACE PROCEDURE wait_for_backend_termination()
+LANGUAGE plpgsql
+AS $$
+DECLARE proccnt int;
+BEGIN
+ LOOP
+ SELECT count(*) INTO proccnt FROM pg_stat_activity
+ WHERE backend_type = 'client backend' AND
+ application_name = 'fdw_retry_check' AND
+ wait_event_type IS NOT NULL AND
+ wait_event IS NOT NULL;
+ EXIT WHEN proccnt = 0;
+ END LOOP;
+END;
+$$;
+-- Connection retry feature test case 001:
+-- Generate a connection to remote. Local backend will cache it.
+SELECT 1 FROM ft1 LIMIT 1;
+ ?column?
+----------
+ 1
+(1 row)
+
+-- Retrieve pid of remote backend with application name fdw_retry_check and
+-- kill intentionally here. Note that, local backend still has the remote
+-- backend info in it's cache.
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+ pg_terminate_backend
+----------------------
+ t
+(1 row)
+
+-- Despite issuing terminate, there are chances that the backend is still not
+-- actually terminated. Hence, wait for the terminated backend to go away.
+CALL wait_for_backend_termination();
+-- Next query using the same foreign server should succeed if connection retry
+-- works.
+SELECT 1 FROM ft1 LIMIT 1;
+ ?column?
+----------
+ 1
+(1 row)
+
+-- Connection retry feature test case 002:
+-- Subtransaction - remote backend is killed in the middle of a remote xact and
+-- we don't do retry connection, hence the subsequent query using the same
+-- foreign server should fail.
+BEGIN;
+DECLARE c CURSOR FOR SELECT 1 FROM ft1 LIMIT 1;
+FETCH c;
+ ?column?
+----------
+ 1
+(1 row)
+
+SAVEPOINT s;
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+ pg_terminate_backend
+----------------------
+ t
+(1 row)
+
+CALL wait_for_backend_termination();
+SELECT 1 FROM ft1 LIMIT 1; -- should fail
+ERROR: server closed the connection unexpectedly
+ This probably means the server terminated abnormally
+ before or while processing the request.
+CONTEXT: remote SQL command: SAVEPOINT s2
+COMMIT;
+-- Clean up
+DROP PROCEDURE wait_for_backend_termination();
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 78156d10b4..17246f4176 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2653,3 +2653,67 @@ SELECT count(*) FROM ft1;
-- error here
PREPARE TRANSACTION 'fdw_tpc';
ROLLBACK;
+
+-- Connection retry feature
+-- Retry cached connections at the beginning of the remote xact in case remote
+-- backend is killed. Let's use a different application name for remote
+-- connection, so that this test will not kill other backends wrongly.
+ALTER SERVER loopback OPTIONS (application_name 'fdw_retry_check');
+
+-- This procedure waits until the terminated backend has really gone away. We
+-- also had to check for wait event type and wait event of the terminated
+-- backend's row from pg_stat_activity. Otherwise, the usage of this procedure
+-- in an explicit txn block(BEGIN - END) after terminating the backend would
+-- still have an entry with wait event type wait event null, as we have not yet
+-- committed the txn. The entry of the terminated backend from pg_stat_activity
+-- would be removed only after the txn commit.
+CREATE OR REPLACE PROCEDURE wait_for_backend_termination()
+LANGUAGE plpgsql
+AS $$
+DECLARE proccnt int;
+BEGIN
+ LOOP
+ SELECT count(*) INTO proccnt FROM pg_stat_activity
+ WHERE backend_type = 'client backend' AND
+ application_name = 'fdw_retry_check' AND
+ wait_event_type IS NOT NULL AND
+ wait_event IS NOT NULL;
+ EXIT WHEN proccnt = 0;
+ END LOOP;
+END;
+$$;
+
+-- Connection retry feature test case 001:
+-- Generate a connection to remote. Local backend will cache it.
+SELECT 1 FROM ft1 LIMIT 1;
+
+-- Retrieve pid of remote backend with application name fdw_retry_check and
+-- kill intentionally here. Note that, local backend still has the remote
+-- backend info in it's cache.
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+
+-- Despite issuing terminate, there are chances that the backend is still not
+-- actually terminated. Hence, wait for the terminated backend to go away.
+CALL wait_for_backend_termination();
+
+-- Next query using the same foreign server should succeed if connection retry
+-- works.
+SELECT 1 FROM ft1 LIMIT 1;
+
+-- Connection retry feature test case 002:
+-- Subtransaction - remote backend is killed in the middle of a remote xact and
+-- we don't do retry connection, hence the subsequent query using the same
+-- foreign server should fail.
+BEGIN;
+DECLARE c CURSOR FOR SELECT 1 FROM ft1 LIMIT 1;
+FETCH c;
+SAVEPOINT s;
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+CALL wait_for_backend_termination();
+SELECT 1 FROM ft1 LIMIT 1; -- should fail
+COMMIT;
+
+-- Clean up
+DROP PROCEDURE wait_for_backend_termination();
\ No newline at end of file
--
2.25.1
On 2020/10/01 21:14, Bharath Rupireddy wrote:
On Wed, Sep 30, 2020 at 11:32 PM Fujii Masao
<masao.fujii@oss.nttdata.com> wrote:And another way, if we don't want to use wait_pid() is to have a
plpgsql stored procedure, that in a loop keeps on checking for the
backed pid from pg_stat_activity, exit when pid is 0. and then proceed
to issue the next foreign table query. Thoughts?+1 for this approach! We can use plpgsql or DO command.
Used plpgsql procedure as we have to use the procedure 2 times.
mypid = -1;
while (mypid != 0)
SELECT pid INTO mypid FROM pg_stat_activity WHERE backend_type =
'client backend' AND application_name = 'fdw_retry_check';Or we can just wait for the number of processes with
appname='fdw_retry_check' to be zero rather than checking the pid.Done.
Attaching v7 patch with above changes. Please review it.
Thanks for updating the patch!
+-- committed the txn. The entry of the terminated backend from pg_stat_activity
+-- would be removed only after the txn commit.
pg_stat_clear_snapshot() can be used to reset the entry.
+ EXIT WHEN proccnt = 0;
+ END LOOP;
Isn't it better to sleep here, to avoid th busy loop?
So what I thought was something like
CREATE OR REPLACE PROCEDURE wait_for_backend_termination()
LANGUAGE plpgsql
AS $$
BEGIN
LOOP
PERFORM * FROM pg_stat_activity WHERE application_name = 'fdw_retry_check';
EXIT WHEN NOT FOUND;
PERFORM pg_sleep(1), pg_stat_clear_snapshot();
END LOOP;
END;
$$;
Regards,
--
Fujii Masao
Advanced Computing Technology Center
Research and Development Headquarters
NTT DATA CORPORATION
On Thu, Oct 1, 2020 at 8:10 PM Fujii Masao <masao.fujii@oss.nttdata.com> wrote:
pg_stat_clear_snapshot() can be used to reset the entry.
Thanks. I wasn't knowing it.
+ EXIT WHEN proccnt = 0;
+ END LOOP;Isn't it better to sleep here, to avoid th busy loop?
+1.
So what I thought was something like
CREATE OR REPLACE PROCEDURE wait_for_backend_termination()
LANGUAGE plpgsql
AS $$
BEGIN
LOOP
PERFORM * FROM pg_stat_activity WHERE application_name = 'fdw_retry_check';
EXIT WHEN NOT FOUND;
PERFORM pg_sleep(1), pg_stat_clear_snapshot();
END LOOP;
END;
$$;
Changed.
Attaching v8 patch, please review it.. Both make check and make
check-world passes on v8.
I have another question not related to this patch: though we have
wait_pid() function, we are not able to use it like
pg_terminate_backend() in other modules, wouldn't be nice if we can
make it generic under the name pg_wait_pid() and usable across all pg
modules?
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
Attachments:
v8-Retry-Cached-Remote-Connections-For-postgres_fdw.patchapplication/octet-stream; name=v8-Retry-Cached-Remote-Connections-For-postgres_fdw.patchDownload
From 7aa1b7e1b9820a92beded06a965275fd237040cd Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddy@enterprisedb.com>
Date: Thu, 1 Oct 2020 21:13:05 +0530
Subject: [PATCH v8] Retry Cached Remote Connections For postgres_fdw
Remote connections are cached for postgres_fdw in the local
backend. There are high chances that the remote backend may
not be available i.e. it can get killed, the subsequent foreign
queries from local backend/session that use cached connection
will fail as the remote backend would have become unavailable.
This patch proposes a retry mechanism. Local backend/session
uses cached connection and if it receives connection bad error,
it deletes the cached entry and retry to get a new connection.
This retry happens only at the start of remote xact.
---
contrib/postgres_fdw/connection.c | 55 +++++++++++---
.../postgres_fdw/expected/postgres_fdw.out | 76 +++++++++++++++++++
contrib/postgres_fdw/sql/postgres_fdw.sql | 55 ++++++++++++++
3 files changed, 174 insertions(+), 12 deletions(-)
diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c
index 08daf26fdf..441124963f 100644
--- a/contrib/postgres_fdw/connection.c
+++ b/contrib/postgres_fdw/connection.c
@@ -108,6 +108,7 @@ PGconn *
GetConnection(UserMapping *user, bool will_prep_stmt)
{
bool found;
+ volatile bool retry_conn = false;
ConnCacheEntry *entry;
ConnCacheKey key;
@@ -159,23 +160,25 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
/* Reject further use of connections which failed abort cleanup. */
pgfdw_reject_incomplete_xact_state_change(entry);
+retry:
/*
* If the connection needs to be remade due to invalidation, disconnect as
- * soon as we're out of all transactions.
+ * soon as we're out of all transactions. Also, if previous attempt to
+ * start new remote transaction failed on the cached connection, disconnect
+ * it to retry a new connection.
*/
- if (entry->conn != NULL && entry->invalidated && entry->xact_depth == 0)
+ if ((entry->conn != NULL && entry->invalidated &&
+ entry->xact_depth == 0) || retry_conn)
{
- elog(DEBUG3, "closing connection %p for option changes to take effect",
- entry->conn);
+ if (retry_conn)
+ elog(DEBUG3, "closing connection %p to reestablish a new one",
+ entry->conn);
+ else
+ elog(DEBUG3, "closing connection %p for option changes to take effect",
+ entry->conn);
disconnect_pg_server(entry);
}
- /*
- * We don't check the health of cached connection here, because it would
- * require some overhead. Broken connection will be detected when the
- * connection is actually used.
- */
-
/*
* If cache entry doesn't have a connection, we have to establish a new
* connection. (If connect_pg_server throws an error, the cache entry
@@ -206,9 +209,37 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
}
/*
- * Start a new transaction or subtransaction if needed.
+ * We check the health of the cached connection here, while the remote xact
+ * is going to begin. Broken connection will be detected and the
+ * connection is retried. We do this only once during each begin remote
+ * xact call, if the connection is still broken after the retry attempt,
+ * then the query will fail.
*/
- begin_remote_xact(entry);
+ PG_TRY();
+ {
+ /* Start a new transaction or subtransaction if needed. */
+ begin_remote_xact(entry);
+ retry_conn = false;
+ }
+ PG_CATCH();
+ {
+ if (!entry->conn ||
+ PQstatus(entry->conn) != CONNECTION_BAD ||
+ entry->xact_depth > 0 ||
+ retry_conn)
+ PG_RE_THROW();
+ retry_conn = true;
+ }
+ PG_END_TRY();
+
+ if (retry_conn)
+ {
+ ereport(DEBUG3,
+ (errmsg("could not start remote transaction on connection %p",
+ entry->conn)),
+ errdetail_internal("%s", pchomp(PQerrorMessage(entry->conn))));
+ goto retry;
+ }
/* Remember if caller will prepare statements */
entry->have_prep_stmt |= will_prep_stmt;
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 10e23d02ed..db039237e1 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8987,3 +8987,79 @@ PREPARE TRANSACTION 'fdw_tpc';
ERROR: cannot PREPARE a transaction that has operated on postgres_fdw foreign tables
ROLLBACK;
WARNING: there is no transaction in progress
+-- Connection retry feature
+-- Retry cached connections at the beginning of the remote xact in case remote
+-- backend is killed. Let's use a different application name for remote
+-- connection, so that this test will not kill other backends wrongly.
+ALTER SERVER loopback OPTIONS (application_name 'fdw_retry_check');
+-- This procedure waits until the terminated backend has really gone away. We
+-- do pg_stat_clear_snapshot() to fetch the new snapshot of the stats.
+CREATE OR REPLACE PROCEDURE wait_for_backend_termination()
+LANGUAGE plpgsql
+AS $$
+BEGIN
+ LOOP
+ PERFORM * FROM pg_stat_activity WHERE application_name = 'fdw_retry_check';
+ EXIT WHEN NOT FOUND;
+ PERFORM pg_sleep(1), pg_stat_clear_snapshot();
+ END LOOP;
+END;
+$$;
+-- Connection retry feature test case 001:
+-- Generate a connection to remote. Local backend will cache it.
+SELECT 1 FROM ft1 LIMIT 1;
+ ?column?
+----------
+ 1
+(1 row)
+
+-- Retrieve pid of remote backend with application name fdw_retry_check and
+-- kill intentionally here. Note that, local backend still has the remote
+-- backend info in it's cache.
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+ pg_terminate_backend
+----------------------
+ t
+(1 row)
+
+-- Despite issuing terminate, there are chances that the backend is still not
+-- actually terminated. Hence, wait for the terminated backend to go away.
+CALL wait_for_backend_termination();
+-- Next query using the same foreign server should succeed if connection retry
+-- works.
+SELECT 1 FROM ft1 LIMIT 1;
+ ?column?
+----------
+ 1
+(1 row)
+
+-- Connection retry feature test case 002:
+-- Subtransaction - remote backend is killed in the middle of a remote xact and
+-- we don't do retry connection, hence the subsequent query using the same
+-- foreign server should fail.
+BEGIN;
+DECLARE c CURSOR FOR SELECT 1 FROM ft1 LIMIT 1;
+FETCH c;
+ ?column?
+----------
+ 1
+(1 row)
+
+SAVEPOINT s;
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+ pg_terminate_backend
+----------------------
+ t
+(1 row)
+
+CALL wait_for_backend_termination();
+SELECT 1 FROM ft1 LIMIT 1; -- should fail
+ERROR: server closed the connection unexpectedly
+ This probably means the server terminated abnormally
+ before or while processing the request.
+CONTEXT: remote SQL command: SAVEPOINT s2
+COMMIT;
+-- Clean up
+DROP PROCEDURE wait_for_backend_termination();
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 78156d10b4..3d4eb8414d 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2653,3 +2653,58 @@ SELECT count(*) FROM ft1;
-- error here
PREPARE TRANSACTION 'fdw_tpc';
ROLLBACK;
+
+-- Connection retry feature
+-- Retry cached connections at the beginning of the remote xact in case remote
+-- backend is killed. Let's use a different application name for remote
+-- connection, so that this test will not kill other backends wrongly.
+ALTER SERVER loopback OPTIONS (application_name 'fdw_retry_check');
+
+-- This procedure waits until the terminated backend has really gone away. We
+-- do pg_stat_clear_snapshot() to fetch the new snapshot of the stats.
+CREATE OR REPLACE PROCEDURE wait_for_backend_termination()
+LANGUAGE plpgsql
+AS $$
+BEGIN
+ LOOP
+ PERFORM * FROM pg_stat_activity WHERE application_name = 'fdw_retry_check';
+ EXIT WHEN NOT FOUND;
+ PERFORM pg_sleep(1), pg_stat_clear_snapshot();
+ END LOOP;
+END;
+$$;
+
+-- Connection retry feature test case 001:
+-- Generate a connection to remote. Local backend will cache it.
+SELECT 1 FROM ft1 LIMIT 1;
+
+-- Retrieve pid of remote backend with application name fdw_retry_check and
+-- kill intentionally here. Note that, local backend still has the remote
+-- backend info in it's cache.
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+
+-- Despite issuing terminate, there are chances that the backend is still not
+-- actually terminated. Hence, wait for the terminated backend to go away.
+CALL wait_for_backend_termination();
+
+-- Next query using the same foreign server should succeed if connection retry
+-- works.
+SELECT 1 FROM ft1 LIMIT 1;
+
+-- Connection retry feature test case 002:
+-- Subtransaction - remote backend is killed in the middle of a remote xact and
+-- we don't do retry connection, hence the subsequent query using the same
+-- foreign server should fail.
+BEGIN;
+DECLARE c CURSOR FOR SELECT 1 FROM ft1 LIMIT 1;
+FETCH c;
+SAVEPOINT s;
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+CALL wait_for_backend_termination();
+SELECT 1 FROM ft1 LIMIT 1; -- should fail
+COMMIT;
+
+-- Clean up
+DROP PROCEDURE wait_for_backend_termination();
--
2.25.1
On 2020/10/02 0:46, Bharath Rupireddy wrote:
On Thu, Oct 1, 2020 at 8:10 PM Fujii Masao <masao.fujii@oss.nttdata.com> wrote:
pg_stat_clear_snapshot() can be used to reset the entry.
Thanks. I wasn't knowing it.
+ EXIT WHEN proccnt = 0;
+ END LOOP;Isn't it better to sleep here, to avoid th busy loop?
+1.
So what I thought was something like
CREATE OR REPLACE PROCEDURE wait_for_backend_termination()
LANGUAGE plpgsql
AS $$
BEGIN
LOOP
PERFORM * FROM pg_stat_activity WHERE application_name = 'fdw_retry_check';
EXIT WHEN NOT FOUND;
PERFORM pg_sleep(1), pg_stat_clear_snapshot();
END LOOP;
END;
$$;Changed.
Attaching v8 patch, please review it.. Both make check and make
check-world passes on v8.
Thanks for updating the patch! It basically looks good to me.
I tweaked the patch as follows.
+ if (!entry->conn ||
+ PQstatus(entry->conn) != CONNECTION_BAD ||
With the above change, if entry->conn is NULL, an error is thrown and no new
connection is reestablished. But why? IMO it's more natural to reestablish
new connection in that case. So I removed "!entry->conn" from the above
condition.
+ ereport(DEBUG3,
+ (errmsg("could not start remote transaction on connection %p",
+ entry->conn)),
I replaced errmsg() with errmsg_internal() because the translation of
this debug message is not necessary.
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE
+backend_type = 'client backend' AND application_name = 'fdw_retry_check';
+CALL wait_for_backend_termination();
Since we always use pg_terminate_backend() and wait_for_backend_termination()
together, I merged them into one function.
I simplied the comments on the regression test.
Attached is the updated version of the patch. If this patch is ok,
I'd like to mark it as ready for committer.
I have another question not related to this patch: though we have
wait_pid() function, we are not able to use it like
pg_terminate_backend() in other modules, wouldn't be nice if we can
make it generic under the name pg_wait_pid() and usable across all pg
modules?
I thought that, too. But I could not come up with good idea for *real* use case
of that function. At least that's useful for the regression test, though.
Anyway, IMO it's worth proposing that and hearing more opinions about that
from other hackers.
Regards,
--
Fujii Masao
Advanced Computing Technology Center
Research and Development Headquarters
NTT DATA CORPORATION
Attachments:
v9-Retry-Cached-Remote-Connections-For-postgres_fdw.patchtext/plain; charset=UTF-8; name=v9-Retry-Cached-Remote-Connections-For-postgres_fdw.patch; x-mac-creator=0; x-mac-type=0Download
diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c
index 08daf26fdf..901d3a4661 100644
--- a/contrib/postgres_fdw/connection.c
+++ b/contrib/postgres_fdw/connection.c
@@ -108,6 +108,7 @@ PGconn *
GetConnection(UserMapping *user, bool will_prep_stmt)
{
bool found;
+ volatile bool retry_conn = false;
ConnCacheEntry *entry;
ConnCacheKey key;
@@ -159,23 +160,25 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
/* Reject further use of connections which failed abort cleanup. */
pgfdw_reject_incomplete_xact_state_change(entry);
+retry:
/*
* If the connection needs to be remade due to invalidation, disconnect as
- * soon as we're out of all transactions.
+ * soon as we're out of all transactions. Also, if previous attempt to
+ * start new remote transaction failed on the cached connection, disconnect
+ * it to retry a new connection.
*/
- if (entry->conn != NULL && entry->invalidated && entry->xact_depth == 0)
+ if ((entry->conn != NULL && entry->invalidated &&
+ entry->xact_depth == 0) || retry_conn)
{
- elog(DEBUG3, "closing connection %p for option changes to take effect",
- entry->conn);
+ if (retry_conn)
+ elog(DEBUG3, "closing connection %p to reestablish a new one",
+ entry->conn);
+ else
+ elog(DEBUG3, "closing connection %p for option changes to take effect",
+ entry->conn);
disconnect_pg_server(entry);
}
- /*
- * We don't check the health of cached connection here, because it would
- * require some overhead. Broken connection will be detected when the
- * connection is actually used.
- */
-
/*
* If cache entry doesn't have a connection, we have to establish a new
* connection. (If connect_pg_server throws an error, the cache entry
@@ -206,9 +209,35 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
}
/*
- * Start a new transaction or subtransaction if needed.
+ * We check the health of the cached connection here when starting
+ * a new remote transaction. If broken connection is detected,
+ * we try to reestablish a new connection. If broken connection is
+ * detected again here, we give up getting a connection.
*/
- begin_remote_xact(entry);
+ PG_TRY();
+ {
+ /* Start a new transaction or subtransaction if needed. */
+ begin_remote_xact(entry);
+ retry_conn = false;
+ }
+ PG_CATCH();
+ {
+ if (PQstatus(entry->conn) != CONNECTION_BAD ||
+ entry->xact_depth > 0 ||
+ retry_conn)
+ PG_RE_THROW();
+ retry_conn = true;
+ }
+ PG_END_TRY();
+
+ if (retry_conn)
+ {
+ ereport(DEBUG3,
+ (errmsg_internal("could not start remote transaction on connection %p",
+ entry->conn)),
+ errdetail_internal("%s", pchomp(PQerrorMessage(entry->conn))));
+ goto retry;
+ }
/* Remember if caller will prepare statements */
entry->have_prep_stmt |= will_prep_stmt;
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 10e23d02ed..2c5614073f 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8987,3 +8987,51 @@ PREPARE TRANSACTION 'fdw_tpc';
ERROR: cannot PREPARE a transaction that has operated on postgres_fdw foreign tables
ROLLBACK;
WARNING: there is no transaction in progress
+-- ===================================================================
+-- reestablish new connection
+-- ===================================================================
+-- Terminate the backend having the specified application_name and wait for
+-- the termination to complete.
+CREATE OR REPLACE PROCEDURE terminate_backend_and_wait(appname text) AS $$
+BEGIN
+ PERFORM pg_terminate_backend(pid) FROM pg_stat_activity
+ WHERE application_name = appname;
+ LOOP
+ PERFORM * FROM pg_stat_activity WHERE application_name = appname;
+ EXIT WHEN NOT FOUND;
+ PERFORM pg_sleep(1), pg_stat_clear_snapshot();
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+-- Change application_name of remote connection to special one
+-- so that we can easily terminate the connection later.
+ALTER SERVER loopback OPTIONS (application_name 'fdw_retry_check');
+SELECT 1 FROM ft1 LIMIT 1;
+ ?column?
+----------
+ 1
+(1 row)
+
+-- Terminate the remote connection.
+CALL terminate_backend_and_wait('fdw_retry_check');
+-- This query should detect the broken connection when starting new remote
+-- transaction, reestablish new connection, and then succeed.
+BEGIN;
+SELECT 1 FROM ft1 LIMIT 1;
+ ?column?
+----------
+ 1
+(1 row)
+
+-- If the query detects the broken connection when starting new remote
+-- subtransaction, it doesn't reestablish new connection and should fail.
+CALL terminate_backend_and_wait('fdw_retry_check');
+SAVEPOINT s;
+SELECT 1 FROM ft1 LIMIT 1; -- should fail
+ERROR: server closed the connection unexpectedly
+ This probably means the server terminated abnormally
+ before or while processing the request.
+CONTEXT: remote SQL command: SAVEPOINT s2
+COMMIT;
+-- Clean up
+DROP PROCEDURE terminate_backend_and_wait(text);
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 78156d10b4..4da1f78956 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2653,3 +2653,44 @@ SELECT count(*) FROM ft1;
-- error here
PREPARE TRANSACTION 'fdw_tpc';
ROLLBACK;
+
+-- ===================================================================
+-- reestablish new connection
+-- ===================================================================
+
+-- Terminate the backend having the specified application_name and wait for
+-- the termination to complete.
+CREATE OR REPLACE PROCEDURE terminate_backend_and_wait(appname text) AS $$
+BEGIN
+ PERFORM pg_terminate_backend(pid) FROM pg_stat_activity
+ WHERE application_name = appname;
+ LOOP
+ PERFORM * FROM pg_stat_activity WHERE application_name = appname;
+ EXIT WHEN NOT FOUND;
+ PERFORM pg_sleep(1), pg_stat_clear_snapshot();
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Change application_name of remote connection to special one
+-- so that we can easily terminate the connection later.
+ALTER SERVER loopback OPTIONS (application_name 'fdw_retry_check');
+SELECT 1 FROM ft1 LIMIT 1;
+
+-- Terminate the remote connection.
+CALL terminate_backend_and_wait('fdw_retry_check');
+
+-- This query should detect the broken connection when starting new remote
+-- transaction, reestablish new connection, and then succeed.
+BEGIN;
+SELECT 1 FROM ft1 LIMIT 1;
+
+-- If the query detects the broken connection when starting new remote
+-- subtransaction, it doesn't reestablish new connection and should fail.
+CALL terminate_backend_and_wait('fdw_retry_check');
+SAVEPOINT s;
+SELECT 1 FROM ft1 LIMIT 1; -- should fail
+COMMIT;
+
+-- Clean up
+DROP PROCEDURE terminate_backend_and_wait(text);
On Fri, Oct 2, 2020 at 11:30 PM Fujii Masao <masao.fujii@oss.nttdata.com> wrote:
Attaching v8 patch, please review it.. Both make check and make
check-world passes on v8.Thanks for updating the patch! It basically looks good to me.
I tweaked the patch as follows.+ if (!entry->conn || + PQstatus(entry->conn) != CONNECTION_BAD ||With the above change, if entry->conn is NULL, an error is thrown and no new
connection is reestablished. But why? IMO it's more natural to reestablish
new connection in that case. So I removed "!entry->conn" from the above
condition.
Yeah, that makes sense.
+ ereport(DEBUG3, + (errmsg("could not start remote transaction on connection %p", + entry->conn)),I replaced errmsg() with errmsg_internal() because the translation of
this debug message is not necessary.
I'm okay with this as we don't have any specific strings that need
translation in the debug message. But, should we also try to have
errmsg_internal in a few other places in connection.c?
errmsg("could not obtain message string for remote error"),
errmsg("cannot PREPARE a transaction that has
operated on postgres_fdw foreign tables")));
errmsg("password is required"),
I see the errmsg() with plain texts in other places in the code base
as well. Is it that we look at the error message and if it is a plain
text(without database objects or table data), we decide to have no
translation? Or is there any other policy?
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE +backend_type = 'client backend' AND application_name = 'fdw_retry_check'; +CALL wait_for_backend_termination();Since we always use pg_terminate_backend() and wait_for_backend_termination()
together, I merged them into one function.I simplied the comments on the regression test.
+1. I slightly adjusted comments in connection.c and ran pg_indent to
keep them upt 80 char limit.
Attached is the updated version of the patch. If this patch is ok,
I'd like to mark it as ready for committer.
Thanks. Attaching v10 patch. Please have a look.
I have another question not related to this patch: though we have
wait_pid() function, we are not able to use it like
pg_terminate_backend() in other modules, wouldn't be nice if we can
make it generic under the name pg_wait_pid() and usable across all pg
modules?I thought that, too. But I could not come up with good idea for *real* use case
of that function. At least that's useful for the regression test, though.
Anyway, IMO it's worth proposing that and hearing more opinions about that
from other hackers.
Yes it will be useful for testing when coupled with
pg_terminate_backend(). I will post the idea in a separate thread soon
for more thoughts.
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
Attachments:
v10-Retry-Cached-Remote-Connections-For-postgres_fdw.patchapplication/x-patch; name=v10-Retry-Cached-Remote-Connections-For-postgres_fdw.patchDownload
From 68d764690fc0409ee15afbbbd00974e0d43116cf Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddy@enterprisedb.com>
Date: Sat, 3 Oct 2020 05:49:18 +0530
Subject: [PATCH v10] Retry Cached Remote Connections For postgres_fdw
Remote connections are cached for postgres_fdw in the local
backend. There are high chances that the remote backend may
not be available i.e. it can get killed, the subsequent foreign
queries from local backend/session that use cached connection
will fail as the remote backend would have become unavailable.
This patch proposes a retry mechanism. Local backend/session
uses cached connection and if it receives connection bad error,
it deletes the cached entry and retry to get a new connection.
This retry happens only at the start of remote xact.
---
contrib/postgres_fdw/connection.c | 53 ++++++++++++++-----
.../postgres_fdw/expected/postgres_fdw.out | 48 +++++++++++++++++
contrib/postgres_fdw/sql/postgres_fdw.sql | 41 ++++++++++++++
3 files changed, 130 insertions(+), 12 deletions(-)
diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c
index 08daf26fdf..5fa4b634c3 100644
--- a/contrib/postgres_fdw/connection.c
+++ b/contrib/postgres_fdw/connection.c
@@ -108,6 +108,7 @@ PGconn *
GetConnection(UserMapping *user, bool will_prep_stmt)
{
bool found;
+ volatile bool retry_conn = false;
ConnCacheEntry *entry;
ConnCacheKey key;
@@ -159,23 +160,25 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
/* Reject further use of connections which failed abort cleanup. */
pgfdw_reject_incomplete_xact_state_change(entry);
+retry:
/*
* If the connection needs to be remade due to invalidation, disconnect as
- * soon as we're out of all transactions.
+ * soon as we're out of all transactions. Also, if previous attempt to
+ * start new remote transaction failed on the cached connection,
+ * disconnect it to retry a new connection.
*/
- if (entry->conn != NULL && entry->invalidated && entry->xact_depth == 0)
+ if ((entry->conn != NULL && entry->invalidated &&
+ entry->xact_depth == 0) || retry_conn)
{
- elog(DEBUG3, "closing connection %p for option changes to take effect",
- entry->conn);
+ if (retry_conn)
+ elog(DEBUG3, "closing connection %p to reestablish a new one",
+ entry->conn);
+ else
+ elog(DEBUG3, "closing connection %p for option changes to take effect",
+ entry->conn);
disconnect_pg_server(entry);
}
- /*
- * We don't check the health of cached connection here, because it would
- * require some overhead. Broken connection will be detected when the
- * connection is actually used.
- */
-
/*
* If cache entry doesn't have a connection, we have to establish a new
* connection. (If connect_pg_server throws an error, the cache entry
@@ -206,9 +209,35 @@ GetConnection(UserMapping *user, bool will_prep_stmt)
}
/*
- * Start a new transaction or subtransaction if needed.
+ * We check the health of the cached connection here when starting a new
+ * remote transaction. If a broken connection is detected in the first
+ * attempt, we try to reestablish a new connection. If broken connection
+ * is detected again here, we give up getting a connection.
*/
- begin_remote_xact(entry);
+ PG_TRY();
+ {
+ /* Start a new transaction or subtransaction if needed. */
+ begin_remote_xact(entry);
+ retry_conn = false;
+ }
+ PG_CATCH();
+ {
+ if (PQstatus(entry->conn) != CONNECTION_BAD ||
+ entry->xact_depth > 0 ||
+ retry_conn)
+ PG_RE_THROW();
+ retry_conn = true;
+ }
+ PG_END_TRY();
+
+ if (retry_conn)
+ {
+ ereport(DEBUG3,
+ (errmsg_internal("could not start remote transaction on connection %p",
+ entry->conn)),
+ errdetail_internal("%s", pchomp(PQerrorMessage(entry->conn))));
+ goto retry;
+ }
/* Remember if caller will prepare statements */
entry->have_prep_stmt |= will_prep_stmt;
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 10e23d02ed..2c5614073f 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8987,3 +8987,51 @@ PREPARE TRANSACTION 'fdw_tpc';
ERROR: cannot PREPARE a transaction that has operated on postgres_fdw foreign tables
ROLLBACK;
WARNING: there is no transaction in progress
+-- ===================================================================
+-- reestablish new connection
+-- ===================================================================
+-- Terminate the backend having the specified application_name and wait for
+-- the termination to complete.
+CREATE OR REPLACE PROCEDURE terminate_backend_and_wait(appname text) AS $$
+BEGIN
+ PERFORM pg_terminate_backend(pid) FROM pg_stat_activity
+ WHERE application_name = appname;
+ LOOP
+ PERFORM * FROM pg_stat_activity WHERE application_name = appname;
+ EXIT WHEN NOT FOUND;
+ PERFORM pg_sleep(1), pg_stat_clear_snapshot();
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+-- Change application_name of remote connection to special one
+-- so that we can easily terminate the connection later.
+ALTER SERVER loopback OPTIONS (application_name 'fdw_retry_check');
+SELECT 1 FROM ft1 LIMIT 1;
+ ?column?
+----------
+ 1
+(1 row)
+
+-- Terminate the remote connection.
+CALL terminate_backend_and_wait('fdw_retry_check');
+-- This query should detect the broken connection when starting new remote
+-- transaction, reestablish new connection, and then succeed.
+BEGIN;
+SELECT 1 FROM ft1 LIMIT 1;
+ ?column?
+----------
+ 1
+(1 row)
+
+-- If the query detects the broken connection when starting new remote
+-- subtransaction, it doesn't reestablish new connection and should fail.
+CALL terminate_backend_and_wait('fdw_retry_check');
+SAVEPOINT s;
+SELECT 1 FROM ft1 LIMIT 1; -- should fail
+ERROR: server closed the connection unexpectedly
+ This probably means the server terminated abnormally
+ before or while processing the request.
+CONTEXT: remote SQL command: SAVEPOINT s2
+COMMIT;
+-- Clean up
+DROP PROCEDURE terminate_backend_and_wait(text);
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 78156d10b4..4da1f78956 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2653,3 +2653,44 @@ SELECT count(*) FROM ft1;
-- error here
PREPARE TRANSACTION 'fdw_tpc';
ROLLBACK;
+
+-- ===================================================================
+-- reestablish new connection
+-- ===================================================================
+
+-- Terminate the backend having the specified application_name and wait for
+-- the termination to complete.
+CREATE OR REPLACE PROCEDURE terminate_backend_and_wait(appname text) AS $$
+BEGIN
+ PERFORM pg_terminate_backend(pid) FROM pg_stat_activity
+ WHERE application_name = appname;
+ LOOP
+ PERFORM * FROM pg_stat_activity WHERE application_name = appname;
+ EXIT WHEN NOT FOUND;
+ PERFORM pg_sleep(1), pg_stat_clear_snapshot();
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Change application_name of remote connection to special one
+-- so that we can easily terminate the connection later.
+ALTER SERVER loopback OPTIONS (application_name 'fdw_retry_check');
+SELECT 1 FROM ft1 LIMIT 1;
+
+-- Terminate the remote connection.
+CALL terminate_backend_and_wait('fdw_retry_check');
+
+-- This query should detect the broken connection when starting new remote
+-- transaction, reestablish new connection, and then succeed.
+BEGIN;
+SELECT 1 FROM ft1 LIMIT 1;
+
+-- If the query detects the broken connection when starting new remote
+-- subtransaction, it doesn't reestablish new connection and should fail.
+CALL terminate_backend_and_wait('fdw_retry_check');
+SAVEPOINT s;
+SELECT 1 FROM ft1 LIMIT 1; -- should fail
+COMMIT;
+
+-- Clean up
+DROP PROCEDURE terminate_backend_and_wait(text);
--
2.25.1
On 2020/10/03 20:40, Bharath Rupireddy wrote:
On Fri, Oct 2, 2020 at 11:30 PM Fujii Masao <masao.fujii@oss.nttdata.com> wrote:
Attaching v8 patch, please review it.. Both make check and make
check-world passes on v8.Thanks for updating the patch! It basically looks good to me.
I tweaked the patch as follows.+ if (!entry->conn || + PQstatus(entry->conn) != CONNECTION_BAD ||With the above change, if entry->conn is NULL, an error is thrown and no new
connection is reestablished. But why? IMO it's more natural to reestablish
new connection in that case. So I removed "!entry->conn" from the above
condition.Yeah, that makes sense.
+ ereport(DEBUG3, + (errmsg("could not start remote transaction on connection %p", + entry->conn)),I replaced errmsg() with errmsg_internal() because the translation of
this debug message is not necessary.I'm okay with this as we don't have any specific strings that need
translation in the debug message. But, should we also try to have
errmsg_internal in a few other places in connection.c?errmsg("could not obtain message string for remote error"),
errmsg("cannot PREPARE a transaction that has
operated on postgres_fdw foreign tables")));
errmsg("password is required"),I see the errmsg() with plain texts in other places in the code base
as well. Is it that we look at the error message and if it is a plain
text(without database objects or table data), we decide to have no
translation? Or is there any other policy?
I was thinking that elog() basically should be used to report this
debug message, instead, but you used ereport() because maybe
you'd like to add detail message about connection error. Is this
understanding right? elog() uses errmsg_internal(). So if ereport()
is used as an aternative of elog() for some reasons,
IMO errmsg_internal() should be used. Thought?
OTOH, the messages you mentioned are not debug ones,
so basically ereport() and errmsg() should be used, I think.
+SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE +backend_type = 'client backend' AND application_name = 'fdw_retry_check'; +CALL wait_for_backend_termination();Since we always use pg_terminate_backend() and wait_for_backend_termination()
together, I merged them into one function.I simplied the comments on the regression test.
+1. I slightly adjusted comments in connection.c and ran pg_indent to
keep them upt 80 char limit.Attached is the updated version of the patch. If this patch is ok,
I'd like to mark it as ready for committer.Thanks. Attaching v10 patch. Please have a look.
Thanks for updating the patch! I will mark the patch as ready for committer in CF.
Barring any objections, I will commit that.
I have another question not related to this patch: though we have
wait_pid() function, we are not able to use it like
pg_terminate_backend() in other modules, wouldn't be nice if we can
make it generic under the name pg_wait_pid() and usable across all pg
modules?I thought that, too. But I could not come up with good idea for *real* use case
of that function. At least that's useful for the regression test, though.
Anyway, IMO it's worth proposing that and hearing more opinions about that
from other hackers.Yes it will be useful for testing when coupled with
pg_terminate_backend(). I will post the idea in a separate thread soon
for more thoughts.
Sounds good!
ISTM that he function should at least check the target process is PostgreSQL one.
Regards,
--
Fujii Masao
Advanced Computing Technology Center
Research and Development Headquarters
NTT DATA CORPORATION
On Mon, Oct 5, 2020 at 9:45 AM Fujii Masao <masao.fujii@oss.nttdata.com> wrote:
I see the errmsg() with plain texts in other places in the code base
as well. Is it that we look at the error message and if it is a plain
text(without database objects or table data), we decide to have no
translation? Or is there any other policy?I was thinking that elog() basically should be used to report this
debug message, instead, but you used ereport() because maybe
you'd like to add detail message about connection error. Is this
understanding right? elog() uses errmsg_internal().
Yes that's correct.
So if ereport() is used as an aternative of elog() for some reasons,
IMO errmsg_internal() should be used. Thought?
Yes, this is apt for our case.
OTOH, the messages you mentioned are not debug ones,
so basically ereport() and errmsg() should be used, I think.
In connection.c file, yes they are of ERROR type. Looks like it's not
a standard to use errmsg_internal for DEBUG messages that require no
translation with ereport
(errmsg("wrote block details for %d blocks", num_blocks)));
(errmsg("MultiXact member stop limit is now %u based on MultiXact %u
(errmsg("oldest MultiXactId member is at offset %u",
However, there are few other places, where errmsg_internal is used for
DEBUG purposes.
(errmsg_internal("finished verifying presence of "
(errmsg_internal("%s(%d) name: %s; blockState:
Having said that, IMHO it's better to keep the way it is currently in
the code base.
Thanks. Attaching v10 patch. Please have a look.
Thanks for updating the patch! I will mark the patch as ready for committer in CF.
Barring any objections, I will commit that.
Thanks a lot for the review comments.
I have another question not related to this patch: though we have
wait_pid() function, we are not able to use it like
pg_terminate_backend() in other modules, wouldn't be nice if we can
make it generic under the name pg_wait_pid() and usable across all pg
modules?I thought that, too. But I could not come up with good idea for *real* use case
of that function. At least that's useful for the regression test, though.
Anyway, IMO it's worth proposing that and hearing more opinions about that
from other hackers.Yes it will be useful for testing when coupled with
pg_terminate_backend(). I will post the idea in a separate thread soon
for more thoughts.Sounds good!
ISTM that he function should at least check the target process is PostgreSQL one.
Thanks. I will take care of this point.
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
On 2020/10/05 20:32, Bharath Rupireddy wrote:
On Mon, Oct 5, 2020 at 9:45 AM Fujii Masao <masao.fujii@oss.nttdata.com> wrote:
I see the errmsg() with plain texts in other places in the code base
as well. Is it that we look at the error message and if it is a plain
text(without database objects or table data), we decide to have no
translation? Or is there any other policy?I was thinking that elog() basically should be used to report this
debug message, instead, but you used ereport() because maybe
you'd like to add detail message about connection error. Is this
understanding right? elog() uses errmsg_internal().Yes that's correct.
So if ereport() is used as an aternative of elog() for some reasons,
IMO errmsg_internal() should be used. Thought?Yes, this is apt for our case.
OTOH, the messages you mentioned are not debug ones,
so basically ereport() and errmsg() should be used, I think.In connection.c file, yes they are of ERROR type. Looks like it's not
a standard to use errmsg_internal for DEBUG messages that require no
translation with ereport(errmsg("wrote block details for %d blocks", num_blocks)));
(errmsg("MultiXact member stop limit is now %u based on MultiXact %u
(errmsg("oldest MultiXactId member is at offset %u",However, there are few other places, where errmsg_internal is used for
DEBUG purposes.(errmsg_internal("finished verifying presence of "
(errmsg_internal("%s(%d) name: %s; blockState:Having said that, IMHO it's better to keep the way it is currently in
the code base.
Agreed.
Thanks. Attaching v10 patch. Please have a look.
Thanks for updating the patch! I will mark the patch as ready for committer in CF.
Barring any objections, I will commit that.Thanks a lot for the review comments.
I pushed the patch. Thanks!
Regards,
--
Fujii Masao
Advanced Computing Technology Center
Research and Development Headquarters
NTT DATA CORPORATION