1919
2020package org .elasticsearch .threadpool ;
2121
22+ import org .apache .logging .log4j .Level ;
23+ import org .apache .logging .log4j .LogManager ;
24+ import org .apache .logging .log4j .Logger ;
25+ import org .apache .logging .log4j .core .LogEvent ;
26+ import org .elasticsearch .common .logging .Loggers ;
2227import org .elasticsearch .common .settings .Settings ;
2328import org .elasticsearch .common .unit .TimeValue ;
2429import org .elasticsearch .common .util .concurrent .AbstractRunnable ;
2530import org .elasticsearch .common .util .concurrent .EsExecutors ;
2631import org .elasticsearch .common .util .concurrent .EsThreadPoolExecutor ;
2732import org .elasticsearch .common .util .concurrent .PrioritizedEsThreadPoolExecutor ;
2833import org .elasticsearch .test .ESTestCase ;
34+ import org .elasticsearch .test .MockLogAppender ;
2935import org .junit .After ;
3036import org .junit .Before ;
3137
3844import java .util .function .Consumer ;
3945
4046import static org .hamcrest .Matchers .containsString ;
47+ import static org .hamcrest .Matchers .equalTo ;
4148import static org .hamcrest .Matchers .hasToString ;
4249import static org .hamcrest .Matchers .instanceOf ;
4350
@@ -108,7 +115,12 @@ public void testExecutionErrorOnSinglePrioritizingThreadPoolExecutor() throws In
108115 try {
109116 checkExecutionError (getExecuteRunner (prioritizedExecutor ));
110117 checkExecutionError (getSubmitRunner (prioritizedExecutor ));
118+ // bias towards timeout
119+ checkExecutionError (r -> prioritizedExecutor .execute (delayMillis (r , 10 ), TimeValue .ZERO , r ));
120+ // race whether timeout or success (but typically biased towards success)
111121 checkExecutionError (r -> prioritizedExecutor .execute (r , TimeValue .ZERO , r ));
122+ // bias towards no timeout.
123+ checkExecutionError (r -> prioritizedExecutor .execute (r , TimeValue .timeValueMillis (10 ), r ));
112124 } finally {
113125 ThreadPool .terminate (prioritizedExecutor , 10 , TimeUnit .SECONDS );
114126 }
@@ -170,10 +182,7 @@ public void testExecutionExceptionOnDefaultThreadPoolTypes() throws InterruptedE
170182 final boolean expectExceptionOnSchedule =
171183 // fixed_auto_queue_size wraps stuff into TimedRunnable, which is an AbstractRunnable
172184 // TODO: this is dangerous as it will silently swallow exceptions, and possibly miss calling a response listener
173- ThreadPool .THREAD_POOL_TYPES .get (executor ) != ThreadPool .ThreadPoolType .FIXED_AUTO_QUEUE_SIZE
174- // scheduler just swallows the exception here
175- // TODO: bubble these exceptions up
176- && ThreadPool .THREAD_POOL_TYPES .get (executor ) != ThreadPool .ThreadPoolType .DIRECT ;
185+ ThreadPool .THREAD_POOL_TYPES .get (executor ) != ThreadPool .ThreadPoolType .FIXED_AUTO_QUEUE_SIZE ;
177186 checkExecutionException (getScheduleRunner (executor ), expectExceptionOnSchedule );
178187 }
179188 }
@@ -219,14 +228,19 @@ public void testExecutionExceptionOnAutoQueueFixedESThreadPoolExecutor() throws
219228 }
220229 }
221230
222- @ AwaitsFix (bugUrl = "https://github.com/elastic/elasticsearch/issues/37708" )
223231 public void testExecutionExceptionOnSinglePrioritizingThreadPoolExecutor () throws InterruptedException {
224232 final PrioritizedEsThreadPoolExecutor prioritizedExecutor = EsExecutors .newSinglePrioritizing ("test" ,
225233 EsExecutors .daemonThreadFactory ("test" ), threadPool .getThreadContext (), threadPool .scheduler ());
226234 try {
227235 checkExecutionException (getExecuteRunner (prioritizedExecutor ), true );
228236 checkExecutionException (getSubmitRunner (prioritizedExecutor ), false );
237+
238+ // bias towards timeout
239+ checkExecutionException (r -> prioritizedExecutor .execute (delayMillis (r , 10 ), TimeValue .ZERO , r ), true );
240+ // race whether timeout or success (but typically biased towards success)
229241 checkExecutionException (r -> prioritizedExecutor .execute (r , TimeValue .ZERO , r ), true );
242+ // bias towards no timeout.
243+ checkExecutionException (r -> prioritizedExecutor .execute (r , TimeValue .timeValueMillis (10 ), r ), true );
230244 } finally {
231245 ThreadPool .terminate (prioritizedExecutor , 10 , TimeUnit .SECONDS );
232246 }
@@ -235,26 +249,39 @@ public void testExecutionExceptionOnSinglePrioritizingThreadPoolExecutor() throw
235249 public void testExecutionExceptionOnScheduler () throws InterruptedException {
236250 final ScheduledThreadPoolExecutor scheduler = Scheduler .initScheduler (Settings .EMPTY );
237251 try {
238- // scheduler just swallows the exceptions
239- // TODO: bubble these exceptions up
240- checkExecutionException (getExecuteRunner (scheduler ), false );
241- checkExecutionException (getSubmitRunner (scheduler ), false );
242- checkExecutionException (r -> scheduler .schedule (r , randomFrom (0 , 1 ), TimeUnit .MILLISECONDS ), false );
252+ checkExecutionException (getExecuteRunner (scheduler ), true );
253+ // while submit does return a Future, we choose to log exceptions anyway,
254+ // since this is the semi-internal SafeScheduledThreadPoolExecutor that is being used,
255+ // which also logs exceptions for schedule calls.
256+ checkExecutionException (getSubmitRunner (scheduler ), true );
257+ checkExecutionException (r -> scheduler .schedule (r , randomFrom (0 , 1 ), TimeUnit .MILLISECONDS ), true );
243258 } finally {
244259 Scheduler .terminate (scheduler , 10 , TimeUnit .SECONDS );
245260 }
246261 }
247262
263+ private Runnable delayMillis (Runnable r , int ms ) {
264+ return () -> {
265+ try {
266+ Thread .sleep (ms );
267+ } catch (InterruptedException e ) {
268+ Thread .currentThread ().interrupt ();
269+ }
270+ r .run ();
271+ };
272+ }
273+
248274 private void checkExecutionException (Consumer <Runnable > runner , boolean expectException ) throws InterruptedException {
249- logger .info ("checking exception for {}" , runner );
250275 final Runnable runnable ;
251276 final boolean willThrow ;
252277 if (randomBoolean ()) {
278+ logger .info ("checking direct exception for {}" , runner );
253279 runnable = () -> {
254280 throw new IllegalStateException ("future exception" );
255281 };
256282 willThrow = expectException ;
257283 } else {
284+ logger .info ("checking abstract runnable exception for {}" , runner );
258285 runnable = new AbstractRunnable () {
259286 @ Override
260287 public void onFailure (Exception e ) {
@@ -275,6 +302,7 @@ protected void doRun() {
275302 o -> {
276303 assertEquals (willThrow , o .isPresent ());
277304 if (willThrow ) {
305+ if (o .get () instanceof Error ) throw (Error ) o .get ();
278306 assertThat (o .get (), instanceOf (IllegalStateException .class ));
279307 assertThat (o .get (), hasToString (containsString ("future exception" )));
280308 }
@@ -313,7 +341,7 @@ Consumer<Runnable> getScheduleRunner(String executor) {
313341 return new Consumer <Runnable >() {
314342 @ Override
315343 public void accept (Runnable runnable ) {
316- threadPool .schedule (randomFrom (TimeValue .ZERO , TimeValue .timeValueMillis (1 )), executor , runnable );
344+ threadPool .schedule (runnable , randomFrom (TimeValue .ZERO , TimeValue .timeValueMillis (1 )), executor );
317345 }
318346
319347 @ Override
@@ -324,42 +352,77 @@ public String toString() {
324352 }
325353
326354 private void runExecutionTest (
327- final Consumer <Runnable > runner ,
328- final Runnable runnable ,
329- final boolean expectThrowable ,
330- final Consumer <Optional <Throwable >> consumer ) throws InterruptedException {
355+ final Consumer <Runnable > runner ,
356+ final Runnable runnable ,
357+ final boolean expectThrowable ,
358+ final Consumer <Optional <Throwable >> consumer ) throws InterruptedException {
331359 final AtomicReference <Throwable > throwableReference = new AtomicReference <>();
332360 final Thread .UncaughtExceptionHandler uncaughtExceptionHandler = Thread .getDefaultUncaughtExceptionHandler ();
333361 final CountDownLatch uncaughtExceptionHandlerLatch = new CountDownLatch (1 );
334362
335363 try {
336364 Thread .setDefaultUncaughtExceptionHandler ((t , e ) -> {
337365 assertTrue (expectThrowable );
338- throwableReference .set ( e );
366+ assertTrue ( "Only one message allowed" , throwableReference .compareAndSet ( null , e ) );
339367 uncaughtExceptionHandlerLatch .countDown ();
340368 });
341369
342370 final CountDownLatch supplierLatch = new CountDownLatch (1 );
343371
344- try {
345- runner .accept (() -> {
346- try {
347- runnable .run ();
348- } finally {
349- supplierLatch .countDown ();
372+ Runnable job = () -> {
373+ try {
374+ runnable .run ();
375+ } finally {
376+ supplierLatch .countDown ();
377+ }
378+ };
379+
380+ // snoop on logging to also handle the cases where exceptions are simply logged in Scheduler.
381+ final Logger schedulerLogger = LogManager .getLogger (Scheduler .SafeScheduledThreadPoolExecutor .class );
382+ final MockLogAppender appender = new MockLogAppender ();
383+ appender .addExpectation (
384+ new MockLogAppender .LoggingExpectation () {
385+ @ Override
386+ public void match (LogEvent event ) {
387+ if (event .getLevel () == Level .WARN ) {
388+ assertThat ("no other warnings than those expected" ,
389+ event .getMessage ().getFormattedMessage (),
390+ equalTo ("uncaught exception in scheduled thread [" + Thread .currentThread ().getName () + "]" ));
391+ assertTrue (expectThrowable );
392+ assertNotNull (event .getThrown ());
393+ assertTrue ("only one message allowed" , throwableReference .compareAndSet (null , event .getThrown ()));
394+ uncaughtExceptionHandlerLatch .countDown ();
395+ }
396+ }
397+
398+ @ Override
399+ public void assertMatched () {
350400 }
351401 });
352- } catch (Throwable t ) {
353- consumer .accept (Optional .of (t ));
354- return ;
355- }
356402
357- supplierLatch .await ();
403+ appender .start ();
404+ Loggers .addAppender (schedulerLogger , appender );
405+ try {
406+ try {
407+ runner .accept (job );
408+ } catch (Throwable t ) {
409+ consumer .accept (Optional .of (t ));
410+ return ;
411+ }
412+
413+ supplierLatch .await ();
358414
359- if (expectThrowable ) {
360- uncaughtExceptionHandlerLatch .await ();
415+ if (expectThrowable ) {
416+ uncaughtExceptionHandlerLatch .await ();
417+ }
418+ } finally {
419+ Loggers .removeAppender (schedulerLogger , appender );
420+ appender .stop ();
361421 }
422+
362423 consumer .accept (Optional .ofNullable (throwableReference .get ()));
424+ } catch (IllegalAccessException e ) {
425+ throw new RuntimeException (e );
363426 } finally {
364427 Thread .setDefaultUncaughtExceptionHandler (uncaughtExceptionHandler );
365428 }
0 commit comments