@@ -245,14 +245,53 @@ function ($e) use (&$wait) {
245245 $ this ->assertFalse ($ wait );
246246 }
247247
248+ public function testQueryStaysPendingWhenClientCanNotSendExcessiveMessageInOneChunk ()
249+ {
250+ $ loop = $ this ->getMockBuilder ('React\EventLoop\LoopInterface ' )->getMock ();
251+ $ loop ->expects ($ this ->once ())->method ('addWriteStream ' );
252+ $ loop ->expects ($ this ->once ())->method ('addReadStream ' );
253+ $ loop ->expects ($ this ->never ())->method ('removeWriteStream ' );
254+ $ loop ->expects ($ this ->never ())->method ('removeReadStream ' );
255+
256+ $ server = stream_socket_server ('tcp://127.0.0.1:0 ' );
257+
258+ $ address = stream_socket_get_name ($ server , false );
259+ $ executor = new TcpTransportExecutor ($ address , $ loop );
260+
261+ $ query = new Query ('google ' . str_repeat ('.com ' , 100 ), Message::TYPE_A , Message::CLASS_IN );
262+
263+ // send a bunch of queries and keep reference to last promise
264+ for ($ i = 0 ; $ i < 8000 ; ++$ i ) {
265+ $ promise = $ executor ->query ($ query );
266+ }
267+
268+ $ client = stream_socket_accept ($ server );
269+ assert (is_resource ($ client ));
270+
271+ $ executor ->handleWritable ();
272+
273+ $ promise ->then (null , 'printf ' );
274+ $ promise ->then ($ this ->expectCallableNever (), $ this ->expectCallableNever ());
275+
276+ $ ref = new \ReflectionProperty ($ executor , 'writePending ' );
277+ $ ref ->setAccessible (true );
278+ $ writePending = $ ref ->getValue ($ executor );
279+
280+ $ this ->assertTrue ($ writePending );
281+ }
282+
248283 public function testQueryStaysPendingWhenClientCanNotSendExcessiveMessageInOneChunkWhenServerClosesSocket ()
249284 {
250- $ writableCallback = null ;
285+ if (PHP_OS === 'Darwin ' ) {
286+ // Skip on macOS because it exhibits what looks like a kernal race condition when sending excessive data to a socket that is about to shut down (EPROTOTYPE)
287+ // Due to this race condition, this is somewhat flaky. Happens around 75% of the time, use `--repeat=100` to reproduce.
288+ // fwrite(): Send of 4260000 bytes failed with errno=41 Protocol wrong type for socket
289+ // @link http://erickt.github.io/blog/2014/11/19/adventures-in-debugging-a-potential-osx-kernel-bug/
290+ $ this ->markTestSkipped ('Skipped on macOS due to possible race condition ' );
291+ }
292+
251293 $ loop = $ this ->getMockBuilder ('React\EventLoop\LoopInterface ' )->getMock ();
252- $ loop ->expects ($ this ->once ())->method ('addWriteStream ' )->with ($ this ->anything (), $ this ->callback (function ($ cb ) use (&$ writableCallback ) {
253- $ writableCallback = $ cb ;
254- return true ;
255- }));
294+ $ loop ->expects ($ this ->once ())->method ('addWriteStream ' );
256295 $ loop ->expects ($ this ->once ())->method ('addReadStream ' );
257296 $ loop ->expects ($ this ->never ())->method ('removeWriteStream ' );
258297 $ loop ->expects ($ this ->never ())->method ('removeReadStream ' );
@@ -262,10 +301,10 @@ public function testQueryStaysPendingWhenClientCanNotSendExcessiveMessageInOneCh
262301 $ address = stream_socket_get_name ($ server , false );
263302 $ executor = new TcpTransportExecutor ($ address , $ loop );
264303
265- $ query = new Query ('google ' . str_repeat ('.com ' , 10000 ), Message::TYPE_A , Message::CLASS_IN );
304+ $ query = new Query ('google ' . str_repeat ('.com ' , 100 ), Message::TYPE_A , Message::CLASS_IN );
266305
267306 // send a bunch of queries and keep reference to last promise
268- for ($ i = 0 ; $ i < 100 ; ++$ i ) {
307+ for ($ i = 0 ; $ i < 2000 ; ++$ i ) {
269308 $ promise = $ executor ->query ($ query );
270309 }
271310
@@ -283,6 +322,57 @@ public function testQueryStaysPendingWhenClientCanNotSendExcessiveMessageInOneCh
283322 $ this ->assertTrue ($ writePending );
284323 }
285324
325+ public function testQueryRejectsWhenClientKeepsSendingWhenServerClosesSocket ()
326+ {
327+ $ loop = $ this ->getMockBuilder ('React\EventLoop\LoopInterface ' )->getMock ();
328+ $ loop ->expects ($ this ->once ())->method ('addWriteStream ' );
329+ $ loop ->expects ($ this ->once ())->method ('addReadStream ' );
330+ $ loop ->expects ($ this ->once ())->method ('removeWriteStream ' );
331+ $ loop ->expects ($ this ->once ())->method ('removeReadStream ' );
332+
333+ $ server = stream_socket_server ('tcp://127.0.0.1:0 ' );
334+
335+ $ address = stream_socket_get_name ($ server , false );
336+ $ executor = new TcpTransportExecutor ($ address , $ loop );
337+
338+ $ query = new Query ('google ' . str_repeat ('.com ' , 100 ), Message::TYPE_A , Message::CLASS_IN );
339+
340+ // send a bunch of queries and keep reference to last promise
341+ for ($ i = 0 ; $ i < 2000 ; ++$ i ) {
342+ $ promise = $ executor ->query ($ query );
343+ }
344+
345+ $ client = stream_socket_accept ($ server );
346+ fclose ($ client );
347+
348+ $ executor ->handleWritable ();
349+
350+ $ ref = new \ReflectionProperty ($ executor , 'writePending ' );
351+ $ ref ->setAccessible (true );
352+ $ writePending = $ ref ->getValue ($ executor );
353+
354+ // We expect an EPIPE (Broken pipe) on second write.
355+ // However, macOS may report EPROTOTYPE (Protocol wrong type for socket) on first write due to kernel race condition.
356+ // fwrite(): Send of 4260000 bytes failed with errno=41 Protocol wrong type for socket
357+ // @link http://erickt.github.io/blog/2014/11/19/adventures-in-debugging-a-potential-osx-kernel-bug/
358+ if ($ writePending ) {
359+ $ executor ->handleWritable ();
360+ }
361+
362+ $ exception = null ;
363+ $ promise ->then (null , function ($ reason ) use (&$ exception ) {
364+ $ exception = $ reason ;
365+ });
366+
367+ // expect EPIPE (Broken pipe), except for macOS kernel race condition or legacy HHVM
368+ $ this ->setExpectedException (
369+ 'RuntimeException ' ,
370+ 'Unable to send query to DNS server ' ,
371+ defined ('SOCKET_EPIPE ' ) && !defined ('HHVM_VERSION ' ) ? (PHP_OS !== 'Darwin ' || $ writePending ? SOCKET_EPIPE : SOCKET_EPROTOTYPE ) : null
372+ );
373+ throw $ exception ;
374+ }
375+
286376 public function testQueryRejectsWhenServerClosesConnection ()
287377 {
288378 $ loop = Factory::create ();
0 commit comments