From 5e8b1e3e5a9abbc81cfab2d3dd8b1eb2b87c8e23 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Mar 2026 22:15:11 +0000 Subject: [PATCH] Extract connection pool decision logic into pure functions (MySQL + PgSQL) Co-authored-by: renecannao <3645227+renecannao@users.noreply.github.com> Agent-Logs-Url: https://github.com/sysown/proxysql/sessions/89d35f54-3313-48d4-8252-70e3c1f3b036 --- include/Base_HostGroups_Manager.h | 69 ++++ lib/MySrvConnList.cpp | 105 +++-- lib/PgSQL_HostGroups_Manager.cpp | 51 +-- .../tests/connection_pool_utils_unit-t.cpp | 389 ++++++++++++++++++ 4 files changed, 557 insertions(+), 57 deletions(-) create mode 100644 test/tap/tests/connection_pool_utils_unit-t.cpp diff --git a/include/Base_HostGroups_Manager.h b/include/Base_HostGroups_Manager.h index 2fcbac4a7..aa62f566f 100644 --- a/include/Base_HostGroups_Manager.h +++ b/include/Base_HostGroups_Manager.h @@ -143,6 +143,75 @@ class MetricsCollector; typedef std::unordered_map umap_mysql_errors; +/** + * @brief Encodes the outcome of a connection-pool evaluation. + * + * Returned by evaluate_pool_state() to tell the caller exactly what actions + * the connection pool should take when a backend connection is requested. + */ +struct ConnectionPoolDecision { + bool create_new_connection; ///< True if a new backend connection must be created. + bool evict_connections; ///< True if free connections should be evicted to stay within limits. + unsigned int num_to_evict; ///< Number of free connections to evict (valid when evict_connections is true). + bool needs_warming; ///< True if the warming threshold was not reached (implies create_new_connection). +}; + +/** + * @brief Calculate how many free connections to evict to stay within 75% of max_connections. + * + * Eviction is triggered when the total connection count (free + used) reaches or exceeds + * 75% of @p max_connections. At least one connection is always evicted when the threshold + * is crossed and at least one free connection exists. + * + * @param conns_free Current number of free (idle) backend connections. + * @param conns_used Current number of in-use backend connections. + * @param max_connections Maximum connections allowed for the server. + * @return Number of free connections to evict; 0 if eviction is not needed. + */ +unsigned int calculate_eviction_count(unsigned int conns_free, unsigned int conns_used, unsigned int max_connections); + +/** + * @brief Decide whether new-connection creation should be throttled. + * + * @param new_connections_now Connections already created in the current second. + * @param throttle_connections_per_sec Per-second creation limit. + * @return true if @p new_connections_now exceeds @p throttle_connections_per_sec. + */ +bool should_throttle_connection_creation(unsigned int new_connections_now, unsigned int throttle_connections_per_sec); + +/** + * @brief Pure decision function for connection-pool create-vs-reuse logic. + * + * Given a snapshot of pool metrics and configuration this function determines + * what the pool should do — create a new connection, reuse an existing one, + * evict stale connections, or signal that connection warming is required. + * + * The function is intentionally free of global state and I/O so that it can + * be unit-tested in isolation. + * + * Connection quality levels: + * - 0 : no good match found (tracked options mismatch) — must create new + * - 1 : tracked options OK but CHANGE_USER / session reset required — may create new + * - 2 : no reset required but some SET / INIT_DB needed — reuse + * - 3 : perfect match — reuse + * + * @param conns_free Current number of idle backend connections. + * @param conns_used Current number of in-use backend connections. + * @param max_connections Maximum connections allowed for the server. + * @param connection_quality_level Quality of the best available pooled connection (0-3). + * @param connection_warming Whether connection warming is enabled for this server. + * @param free_connections_pct Target percentage of max_connections to keep warm (0-100). + * @return A ConnectionPoolDecision describing the required action. + */ +ConnectionPoolDecision evaluate_pool_state( + unsigned int conns_free, + unsigned int conns_used, + unsigned int max_connections, + unsigned int connection_quality_level, + bool connection_warming, + int free_connections_pct +); + class MySrvConnList; class MySrvC; class MySrvList; diff --git a/lib/MySrvConnList.cpp b/lib/MySrvConnList.cpp index e2175c830..801a5dd6a 100644 --- a/lib/MySrvConnList.cpp +++ b/lib/MySrvConnList.cpp @@ -47,6 +47,65 @@ void MySrvConnList::drop_all_connections() { } } +unsigned int calculate_eviction_count(unsigned int conns_free, unsigned int conns_used, unsigned int max_connections) { + if (conns_free < 1) return 0; + unsigned int pct_max_connections = (3 * max_connections) / 4; + unsigned int total = conns_free + conns_used; + if (pct_max_connections <= total) { + unsigned int count = total - pct_max_connections; + return (count == 0) ? 1 : count; + } + return 0; +} + +bool should_throttle_connection_creation(unsigned int new_connections_now, unsigned int throttle_connections_per_sec) { + return new_connections_now > throttle_connections_per_sec; +} + +ConnectionPoolDecision evaluate_pool_state( + unsigned int conns_free, + unsigned int conns_used, + unsigned int max_connections, + unsigned int connection_quality_level, + bool connection_warming, + int free_connections_pct +) { + ConnectionPoolDecision decision = { false, false, 0, false }; + + // Check connection warming threshold first + if (connection_warming) { + unsigned int total = conns_free + conns_used; + unsigned int expected_warm = (unsigned int)(free_connections_pct) * max_connections / 100; + if (total < expected_warm) { + decision.needs_warming = true; + decision.create_new_connection = true; + return decision; + } + } + + switch (connection_quality_level) { + case 0: // no good match — must create new, possibly after evicting stale free connections + decision.create_new_connection = true; + decision.num_to_evict = calculate_eviction_count(conns_free, conns_used, max_connections); + decision.evict_connections = (decision.num_to_evict > 0); + break; + case 1: // tracked options OK but CHANGE_USER / session reset required — may create new + if ((conns_used > conns_free) && (max_connections > (conns_free / 2 + conns_used / 2))) { + decision.create_new_connection = true; + } + break; + case 2: // partial match — reuse + case 3: // perfect match — reuse + decision.create_new_connection = false; + break; + default: + decision.create_new_connection = true; + break; + } + + return decision; +} + void MySrvConnList::get_random_MyConn_inner_search(unsigned int start, unsigned int end, unsigned int& conn_found_idx, unsigned int& connection_quality_level, unsigned int& number_of_matching_session_variables, const MySQL_Connection * client_conn) { char *schema = client_conn->userinfo->schemaname; MySQL_Connection * conn=NULL; @@ -115,10 +174,9 @@ void MySrvConnList::get_random_MyConn_inner_search(unsigned int start, unsigned MySQL_Connection * MySrvConnList::get_random_MyConn(MySQL_Session *sess, bool ff) { MySQL_Connection * conn=NULL; unsigned int i; - unsigned int conn_found_idx; + unsigned int conn_found_idx = 0; unsigned int l=conns_length(); unsigned int connection_quality_level = 0; - bool needs_warming = false; // connection_quality_level: // 0 : not found any good connection, tracked options are not OK // 1 : tracked options are OK , but CHANGE USER is required @@ -132,9 +190,12 @@ MySQL_Connection * MySrvConnList::get_random_MyConn(MySQL_Session *sess, bool ff connection_warming = mysrvc->myhgc->attributes.connection_warming; free_connections_pct = mysrvc->myhgc->attributes.free_connections_pct; } + unsigned int conns_free = mysrvc->ConnectionsFree->conns_length(); + unsigned int conns_used = mysrvc->ConnectionsUsed->conns_length(); + bool needs_warming = false; if (connection_warming == true) { - unsigned int total_connections = mysrvc->ConnectionsFree->conns_length()+mysrvc->ConnectionsUsed->conns_length(); - unsigned int expected_warm_connections = free_connections_pct*mysrvc->max_connections/100; + unsigned int total_connections = conns_free + conns_used; + unsigned int expected_warm_connections = (unsigned int)free_connections_pct * mysrvc->max_connections / 100; if (total_connections < expected_warm_connections) { needs_warming = true; } @@ -151,6 +212,11 @@ MySQL_Connection * MySrvConnList::get_random_MyConn(MySQL_Session *sess, bool ff if (connection_quality_level !=3 ) { // we didn't find the perfect connection get_random_MyConn_inner_search(0, i, conn_found_idx, connection_quality_level, number_of_matching_session_variables, client_conn); } + // Evaluate pool state to determine create-vs-reuse and eviction (warming already handled above) + ConnectionPoolDecision decision = evaluate_pool_state( + conns_free, conns_used, (unsigned int)mysrvc->max_connections, + connection_quality_level, false, 0 + ); // connection_quality_level: // 1 : tracked options are OK , but CHANGE USER is required // 2 : tracked options are OK , CHANGE USER is not required, but some SET statement or INIT_DB needs to be executed @@ -159,25 +225,14 @@ MySQL_Connection * MySrvConnList::get_random_MyConn(MySQL_Session *sess, bool ff // we must check if connections need to be freed before // creating a new connection { - unsigned int conns_free = mysrvc->ConnectionsFree->conns_length(); - unsigned int conns_used = mysrvc->ConnectionsUsed->conns_length(); - unsigned int pct_max_connections = (3 * mysrvc->max_connections) / 4; - unsigned int connections_to_free = 0; - - if (conns_free >= 1) { - // connection cleanup is triggered when connections exceed 3/4 of the total - // allowed max connections, this cleanup ensures that at least *one connection* - // will be freed. - if (pct_max_connections <= (conns_free + conns_used)) { - connections_to_free = (conns_free + conns_used) - pct_max_connections; - if (connections_to_free == 0) connections_to_free = 1; - } - - while (conns_free && connections_to_free) { - MySQL_Connection* conn = mysrvc->ConnectionsFree->remove(0); - delete conn; + if (decision.evict_connections) { + unsigned int cur_free = conns_free; + unsigned int connections_to_free = decision.num_to_evict; + while (cur_free && connections_to_free) { + MySQL_Connection* c = mysrvc->ConnectionsFree->remove(0); + delete c; - conns_free = mysrvc->ConnectionsFree->conns_length(); + cur_free = mysrvc->ConnectionsFree->conns_length(); connections_to_free -= 1; } } @@ -194,9 +249,7 @@ MySQL_Connection * MySrvConnList::get_random_MyConn(MySQL_Session *sess, bool ff case 1: //tracked options are OK , but CHANGE USER is required // we may consider creating a new connection { - unsigned int conns_free = mysrvc->ConnectionsFree->conns_length(); - unsigned int conns_used = mysrvc->ConnectionsUsed->conns_length(); - if ((conns_used > conns_free) && (mysrvc->max_connections > (conns_free/2 + conns_used/2)) ) { + if (decision.create_new_connection) { conn = new MySQL_Connection(); conn->parent=mysrvc; // if attributes.multiplex == true , STATUS_MYSQL_CONNECTION_NO_MULTIPLEX_HG is set to false. And vice-versa @@ -238,7 +291,7 @@ MySQL_Connection * MySrvConnList::get_random_MyConn(MySQL_Session *sess, bool ff // mysql_hostgroup_attributes takes priority throttle_connections_per_sec_to_hostgroup = _myhgc->attributes.throttle_connections_per_sec; } - if (_myhgc->new_connections_now > (unsigned int) throttle_connections_per_sec_to_hostgroup) { + if (should_throttle_connection_creation(_myhgc->new_connections_now, throttle_connections_per_sec_to_hostgroup)) { __sync_fetch_and_add(&MyHGM->status.server_connections_delayed, 1); return NULL; } else { diff --git a/lib/PgSQL_HostGroups_Manager.cpp b/lib/PgSQL_HostGroups_Manager.cpp index 8dfc89c8e..ea5b19404 100644 --- a/lib/PgSQL_HostGroups_Manager.cpp +++ b/lib/PgSQL_HostGroups_Manager.cpp @@ -2330,7 +2330,7 @@ void PgSQL_SrvConnList::get_random_MyConn_inner_search(unsigned int start, unsig PgSQL_Connection * PgSQL_SrvConnList::get_random_MyConn(PgSQL_Session *sess, bool ff) { PgSQL_Connection * conn=NULL; unsigned int i; - unsigned int conn_found_idx; + unsigned int conn_found_idx = 0; unsigned int l=conns_length(); unsigned int connection_quality_level = 0; bool needs_warming = false; @@ -2347,19 +2347,16 @@ PgSQL_Connection * PgSQL_SrvConnList::get_random_MyConn(PgSQL_Session *sess, boo connection_warming = mysrvc->myhgc->attributes.connection_warming; free_connections_pct = mysrvc->myhgc->attributes.free_connections_pct; } + unsigned int conns_free = mysrvc->ConnectionsFree->conns_length(); + unsigned int conns_used = mysrvc->ConnectionsUsed->conns_length(); if (connection_warming == true) { - unsigned int total_connections = mysrvc->ConnectionsFree->conns_length()+mysrvc->ConnectionsUsed->conns_length(); - unsigned int expected_warm_connections = free_connections_pct*mysrvc->max_connections/100; + unsigned int total_connections = conns_free + conns_used; + unsigned int expected_warm_connections = (unsigned int)free_connections_pct * mysrvc->max_connections / 100; if (total_connections < expected_warm_connections) { needs_warming = true; } } if (l && ff==false && needs_warming==false) { - //if (l>32768) { - // i=rand()%l; - //} else { - // i=fastrand()%l; - //} i = rand_fast() % l; if (sess && sess->client_myds && sess->client_myds->myconn && sess->client_myds->myconn->userinfo) { PgSQL_Connection * client_conn = sess->client_myds->myconn; @@ -2367,6 +2364,11 @@ PgSQL_Connection * PgSQL_SrvConnList::get_random_MyConn(PgSQL_Session *sess, boo if (connection_quality_level !=3 ) { // we didn't find the perfect connection get_random_MyConn_inner_search(0, i, conn_found_idx, connection_quality_level, number_of_matching_session_variables, client_conn); } + // Evaluate pool state to determine create-vs-reuse and eviction (warming already handled above) + ConnectionPoolDecision decision = evaluate_pool_state( + conns_free, conns_used, (unsigned int)mysrvc->max_connections, + connection_quality_level, false, 0 + ); // connection_quality_level: // 1 : tracked options are OK , but RESETTING SESSION is required // 2 : tracked options are OK , RESETTING SESSION is not required, but some SET statement or INIT_DB needs to be executed @@ -2375,25 +2377,14 @@ PgSQL_Connection * PgSQL_SrvConnList::get_random_MyConn(PgSQL_Session *sess, boo // we must check if connections need to be freed before // creating a new connection { - unsigned int conns_free = mysrvc->ConnectionsFree->conns_length(); - unsigned int conns_used = mysrvc->ConnectionsUsed->conns_length(); - unsigned int pct_max_connections = (3 * mysrvc->max_connections) / 4; - unsigned int connections_to_free = 0; - - if (conns_free >= 1) { - // connection cleanup is triggered when connections exceed 3/4 of the total - // allowed max connections, this cleanup ensures that at least *one connection* - // will be freed. - if (pct_max_connections <= (conns_free + conns_used)) { - connections_to_free = (conns_free + conns_used) - pct_max_connections; - if (connections_to_free == 0) connections_to_free = 1; - } - - while (conns_free && connections_to_free) { - PgSQL_Connection* conn = mysrvc->ConnectionsFree->remove(0); - delete conn; - - conns_free = mysrvc->ConnectionsFree->conns_length(); + if (decision.evict_connections) { + unsigned int cur_free = conns_free; + unsigned int connections_to_free = decision.num_to_evict; + while (cur_free && connections_to_free) { + PgSQL_Connection* c = mysrvc->ConnectionsFree->remove(0); + delete c; + + cur_free = mysrvc->ConnectionsFree->conns_length(); connections_to_free -= 1; } } @@ -2410,9 +2401,7 @@ PgSQL_Connection * PgSQL_SrvConnList::get_random_MyConn(PgSQL_Session *sess, boo case 1: //tracked options are OK , but RESETTING SESSION is required // we may consider creating a new connection { - unsigned int conns_free = mysrvc->ConnectionsFree->conns_length(); - unsigned int conns_used = mysrvc->ConnectionsUsed->conns_length(); - if ((conns_used > conns_free) && (mysrvc->max_connections > (conns_free/2 + conns_used/2)) ) { + if (decision.create_new_connection) { conn = new PgSQL_Connection(false); conn->parent=mysrvc; // if attributes.multiplex == true , STATUS_PGSQL_CONNECTION_NO_MULTIPLEX_HG is set to false. And vice-versa @@ -2454,7 +2443,7 @@ PgSQL_Connection * PgSQL_SrvConnList::get_random_MyConn(PgSQL_Session *sess, boo // pgsql_hostgroup_attributes takes priority throttle_connections_per_sec_to_hostgroup = _myhgc->attributes.throttle_connections_per_sec; } - if (_myhgc->new_connections_now > (unsigned int) throttle_connections_per_sec_to_hostgroup) { + if (should_throttle_connection_creation(_myhgc->new_connections_now, throttle_connections_per_sec_to_hostgroup)) { __sync_fetch_and_add(&PgHGM->status.server_connections_delayed, 1); return NULL; } else { diff --git a/test/tap/tests/connection_pool_utils_unit-t.cpp b/test/tap/tests/connection_pool_utils_unit-t.cpp new file mode 100644 index 000000000..059f98f82 --- /dev/null +++ b/test/tap/tests/connection_pool_utils_unit-t.cpp @@ -0,0 +1,389 @@ +/** + * @file connection_pool_utils_unit-t.cpp + * @brief TAP unit tests for connection-pool decision functions. + * + * Tests the three pure functions extracted from get_random_MyConn(): + * - calculate_eviction_count() + * - should_throttle_connection_creation() + * - evaluate_pool_state() + * + * These tests are intentionally standalone: the functions under test are pure + * (no global state, no I/O) and can be exercised without a running ProxySQL + * instance or live database connections. + * + * Test categories: + * 1. calculate_eviction_count — eviction threshold arithmetic + * 2. should_throttle_connection_creation — throttle gate + * 3. evaluate_pool_state — full decision logic (create/reuse/evict/warming) + */ + +#include "tap.h" +#include +#include +#include +#include + +// Stubs for tap noise-tools symbols (unused in standalone unit tests) +std::vector noise_failures; +std::mutex noise_failure_mutex; +extern "C" int get_noise_tools_count() { return 0; } +extern "C" void stop_noise_tools() {} + +// ============================================================================ +// Standalone reimplementation of the three pure functions +// (mirrors the implementations in lib/MySrvConnList.cpp) +// ============================================================================ + +struct ConnectionPoolDecision { + bool create_new_connection; + bool evict_connections; + unsigned int num_to_evict; + bool needs_warming; +}; + +/** + * @brief Calculate how many free connections to evict to stay within 75% of max_connections. + */ +static unsigned int calculate_eviction_count(unsigned int conns_free, unsigned int conns_used, unsigned int max_connections) { + if (conns_free < 1) return 0; + unsigned int pct_max_connections = (3 * max_connections) / 4; + unsigned int total = conns_free + conns_used; + if (pct_max_connections <= total) { + unsigned int count = total - pct_max_connections; + return (count == 0) ? 1 : count; + } + return 0; +} + +/** + * @brief Decide whether new-connection creation should be throttled. + */ +static bool should_throttle_connection_creation(unsigned int new_connections_now, unsigned int throttle_connections_per_sec) { + return new_connections_now > throttle_connections_per_sec; +} + +/** + * @brief Pure decision function for connection-pool create-vs-reuse logic. + */ +static ConnectionPoolDecision evaluate_pool_state( + unsigned int conns_free, + unsigned int conns_used, + unsigned int max_connections, + unsigned int connection_quality_level, + bool connection_warming, + int free_connections_pct +) { + ConnectionPoolDecision decision = { false, false, 0, false }; + + if (connection_warming) { + unsigned int total = conns_free + conns_used; + unsigned int expected_warm = (unsigned int)(free_connections_pct) * max_connections / 100; + if (total < expected_warm) { + decision.needs_warming = true; + decision.create_new_connection = true; + return decision; + } + } + + switch (connection_quality_level) { + case 0: + decision.create_new_connection = true; + decision.num_to_evict = calculate_eviction_count(conns_free, conns_used, max_connections); + decision.evict_connections = (decision.num_to_evict > 0); + break; + case 1: + if ((conns_used > conns_free) && (max_connections > (conns_free / 2 + conns_used / 2))) { + decision.create_new_connection = true; + } + break; + case 2: + case 3: + decision.create_new_connection = false; + break; + default: + decision.create_new_connection = true; + break; + } + + return decision; +} + +// ============================================================================ +// Test: calculate_eviction_count +// ============================================================================ + +static void test_calculate_eviction_count() { + diag("=== calculate_eviction_count ==="); + + // No free connections → no eviction regardless of totals + ok(calculate_eviction_count(0, 100, 100) == 0, + "conns_free=0: no eviction even when used is high"); + + // Total well below 75% threshold → no eviction + ok(calculate_eviction_count(5, 5, 100) == 0, + "total=10 < 75 of 100: no eviction needed"); + + // Total exactly at 75% threshold → eviction triggered (condition uses <=) + unsigned int e_at = calculate_eviction_count(25, 50, 100); + ok(e_at == 1, + "total=75 == 75%% of 100: eviction triggered at threshold (got %u)", e_at); + + // Total slightly over 75% → evict 1 + ok(calculate_eviction_count(26, 50, 100) == 1, + "total=76 > 75%% of 100: evict 1"); + + // Total well over 75% → evict proportionally + unsigned int e1 = calculate_eviction_count(50, 100, 100); + ok(e1 == 75, + "total=150, 75%%=75: evict 75 (got %u)", e1); + + // All connections free, over threshold + unsigned int e2 = calculate_eviction_count(80, 0, 100); + ok(e2 == 5, + "conns_free=80, used=0, 75%%=75: evict 5 (got %u)", e2); + + // Edge: max_connections=0 → pct=0 → everything is over threshold + // with conns_free=1, eviction is triggered + unsigned int e3 = calculate_eviction_count(1, 0, 0); + ok(e3 >= 1, + "max_connections=0, conns_free=1: eviction triggered (got %u)", e3); + + // Edge: max_connections=1, one free connection → at 100%, threshold=0 + unsigned int e4 = calculate_eviction_count(1, 0, 1); + ok(e4 >= 1, + "max_connections=1, conns_free=1: over 75%% threshold (got %u)", e4); + + // Minimum eviction is always 1 when threshold crossed (result is never 0 if threshold crossed) + // conns_free=1, conns_used=73, max=100 → total=74, pct=75 → 75 <= 74 is FALSE → no eviction + ok(calculate_eviction_count(1, 73, 100) == 0, + "total=74 < pct=75: no eviction"); + + // conns_free=1, conns_used=74, max=100 → total=75, pct=75 → 75 <= 75 → count=0→1 + unsigned int e_at_threshold = calculate_eviction_count(1, 74, 100); + ok(e_at_threshold == 1, + "total=75 == pct=75: eviction triggered (at threshold, got %u)", e_at_threshold); +} + +// ============================================================================ +// Test: should_throttle_connection_creation +// ============================================================================ + +static void test_should_throttle_connection_creation() { + diag("=== should_throttle_connection_creation ==="); + + // Exactly at limit → not throttled (strictly greater-than semantics) + ok(!should_throttle_connection_creation(100, 100), + "new_conns==limit: not throttled"); + + // One over limit → throttled + ok(should_throttle_connection_creation(101, 100), + "new_conns > limit: throttled"); + + // Well below limit → not throttled + ok(!should_throttle_connection_creation(0, 1000000), + "new_conns=0: not throttled"); + + // Zero limit, one connection → throttled + ok(should_throttle_connection_creation(1, 0), + "limit=0, new_conns=1: throttled"); + + // Zero limit, zero connections → not throttled + ok(!should_throttle_connection_creation(0, 0), + "limit=0, new_conns=0: not throttled (0 is not > 0)"); + + // Very large values + ok(!should_throttle_connection_creation(999999, 1000000), + "new_conns=999999, limit=1000000: not throttled"); + + ok(should_throttle_connection_creation(1000001, 1000000), + "new_conns=1000001, limit=1000000: throttled"); +} + +// ============================================================================ +// Test: evaluate_pool_state — quality level decisions +// ============================================================================ + +static void test_evaluate_pool_state_quality_levels() { + diag("=== evaluate_pool_state: quality-level decisions ==="); + + // Quality 0 (no match) → must create new + ConnectionPoolDecision d0 = evaluate_pool_state(5, 5, 100, 0, false, 0); + ok(d0.create_new_connection, + "quality=0: create_new_connection=true"); + ok(!d0.needs_warming, + "quality=0, warming off: needs_warming=false"); + + // Quality 1 with conns_used > conns_free and room to grow → create new + ConnectionPoolDecision d1a = evaluate_pool_state(3, 10, 100, 1, false, 0); + ok(d1a.create_new_connection, + "quality=1, used>free, room to grow: create_new=true"); + + // Quality 1 with conns_used <= conns_free → reuse existing + ConnectionPoolDecision d1b = evaluate_pool_state(10, 3, 100, 1, false, 0); + ok(!d1b.create_new_connection, + "quality=1, used<=free: create_new=false (reuse)"); + + // Quality 1 with conns_used > conns_free but NO room to grow → reuse + // max_connections <= (conns_free/2 + conns_used/2) → no new + // conns_free=3, conns_used=10 → avg=6.5; max_connections=6 <= 6 → no new + ConnectionPoolDecision d1c = evaluate_pool_state(3, 10, 6, 1, false, 0); + ok(!d1c.create_new_connection, + "quality=1, used>free but max<=avg: create_new=false (reuse)"); + + // Quality 2 → always reuse + ConnectionPoolDecision d2 = evaluate_pool_state(5, 5, 100, 2, false, 0); + ok(!d2.create_new_connection, + "quality=2: create_new=false (reuse)"); + + // Quality 3 → always reuse (perfect match) + ConnectionPoolDecision d3 = evaluate_pool_state(5, 5, 100, 3, false, 0); + ok(!d3.create_new_connection, + "quality=3: create_new=false (perfect reuse)"); +} + +// ============================================================================ +// Test: evaluate_pool_state — eviction +// ============================================================================ + +static void test_evaluate_pool_state_eviction() { + diag("=== evaluate_pool_state: eviction ==="); + + // Quality 0 with pool below 75% → no eviction + ConnectionPoolDecision d_no_evict = evaluate_pool_state(5, 5, 100, 0, false, 0); + ok(!d_no_evict.evict_connections, + "quality=0, total=10 < 75: no eviction"); + ok(d_no_evict.num_to_evict == 0, + "quality=0, total=10 < 75: num_to_evict=0"); + + // Quality 0 with pool over 75% → eviction triggered + ConnectionPoolDecision d_evict = evaluate_pool_state(10, 80, 100, 0, false, 0); + ok(d_evict.evict_connections, + "quality=0, total=90 > 75: eviction triggered"); + ok(d_evict.num_to_evict > 0, + "quality=0, total=90 > 75: num_to_evict > 0 (got %u)", d_evict.num_to_evict); + + // Quality 2/3 never triggers eviction (just reuse) + ConnectionPoolDecision d_q2 = evaluate_pool_state(10, 80, 100, 2, false, 0); + ok(!d_q2.evict_connections, + "quality=2: no eviction even when over 75%%"); + + // Edge: max_connections=0 + ConnectionPoolDecision d_max0 = evaluate_pool_state(1, 0, 0, 0, false, 0); + ok(d_max0.create_new_connection, + "max_connections=0, quality=0: create_new=true"); + + // Edge: max_connections=1, single free conn, no used + ConnectionPoolDecision d_max1 = evaluate_pool_state(1, 0, 1, 0, false, 0); + ok(d_max1.create_new_connection, + "max_connections=1, conns_free=1: create_new=true"); + ok(d_max1.evict_connections, + "max_connections=1, conns_free=1: eviction triggered (over 75%%)"); +} + +// ============================================================================ +// Test: evaluate_pool_state — warming +// ============================================================================ + +static void test_evaluate_pool_state_warming() { + diag("=== evaluate_pool_state: connection warming ==="); + + // Warming disabled → no warming signal + ConnectionPoolDecision d_no_warm = evaluate_pool_state(0, 0, 100, 3, false, 10); + ok(!d_no_warm.needs_warming, + "warming disabled: needs_warming=false"); + + // Warming enabled, pool well below threshold → warming needed + // free_connections_pct=10, max=100 → expected_warm=10; total=0 < 10 + ConnectionPoolDecision d_warm = evaluate_pool_state(0, 0, 100, 3, true, 10); + ok(d_warm.needs_warming, + "warming enabled, total=0 < expected=10: needs_warming=true"); + ok(d_warm.create_new_connection, + "warming needed → create_new=true"); + + // Warming enabled, pool meets threshold → no warming needed + // free_connections_pct=10, max=100 → expected_warm=10; total=10 == 10, NOT less than + ConnectionPoolDecision d_warm_met = evaluate_pool_state(5, 5, 100, 3, true, 10); + ok(!d_warm_met.needs_warming, + "warming enabled, total=10 >= expected=10: needs_warming=false"); + + // Warming enabled, pool exceeds threshold → no warming + ConnectionPoolDecision d_warm_over = evaluate_pool_state(20, 10, 100, 3, true, 10); + ok(!d_warm_over.needs_warming, + "warming enabled, total=30 > expected=10: needs_warming=false"); + + // Warming overrides quality level: even with quality=3 (perfect match), warming forces create + ConnectionPoolDecision d_warm_q3 = evaluate_pool_state(2, 3, 100, 3, true, 10); + ok(d_warm_q3.needs_warming && d_warm_q3.create_new_connection, + "warming+quality=3, total=5 < expected=10: create_new=true (warming override)"); + + // Warming with free_connections_pct=0 → expected_warm=0 → never triggers + ConnectionPoolDecision d_warm_pct0 = evaluate_pool_state(0, 0, 100, 3, true, 0); + ok(!d_warm_pct0.needs_warming, + "warming, free_connections_pct=0: expected_warm=0, needs_warming=false"); + + // Warming with max_connections=0 → expected_warm=0 → never triggers + ConnectionPoolDecision d_warm_max0 = evaluate_pool_state(0, 0, 0, 3, true, 10); + ok(!d_warm_max0.needs_warming, + "warming, max_connections=0: expected_warm=0, needs_warming=false"); +} + +// ============================================================================ +// Test: evaluate_pool_state — combined scenarios +// ============================================================================ + +static void test_evaluate_pool_state_combined() { + diag("=== evaluate_pool_state: combined scenarios ==="); + + // Pool empty (conns_free=0, conns_used=0) with quality=0 → create, no eviction + ConnectionPoolDecision d_empty = evaluate_pool_state(0, 0, 100, 0, false, 0); + ok(d_empty.create_new_connection, + "empty pool, quality=0: create_new=true"); + ok(!d_empty.evict_connections, + "empty pool, quality=0: no eviction (nothing to evict)"); + ok(d_empty.num_to_evict == 0, + "empty pool: num_to_evict=0"); + + // Pool has perfect match → reuse even when near capacity + ConnectionPoolDecision d_reuse = evaluate_pool_state(30, 60, 100, 3, false, 0); + ok(!d_reuse.create_new_connection, + "quality=3, near capacity: reuse (no create)"); + ok(!d_reuse.evict_connections, + "quality=3: no eviction for reuse path"); + + // Quality=1 at boundary: used=free (equal) → reuse (condition is strictly used>free) + ConnectionPoolDecision d_q1_eq = evaluate_pool_state(5, 5, 100, 1, false, 0); + ok(!d_q1_eq.create_new_connection, + "quality=1, used==free: reuse (used not > free)"); + + // Warming off, quality=0, heavily over threshold → large eviction + ConnectionPoolDecision d_big_evict = evaluate_pool_state(100, 100, 100, 0, false, 0); + ok(d_big_evict.evict_connections, + "quality=0, total=200 >> 75: eviction"); + ok(d_big_evict.num_to_evict == 125, + "quality=0, total=200, pct=75: evict 125 (got %u)", d_big_evict.num_to_evict); +} + +// ============================================================================ +// Main +// ============================================================================ + +int main() { + // Plan: + // calculate_eviction_count: 10 tests + // should_throttle: 7 tests + // evaluate_pool_state/quality: 7 tests + // evaluate_pool_state/eviction: 8 tests + // evaluate_pool_state/warming: 8 tests + // evaluate_pool_state/combined: 8 tests + // Total: 48 tests + plan(48); + + test_calculate_eviction_count(); + test_should_throttle_connection_creation(); + test_evaluate_pool_state_quality_levels(); + test_evaluate_pool_state_eviction(); + test_evaluate_pool_state_warming(); + test_evaluate_pool_state_combined(); + + return exit_status(); +}