mirror of
https://github.com/neondatabase/neon.git
synced 2026-01-07 21:42:56 +00:00
[sql-over-http] Reset session state between pooled connection re-use (#12681)
Session variables can be set during one sql-over-http query and observed on another when that pooled connection is re-used. To address this we can use `RESET ALL;` before re-using the connection. LKB-2495 To be on the safe side, we can opt for a full `DISCARD ALL;`, but that might have performance regressions since it also clears any query plans. See pgbouncer docs https://www.pgbouncer.org/config.html#server_reset_query. `DISCARD ALL` is currently defined as: ``` CLOSE ALL; SET SESSION AUTHORIZATION DEFAULT; RESET ALL; DEALLOCATE ALL; UNLISTEN *; SELECT pg_advisory_unlock_all(); DISCARD PLANS; DISCARD TEMP; DISCARD SEQUENCES; ``` I've opted to keep everything here except the `DISCARD PLANS`. I've modified the code so that this query is executed in the background when a connection is returned to the pool, rather than when taken from the pool. This should marginally improve performance for Neon RLS by removing 1 (localhost) round trip. I don't believe that keeping query plans could be a security concern. It's a potential side channel, but I can't imagine what you could extract from it. --- Thanks to https://github.com/neondatabase/neon/pull/12659#discussion_r2219016205 for probing the idea in my head.
This commit is contained in:
@@ -3910,6 +3910,41 @@ class NeonProxy(PgProtocol):
|
||||
assert response.status_code == expected_code, f"response: {response.json()}"
|
||||
return response.json()
|
||||
|
||||
def http_multiquery(self, *queries, **kwargs):
|
||||
# TODO maybe use default values if not provided
|
||||
user = quote(kwargs["user"])
|
||||
password = quote(kwargs["password"])
|
||||
expected_code = kwargs.get("expected_code")
|
||||
timeout = kwargs.get("timeout")
|
||||
|
||||
json_queries = []
|
||||
for query in queries:
|
||||
if type(query) is str:
|
||||
json_queries.append({"query": query})
|
||||
else:
|
||||
[query, params] = query
|
||||
json_queries.append({"query": query, "params": params})
|
||||
|
||||
queries_str = [j["query"] for j in json_queries]
|
||||
log.info(f"Executing http queries: {queries_str}")
|
||||
|
||||
connstr = f"postgresql://{user}:{password}@{self.domain}:{self.proxy_port}/postgres"
|
||||
response = requests.post(
|
||||
f"https://{self.domain}:{self.external_http_port}/sql",
|
||||
data=json.dumps({"queries": json_queries}),
|
||||
headers={
|
||||
"Content-Type": "application/sql",
|
||||
"Neon-Connection-String": connstr,
|
||||
"Neon-Pool-Opt-In": "true",
|
||||
},
|
||||
verify=str(self.test_output_dir / "proxy.crt"),
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
if expected_code is not None:
|
||||
assert response.status_code == expected_code, f"response: {response.json()}"
|
||||
return response.json()
|
||||
|
||||
async def http2_query(self, query, args, **kwargs):
|
||||
# TODO maybe use default values if not provided
|
||||
user = kwargs["user"]
|
||||
|
||||
@@ -17,9 +17,6 @@ if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
|
||||
GET_CONNECTION_PID_QUERY = "SELECT pid FROM pg_stat_activity WHERE state = 'active'"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_pool_begin_1(static_proxy: NeonProxy):
|
||||
static_proxy.safe_psql("create user http_auth with password 'http' superuser")
|
||||
@@ -479,7 +476,7 @@ def test_sql_over_http_pool(static_proxy: NeonProxy):
|
||||
|
||||
def get_pid(status: int, pw: str, user="http_auth") -> Any:
|
||||
return static_proxy.http_query(
|
||||
GET_CONNECTION_PID_QUERY,
|
||||
"SELECT pg_backend_pid() as pid",
|
||||
[],
|
||||
user=user,
|
||||
password=pw,
|
||||
@@ -513,6 +510,35 @@ def test_sql_over_http_pool(static_proxy: NeonProxy):
|
||||
assert "password authentication failed for user" in res["message"]
|
||||
|
||||
|
||||
def test_sql_over_http_pool_settings(static_proxy: NeonProxy):
|
||||
static_proxy.safe_psql("create user http_auth with password 'http' superuser")
|
||||
|
||||
def multiquery(*queries) -> Any:
|
||||
results = static_proxy.http_multiquery(
|
||||
*queries,
|
||||
user="http_auth",
|
||||
password="http",
|
||||
expected_code=200,
|
||||
)
|
||||
|
||||
return [result["rows"] for result in results["results"]]
|
||||
|
||||
[[intervalstyle]] = static_proxy.safe_psql("SHOW IntervalStyle")
|
||||
assert intervalstyle == "postgres", "'postgres' is the default IntervalStyle in postgres"
|
||||
|
||||
result = multiquery("select '0 seconds'::interval as interval")
|
||||
assert result[0][0]["interval"] == "00:00:00", "interval is expected in postgres format"
|
||||
|
||||
result = multiquery(
|
||||
"SET IntervalStyle = 'iso_8601'",
|
||||
"select '0 seconds'::interval as interval",
|
||||
)
|
||||
assert result[1][0]["interval"] == "PT0S", "interval is expected in ISO-8601 format"
|
||||
|
||||
result = multiquery("select '0 seconds'::interval as interval")
|
||||
assert result[0][0]["interval"] == "00:00:00", "interval is expected in postgres format"
|
||||
|
||||
|
||||
def test_sql_over_http_urlencoding(static_proxy: NeonProxy):
|
||||
static_proxy.safe_psql("create user \"http+auth$$\" with password '%+$^&*@!' superuser")
|
||||
|
||||
@@ -544,23 +570,37 @@ def test_http_pool_begin(static_proxy: NeonProxy):
|
||||
query(200, "SELECT 1;") # Query that should succeed regardless of the transaction
|
||||
|
||||
|
||||
def test_sql_over_http_pool_idle(static_proxy: NeonProxy):
|
||||
def test_sql_over_http_pool_tx_reuse(static_proxy: NeonProxy):
|
||||
static_proxy.safe_psql("create user http_auth2 with password 'http' superuser")
|
||||
|
||||
def query(status: int, query: str) -> Any:
|
||||
def query(status: int, query: str, *args) -> Any:
|
||||
return static_proxy.http_query(
|
||||
query,
|
||||
[],
|
||||
args,
|
||||
user="http_auth2",
|
||||
password="http",
|
||||
expected_code=status,
|
||||
)
|
||||
|
||||
pid1 = query(200, GET_CONNECTION_PID_QUERY)["rows"][0]["pid"]
|
||||
def query_pid_txid() -> Any:
|
||||
result = query(
|
||||
200,
|
||||
"SELECT pg_backend_pid() as pid, pg_current_xact_id() as txid",
|
||||
)
|
||||
|
||||
return result["rows"][0]
|
||||
|
||||
res0 = query_pid_txid()
|
||||
|
||||
time.sleep(0.02)
|
||||
query(200, "BEGIN")
|
||||
pid2 = query(200, GET_CONNECTION_PID_QUERY)["rows"][0]["pid"]
|
||||
assert pid1 != pid2
|
||||
|
||||
res1 = query_pid_txid()
|
||||
res2 = query_pid_txid()
|
||||
|
||||
assert res0["pid"] == res1["pid"], "connection should be reused"
|
||||
assert res0["pid"] == res2["pid"], "connection should be reused"
|
||||
assert res1["txid"] != res2["txid"], "txid should be different"
|
||||
|
||||
|
||||
@pytest.mark.timeout(60)
|
||||
|
||||
Reference in New Issue
Block a user