Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1vxWaW-00GtFh-0Q for pgsql-bugs@arkaria.postgresql.org; Tue, 03 Mar 2026 20:43:28 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1vxWaU-008i78-1H for pgsql-bugs@arkaria.postgresql.org; Tue, 03 Mar 2026 20:43:26 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1vxVKl-008Rdk-0Q for pgsql-bugs@lists.postgresql.org; Tue, 03 Mar 2026 19:23:07 +0000 Received: from mahout.postgresql.org ([2001:4800:3e1:1::227]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1vxVKi-00000000InI-3lFy for pgsql-bugs@lists.postgresql.org; Tue, 03 Mar 2026 19:23:07 +0000 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=postgresql.org; s=20171124; h=Message-ID:Date:Reply-To:Cc:From:To:Subject: Content-Transfer-Encoding:MIME-Version:Content-Type:Sender:Content-ID: Content-Description:In-Reply-To:References; bh=q2bXSt1g57qqtWhNHxOwPqQHWTjon+Z3WGVuB4hQOqI=; b=I096JPt8Nb8Vun8A+aLG0VJMmv 7ULgfRkcr0EQI1V7f35i2d7iB+xIOp/fqU2z+Wd6OPtZOVZ35xrChOg6OBucCt/sBeTKkqbPPrEm6 o+92581gCSerRf3ja/ZPnqcafQsZSn12r4q934bu9xRgry8Me1URXTI29bklY5eVzRhm59WH0Ey+O JdER0vSBHFda1dno9gt8vPJjH/ko6Cde5jvkiGJzygXX4gRLN+h09WLvwIcbSx2HxQvdcEOtnnM2m vy1g/foLMXbVRTaJd8jW0vnoSWjhidGObokhc87UbnNOq/72l/xml8RqlujQYZbk/DdiC7xDyONYr RsE/ON0A==; Received: from wrigleys.postgresql.org ([2a02:16a8:dc51::60]) by mahout.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1vxVKh-0008UZ-1w for pgsql-bugs@lists.postgresql.org; Tue, 03 Mar 2026 19:23:04 +0000 Received: from localhost ([127.0.0.1] helo=wrigleys.postgresql.org) by wrigleys.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1vxVKf-001A9C-2F for pgsql-bugs@lists.postgresql.org; Tue, 03 Mar 2026 19:23:02 +0000 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Subject: BUG #19424: Concurrent PQconnectdb() calls hang on Windows To: pgsql-bugs@lists.postgresql.org From: PG Bug reporting form Cc: david.ritter@gmail.com Reply-To: david.ritter@gmail.com, pgsql-bugs@lists.postgresql.org Date: Tue, 03 Mar 2026 19:23:01 +0000 Message-ID: <19424-0ab4342f914b6296@postgresql.org> X-Auto-Response-Suppress: All Auto-Submitted: auto-generated List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk The following bug has been logged on the website: Bug reference: 19424 Logged by: David Ritter Email address: david.ritter@gmail.com PostgreSQL version: 18.3 Operating system: Windows 11 Description: =20 Versions affected: libpq 17.4, 18.3 (likely all 17.x+) Platform: Windows (MSVC 19.x, x86_64). Not reproducible on Linux (RHEL 9, tested 1000 runs with 100 threads each). Description: When multiple threads each call PQconnectdb() concurrently with independent connection strings, most or all threads hang indefinitely. PQisthreadsafe() returns 1. A single serial warmup connection succeeds. The attached reproducer spawns N threads and reports how many complete within 30 seconds. =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D /* * pq_thread_test.c =E2=80=94 Minimal reproducer: concurrent PQconnectdb() = hangs on Windows. * * Spawns N threads, each calling PQconnectdb() on an independent PGconn. * On Windows (tested with MSVC 19.x), most or all threads hang indefinitely * inside PQconnectdb(). The same program works correctly on Linux. * * Tested with: * - libpq 17.4 (PQlibVersion() =3D 170004) =E2=80=94 hangs on Windows * - libpq 18.3 (PQlibVersion() =3D 180300) =E2=80=94 hangs on Windows * - libpq 17.4 on RHEL 9 / x86_64 =E2=80=94 works (1000 consecutiv= e runs, 0 failures) * * PQisthreadsafe() returns 1 in all cases. * * Build (MSVC / Windows): * cl /nologo /MT pq_thread_test.c ^ * /I "\include" ^ * /link /LIBPATH:"\lib" libpq.lib ws2_32.lib * * Build (GCC / Linux): * gcc -o pq_thread_test pq_thread_test.c \ * -I/include -L/lib -lpq -lpthread -lm * * Run: * # Windows =E2=80=94 ensure libpq.dll is on PATH: * set PATH=3D\bin;%PATH% * pq_thread_test.exe [num_threads] * * # Linux: * export LD_LIBRARY_PATH=3D/lib:$LD_LIBRARY_PATH * ./pq_thread_test [num_threads] * * Connection parameters (override via environment variables): * PG_HOST (default: 127.0.0.1) * PG_USER (default: postgres) * PG_PASS (default: postgres) * PG_DB (default: postgres) * * Example: * PG_HOST=3D127.0.0.1 PG_USER=3Dpostgres PG_PASS=3Dsecret PG_DB=3Dmydb ./pq_thread_test 100 */ #include #include #include #include #ifdef _WIN32 # include #else # include # include # include #endif #define DEFAULT_NUM_THREADS 100 #define DEFAULT_HOST "127.0.0.1" #define DEFAULT_USER "postgres" #define DEFAULT_PASS "postgres" #define DEFAULT_DB "postgres" #define TIMEOUT_SECONDS 30 #define CONNINFO_MAXLEN 512 /* Global conninfo string, built in main() from env vars or defaults */ static char g_conninfo[CONNINFO_MAXLEN]; static double timer_now(void) { #ifdef _WIN32 LARGE_INTEGER cnt, freq; QueryPerformanceFrequency(&freq); QueryPerformanceCounter(&cnt); return (double)cnt.QuadPart / (double)freq.QuadPart; #else struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec + ts.tv_nsec / 1e9; #endif } typedef struct { int thread_id; volatile int done; /* 1 =3D finished, 0 =3D still running */ ConnStatusType status; char errmsg[256]; } ThreadResult; static #ifdef _WIN32 DWORD WINAPI #else void * #endif connect_thread(void *arg) { ThreadResult *res =3D (ThreadResult *)arg; PGconn *conn; double t_start =3D timer_now(); conn =3D PQconnectdb(g_conninfo); double elapsed_ms =3D (timer_now() - t_start) * 1000.0; if (conn =3D=3D NULL) { res->status =3D CONNECTION_BAD; strncpy(res->errmsg, "PQconnectdb returned NULL", sizeof(res->errmsg) - 1); } else { res->status =3D PQstatus(conn); if (res->status !=3D CONNECTION_OK) { const char *msg =3D PQerrorMessage(conn); strncpy(res->errmsg, msg ? msg : "(no message)", sizeof(res->errmsg) - 1); fprintf(stderr, "[Thread %d] FAILED (%.1f ms): %s\n", res->thread_id, elapsed_ms, res->errmsg); } else { fprintf(stderr, "[Thread %d] OK (%.1f ms)\n", res->thread_id, elapsed_ms); } PQfinish(conn); } res->done =3D 1; #ifdef _WIN32 return 0; #else return NULL; #endif } int main(int argc, char *argv[]) { int i; int num_threads =3D DEFAULT_NUM_THREADS; if (argc > 1) { num_threads =3D atoi(argv[1]); if (num_threads < 1 || num_threads > 1000) { fprintf(stderr, "Usage: %s [num_threads (1-1000)]\n", argv[0]); return 1; } } /* Build conninfo from env vars, falling back to defaults */ { const char *host =3D getenv("PG_HOST"); const char *user =3D getenv("PG_USER"); const char *pass =3D getenv("PG_PASS"); const char *db =3D getenv("PG_DB"); snprintf(g_conninfo, CONNINFO_MAXLEN, "host=3D%s user=3D%s password=3D%s dbname=3D%s connect_tim= eout=3D10", host ? host : DEFAULT_HOST, user ? user : DEFAULT_USER, pass ? pass : DEFAULT_PASS, db ? db : DEFAULT_DB); } fprintf(stderr, "=3D=3D=3D libpq concurrent PQconnectdb() test =3D=3D= =3D\n"); fprintf(stderr, " Threads: %d\n", num_threads); fprintf(stderr, " Timeout: %d seconds\n", TIMEOUT_SECONDS); fprintf(stderr, " ConnInfo: %s\n", g_conninfo); fprintf(stderr, " PQisthreadsafe(): %d\n", PQisthreadsafe()); fprintf(stderr, " PQlibVersion(): %d\n", PQlibVersion()); #ifdef _WIN32 fprintf(stderr, " Platform: Windows\n"); #else fprintf(stderr, " Platform: POSIX/Linux\n"); #endif fprintf(stderr, "\n"); /* Allocate arrays */ #ifdef _WIN32 HANDLE *threads =3D (HANDLE *)calloc(num_threads, sizeof(HANDLE)); #else pthread_t *threads =3D (pthread_t *)calloc(num_threads, sizeof(pthread_t)); #endif ThreadResult *results =3D (ThreadResult *)calloc(num_threads, sizeof(ThreadResult)); if (!threads || !results) { fprintf(stderr, "Allocation failed\n"); return 1; } /* Verify connectivity with a single serial connection first */ fprintf(stderr, "--- Warmup: single serial connection ---\n"); { PGconn *warmup =3D PQconnectdb(g_conninfo); if (warmup && PQstatus(warmup) =3D=3D CONNECTION_OK) { fprintf(stderr, "Warmup OK (server version %d)\n\n", PQserverVersion(warmup)); } else { fprintf(stderr, "Warmup FAILED: %s\n", warmup ? PQerrorMessage(warmup) : "NULL"); if (warmup) PQfinish(warmup); free(threads); free(results); return 1; } PQfinish(warmup); } /* Launch all threads simultaneously */ fprintf(stderr, "--- Launching %d concurrent threads ---\n", num_threads); double t0 =3D timer_now(); for (i =3D 0; i < num_threads; i++) { results[i].thread_id =3D i; results[i].done =3D 0; results[i].status =3D CONNECTION_BAD; results[i].errmsg[0] =3D '\0'; #ifdef _WIN32 threads[i] =3D CreateThread(NULL, 0, connect_thread, &results[i], 0, NULL); if (threads[i] =3D=3D NULL) { fprintf(stderr, "CreateThread(%d) failed: %lu\n", i, GetLastError()); return 1; } #else int rc =3D pthread_create(&threads[i], NULL, connect_thread, &results[i]); if (rc !=3D 0) { fprintf(stderr, "pthread_create(%d) failed: %d\n", i, rc); return 1; } #endif } /* Wait with a timeout */ #ifdef _WIN32 WaitForMultipleObjects(num_threads, threads, TRUE, TIMEOUT_SECONDS * 1000); #else { struct timespec deadline; clock_gettime(CLOCK_REALTIME, &deadline); deadline.tv_sec +=3D TIMEOUT_SECONDS; for (i =3D 0; i < num_threads; i++) { #if defined(__linux__) || defined(__GLIBC__) int rc =3D pthread_timedjoin_np(threads[i], NULL, &deadline); if (rc =3D=3D ETIMEDOUT) { /* Thread hung =E2=80=94 leave it; we'll report below */ } #else pthread_join(threads[i], NULL); #endif } } #endif double total_ms =3D (timer_now() - t0) * 1000.0; fprintf(stderr, "\n--- Results (%.1f ms total) ---\n", total_ms); int ok_count =3D 0, fail_count =3D 0, hung_count =3D 0; for (i =3D 0; i < num_threads; i++) { if (results[i].done) { if (results[i].status =3D=3D CONNECTION_OK) { ok_count++; } else { fprintf(stderr, " Thread %d: FAILED - %s\n", i, results[i].errmsg); fail_count++; } } else { fprintf(stderr, " Thread %d: HUNG (did not complete in %ds)\n", i, TIMEOUT_SECONDS); hung_count++; } #ifdef _WIN32 CloseHandle(threads[i]); #endif } fprintf(stderr, "\nSummary: %d OK, %d failed, %d hung (of %d)\n", ok_count, fail_count, hung_count, num_threads); free(threads); free(results); if (hung_count > 0) { fprintf(stderr, "\n*** BUG: %d threads hung in PQconnectdb() ***\n", hung_count); return 2; } if (fail_count > 0) return 1; fprintf(stderr, "\nAll threads connected successfully.\n"); return 0; }