1616use Hyperf \Context \ApplicationContext ;
1717use Hyperf \Contract \ConfigInterface ;
1818use Hyperf \Contract \StdoutLoggerInterface ;
19+ use Hyperf \Coroutine \Waiter ;
1920use Hyperf \Di \Container ;
2021use Hyperf \Engine \Channel as Chan ;
2122use Hyperf \Pool \Channel ;
3031use PHPUnit \Framework \TestCase ;
3132use Psr \EventDispatcher \EventDispatcherInterface ;
3233use RedisCluster ;
34+ use Throwable ;
3335
3436use function Hyperf \Coroutine \go ;
3537
@@ -213,14 +215,195 @@ public function testRedisPipeline()
213215 $ this ->assertSame ([['C ' , 'D ' ], true ], $ chan2 ->pop ());
214216 }
215217
218+ public function testPipelineCallbackAndSelect ()
219+ {
220+ $ redis = $ this ->getRedis ();
221+ (new Waiter ())->wait (function () use ($ redis ) {
222+ $ redis ->select (1 );
223+
224+ $ redis ->set ('concurrent_pipeline_test_callback_and_select_value ' , $ id = uniqid (), 600 );
225+
226+ $ key = 'concurrent_pipeline_test_callback_and_select ' ;
227+
228+ $ redis ->pipeline (function (\Redis $ pipe ) use ($ key ) {
229+ $ pipe ->set ($ key , "value_ {$ key }" );
230+ $ pipe ->incr ("{$ key }_counter " );
231+ $ pipe ->get ($ key );
232+ $ pipe ->get ("{$ key }_counter " );
233+ });
234+
235+ $ this ->assertSame ($ id , $ redis ->get ('concurrent_pipeline_test_callback_and_select_value ' ));
236+ });
237+ }
238+
239+ public function testPipelineCallbackAndPipeline ()
240+ {
241+ $ redis = $ this ->getRedis ();
242+ (new Waiter ())->wait (function () use ($ redis ) {
243+ $ r = $ redis ->pipeline ();
244+
245+ $ redis ->set ('concurrent_pipeline_test_callback_and_select_value ' , $ id = uniqid (), 600 );
246+
247+ $ key = 'concurrent_pipeline_test_callback_and_select ' ;
248+
249+ $ redis ->pipeline (function (\Redis $ pipe ) use ($ key ) {
250+ $ pipe ->set ($ key , "value_ {$ key }" );
251+ $ pipe ->incr ("{$ key }_counter " );
252+ $ pipe ->get ($ key );
253+ $ pipe ->get ("{$ key }_counter " );
254+ });
255+
256+ go (static function () use ($ redis ) {
257+ $ redis ->select (1 );
258+ $ redis ->set ('xxx ' , 'x ' );
259+ $ redis ->set ('xxx ' , 'x ' );
260+ $ redis ->set ('xxx ' , 'x ' );
261+ });
262+
263+ $ r ->set ('xxxxxx ' , 'x ' );
264+ $ r ->set ('xxxxxx ' , 'x ' );
265+ $ r ->set ('xxxxxx ' , 'x ' );
266+ $ r ->set ('xxxxxx ' , 'x ' );
267+
268+ $ this ->assertTrue (true );
269+ });
270+ }
271+
272+ public function testConcurrentPipelineCallbacksWithLimitedConnectionPool ()
273+ {
274+ $ redis = $ this ->getRedis ([], 3 ); // max_connections = 3
275+
276+ $ concurrentOperations = 20 ; // More than max_connections
277+ $ channels = [];
278+
279+ for ($ i = 0 ; $ i < $ concurrentOperations ; ++$ i ) {
280+ $ channels [$ i ] = new Chan (1 );
281+ }
282+
283+ // Start concurrent coroutines using pipeline with callbacks
284+ for ($ i = 0 ; $ i < $ concurrentOperations ; ++$ i ) {
285+ go (function () use ($ redis , $ channels , $ i ) {
286+ try {
287+ $ key = "concurrent_pipeline_test_ {$ i }" ;
288+
289+ $ results = $ redis ->pipeline (function (\Redis $ pipe ) use ($ key ) {
290+ $ pipe ->set ($ key , "value_ {$ key }" );
291+ $ pipe ->incr ("{$ key }_counter " );
292+ $ pipe ->get ($ key );
293+ $ pipe ->get ("{$ key }_counter " );
294+ });
295+
296+ // Simulate work after callback
297+ sleep (1 );
298+
299+ $ this ->assertCount (4 , $ results );
300+ $ this ->assertTrue ($ results [0 ]);
301+ $ this ->assertSame (1 , $ results [1 ]);
302+ $ this ->assertSame ("value_ {$ key }" , $ results [2 ]);
303+ $ this ->assertSame ('1 ' , $ results [3 ]);
304+
305+ $ channels [$ i ]->push (['success ' => true , 'operation ' => 'pipeline ' ]);
306+ } catch (Throwable $ e ) {
307+ $ channels [$ i ]->push (['success ' => false , 'error ' => $ e ->getMessage ()]);
308+ }
309+ });
310+ }
311+
312+ $ successCount = 0 ;
313+ for ($ i = 0 ; $ i < $ concurrentOperations ; ++$ i ) {
314+ $ result = $ channels [$ i ]->pop (10.0 );
315+ $ this ->assertNotFalse ($ result , "Operation {$ i } timed out - possible connection pool exhaustion " );
316+
317+ if ($ result ['success ' ]) {
318+ ++$ successCount ;
319+ } else {
320+ $ this ->fail ("Concurrent operation {$ i } failed: " . $ result ['error ' ]);
321+ }
322+ }
323+
324+ $ this ->assertSame (
325+ $ concurrentOperations ,
326+ $ successCount ,
327+ "All {$ concurrentOperations } concurrent pipeline operations should succeed with only 3 max connections "
328+ );
329+
330+ // Clean up
331+ for ($ i = 0 ; $ i < $ concurrentOperations ; ++$ i ) {
332+ $ redis ->del ("concurrent_pipeline_test_ {$ i }" );
333+ $ redis ->del ("concurrent_pipeline_test_ {$ i }_counter " );
334+ }
335+ }
336+
337+ public function testConcurrentTransactionCallbacksWithLimitedConnectionPool ()
338+ {
339+ $ redis = $ this ->getRedis ([], 3 ); // max_connections = 3
340+
341+ $ concurrentOperations = 20 ; // More than max_connections
342+ $ channels = [];
343+
344+ for ($ i = 0 ; $ i < $ concurrentOperations ; ++$ i ) {
345+ $ channels [$ i ] = new Chan (1 );
346+ }
347+
348+ // Start concurrent coroutines using transaction with callbacks
349+ for ($ i = 0 ; $ i < $ concurrentOperations ; ++$ i ) {
350+ go (function () use ($ redis , $ channels , $ i ) {
351+ try {
352+ $ key = "concurrent_transaction_test_ {$ i }" ;
353+
354+ $ results = $ redis ->transaction (function (\Redis $ transaction ) use ($ key ) {
355+ $ transaction ->set ($ key , "tx_value_ {$ key }" );
356+ $ transaction ->incr ("{$ key }_counter " );
357+ $ transaction ->get ($ key );
358+ });
359+
360+ // Simulate work after callback
361+ sleep (1 );
362+
363+ $ this ->assertCount (3 , $ results );
364+ $ this ->assertTrue ($ results [0 ]);
365+ $ this ->assertSame (1 , $ results [1 ]);
366+ $ this ->assertSame ("tx_value_ {$ key }" , $ results [2 ]);
367+
368+ $ channels [$ i ]->push (['success ' => true , 'operation ' => 'transaction ' ]);
369+ } catch (Throwable $ e ) {
370+ $ channels [$ i ]->push (['success ' => false , 'error ' => $ e ->getMessage ()]);
371+ }
372+ });
373+ }
374+
375+ $ successCount = 0 ;
376+ for ($ i = 0 ; $ i < $ concurrentOperations ; ++$ i ) {
377+ $ result = $ channels [$ i ]->pop (10.0 );
378+ $ this ->assertNotFalse ($ result , "Transaction operation {$ i } timed out - possible connection pool exhaustion " );
379+
380+ if ($ result ['success ' ]) {
381+ ++$ successCount ;
382+ } else {
383+ $ this ->fail ("Concurrent transaction {$ i } failed: " . $ result ['error ' ]);
384+ }
385+ }
386+
387+ $ this ->assertSame (
388+ $ concurrentOperations ,
389+ $ successCount ,
390+ "All {$ concurrentOperations } concurrent transaction operations should succeed with only 3 max connections "
391+ );
392+
393+ // Clean up
394+ for ($ i = 0 ; $ i < $ concurrentOperations ; ++$ i ) {
395+ $ redis ->del ("concurrent_transaction_test_ {$ i }" );
396+ $ redis ->del ("concurrent_transaction_test_ {$ i }_counter " );
397+ }
398+ }
399+
216400 /**
217401 * @param mixed $options
218402 * @return \Redis|Redis
219403 */
220- private function getRedis ($ options = [])
404+ private function getRedis ($ options = [], int $ maxConnections = 30 )
221405 {
222406 $ container = Mockery::mock (Container::class);
223- $ container ->shouldReceive ('has ' )->with (StdoutLoggerInterface::class)->andReturnFalse ();
224407 $ container ->shouldReceive ('get ' )->once ()->with (ConfigInterface::class)->andReturn (new Config ([
225408 'redis ' => [
226409 'default ' => [
@@ -231,7 +414,7 @@ private function getRedis($options = [])
231414 'options ' => $ options ,
232415 'pool ' => [
233416 'min_connections ' => 1 ,
234- 'max_connections ' => 30 ,
417+ 'max_connections ' => $ maxConnections ,
235418 'connect_timeout ' => 10.0 ,
236419 'wait_timeout ' => 3.0 ,
237420 'heartbeat ' => -1 ,
@@ -240,18 +423,21 @@ private function getRedis($options = [])
240423 ],
241424 ],
242425 ]));
243- $ pool = new RedisPool ($ container , 'default ' );
244426 $ frequency = Mockery::mock (LowFrequencyInterface::class);
245427 $ frequency ->shouldReceive ('isLowFrequency ' )->andReturn (false );
246428 $ container ->shouldReceive ('make ' )->with (Frequency::class, Mockery::any ())->andReturn ($ frequency );
247- $ container ->shouldReceive ('make ' )->with (RedisPool::class, ['name ' => 'default ' ])->andReturn ($ pool );
248- $ container ->shouldReceive ('make ' )->with (Channel::class, ['size ' => 30 ])->andReturn (new Channel (30 ));
429+ $ container ->shouldReceive ('make ' )->with (Channel::class, Mockery::any ())->andReturnUsing (function ($ class , $ args ) {
430+ return new Channel ($ args ['size ' ]);
431+ });
249432 $ container ->shouldReceive ('make ' )->with (PoolOption::class, Mockery::any ())->andReturnUsing (function ($ class , $ args ) {
250433 return new PoolOption (...array_values ($ args ));
251434 });
252435 $ container ->shouldReceive ('has ' )->with (StdoutLoggerInterface::class)->andReturnFalse ();
253436 $ container ->shouldReceive ('has ' )->with (EventDispatcherInterface::class)->andReturnFalse ();
254437
438+ $ pool = new RedisPool ($ container , 'default ' );
439+ $ container ->shouldReceive ('make ' )->with (RedisPool::class, ['name ' => 'default ' ])->andReturn ($ pool );
440+
255441 ApplicationContext::setContainer ($ container );
256442
257443 $ factory = new PoolFactory ($ container );
0 commit comments