| 1 | // vtest build: !windows // msvc hung, maybe sync/atomic bug, gcc on windows does too, although less frequently |
| 2 | import time |
| 3 | import sync |
| 4 | import pool |
| 5 | import rand |
| 6 | |
| 7 | // Mock connection implementation |
| 8 | struct MockConn { |
| 9 | mut: |
| 10 | healthy bool |
| 11 | close_flag bool |
| 12 | reset_flag bool |
| 13 | closed int |
| 14 | id string |
| 15 | } |
| 16 | |
| 17 | fn (mut c MockConn) validate() !bool { |
| 18 | return c.healthy |
| 19 | } |
| 20 | |
| 21 | fn (mut c MockConn) close() ! { |
| 22 | if c.close_flag { |
| 23 | return error('simulated close error') |
| 24 | } |
| 25 | c.closed++ |
| 26 | } |
| 27 | |
| 28 | fn (mut c MockConn) reset() ! { |
| 29 | if c.reset_flag { |
| 30 | return error('simulated reset error') |
| 31 | } |
| 32 | c.reset_flag = true |
| 33 | } |
| 34 | |
| 35 | // Test utility functions |
| 36 | fn create_mock_factory(mut arr []&pool.ConnectionPoolable, healthy bool, fail_times int) fn () !&pool.ConnectionPoolable { |
| 37 | mut count := 0 |
| 38 | return fn [mut arr, healthy, fail_times, mut count] () !&pool.ConnectionPoolable { |
| 39 | if count < fail_times { |
| 40 | count++ |
| 41 | return error('connection creation failed') |
| 42 | } |
| 43 | mut conn := &MockConn{ |
| 44 | healthy: healthy |
| 45 | id: rand.uuid_v7() |
| 46 | } |
| 47 | arr << conn |
| 48 | return conn |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | fn is_same_conn(conn1 &pool.ConnectionPoolable, conn2 &pool.ConnectionPoolable) bool { |
| 53 | c1 := conn1 as MockConn |
| 54 | c2 := conn2 as MockConn |
| 55 | return c1.id == c2.id |
| 56 | } |
| 57 | |
| 58 | // Test cases |
| 59 | fn test_basic_usage() { |
| 60 | for _ in 0 .. 1 { |
| 61 | mut test_conns := []&pool.ConnectionPoolable{} |
| 62 | factory := create_mock_factory(mut test_conns, true, 0) |
| 63 | config := pool.ConnectionPoolConfig{ |
| 64 | max_conns: 5 |
| 65 | min_idle_conns: 2 |
| 66 | idle_timeout: 100 * time.millisecond |
| 67 | get_timeout: 50 * time.millisecond |
| 68 | } |
| 69 | |
| 70 | mut p := pool.new_connection_pool(factory, config)! |
| 71 | |
| 72 | // Acquire a connection |
| 73 | mut conn1 := p.get()! |
| 74 | assert p.stats().active_conns == 1 |
| 75 | |
| 76 | // Acquire multiple connections |
| 77 | mut conns := [p.get()!, p.get()!, p.get()!] |
| 78 | assert p.stats().active_conns == 4 |
| 79 | |
| 80 | // Return connections |
| 81 | for c in conns { |
| 82 | p.put(c)! |
| 83 | } |
| 84 | p.put(conn1)! |
| 85 | assert p.stats().active_conns == 0 |
| 86 | assert p.stats().total_conns >= 4 |
| 87 | p.close() |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | fn test_pool_exhaustion() { |
| 92 | for i in 0 .. 1 { |
| 93 | mut test_conns := []&pool.ConnectionPoolable{} |
| 94 | factory := create_mock_factory(mut test_conns, true, 0) |
| 95 | config := pool.ConnectionPoolConfig{ |
| 96 | max_conns: 2 |
| 97 | min_idle_conns: 1 |
| 98 | get_timeout: 10 * time.millisecond |
| 99 | } |
| 100 | |
| 101 | mut p := pool.new_connection_pool(factory, config)! |
| 102 | |
| 103 | // Acquire all connections |
| 104 | c1 := p.get()! |
| 105 | c2 := p.get()! |
| 106 | assert p.stats().active_conns == 2 |
| 107 | |
| 108 | // Attempt to acquire third connection (should timeout) |
| 109 | p.get() or { assert err.msg().contains('timeout') } |
| 110 | |
| 111 | // After returning, should be able to acquire again |
| 112 | p.put(c2)! |
| 113 | c3 := p.get()! |
| 114 | assert is_same_conn(c3, c2) |
| 115 | assert p.stats().active_conns == 2 |
| 116 | p.close() |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | fn test_connection_validation() { |
| 121 | mut test_conns := []&pool.ConnectionPoolable{} |
| 122 | factory := create_mock_factory(mut test_conns, false, 0) // Create unhealthy connections |
| 123 | config := pool.ConnectionPoolConfig{ |
| 124 | min_idle_conns: 1 |
| 125 | } |
| 126 | |
| 127 | mut p := pool.new_connection_pool(factory, config) or { |
| 128 | assert err.msg().contains('connection validation failed') |
| 129 | return |
| 130 | } |
| 131 | defer { |
| 132 | p.close() |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | fn test_eviction() { |
| 137 | mut test_conns := []&pool.ConnectionPoolable{} |
| 138 | factory := create_mock_factory(mut test_conns, true, 0) |
| 139 | config := pool.ConnectionPoolConfig{ |
| 140 | max_lifetime: 10 * time.millisecond |
| 141 | idle_timeout: 10 * time.millisecond |
| 142 | min_idle_conns: 0 |
| 143 | } |
| 144 | |
| 145 | mut p := pool.new_connection_pool(factory, config)! |
| 146 | defer { |
| 147 | p.close() |
| 148 | } |
| 149 | |
| 150 | // Acquire and return a connection |
| 151 | c1 := p.get()! |
| 152 | p.put(c1)! |
| 153 | |
| 154 | // Wait longer than timeout thresholds |
| 155 | time.sleep(100 * time.millisecond) |
| 156 | p.send_eviction(.urgent) |
| 157 | time.sleep(10 * time.millisecond) |
| 158 | stats := p.stats() |
| 159 | assert stats.evicted_count > 0 |
| 160 | assert stats.total_conns == 0 |
| 161 | assert stats.idle_conns == 0 |
| 162 | c2 := p.get()! |
| 163 | assert !is_same_conn(c1, c2) |
| 164 | assert p.stats().total_conns == 1 |
| 165 | assert p.stats().evicted_count > 0 |
| 166 | } |
| 167 | |
| 168 | fn test_retry_mechanism() { |
| 169 | mut test_conns := []&pool.ConnectionPoolable{} |
| 170 | factory := create_mock_factory(mut test_conns, true, 3) // First 3 attempts fail |
| 171 | config := pool.ConnectionPoolConfig{ |
| 172 | max_retry_attempts: 5 |
| 173 | retry_base_delay: 10 * time.millisecond |
| 174 | min_idle_conns: 0 // No idle connections |
| 175 | } |
| 176 | |
| 177 | mut p := pool.new_connection_pool(factory, config)! |
| 178 | defer { |
| 179 | p.close() |
| 180 | } |
| 181 | // Should successfully create connection after retries |
| 182 | conn := p.get()! |
| 183 | assert test_conns.len == 1 |
| 184 | assert p.stats().creation_errors == 3 |
| 185 | } |
| 186 | |
| 187 | fn test_concurrent_access() { |
| 188 | mut test_conns := []&pool.ConnectionPoolable{} |
| 189 | factory := create_mock_factory(mut test_conns, true, 0) |
| 190 | config := pool.ConnectionPoolConfig{ |
| 191 | max_conns: 10 |
| 192 | } |
| 193 | |
| 194 | mut p := pool.new_connection_pool(factory, config)! |
| 195 | defer { |
| 196 | p.close() |
| 197 | } |
| 198 | mut wg := sync.new_waitgroup() |
| 199 | |
| 200 | for _ in 0 .. 20 { |
| 201 | wg.add(1) |
| 202 | spawn fn (mut p pool.ConnectionPool, mut wg sync.WaitGroup) ! { |
| 203 | defer { wg.done() } |
| 204 | conn := p.get()! |
| 205 | time.sleep(5 * time.millisecond) |
| 206 | p.put(conn)! |
| 207 | }(mut p, mut wg) |
| 208 | } |
| 209 | |
| 210 | wg.wait() |
| 211 | stats := p.stats() |
| 212 | assert stats.total_conns <= 10 |
| 213 | assert stats.active_conns == 0 |
| 214 | } |
| 215 | |
| 216 | fn test_pool_close() { |
| 217 | mut test_conns := []&pool.ConnectionPoolable{} |
| 218 | factory := create_mock_factory(mut test_conns, true, 0) |
| 219 | |
| 220 | mut p := pool.new_connection_pool(factory, pool.ConnectionPoolConfig{})! |
| 221 | c := p.get()! |
| 222 | p.put(c)! |
| 223 | assert p.stats().active_conns == 0 |
| 224 | assert p.stats().idle_conns >= 5 |
| 225 | |
| 226 | p.close() |
| 227 | |
| 228 | // Attempt to acquire connection after close |
| 229 | p.get() or { assert err.msg().contains('closed') } |
| 230 | assert p.stats().active_conns == 0 |
| 231 | assert p.stats().idle_conns == 0 |
| 232 | |
| 233 | // Verify connection was closed |
| 234 | mock_conn := test_conns[0] as MockConn |
| 235 | assert mock_conn.closed == 1 |
| 236 | } |
| 237 | |
| 238 | fn test_config_update() { |
| 239 | mut dummy := []&pool.ConnectionPoolable{} |
| 240 | mut p := pool.new_connection_pool(create_mock_factory(mut dummy, true, 0), pool.ConnectionPoolConfig{ |
| 241 | max_conns: 2 |
| 242 | min_idle_conns: 1 |
| 243 | })! |
| 244 | defer { |
| 245 | p.close() |
| 246 | } |
| 247 | assert p.stats().idle_conns == 1 |
| 248 | |
| 249 | // Modify configuration |
| 250 | new_config := pool.ConnectionPoolConfig{ |
| 251 | max_conns: 5 |
| 252 | min_idle_conns: 3 |
| 253 | } |
| 254 | |
| 255 | p.update_config(new_config)! |
| 256 | |
| 257 | // Trigger idle connection replenishment |
| 258 | time.sleep(100 * time.millisecond) |
| 259 | assert p.stats().idle_conns >= 3 |
| 260 | } |
| 261 | |
| 262 | fn test_error_handling() { |
| 263 | // Test close error handling |
| 264 | mut test_conns := []&pool.ConnectionPoolable{} |
| 265 | factory := create_mock_factory(mut test_conns, true, 0) |
| 266 | mut p := pool.new_connection_pool(factory, pool.ConnectionPoolConfig{})! |
| 267 | defer { |
| 268 | p.close() |
| 269 | } |
| 270 | |
| 271 | // default configuration has 5 idle_conns |
| 272 | assert p.stats().idle_conns == 5 |
| 273 | |
| 274 | // Bug Fix Needed! msvc generated wrong code for `mut conn := p.get()! as MockConn` |
| 275 | mut connx := p.get()! |
| 276 | mut conn := connx as MockConn |
| 277 | assert p.stats().active_conns == 1 |
| 278 | // it depend on `background_maintenance` thread to keep 5 idle_conns |
| 279 | assert p.stats().idle_conns >= 4 |
| 280 | conn.close_flag = true |
| 281 | p.put(conn) or { assert err.msg().contains('simulated close error') } |
| 282 | assert p.stats().active_conns == 0 |
| 283 | // it depend on `background_maintenance` thread to keep 5 idle_conns |
| 284 | assert p.stats().idle_conns >= 4 |
| 285 | |
| 286 | // Test reset error handling |
| 287 | conn.reset_flag = true |
| 288 | p.put(conn) or { assert err.msg().contains('reset') } |
| 289 | assert p.stats().active_conns == 0 |
| 290 | // it depend on `background_maintenance` thread to keep 5 idle_conns |
| 291 | assert p.stats().idle_conns >= 4 |
| 292 | } |
| 293 | |