Multi-host: a host that accepts TCP but never responds is never marked offline, so every new connection waits for the full Timeout
維護者通常 6 天內回覆
評估
研究方向
The bug is in the connector state handling: start with the read timeout in src/Npgsql/Internal/NpgsqlReadBuffer.cs, then Break and Open in src/Npgsql/Internal/NpgsqlConnector.cs, and IsPreferred in src/Npgsql/NpgsqlMultiHostDataSource.cs. Run the repro program against a silent TCP listener first host to confirm every open waits the full Timeout. Done means a host that accepts TCP but never responds is marked offline after one failed open, so later opens skip it and return quickly.
由索引模型根據 Issue 內容生成。
描述
Steps to reproduce
- Start a PostgreSQL instance:
docker run -d -e POSTGRES_PASSWORD=postgres -p 55432:5432 postgres:17 - Run the program below. It opens a local TCP listener that accepts connections but never sends anything (simulating a frozen PostgreSQL host), and puts it first in a multi-host connection string. For comparison, it runs the same loop with a port that refuses connections.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Npgsql" Version="10.0.3" />
</ItemGroup>
</Project>
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using Npgsql;
// A real PostgreSQL, e.g.: docker run -d -e POSTGRES_PASSWORD=postgres -p 55432:5432 postgres:17
const string RealHost = "127.0.0.1:55432";
// An unresponsive server: accepts TCP connections but never sends anything back
// (simulates a hung/frozen PostgreSQL host).
var silent = new TcpListener(IPAddress.Loopback, 0);
silent.Start();
var silentPort = ((IPEndPoint)silent.LocalEndpoint).Port;
var accepted = new List<TcpClient>();
_ = Task.Run(async () => { while (true) accepted.Add(await silent.AcceptTcpClientAsync()); });
// A port nobody listens on: connection is refused immediately.
var closed = new TcpListener(IPAddress.Loopback, 0);
closed.Start();
var closedPort = ((IPEndPoint)closed.LocalEndpoint).Port;
closed.Stop();
Console.WriteLine($"Npgsql {typeof(NpgsqlConnection).Assembly.GetName().Version}");
await Run("first host refuses connections", $"127.0.0.1:{closedPort}");
await Run("first host accepts TCP but never responds", $"127.0.0.1:{silentPort}");
static async Task Run(string title, string badHost)
{
Console.WriteLine($"\n== {title}");
var cs = $"Host={badHost},{RealHost};Username=postgres;Password=postgres;Timeout=3";
await using var dataSource = NpgsqlDataSource.Create(cs);
for (var i = 1; i <= 5; i++)
{
var sw = Stopwatch.StartNew();
await using var conn = await dataSource.OpenConnectionAsync();
Console.WriteLine($"open #{i}: {sw.ElapsedMilliseconds,5} ms -> {conn.Host}:{conn.Port}");
}
}
The issue
Output:
Npgsql 10.0.3.0
== first host refuses connections
open #1: 2096 ms -> 127.0.0.1:55432
open #2: 0 ms -> 127.0.0.1:55432
open #3: 0 ms -> 127.0.0.1:55432
open #4: 0 ms -> 127.0.0.1:55432
open #5: 0 ms -> 127.0.0.1:55432
== first host accepts TCP but never responds
open #1: 3017 ms -> 127.0.0.1:55432
open #2: 3014 ms -> 127.0.0.1:55432
open #3: 3000 ms -> 127.0.0.1:55432
open #4: 3007 ms -> 127.0.0.1:55432
open #5: 3033 ms -> 127.0.0.1:55432
When the first host refuses the connection, it is marked offline and later opens go straight to the healthy host (using the idle pooled connection).
When the first host accepts the TCP connection but does not respond, it is never marked offline. Its state stays Unknown, which is treated as preferred, it has no idle connectors, so every open attempts a new physical connection to it and waits for the full Timeout before falling back to the healthy host, even though an idle connection to the healthy host is in the pool. Host Recheck Seconds has no effect, since there is no cached state to expire.
On 10.0.2 I observed the same with TargetSessionAttributes ReadOnly, PreferStandby and Any (via NpgsqlMultiHostDataSource.WithTargetSession) against a 3-node Patroni cluster with one node frozen (docker pause). With the default Timeout=15, every query waits 15 s for as long as the host stays unresponsive. We hit this in production during a Patroni failover: the old primary stopped responding and all read-only queries were delayed by 15 s until the node was fixed; restarting the application did not help.
What I think is happening
- The startup/auth read times out in
NpgsqlReadBufferand callsconnector.Break(...)withoutmarkHostAsOfflineOnConnecting(NpgsqlReadBuffer.cs#L360). The connector is stillConnecting, so the host is not marked offline (NpgsqlConnector.cs#L2501), but the connector becomesBroken. Openthen catches the exception and callsBreak(e, markHostAsOfflineOnConnecting: true)(NpgsqlConnector.cs#L585), which would mark the host offline, but it returns early because the connector is alreadyBroken(NpgsqlConnector.cs#L2464).- The host stays
Unknown, whichIsPreferredaccepts (NpgsqlMultiHostDataSource.cs#L129), so it is tried again on the next open.
The same structure is still present on main (b35800b).
Possibly related: #5789, #6483 (those are about the delay after a host was marked offline and Host Recheck Seconds expires; here the host is never marked offline at all).
Further technical details
Npgsql version: 10.0.3 (also reproduced with 10.0.2)
PostgreSQL version: 17.11
Operating system: Windows 11 (client), PostgreSQL in Docker (Linux)
Other details about my project setup: .NET 10, multi-host connection string, no load balancing.
- 主要語言
- C#
- 星號
- 3.7k
- 分支
- 890
- 平均合併
- 4 天 11 小時
- 30 天內合併 PR
- 19
環境準備
在瀏覽器裡用你自己的 GitHub 帳號啟動這個專案的開發容器。
- 沒有 Dockerfile 或 Docker Compose 檔案
- 沒有 Pull Request 範本
- 沒有貢獻指南
從這裡開始
- 先讀完整個 Issue,再讀專案的貢獻指南。
- 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
npgsql/npgsql 的其他 Issue
-
Writing a decimal as numeric allocates a short[9] per value可能已有人在做 @tamasprincz97-dot 於 13 天前認領。 未關閉
難度 2/5 1-3 小時 新手友好度 89/100
維護者通常 6 天內回覆
-
Reading a large interval as TimeSpan returns a wrong value instead of throwing可能已有人在做 @Mr444-dev 於 7 天前認領。 未關閉
難度 2/5 1-3 小時 新手友好度 66/100
維護者通常 6 天內回覆
-
難度 2/5 1-3 小時 新手友好度 68/100
維護者通常 6 天內回覆
-
難度 2/5 1-3 小時 新手友好度 70/100
維護者通常 6 天內回覆
-
ReadReplicationSlot returns the slot name as passed, not the one the server read可能已有人在做 @Brar 於 8 天前認領。 未關閉replication
npgsql/npgsql#6735 · 已指派 1 人 ·
維護者通常 6 天內回覆
相似的 Issue
-
Issue-Bug Needs-Triage
難度 2/5 1-3 小時 新手友好度 78/100
維護者通常 2 天內回覆
-
難度 2/5 1-3 小時 新手友好度 66/100
JiaFeiMiao-K-Cat/DiscImageStudio#281 ·
維護者通常 1 天內回覆
-
area/access-management Frontend onboarding candidate :handshake:
難度 2/5 1-3 小時 新手友好度 65/100
Altinn/altinn-auth#4941 ·
維護者通常 1 天內回覆
-
難度 2/5 1-3 小時 新手友好度 78/100
aws/aws-sdk-net#4586 ·
維護者通常 1 天內回覆
-
難度 2/5 1-3 小時 新手友好度 82/100
維護者通常 1 天內回覆