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

Abierto
#6,733 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 6 días

@mt89vein ya está trabajando en esto.

Desde el 3/11/2026.

  • #6740 de @mt89vein — abierto

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
22/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
csharp, postgresql
Área
databases

Línea de trabajo

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.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

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.

Lenguaje dominante
C#
Estrellas
3.7k
Forks
890
Merge medio
4 d 11 h
PR fusionados (30 d)
19

Preparar el entorno

Abrir en Codespaces

Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.

  • Sin Dockerfile ni archivo de Docker Compose
  • Sin plantilla de pull request
  • Sin guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de npgsql/npgsql

Todos los issues de npgsql/npgsql

Issues similares

Más issues de C#

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.