Multi-host: a host that accepts TCP but never responds is never marked offline, so every new connection waits for the full Timeout

未關閉
#6,733 0 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視

維護者通常 6 天內回覆

@mt89vein 已經在處理了。

開始於 2026年11月3日。

  • #6740 來自 @mt89vein —— 未關閉

評估

難度
4/5
預估耗時
3-5 天
新手友好度
22/100
Issue 類型
缺陷
描述清晰度
描述清楚
活躍度
活躍
技術堆疊
csharp, postgresql
領域
databases

研究方向

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
  1. Start a PostgreSQL instance:
    docker run -d -e POSTGRES_PASSWORD=postgres -p 55432:5432 postgres:17
    
  2. 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
  1. The startup/auth read times out in NpgsqlReadBuffer and calls connector.Break(...) without markHostAsOfflineOnConnecting (NpgsqlReadBuffer.cs#L360). The connector is still Connecting, so the host is not marked offline (NpgsqlConnector.cs#L2501), but the connector becomes Broken.
  2. Open then catches the exception and calls Break(e, markHostAsOfflineOnConnecting: true) (NpgsqlConnector.cs#L585), which would mark the host offline, but it returns early because the connector is already Broken (NpgsqlConnector.cs#L2464).
  3. The host stays Unknown, which IsPreferred accepts (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

環境準備

在 Codespaces 中開啟

在瀏覽器裡用你自己的 GitHub 帳號啟動這個專案的開發容器。

  • 沒有 Dockerfile 或 Docker Compose 檔案
  • 沒有 Pull Request 範本
  • 沒有貢獻指南

從這裡開始

  1. 先讀完整個 Issue,再讀專案的貢獻指南。
  2. 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
  3. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

npgsql/npgsql 的其他 Issue

查看 npgsql/npgsql 的全部 Issue

相似的 Issue

更多 C# Issue

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。