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
pull/5502/head
copilot-swe-agent[bot] 1 month ago committed by René Cannaò
parent f3b9eda296
commit 5e8b1e3e5a

@ -143,6 +143,75 @@ class MetricsCollector;
typedef std::unordered_map<std::uint64_t, void *> 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;

@ -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 {

@ -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 {

@ -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 <cstdio>
#include <mutex>
#include <string>
#include <vector>
// Stubs for tap noise-tools symbols (unused in standalone unit tests)
std::vector<std::string> 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();
}
Loading…
Cancel
Save