From a4d76d23fd4bff882dc404ffb6a789d02af916ca Mon Sep 17 00:00:00 2001
From: Derek Libby <dlibby@deng-dlibb.localdomain>
Date: Wed, 19 Jan 2011 14:31:40 -0500
Subject: [PATCH] Addition of AsyncAppender class and tests.

---
 .../java/ch/qos/logback/classic/AsyncAppender.java |  347 +++++++++++++++++
 .../classic/AsyncAppenderInitializationTest.java   |   59 +++
 .../ch/qos/logback/classic/AsyncAppenderTest.java  |  392 ++++++++++++++++++++
 .../test/resources/AsyncAppender_logback-test.xml  |   21 +
 4 files changed, 819 insertions(+), 0 deletions(-)
 create mode 100644 logback-classic/src/main/java/ch/qos/logback/classic/AsyncAppender.java
 create mode 100644 logback-classic/src/test/java/ch/qos/logback/classic/AsyncAppenderInitializationTest.java
 create mode 100644 logback-classic/src/test/java/ch/qos/logback/classic/AsyncAppenderTest.java
 create mode 100644 logback-classic/src/test/resources/AsyncAppender_logback-test.xml

diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/AsyncAppender.java b/logback-classic/src/main/java/ch/qos/logback/classic/AsyncAppender.java
new file mode 100644
index 0000000..1e6ff45
--- /dev/null
+++ b/logback-classic/src/main/java/ch/qos/logback/classic/AsyncAppender.java
@@ -0,0 +1,347 @@
+/**
+ * Logback: the reliable, generic, fast and flexible logging framework.
+ * Copyright (C) 1999-2009, QOS.ch. All rights reserved.
+ *
+ * This program and the accompanying materials are dual-licensed under
+ * either the terms of the Eclipse Public License v1.0 as published by
+ * the Eclipse Foundation
+ *
+ *   or (per the licensee's choosing)
+ *
+ * under the terms of the GNU Lesser General Public License version 2.1
+ * as published by the Free Software Foundation.
+ */
+
+package ch.qos.logback.classic;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.LoggingEvent;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.UnsynchronizedAppenderBase;
+import ch.qos.logback.core.spi.AppenderAttachable;
+import ch.qos.logback.core.spi.AppenderAttachableImpl;
+import java.util.Iterator;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * This {@link Appender} logs {@link ILoggingEvent}s asynchronously. It acts solely as an event dispatcher and must
+ * therefore be attached to one or more child appenders in order to do useful logging. It is the user's responsibility
+ * to close appenders, typically at the end of the application lifecycle.
+ * <p>
+ * The appender buffers events in a {@link BlockingQueue} on the application thread(s). The appenders {@link Dispatcher}
+ * thread takes events from the head of the queue, and dispatches them to all the appenders that are attached to this
+ * appender. With the method {@link #getQueueSize()} the number of events currently stored in the event queue can be
+ * retrieved.
+ * <p>
+ * The appenders event queue is configured with a maximum capacity of {@link #DEFAULT_QUEUE_CAPACITY 1000} (defined by
+ * the variable {@link #setQueueCapacity(int) QueueCapacity}). If the queue is filled up, then application threads are
+ * blocked from logging new events until the dispatcher thread has had a chance to dispatch one or more events. When the
+ * queue is no longer at its maximum configured capacity, application threads are able to start logging events once
+ * more. Asynchronous logging therefore becomes pseudo-synchronous when the appender is operating at or near the
+ * capacity of its event buffer. This is not necessarily a bad thing. It's the price a threaded application will have to
+ * pay sometimes. The appender is designed to allow the application to keep on running, albeit taking slightly more time
+ * to log events until the pressure on the appenders buffer eases.
+ * <p>
+ * Optimally tuning the size of the appenders event queue for maximum application throughput depends upon several
+ * factors. Any or all of the following factors are likely to cause pseudo-synchronous behavior to be exhibited:
+ * <ul>
+ * <li>Large numbers of application threads</li>
+ * <li>Large numbers of logging events per application call</li>
+ * <li>Large amounts of data per logging event</li>
+ * <li>High latency of child appenders</li>
+ * </ul>
+ * To keep things moving, increasing the size of the appenders queue will generally help, at the expense of heap
+ * available to the application when large numbers of logging events are queued.
+ * <p>
+ * It's possible to attach separate instances of this appender to one another to achieve multi-threaded dispatch. For
+ * example, assume a requirement for two child appenders that each perform relatively expensive operations such as
+ * messaging and file IO. For this case, one could set up a graph of appenders such that the parent asynchronous
+ * appender, A, has two child asynchronous appenders attached, B and C. Let's say B in turn has a child file I/O
+ * appender attached, and C has a child messaging appender attached. This will result in fast dispatch from A to both B
+ * and C via As dispatch thread. Bs dispatch thread will be dedicated to logging to file I/O, and Cs dispatch thread
+ * will be dedicated to logging via messaging.
+ * <p>
+ * Due to performance reasons the "expensive" caller data associated with an event is omitted while the event is
+ * prepared before being added to the event queue. By default only "cheap" data like the applications thread name and
+ * the <a href="http://logback.qos.ch/manual/mdc.html">MDC</a> are copied. To configure the appender to copy the caller
+ * data as well, set the variable {@link #setIncludeCallerData(boolean) IncludeCallerData} to <code>true</code>.
+ * <p>
+ * <u>Sample configuration:</u>
+ * <p>
+ * <code>
+ * &lt;appender name=&quot;ASYNC&quot;
+ * class=&quot;ch.qos.logback.classic.AsyncAppender&quot;&gt;<br/>
+ * &nbsp;&nbsp;&lt;param name=&quot;QueueCapacity&quot; value=&quot;5000&quot;/&gt; &lt;!--
+ * Default is 1000 --&gt;<br/>
+ * &nbsp;&nbsp;&lt;param name=&quot;IncludeCallerData&quot; value=&quot;true&quot;/&gt; &lt;!--
+ * Default is false --&gt;<br/>
+ * &nbsp;&nbsp;&lt;appender-ref ref=&quot;STDOUT&quot;/&gt;<br/>
+ * &lt;/appender&gt;
+ * </code>
+ * @author Torsten Juergeleit
+ * @since 0.9.19
+ */
+public final class AsyncAppender<E> extends UnsynchronizedAppenderBase<E> implements AppenderAttachable<E> {
+
+    public static final String DISPATCHER_THREAD_NAME_PREFIX = "Logback AsyncAppender Dispatcher";
+
+    public static final int DEFAULT_QUEUE_CAPACITY = 1000;
+    public static final boolean DEFAULT_INCLUDE_CALLER_DATA = false;
+    public static final boolean DEFAULT_BLOCK_WHEN_FULL = false;
+
+    /**
+     * The internally used queue is bound to this number of {@link LoggingEvent}s.
+     * <p>
+     * The <code>QueueCapacity</code> variable is set to <code>1000</code> by default.
+     */
+    private int queueCapacity = DEFAULT_QUEUE_CAPACITY;
+
+    /**
+     * Before queuing a {@link LoggingEvent} the caller data is retrieved from the application thread.
+     * <p>
+     * <b>This operation is expensive (a stack trace is created). So don't use it if the caller data is not logged.</b>
+     * <p>
+     * The <code>IncludeCallerData</code> variable is set to <code>false</code> by default.
+     */
+    private boolean includeCallerData = DEFAULT_INCLUDE_CALLER_DATA;
+
+    /**
+     * When the internal queue has reach the capacity indicated by the <code>QueueCapacity</code> variable, this flag
+     * determines what should happen to all subsequent {@link LoggingEvent} received by the appender.  If the flag is
+     * set to true the appender will block until space becomes available.  If set to false the {@link LoggingEvent}
+     * objects will be dropped.
+     * <p>
+     * The <code>BlockWhenFull</code> variable is set to <code>false</code> by default.
+     */
+    private boolean blockWhenFull = DEFAULT_BLOCK_WHEN_FULL;
+
+    /** The appenders we are forwarding events to */
+    private final AppenderAttachableImpl<E> appenders = new AppenderAttachableImpl<E>();
+
+    /** Queue that is used to forward events to the dispatcher thread */
+    private BlockingQueue<E> queue;
+
+    /**
+     * {@link Runnable} which forwards {@link LoggingEvent}s to the attached appenders
+     */
+    private Dispatcher<E> dispatcher;
+
+    /** Thread running the {@link LoggingEvent} {@link Dispatcher} */
+    private Thread dispatcherThread;
+
+    private static final int MAX_EXCEPTION_REPEATS = 3;
+    private int exceptionRepeatCount = 0;
+
+    /**
+     * The default constructor does nothing.
+     */
+    public AsyncAppender() {}
+
+    /**
+     * Sets the capacity of the event queue. The default value is 1000.
+     * @param queueCapacity must be within the range of 1 and {@link Integer.MAX_VALUE}
+     */
+    public void setQueueCapacity(int queueCapacity) {
+        this.queueCapacity = queueCapacity;
+    }
+
+    /**
+     * Specifies if an events caller data should be calculated before the event is added to the event queue. The default
+     * value is <code>false</code>.
+     * @param includeCallerData if <code>true</code> then caller data is calculated
+     */
+    public void setIncludeCallerData(boolean includeCallerData) {
+        this.includeCallerData = includeCallerData;
+    }
+
+    /**
+     * Specifies if the appender should block when the queue backing the appender becomes full. The default value is
+     * <code>false</code>.
+     * @param blockWhenFull if <code>true</code> the appender will block
+     */
+    public void setBlockWhenFull(boolean blockWhenFull) {
+        this.blockWhenFull = blockWhenFull;
+    }
+
+    /**
+     * Returns the number of {@link LoggingEvent}s currently stored in the appenders event queue.
+     * @return current queue size or <code>-1</code> if the appender is not started
+     */
+    public int getQueueSize() {
+        return (super.isStarted() ? queue.size() : -1);
+    }
+
+    @Override
+    public void start() {
+        if (!super.isStarted()) {
+
+            // Start all attached appenders
+            Iterator<Appender<E>> iter = appenders.iteratorForAppenders();
+            int size = 0;
+            while (iter.hasNext()) {
+                iter.next().start();
+                size++;
+            }
+            if (size == 0) {
+                addError("No appender configured");
+                return;
+            }
+
+            // Initialize event queue
+            if (queueCapacity < 1 || queueCapacity > Integer.MAX_VALUE) {
+                addError("Invalid queue capacity of " + queueCapacity);
+                return;
+            }
+            queue = new LinkedBlockingQueue<E>(queueCapacity);
+
+            // Initialize event dispatcher thread
+            dispatcher = new Dispatcher<E>(this, appenders, queue);
+            dispatcherThread = new Thread(dispatcher);
+            dispatcherThread.setName(DISPATCHER_THREAD_NAME_PREFIX + " [" + getName() + "] - " + dispatcherThread.getName());
+            dispatcherThread.setDaemon(true);
+            dispatcherThread.start();
+            super.start();
+        }
+    }
+
+    @Override
+    public void stop() {
+        if (super.isStarted()) {
+            super.stop();
+
+            // Tell the dispatcher we want to stop
+            dispatcher.stop();
+
+            // Interrupt the dispatcher thread to allow it to exit if it's blocking on
+            // the queue
+            dispatcherThread.interrupt();
+            try {
+                dispatcherThread.join();
+            } catch (InterruptedException e) {
+                addError("We're interrupted while waiting for the " + "dispatcher thread to finish", e);
+            }
+
+            // Stop all attached appenders
+            Iterator<Appender<E>> iter = appenders.iteratorForAppenders();
+            while (iter.hasNext()) {
+                iter.next().stop();
+            }
+        }
+    }
+
+    @Override
+    protected void append(E event) {
+        if (!dispatcher.isStopRequested() && dispatcherThread.isAlive()) {
+
+            // Populates the event with information from caller thread
+            if (event instanceof ILoggingEvent) {
+                ILoggingEvent le = (ILoggingEvent)event;
+                le.prepareForDeferredProcessing();
+                if (includeCallerData) {
+                    le.getCallerData();
+                }
+            }
+
+            // Queue up the event -- it will be processed by the dispatcher
+            // thread
+            try {
+                if (!queue.offer(event)) {
+                    addInfo("Blocking queue is full");
+                    if (blockForEvent(event)) {
+                        addInfo("Blocking for event");
+                        queue.put(event);
+                    }
+                }
+            } catch (InterruptedException e) {
+                if (exceptionRepeatCount++ < MAX_EXCEPTION_REPEATS) {
+                    addError("Error while adding event to the work queue", e);
+                }
+            }
+        } else {
+
+            // If the dispatcher is no longer running, handle events
+            // synchronously
+            appenders.appendLoopOnAppenders(event);
+        }
+    }
+
+    protected boolean blockForEvent(E event) throws InterruptedException {
+        // Block if configured to do so, drop otherwise
+        return blockWhenFull;
+    }
+
+    public void addAppender(Appender<E> newAppender) {
+        appenders.addAppender(newAppender);
+    }
+
+    public Iterator<Appender<E>> iteratorForAppenders() {
+        return appenders.iteratorForAppenders();
+    }
+
+    public Appender<E> getAppender(String name) {
+        return appenders.getAppender(name);
+    }
+
+    public boolean isAttached(Appender<E> appender) {
+        return appenders.isAttached(appender);
+    }
+
+    public void detachAndStopAllAppenders() {
+        appenders.detachAndStopAllAppenders();
+    }
+
+    public boolean detachAppender(Appender<E> appender) {
+        return appenders.detachAppender(appender);
+    }
+
+    public boolean detachAppender(String name) {
+        return appenders.detachAppender(name);
+    }
+
+    /**
+     * Used by thread to retrieve {@link LoggingEvent}s from a {@link BlockingQueue} in a loop and forward them to
+     * attached {@link Appender}s. Loop exits if {@link #stop()} is called.
+     */
+    private static class Dispatcher<E> implements Runnable {
+
+        private final Appender<E> asyncAppender;
+        private final AppenderAttachableImpl<E> appenders;
+        private final BlockingQueue<E> queue;
+        private int exceptionRepeatCount = 0;
+
+        // Set to true when the dispatcher thread should exit
+        private final AtomicBoolean stopRequested = new AtomicBoolean(false);
+
+        private Dispatcher(Appender<E> asyncAppender, AppenderAttachableImpl<E> appenders, BlockingQueue<E> queue) {
+            this.asyncAppender = asyncAppender;
+            this.appenders = appenders;
+            this.queue = queue;
+        }
+
+        public void run() {
+            do {
+                try {
+                    E event = queue.take();
+                    appenders.appendLoopOnAppenders(event);
+                } catch (InterruptedException e) {
+                    // We're here ignoring that a taken event may not be consumed by any
+                    // appender
+                } catch (Exception e) {
+                    if (exceptionRepeatCount++ < MAX_EXCEPTION_REPEATS) {
+                        asyncAppender.addError("Error while dispatching event", e);
+                    }
+                }
+            } while (!stopRequested.get());
+        }
+
+        public void stop() {
+            stopRequested.set(true);
+        }
+
+        public boolean isStopRequested() {
+            return stopRequested.get();
+        }
+    }
+}
diff --git a/logback-classic/src/test/java/ch/qos/logback/classic/AsyncAppenderInitializationTest.java b/logback-classic/src/test/java/ch/qos/logback/classic/AsyncAppenderInitializationTest.java
new file mode 100644
index 0000000..1ae2509
--- /dev/null
+++ b/logback-classic/src/test/java/ch/qos/logback/classic/AsyncAppenderInitializationTest.java
@@ -0,0 +1,59 @@
+/**
+ * Logback: the reliable, generic, fast and flexible logging framework.
+ * Copyright (C) 1999-2009, QOS.ch. All rights reserved.
+ *
+ * This program and the accompanying materials are dual-licensed under
+ * either the terms of the Eclipse Public License v1.0 as published by
+ * the Eclipse Foundation
+ *
+ *   or (per the licensee's choosing)
+ *
+ * under the terms of the GNU Lesser General Public License version 2.1
+ * as published by the Free Software Foundation.
+ */
+package ch.qos.logback.classic;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.LoggerFactory;
+
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.util.ContextInitializer;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.joran.spi.JoranException;
+import ch.qos.logback.core.status.StatusManager;
+import ch.qos.logback.core.util.StatusPrinter;
+
+public class AsyncAppenderInitializationTest {
+
+  org.slf4j.Logger logger = LoggerFactory.getLogger(AsyncAppenderInitializationTest.class);
+  LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
+  Logger root = (Logger) LoggerFactory.getLogger("root");
+
+  @Before
+  public void setUp() throws Exception {
+      logger.info("AsyncAppenderInitializationTest");
+  }
+
+  @After
+  public void tearDown() throws Exception {
+      System.clearProperty(ContextInitializer.CONFIG_FILE_PROPERTY);
+      lc.reset(); // we are going to need this context
+  }
+
+  @Test
+  public void testLogbackInitialization() throws JoranException {
+    System.setProperty(ContextInitializer.CONFIG_FILE_PROPERTY, "AsyncAppender_logback-test.xml");
+    new ContextInitializer(lc).autoConfig();
+    Appender appender = root.getAppender("ASYNC");
+    assertNotNull(appender);
+    //StatusPrinter.printInCaseOfErrorsOrWarnings(lc);
+    //StatusManager sm = lc.getStatusManager();
+    //assertEquals("Was expecting no errors", 0, sm.getLevel());
+  }
+}
diff --git a/logback-classic/src/test/java/ch/qos/logback/classic/AsyncAppenderTest.java b/logback-classic/src/test/java/ch/qos/logback/classic/AsyncAppenderTest.java
new file mode 100644
index 0000000..0862757
--- /dev/null
+++ b/logback-classic/src/test/java/ch/qos/logback/classic/AsyncAppenderTest.java
@@ -0,0 +1,392 @@
+/**
+ * Logback: the reliable, generic, fast and flexible logging framework.
+ * Copyright (C) 1999-2009, QOS.ch. All rights reserved.
+ *
+ * This program and the accompanying materials are dual-licensed under
+ * either the terms of the Eclipse Public License v1.0 as published by
+ * the Eclipse Foundation
+ *
+ *   or (per the licensee's choosing)
+ *
+ * under the terms of the GNU Lesser General Public License version 2.1
+ * as published by the Free Software Foundation.
+ */
+package ch.qos.logback.classic;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Test;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.LoggingEvent;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.AppenderBase;
+import ch.qos.logback.core.Context;
+import ch.qos.logback.core.ContextBase;
+import ch.qos.logback.core.Layout;
+import ch.qos.logback.core.LogbackException;
+import ch.qos.logback.core.appender.AbstractAppenderTest;
+import ch.qos.logback.core.helpers.NOPAppender;
+import ch.qos.logback.core.filter.Filter;
+import ch.qos.logback.core.read.ListAppender;
+import ch.qos.logback.core.spi.FilterReply;
+import ch.qos.logback.core.status.Status;
+import ch.qos.logback.core.status.StatusManager;
+
+public class AsyncAppenderTest extends AbstractAppenderTest<Object> {
+
+  protected Appender<Object> getAppender() {
+    return new AsyncAppender<Object>();
+  }
+
+  protected Appender<Object> getConfiguredAppender() {
+    AsyncAppender<Object> aa = new AsyncAppender<Object>();
+    aa.setContext(new ContextBase());
+    aa.addAppender(new NOPAppender<Object>());
+    aa.start();
+    return aa;
+  }
+
+  @Test
+  public void testMissingAppender() {
+    LoggerContext context = new LoggerContext();
+    AsyncAppender<Object> aa = new AsyncAppender<Object>();
+    aa.setContext(context);
+    aa.start();
+    assertFalse(aa.isStarted());
+
+    // Check if the status manager has the exception logged
+    StatusManager manager = context.getStatusManager();
+    assertEquals(Status.ERROR, manager.getLevel());
+    assertEquals(1, manager.getCount());
+  }
+
+  @Test
+  public void testBadAppender() {
+    LoggerContext context = new LoggerContext();
+
+    // Start AsyncAppender with embedded NPE appender
+    AsyncAppender<Object> aa = new AsyncAppender<Object>();
+    aa.setContext(context);
+    aa.addAppender(new NPEAppender<Object>());
+    aa.start();
+    assertTrue(aa.isStarted());
+
+    // Do some logging
+    ILoggingEvent le = createLoggingEvent(context);
+    aa.append(le);
+    aa.append(le);
+    aa.append(le);
+    aa.append(le);
+    aa.append(le);
+
+    // Wait for dispatcher to catch up
+    sleep(100);
+    assertEquals(0, aa.getQueueSize());
+
+    // Check if the status manager has the exceptions logged
+    StatusManager manager = context.getStatusManager();
+    assertEquals(Status.ERROR, manager.getLevel());
+    assertEquals(3, manager.getCount());
+
+    // Stop AsyncAppender
+    aa.stop();
+    assertFalse(aa.isStarted());
+  }
+
+  @Test
+  public void testInvalidQueueCapacity() {
+    LoggerContext context = new LoggerContext();
+
+    // Start AsyncAppender with queue capacity of zero
+    AsyncAppender<Object> aa = new AsyncAppender<Object>();
+    aa.setContext(context);
+    aa.addAppender(new NOPAppender<Object>());
+    aa.setQueueCapacity(0);
+    assertEquals(-1, aa.getQueueSize());
+    aa.start();
+    assertFalse(aa.isStarted());
+
+    // Check if the status manager has the exception logged
+    StatusManager manager = context.getStatusManager();
+    assertEquals(Status.ERROR, manager.getLevel());
+    assertEquals(1, manager.getCount());
+  }
+
+  @Test
+  public void testBasic() {
+    LoggerContext context = new LoggerContext();
+
+    // Start AsyncAppender with embedded list appender
+    ListAppender<ILoggingEvent> la = new ListAppender<ILoggingEvent>();
+    AsyncAppender<ILoggingEvent> aa = new AsyncAppender<ILoggingEvent>();
+    aa.setContext(context);
+    aa.addAppender(la);
+    aa.start();
+    assertTrue(aa.isStarted());
+
+    // Check embedded list appender
+    assertTrue(la.isStarted());
+    assertEquals(0, la.list.size());
+
+    // Do some logging and give the dispatcher thread a chance to finish its
+    // work
+    aa.append(createLoggingEvent(context));
+    sleep(100);
+
+    // Stop AsyncAppender
+    aa.stop();
+    assertFalse(aa.isStarted());
+
+    // Check embedded list appender
+    assertFalse(la.isStarted());
+    assertEquals(1, la.list.size());
+  }
+  
+  @Test
+  public void testLogEventsPreparedForDeferredProcessing() {
+    LoggerContext context = new LoggerContext();
+
+    // Start AsyncAppender with embedded list appender
+    ListAppender<ILoggingEvent> la = new ListAppender<ILoggingEvent>();
+    AsyncAppender<ILoggingEvent> aa = new AsyncAppender<ILoggingEvent>();
+    aa.setContext(context);
+    aa.addAppender(la);
+    aa.start();
+
+    // Do some logging and give the dispatcher thread a chance to finish its
+    // work
+    aa.append(createLoggingEvent(context));
+    sleep(100);
+
+    // Check the event
+    assertEquals(1, la.list.size());
+    ILoggingEvent e = la.list.get(0);
+    assertEquals(Thread.currentThread().getName(), e.getThreadName());
+    assertFalse(e.hasCallerData());
+
+    // Stop AsyncAppender
+    aa.stop();
+    assertFalse(aa.isStarted());
+  }
+
+  @Test
+  public void testLogEventsWithCallerData() {
+    LoggerContext context = new LoggerContext();
+
+    // Start AsyncAppender with embedded list appender
+    ListAppender<ILoggingEvent> la = new ListAppender<ILoggingEvent>();
+    AsyncAppender<ILoggingEvent> aa = new AsyncAppender<ILoggingEvent>();
+    aa.setContext(new ContextBase());
+    aa.addAppender(la);
+    aa.setIncludeCallerData(true);
+    aa.start();
+
+    // Do some logging and give the dispatcher thread a chance to finish its
+    // work
+    aa.append(createLoggingEvent(context));
+    sleep(100);
+
+    // Check the events caller data
+    assertEquals(1, la.list.size());
+    ILoggingEvent e = la.list.get(0);
+    assertTrue(e.hasCallerData());
+
+    // Stop AsyncAppender
+    aa.stop();
+    assertFalse(aa.isStarted());
+  }
+
+  @Test
+  public void testStartStopDispatcherThread() {
+
+    // Start AsyncAppender and wait for dispatcher to start up
+    AsyncAppender<Object> aa = new AsyncAppender<Object>();
+    aa.setContext(new ContextBase());
+    aa.setName("testStartStopDispatcherThread");
+    aa.addAppender(new NOPAppender<Object>());
+    aa.start();
+    assertTrue(aa.isStarted());
+    sleep(100);
+    assertTrue(isDispatcherThreadAlive(aa.getName()));
+
+    // Stop AsyncAppender and check if dispatcher is stopped as well
+    aa.stop();
+    assertFalse(aa.isStarted());
+    assertFalse(isDispatcherThreadAlive(aa.getName()));
+  }
+
+  @Test
+  public void testBlockedCaller() {
+    final LoggerContext context = new LoggerContext();
+
+    // Prepare latches used for thread synchronization
+    final CountDownLatch appenderCalledSignal = new CountDownLatch(1);
+    final CountDownLatch unblockAppenderSignal = new CountDownLatch(1);
+    final CountDownLatch loggedEventsSentSignal = new CountDownLatch(3);
+    final CountDownLatch logEventsReceivedSignal = new CountDownLatch(3);
+
+    // Start AsyncAppender with embedded blocking appender
+    final AsyncAppender<Object> aa = new AsyncAppender<Object>();
+    aa.addAppender(new AppenderBase<Object>() {
+      protected void append(Object event) {
+        logEventsReceivedSignal.countDown();
+        appenderCalledSignal.countDown();
+        try {
+          unblockAppenderSignal.await();
+        } catch (InterruptedException ignored) {}
+      }
+    });
+    aa.setContext(new ContextBase());
+    aa.setQueueCapacity(1);
+    aa.start();
+
+    // Start caller thread which appends 3 log events to the AsyncAppender
+    Thread t = new Thread(new Runnable() {
+      public void run() {
+        ILoggingEvent le = createLoggingEvent(context);
+        aa.append(le); // immediately consumed by the blocking appender
+        loggedEventsSentSignal.countDown();
+        aa.append(le); // fills up the event queue
+        loggedEventsSentSignal.countDown();
+        aa.append(le);
+        loggedEventsSentSignal.countDown();
+      }
+    });
+    t.start();
+
+    // Wait until the blocking appender is called the first time
+    try {
+      appenderCalledSignal.await(100, TimeUnit.MILLISECONDS);
+    } catch (InterruptedException ignored) {}
+    assertEquals("Embedded blocking appender was not called", 2,
+        logEventsReceivedSignal.getCount());
+
+    // Unblock the blocking appender and check the logger thread
+    assertEquals("Logger thread was not blocked", 1, aa.getQueueSize());
+    unblockAppenderSignal.countDown();
+    try {
+      logEventsReceivedSignal.await(100, TimeUnit.MILLISECONDS);
+    } catch (InterruptedException ignored) {
+    }
+    assertEquals("Logger thread still blocked", 0, loggedEventsSentSignal
+        .getCount());
+
+    // Stop AsyncAppender
+    aa.stop();
+    assertFalse(aa.isStarted());
+  }
+
+  private void sleep(long millis) {
+    try {
+      Thread.sleep(millis);
+    } catch (InterruptedException ignored) {
+    }
+  }
+
+  private ILoggingEvent createLoggingEvent(LoggerContext context) {
+    Logger logger = context.getLogger(Logger.ROOT_LOGGER_NAME);
+    ILoggingEvent le = new LoggingEvent("Test", logger, Level.INFO,
+        "Hello World", null, null);
+    return le;
+  }
+
+  private boolean isDispatcherThreadAlive(String appenderName) {
+    String threadNamePrefix = AsyncAppender.DISPATCHER_THREAD_NAME_PREFIX
+        + " [" + appenderName + "] - ";
+    ThreadGroup tg = Thread.currentThread().getThreadGroup();
+    Thread[] threads = new Thread[tg.activeCount()];
+    tg.enumerate(threads);
+    for (Thread thread : threads) {
+      if (thread.getName().startsWith(threadNamePrefix)) {
+        return true;
+      }
+    }
+    return false;
+
+  }
+
+  private static class NPEAppender<E> implements Appender<E> {
+
+    public void doAppend(E event) throws LogbackException {
+      throw new NullPointerException();
+    }
+
+    public Layout<E> getLayout() {
+      return null;
+    }
+
+    public String getName() {
+      return "NPE Appender";
+    }
+
+    public void setLayout(Layout<E> layout) {
+    }
+
+    public void setName(String name) {
+    }
+
+    public boolean isStarted() {
+      return true;
+    }
+
+    public void start() {
+    }
+
+    public void stop() {
+    }
+
+    public void addError(String msg) {
+    }
+
+    public void addError(String msg, Throwable ex) {
+    }
+
+    public void addInfo(String msg) {
+    }
+
+    public void addInfo(String msg, Throwable ex) {
+    }
+
+    public void addStatus(Status status) {
+    }
+
+    public void addWarn(String msg) {
+    }
+
+    public void addWarn(String msg, Throwable ex) {
+    }
+
+    public Context getContext() {
+      return null;
+    }
+
+    public void setContext(Context context) {
+    }
+
+    public void addFilter(Filter<E> newFilter) {
+    }
+
+    public void clearAllFilters() {
+    }
+
+    public List<Filter<E>> getCopyOfAttachedFiltersList() {
+      return null;
+    }
+
+    public FilterReply getFilterChainDecision(E event) {
+      return null;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Filter getFirstFilter() {
+      return null;
+    }
+  }
+}
diff --git a/logback-classic/src/test/resources/AsyncAppender_logback-test.xml b/logback-classic/src/test/resources/AsyncAppender_logback-test.xml
new file mode 100644
index 0000000..5bf344a
--- /dev/null
+++ b/logback-classic/src/test/resources/AsyncAppender_logback-test.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<configuration>
+  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+    <layout class="ch.qos.logback.classic.PatternLayout">
+      <param name="Pattern" value="TEST %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
+    </layout>
+  </appender>
+
+  <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
+    <param name="QueueCapacity" value="5000"/>
+    <!-- Default is 1000 -->
+    <param name="IncludeCallerData" value="true"/>
+    <!-- Default is false -->
+    <appender-ref ref="STDOUT"/>
+  </appender>
+
+  <root>
+    <level value="ERROR"/>
+    <appender-ref ref="ASYNC"/>
+  </root>
+</configuration>
-- 
1.6.0.6

