Uploaded image for project: 'logback'
  1. logback
  2. LOGBACK-1492

AppenderBase is deadlock prone

    XMLWordPrintable

Details

    • Icon: Bug Bug
    • Resolution: Unresolved
    • Icon: Major Major
    • None
    • None
    • None
    • None

    Description

      We encountered a deadlock with a subclass of ch.qos.logback.core.AppenderBase.

      The implementation of append() is:

      @Override
      protected void append(ILoggingEvent event) {
          event.prepareForDeferredProcessing();
          try {
              queue.put(event);
          } catch (InterruptedException e) {
              Thread.currentThread().interrupt();
          }
      }
      

      AppenderBase#doAppend has the signature:

      public synchronized void doAppend(E eventObject)
      

      And it calls the subclass append().

      The deadlock occurred because the subclass of AppenderBase called toString() on an object (via ILoggingEvent#prepareForDeferredProcessing()) while holding a lock in AppenderBase#doAppend. The objects toString() method grabbed a second lock, leading to a deadlock.

      One might argue that an implementation of toString() should not grab a lock, but it's also bad practice to call an alien method while holding a lock.

      From JCIP 10.1.3:
      Invoking an alien method with a lock held is asking for liveness trouble. The alien method might acquire other locks (risking deadlock) or block for an unexpectedly long time, stalling other threads that need the lock you hold

      The test below demonstrates the problem we encountered:

      package ch.qos.logback.core;
      
      import ch.qos.logback.classic.Level;
      import ch.qos.logback.classic.Logger;
      import ch.qos.logback.classic.LoggerContext;
      import ch.qos.logback.classic.spi.ILoggingEvent;
      import org.junit.Before;
      import org.junit.Test;
      
      import java.lang.management.ManagementFactory;
      import java.util.concurrent.CountDownLatch;
      import java.util.concurrent.ExecutorService;
      import java.util.concurrent.Executors;
      import java.util.concurrent.TimeUnit;
      
      import static java.util.concurrent.Executors.newFixedThreadPool;
      import static org.junit.Assert.assertNull;
      
      public class AppenderBaseDeadlockTest {
      
          static CountDownLatch inReload = new CountDownLatch(1);
      
          Logger logger;
          ExecutorService executor;
      
          @Before
          public void setUp() throws Exception {
              LoggerContext context = new LoggerContext();
      
              logger = context.getLogger(Logger.ROOT_LOGGER_NAME);
              logger.detachAndStopAllAppenders();
              logger.setLevel(Level.TRACE);
              logger.addAppender(new AppenderBase<ILoggingEvent>() {
                  {
                      start();
                  }
      
                  @Override
                  protected void append(ILoggingEvent eventObject) {
                      inReload.countDown();
                      eventObject.prepareForDeferredProcessing();
                  }
              });
      
              executor = newFixedThreadPool(2, runnable -> {
                  Thread thread = Executors.defaultThreadFactory().newThread(runnable);
                  thread.setDaemon(true);
                  return thread;
              });
          }
      
          @Test
          public void testDeadlock() throws Exception {
              ClassWithLockOnToString classWithLockOnToString = new ClassWithLockOnToString(logger);
              CountDownLatch start = new CountDownLatch(1);
      
              executor.submit(() -> {
                  start.await();
                  logger.debug("{}", classWithLockOnToString);
                  return null;
              });
      
              executor.submit(() -> {
                  start.await();
                  classWithLockOnToString.reload();
                  return null;
              });
      
              start.countDown();
              executor.shutdown();
      
              if(!executor.awaitTermination(1, TimeUnit.SECONDS)){
                  assertNull(ManagementFactory.getThreadMXBean().findDeadlockedThreads());
              }
          }
      
          static class ClassWithLockOnToString {
              private final Logger logger;
      
              ClassWithLockOnToString(Logger logger) {
                  this.logger = logger;
              }
      
              void reload(){
                  synchronized (this){
                      try {
                          inReload.await();
                      } catch (InterruptedException e) {
                          Thread.currentThread().interrupt();
                      }
                      logger.debug("ClassWithLockOnToString#toStringwreload()");
                  }
              }
      
              @Override
              public String toString() {
                  reload();
                  return "ClassWithLockOnToString#toString()";
              }
          }
      }
      

      A simplified stacktrace:

      ThreadA
        -> logger#debug
        -> AppenderBase#doAppend <-- Grabs Lock A
        -> LoggingEvent#prepareForDeferredProcessing
        -> ClassWithLockOnToString#toString
        -> ClassWithLockOnToString#reload <-- Grabs Lock B
      
      ThreadB
        -> ClassWithLockOnToString#reload <-- Grabs Lock B
        -> logger#debug
        -> AppenderBase#doAppend <-- Grabs Lock A
      

      Full stacktraces of the deadlocked threads

      "pool-1-thread-1@1038" daemon prio=5 tid=0xd nid=NA waiting for monitor entry
        java.lang.Thread.State: BLOCKED
      	 blocks pool-2-thread-1@1043
      	 waiting for pool-2-thread-1@1043 to release lock on <0x42d> (a ch.qos.logback.core.AppenderBaseDeadlockTest$ClassWithLockOnToString)
      	  at ch.qos.logback.core.AppenderBaseDeadlockTest$ClassWithLockOnToString.reload(AppenderBaseDeadlockTest.java:95)
      	  at ch.qos.logback.core.AppenderBaseDeadlockTest$ClassWithLockOnToString.toString(AppenderBaseDeadlockTest.java:105)
      	  at org.slf4j.helpers.MessageFormatter.safeObjectAppend(MessageFormatter.java:292)
      	  at org.slf4j.helpers.MessageFormatter.deeplyAppendParameter(MessageFormatter.java:264)
      	  at org.slf4j.helpers.MessageFormatter.arrayFormat(MessageFormatter.java:226)
      	  at org.slf4j.helpers.MessageFormatter.arrayFormat(MessageFormatter.java:161)
      	  at ch.qos.logback.classic.spi.LoggingEvent.getFormattedMessage(LoggingEvent.java:314)
      	  at ch.qos.logback.classic.spi.LoggingEvent.prepareForDeferredProcessing(LoggingEvent.java:218)
      	  at ch.qos.logback.core.AppenderBaseDeadlockTest$1.append(AppenderBaseDeadlockTest.java:42)
      	  at ch.qos.logback.core.AppenderBaseDeadlockTest$1.append(AppenderBaseDeadlockTest.java:34)
      	  at ch.qos.logback.core.AppenderBase.doAppend(AppenderBase.java:82)
      	  - locked <0x438> (a ch.qos.logback.core.AppenderBaseDeadlockTest$1)
      	  at ch.qos.logback.core.spi.AppenderAttachableImpl.appendLoopOnAppenders(AppenderAttachableImpl.java:51)
      	  at ch.qos.logback.classic.Logger.appendLoopOnAppenders(Logger.java:272)
      	  at ch.qos.logback.classic.Logger.callAppenders(Logger.java:259)
      	  at ch.qos.logback.classic.Logger.buildLoggingEventAndAppend(Logger.java:423)
      	  at ch.qos.logback.classic.Logger.filterAndLog_1(Logger.java:400)
      	  at ch.qos.logback.classic.Logger.debug(Logger.java:488)
      	  at ch.qos.logback.core.AppenderBaseDeadlockTest.lambda$testDeadlock$1(AppenderBaseDeadlockTest.java:60)
      	  at ch.qos.logback.core.AppenderBaseDeadlockTest$$Lambda$2.171497379.call(Unknown Source:-1)
      	  at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
      	  at java.util.concurrent.FutureTask.run(FutureTask.java:-1)
      	  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
      	  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
      	  at java.lang.Thread.run(Thread.java:748)
      
      "pool-2-thread-1@1043" daemon prio=5 tid=0xe nid=NA waiting for monitor entry
        java.lang.Thread.State: BLOCKED
      	 blocks pool-1-thread-1@1038
      	 blocks main@1
      	 waiting for pool-1-thread-1@1038 to release lock on <0x438> (a ch.qos.logback.core.AppenderBaseDeadlockTest$1)
      	  at ch.qos.logback.core.AppenderBase.doAppend(AppenderBase.java:63)
      	  at ch.qos.logback.core.spi.AppenderAttachableImpl.appendLoopOnAppenders(AppenderAttachableImpl.java:51)
      	  at ch.qos.logback.classic.Logger.appendLoopOnAppenders(Logger.java:272)
      	  at ch.qos.logback.classic.Logger.callAppenders(Logger.java:259)
      	  at ch.qos.logback.classic.Logger.buildLoggingEventAndAppend(Logger.java:423)
      	  at ch.qos.logback.classic.Logger.filterAndLog_0_Or3Plus(Logger.java:385)
      	  at ch.qos.logback.classic.Logger.debug(Logger.java:484)
      	  at ch.qos.logback.core.AppenderBaseDeadlockTest$ClassWithLockOnToString.reload(AppenderBaseDeadlockTest.java:99)
      	  - locked <0x42d> (a ch.qos.logback.core.AppenderBaseDeadlockTest$ClassWithLockOnToString)
      	  at ch.qos.logback.core.AppenderBaseDeadlockTest.lambda$testDeadlock$2(AppenderBaseDeadlockTest.java:66)
      	  at ch.qos.logback.core.AppenderBaseDeadlockTest$$Lambda$3.415186196.call(Unknown Source:-1)
      	  at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
      	  at java.util.concurrent.FutureTask.run(FutureTask.java:-1)
      	  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
      	  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
      	  at java.lang.Thread.run(Thread.java:748)
      

      Attachments

        Activity

          People

            logback-dev Logback dev list
            eiden Christoffer Eide
            Votes:
            4 Vote for this issue
            Watchers:
            3 Start watching this issue

            Dates

              Created:
              Updated: