module

Krikri::PluginHelpers::PostgresqlConnection

PostgresqlConnection - pure logic for building a postgres:// connection URI from Ansible-style login_* params. No I/O - postgresql_db.cr/postgresql_user.cr do the actual DB.open.

Constants

DEFAULT_UNIX_SOCKET_DIR = "/var/run/postgresql"

unix_socket, given, takes precedence over host/port (matches Ansible's postgresql_db/postgresql_user own login_unix_socket precedence). crystal-pg expects a unix socket path via a "host" query param, not the URI's own host component (verified against its actual conninfo parsing source, not assumed) - the URI host is left blank in that case. Debian/Ubuntu's postgresql-common packaging (the target of every real-host benchmark round so far) compiles libpq's default Unix socket directory to this path - matches postgresql.conf's own unix_socket_directories default there.

SERVER_CLOSED_TEXT = "server closed the connection unexpectedly\n\tThis probably means the server terminated abnormally\n\tbefore or while processing the request.\n"

libpq's wording for a connection the server accepted and then dropped before/during startup packet exchange (both a clean close and an RST - live-verified against ansible-core 2.19.11 + community.postgresql 4.2.0: SO_LINGER-0 RST on the accepted socket reports this same text, no "Is the server running..." hint line either). Ends with its own newline, like libpq's PQerrorMessage buffer always does.

Class methods

build_uri(host : String | Nil = nil, port : String | Nil = nil, user : String | Nil = nil, password : String | Nil = nil, unix_socket : String | Nil = nil, dbname : String | Nil = nil, sslmode : String | Nil = nil) : String
Source
connect_timeout(params : Hash(String, String)) : Time::Span | Nil

Resolves the effective connect timeout for a module run, the two ways community.postgresql lets one be set (it has no top-level connect_timeout option): connect_params: {connect_timeout: N} first, then PGCONNECT_TIMEOUT. Nil = unbounded (no DB-level deadline).

Source
effective_target(params : Hash(String, String)) : NamedTuple(host: String | Nil, port: String, unix_socket: String | Nil)

The connection target a params hash resolves to, for diagnostics that have to name the server the plugin actually dialed (libpq's connection-error text quotes the host/port or the socket path verbatim). Applies the same defaults #build_uri does: an absent host means a Unix socket in DEFAULT_UNIX_SOCKET_DIR, an absent port means 5432.

Source
gai_error_text(host : String, code : Int32) : String

libpq's "could not translate host name" wording for an EAI_* code: gai_strerror's text for the code itself, except EAI_SYSTEM which libpq's connectDBStart words with strerror(errno) instead (glibc's gai_strerror would say "System error").

Source
gai_strerror_text(code : Int32) : String

gai_strerror(3)'s text for an EAI_* code - glibc's on this platform, which is what libpq prints verbatim.

Source
libpq_connect_error(ex : DB::ConnectionRefused, target : NamedTuple(host: String | Nil, port: String, unix_socket: String | Nil), timeout_set : Bool = false) : String

libpq's own connection-error wording, which is what real (psycopg2 -> libpq) embeds in community.postgresql's "unable to connect to database: %s" fail_json. Live-verified against ansible-core 2.19.11 + community.postgresql 4.2.0 for every case reproduced below:

  • TCP, nothing listening (ECONNREFUSED): connection to server at "127.0.0.1", port 59999 failed: Connection refused\n \tIs the server running on that host and accepting TCP/IP connections?\n
  • TCP, unroutable netns (ENETUNREACH, unshare -rn): connection to server at "127.0.0.1", port 59999 failed: Network is unreachable\n \tIs the server running on that host and accepting TCP/IP connections?\n
  • TCP, blackhole address, natural (unbounded) connect timeout: connection to server at "192.0.2.1", port 5432 failed: Connection timed out\n \tIs the server running on that host and accepting TCP/IP connections?\n (real words a libpq connect_timeout expiry differently - "timeout expired", no hint - when a connect_timeout is set; krikri models that wording via PostgresqlConnection's own bounded open, see the module's timeout comment.)
  • TCP, server accepted the connection and then dropped it (both a clean close and an SO_LINGER-0 RST on the accepted socket): connection to server at "127.0.0.1", port 59999 failed: server closed the connection unexpectedly\n \tThis probably means the server terminated abnormally\n \tbefore or while processing the request.\n
  • Unix socket, socket file/dir missing (ENOENT): connection to server on socket "/nonexistent/sockdir/.s.PGSQL.5432" failed: No such file or directory\n \tIs the server running locally and accepting connections on that socket?\n
  • server answered and rejected us (its ErrorResponse carries the text, so the server-side severity is rebuilt as "<severity>: <message>\n", and NO hint line follows): connection to server at "127.0.0.1", port 35433 failed: FATAL: password authentication failed for user "postgres"\n connection to server at "127.0.0.1", port 35433 failed: FATAL: database "nosuchdb" does not exist\n
  • unresolvable host (libpq's own DNS wording, no hint line): could not translate host name "nosuchhost.invalid" to address: Name or service not known\n

Reproducible vs not: the errno behind a TCP connect failure is re-derived with a direct non-blocking probe connect at error-report time (see #io_error_detail), because Crystal's Socket::ConnectError misreports the errno ("Resource temporarily unavailable" for a refused connect) and collapses ECONNRESET/ENETUNREACH/ETIMEDOUT into the same exception class. What krikri CANNOT reproduce: a temporary resolver failure, which libpq renders with gai_strerror's "Temporary failure in name resolution" (Crystal's Socket::Addrinfo::Error carries no gai code, so krikri prints the EAI_NONAME wording instead), any strerror string from a non-glibc libc (libpq prints the system's own, locale-dependent strings). The libpq connect_timeout wording ("timeout expired"), formerly in this not-reproducible list, is now modeled - see ConnectTimeoutCause / #libpq_connect_error below.

Source
open(uri : String, params : Hash(String, String), &block : DB::Database -> _)

DB.open bounded by timeout (nil = plain DB.open). Only the connection attempt runs under the deadline - the module block runs on the connected database afterwards, in the caller's fiber, so a slow query is never misreported as a connect timeout. Raises DB::ConnectionRefused (cause: ConnectTimeoutCause) when the deadline expires; a database that finishes connecting after the deadline is closed by the fiber that built it (see #connect_within_deadline), so neither that fiber nor the connection leaks. crystal-db's pool builds its initial connection eagerly inside DB::Database.new and, with retry_attempts=0, ConnectionRefused is never retried or swallowed there, so the only connection attempt in flight is the one this bounds.

Source
probe_wording(probe : NamedTuple(errno: Int32, pending: Bool) | Nil, target : NamedTuple(host: String | Nil, port: String, unix_socket: String | Nil), timeout_set : Bool) : String

The wording for a probe result (see #io_error_detail for the live-verified cases). Split from the probing itself so the deadline-vs-errno decision stays unit-testable without a real blackhole route.

Source
psycopg2_gate

community.postgresql's shared ensure_required_libs() (module_utils/ postgres.py, 4.2.0) fails the task with Ansible's missing_required_lib wording when the target's Python cannot import psycopg2 - after the argspec checks but before get_conn_params() and the connect attempt. krikri's postgresql_* plugins talk the wire protocol through crystal-pg and need no psycopg2, so they used to carry on past the point real stops and report their own downstream connect failure instead (consensys.web3signer's postgresql_query task, issue #467: "unable to connect to database: ... Connection refused" where real failed with "Failed to import the required Python library (psycopg2) ..."). The dependency still decides the outcome, so it is reproduced here - same message, same ordering - like the passlib gate in htpasswd. Real passes no exception= to fail_json here, so the [ERROR] block carries the msg alone, unlike deb822_repository. (expect's own pexpect gate is gone: krikri's native pty path has no pexpect dependency, and the generator's podman pair provisions pexpect on the real side only, so probing the local interpreter there failed tasks the reference ansible carried out.)

nil when the target's Python imports psycopg2 cleanly - the module connects, exactly as real's does.

Source
resolve_login_params(params : Hash(String, String)) : NamedTuple(host: String | Nil, port: String | Nil, user: String | Nil, password: String | Nil, unix_socket: String | Nil)

Resolves the login_host:/login_port:/login_user:/ login_unix_socket: params from a plugin's raw @params, falling back to each one's real-Ansible deprecated alias (host:/port:/login:/unix_socket: respectively - login_password: has none) when the canonical login_* form isn't set. Shared by postgresql_db.cr/postgresql_user.cr/ postgresql_privs.cr so the alias list only lives in one place.

Real bug found benchmarking robertdebock.postgres (round 43): its own "Create postgres users" task writes port: "{{ postgres_port }}" (the alias, for a non-default port like 6543) - each plugin only ever read login_port:, so the connection silently fell back to port 5432's Unix socket instead, which doesn't exist on this host at all (DB::ConnectionRefused / PQ::ConnectionError: Cannot establish connection, confirmed via direct debug instrumentation against a live host - the same URI, built by hand with the correct port included, connected via psql immediately).

Source

Nested types