You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2017/10/18 23:19:45 UTC

[sling-org-apache-sling-event-api] 01/11: SLING-6739 : copied bundles/extensions/event into event/api and event/resources - done via svn cp to preserve history of both new parts

This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-event-api.git

commit 139dab1d32d494e3b73674c45f85611c27965a4d
Author: Stefan Egli <st...@apache.org>
AuthorDate: Wed Mar 29 07:57:17 2017 +0000

    SLING-6739 : copied bundles/extensions/event into event/api and event/resources - done via svn cp to preserve history of both new parts
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1789290 13f79535-47bb-0310-9956-ffa450edef68
---
 .gitignore                                         |   1 +
 README.md                                          |  85 +++
 pom.xml                                            | 377 ++++++++++
 .../sling/event/impl/EnvironmentComponent.java     |  82 +++
 .../sling/event/impl/EventingThreadPool.java       | 129 ++++
 .../sling/event/impl/jobs/InternalJobState.java    |  42 ++
 .../sling/event/impl/jobs/JobBuilderImpl.java      |  70 ++
 .../sling/event/impl/jobs/JobConsumerManager.java  | 511 ++++++++++++++
 .../apache/sling/event/impl/jobs/JobHandler.java   | 284 ++++++++
 .../org/apache/sling/event/impl/jobs/JobImpl.java  | 411 +++++++++++
 .../sling/event/impl/jobs/JobManagerImpl.java      | 759 +++++++++++++++++++++
 .../sling/event/impl/jobs/JobTopicTraverser.java   | 176 +++++
 .../org/apache/sling/event/impl/jobs/Utility.java  | 281 ++++++++
 .../jobs/config/ConfigurationChangeListener.java   |  33 +
 .../impl/jobs/config/ConfigurationConstants.java   |  48 ++
 .../jobs/config/InternalQueueConfiguration.java    | 402 +++++++++++
 .../impl/jobs/config/JobManagerConfiguration.java  | 661 ++++++++++++++++++
 .../impl/jobs/config/MainQueueConfiguration.java   | 115 ++++
 .../jobs/config/QueueConfigurationManager.java     | 169 +++++
 .../impl/jobs/config/TopologyCapabilities.java     | 324 +++++++++
 .../event/impl/jobs/config/TopologyHandler.java    | 114 ++++
 .../event/impl/jobs/console/InventoryPlugin.java   | 471 +++++++++++++
 .../event/impl/jobs/console/WebConsolePlugin.java  | 453 ++++++++++++
 .../event/impl/jobs/jmx/AbstractJobStatistics.java | 100 +++
 .../event/impl/jobs/jmx/AllJobStatisticsMBean.java |  57 ++
 .../sling/event/impl/jobs/jmx/EmptyStatistics.java |  79 +++
 .../sling/event/impl/jobs/jmx/QueueMBeanImpl.java  |  50 ++
 .../event/impl/jobs/jmx/QueueStatusEvent.java      |  51 ++
 .../sling/event/impl/jobs/jmx/QueuesMBeanImpl.java | 185 +++++
 .../impl/jobs/notifications/NewJobSender.java      | 123 ++++
 .../jobs/notifications/NotificationUtility.java    |  77 +++
 .../impl/jobs/queues/JobExecutionContextImpl.java  | 124 ++++
 .../impl/jobs/queues/JobExecutionResultImpl.java   |  97 +++
 .../sling/event/impl/jobs/queues/JobQueueImpl.java | 708 +++++++++++++++++++
 .../event/impl/jobs/queues/QueueJobCache.java      | 344 ++++++++++
 .../sling/event/impl/jobs/queues/QueueManager.java | 447 ++++++++++++
 .../event/impl/jobs/queues/QueueServices.java      |  49 ++
 .../event/impl/jobs/queues/ResultBuilderImpl.java  |  57 ++
 .../jobs/scheduling/JobScheduleBuilderImpl.java    | 120 ++++
 .../impl/jobs/scheduling/JobSchedulerImpl.java     | 566 +++++++++++++++
 .../impl/jobs/scheduling/ScheduledJobHandler.java  | 545 +++++++++++++++
 .../impl/jobs/scheduling/ScheduledJobInfoImpl.java | 193 ++++++
 .../event/impl/jobs/stats/StatisticsImpl.java      | 319 +++++++++
 .../event/impl/jobs/stats/StatisticsManager.java   | 182 +++++
 .../event/impl/jobs/stats/TopicStatisticsImpl.java | 169 +++++
 .../event/impl/jobs/tasks/CheckTopologyTask.java   | 333 +++++++++
 .../sling/event/impl/jobs/tasks/CleanUpTask.java   | 275 ++++++++
 .../impl/jobs/tasks/FindUnfinishedJobsTask.java    | 137 ++++
 .../event/impl/jobs/tasks/HistoryCleanUpTask.java  | 255 +++++++
 .../sling/event/impl/jobs/tasks/UpgradeTask.java   | 279 ++++++++
 .../event/impl/support/BatchResourceRemover.java   |  55 ++
 .../sling/event/impl/support/Environment.java      |  35 +
 .../event/impl/support/ExactTopicMatcher.java      |  44 ++
 .../event/impl/support/PackageTopicMatcher.java    |  51 ++
 .../sling/event/impl/support/ResourceHelper.java   | 426 ++++++++++++
 .../sling/event/impl/support/ScheduleInfoImpl.java | 421 ++++++++++++
 .../impl/support/SubPackagesTopicMatcher.java      |  52 ++
 .../sling/event/impl/support/TopicMatcher.java     |  28 +
 .../event/impl/support/TopicMatcherHelper.java     |  69 ++
 src/main/java/org/apache/sling/event/jobs/Job.java | 351 ++++++++++
 .../org/apache/sling/event/jobs/JobBuilder.java    | 161 +++++
 .../org/apache/sling/event/jobs/JobManager.java    | 223 ++++++
 .../sling/event/jobs/NotificationConstants.java    | 106 +++
 .../java/org/apache/sling/event/jobs/Queue.java    |  97 +++
 .../sling/event/jobs/QueueConfiguration.java       | 111 +++
 .../org/apache/sling/event/jobs/ScheduleInfo.java  |  89 +++
 .../apache/sling/event/jobs/ScheduledJobInfo.java  |  89 +++
 .../org/apache/sling/event/jobs/Statistics.java    | 112 +++
 .../apache/sling/event/jobs/TopicStatistics.java   |  87 +++
 .../sling/event/jobs/consumer/JobConsumer.java     | 133 ++++
 .../event/jobs/consumer/JobExecutionContext.java   | 144 ++++
 .../event/jobs/consumer/JobExecutionResult.java    |  71 ++
 .../sling/event/jobs/consumer/JobExecutor.java     | 104 +++
 .../sling/event/jobs/consumer/package-info.java    |  23 +
 .../apache/sling/event/jobs/jmx/QueuesMBean.java   |  28 +
 .../sling/event/jobs/jmx/StatisticsMBean.java      |  34 +
 .../apache/sling/event/jobs/jmx/package-info.java  |  23 +
 .../org/apache/sling/event/jobs/package-info.java  |  23 +
 src/main/resources/SLING-INF/nodetypes/event.cnd   |  42 ++
 .../java/org/apache/sling/event/impl/Barrier.java  |  57 ++
 .../java/org/apache/sling/event/impl/TestUtil.java |  53 ++
 .../jobs/InstanceDescriptionComparatorTest.java    | 199 ++++++
 .../event/impl/jobs/JobConsumerManagerTest.java    | 198 ++++++
 .../apache/sling/event/impl/jobs/JobsImplTest.java |  62 ++
 .../sling/event/impl/jobs/StatisticsImplTest.java  | 268 ++++++++
 .../apache/sling/event/impl/jobs/UtilityTest.java  |  80 +++
 .../config/InternalQueueConfigurationTest.java     | 195 ++++++
 .../jobs/config/JobManagerConfigurationTest.java   | 259 +++++++
 .../impl/jobs/config/TopologyCapabilitiesTest.java |  71 ++
 .../impl/jobs/jmx/AllJobStatisticsMBeanTest.java   |  69 ++
 .../sling/event/impl/jobs/jmx/DummyStatistics.java |  84 +++
 .../event/impl/jobs/jmx/QueuesMBeanImplTest.java   | 134 ++++
 .../impl/jobs/tasks/HistoryCleanUpTaskTest.java    | 106 +++
 .../sling/event/it/AbstractJobHandlingTest.java    | 462 +++++++++++++
 .../java/org/apache/sling/event/it/ChaosTest.java  | 402 +++++++++++
 .../apache/sling/event/it/ClassloadingTest.java    | 211 ++++++
 .../org/apache/sling/event/it/HistoryTest.java     | 141 ++++
 .../org/apache/sling/event/it/JobHandlingTest.java | 436 ++++++++++++
 .../apache/sling/event/it/OrderedQueueTest.java    | 173 +++++
 .../apache/sling/event/it/RoundRobinQueueTest.java | 172 +++++
 .../org/apache/sling/event/it/SchedulingTest.java  |  87 +++
 .../org/apache/sling/event/it/TimedJobsTest.java   |  86 +++
 .../apache/sling/event/it/TopicMatchingTest.java   | 168 +++++
 .../apache/sling/event/it/UnorderedQueueTest.java  | 172 +++++
 104 files changed, 19496 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..33554c7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+jackrabbit
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a017cb5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,85 @@
+# Sling Event (Jobs) bundle.
+
+For user documentation see https://sling.apache.org/documentation/bundles/apache-sling-eventing-and-job-handling.html. 
+This README contains information on the bundle, APIs and implementation details.
+
+# Bundle
+
+Sling Event contains support for Jobs. It provides an Api for Job, JobManager and Queue, as well as consumer Apis for a 
+JobConsumer. There are ancillary APIs to support the work of these core interfaces. The core APIs are exported from 
+org.apache.sling.event.jobs with the consumers exported from org.apache.sling.event.jobs.consumer.
+
+
+# Design and implementation
+
+
+## Processing model
+
+Jobs are created using the JobManager API. When a Job is created the JobManager writes an entry into the resource tree
+(usually backed up by JCR) via the ResourceResolver. 
+
+For notification of interested parties - not for processing the jobs(!) - adding a job emits an OSGi 
+Event on org/apache/sling/api/resource/Resource/ADDED topic, which is picked up by the 
+[NewJobSender](src/main/java/org/apache/sling/event/impl/jobs/notifications/NewJobSender.java) which emits a new OSGi
+Event on the org/apache/sling/event/notification/job/ADDED topic.
+
+Similar other notification events are sent out if the state of a job changes. However these events are just FYI events.
+The events used are contained in [NotificationConstants.java](src/main/java/org/apache/sling/event/jobs/NotificationConstants.java).
+
+The QueueManager which identifies the queue from s job is periodically scanning the resource tree for new jobs. In
+addition it listens to the org/apache/sling/event/notification/job/ADDED topic for optimization and picking up new
+jobs faster (than by scanning). Once a new job is found, the manager triggers the JobQueueImpl to start processing. 
+Various other operations ensure that the JobQueueImpl runs jobs according to its configuration. These are either 
+periodic maintenance classes or triggered by calls to the QueueManager or triggered by Jobs on the queue changing state.
+
+The queue is maintained by the JobManagerImpl, but each Queue is managed by a JobQueueImpl that receives calls from the
+QueueManager to process jobs. Any thread may update the persisted job state, by resolving the Job name and performing the operation.
+
+## Storage
+
+The JobManager uses the resource tree and therefore by default the JCR repository provided by Oak for persisting Jobs. The 
+content tree structure was developed in conjunction with advice from the Oak team to avoid write concurrency issues and the 
+need for maintaining in Oak repository locks. To avoid OakMergeConflicts on write, each job gets a UUID. If a new job is
+created on a Sling instance, this instance decides - based on configuration - which Sling instance will process the job
+and writes the new job to an area dedicated to that instance (/var/eventing/jobs/assigned/<SlingID>). Each Sling instance
+only reads and modifies its own subtree under /var/eventing/jobs/assigned/<SlingID>. This prevents more than one instance from
+attempting to process a Job at the same time. This means that when a Job is started, the path of the job is formed from
+the target SlingID and the queue name. The target SlingID is formed from the queue configuration informed
+by properties contained within Topology. Every Sling instance advertises is capabilities for processing jobs via topology,
+hence the JobManager pre-allocates Jobs to instance when the job is created.
+
+The JobQueueImpl then receives the job. The JobQueueImpl only considers jobs allocated to the local instance, and runs
+those jobs. 
+
+Since the SlingID is part of the JobID, there is no risk of 2 instances writing to the same job at the same time when the 
+job is allocated or re-allocated to an instance. In the case of first allocation, the creating sling instance will perform 
+the write operation and the target sling instance wont know about the job until after Oak commits. In the case of re-allocation
+the target sling instance is dead, the cluster leader performs the write and the new target sling instnance wont see the job 
+until after Oak commits.
+
+## Topology changes
+
+When a Sling instance in a cluster is shutdown, it will stop processing all the jobs allocated to it. When it shuts down
+a Topology change event propagates and the cluster leader scans all instances under /var/eventing/jobs/assigned/ to see
+if there are any instances that don't exist any more. If there are, the topology leader moves those jobs to a different
+node by deleting the Oak node and writing a new node into the new targetId assigned location. Any jobs that cant be re-assigned
+are written to the unassigned location.
+
+## Known issues with current implementation and design
+
+These issues may have been addressed since this document was written, if they have please remove the known issues.
+
+1. Pre-allocation of jobs to queues bound to instances will not ensure load is distributed amongst available instances
+especially when the queues are large, as jobs complexity and resource requirements vary wildly.
+2. When the topology changes, with many jobs the cost of reallocating jobs may be prohibitive.
+
+
+# Scheduled Jobs.
+
+In addition to one off jobs the bundle has support for scheduled jobs. The schedule is stored in /var/eventing/scheduled-jobs, 
+and the cluster leader uses the Sling commons Scheduler service to run a schedules which add jobs to the appropriate queues
+using the job manager. For info see org.apache.sling.event.impl.jobs.scheduling.JobSchedulerImpl.execute which is called by the 
+Sling commons Scheduler service.
+
+
+ 
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..4c12105
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,377 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.sling</groupId>
+        <artifactId>sling</artifactId>
+        <version>30</version>
+        <relativePath />
+    </parent>
+
+    <artifactId>org.apache.sling.event</artifactId>
+    <packaging>bundle</packaging>
+    <version>4.2.3-SNAPSHOT</version>
+
+    <name>Apache Sling Event Support</name>
+    <description>
+        Support for eventing.
+    </description>
+
+    <scm>
+        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/event</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/event</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/event</url>
+    </scm>
+
+    <properties>
+        <site.jira.version.id>12315369</site.jira.version.id>
+        <exam.version>4.4.0</exam.version>
+        <url.version>2.4.5</url.version>
+        <bundle.build.dir>${basedir}/target</bundle.build.dir>
+        <bundle.file.name>${bundle.build.dir}/${project.build.finalName}.jar</bundle.file.name>
+        <min.port>37000</min.port>
+        <max.port>37999</max.port>
+    </properties>
+
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Import-Package>
+                            javax.servlet;javax.servlet.http;resolution:=optional,
+                            org.apache.felix.inventory;resolution:=optional,
+                            *
+                        </Import-Package>
+                       <DynamicImport-Package>
+                            javax.servlet,
+                            javax.servlet.http,
+                            org.apache.felix.inventory
+                        </DynamicImport-Package>
+                        <Sling-Nodetypes>
+                            SLING-INF/nodetypes/event.cnd
+                        </Sling-Nodetypes>
+                        <Sling-Namespaces>
+                            slingevent=http://sling.apache.org/jcr/event/1.0
+                        </Sling-Namespaces>
+                        <Embed-Dependency>
+                            jackrabbit-jcr-commons;inline="org/apache/jackrabbit/util/ISO9075.*|org/apache/jackrabbit/util/ISO8601.*|org/apache/jackrabbit/util/XMLChar.*",
+                            org.apache.sling.commons.osgi;inline="org/apache/sling/commons/osgi/PropertiesUtil.*",
+                            quartz;inline="org/quartz/CronExpression.*|org/quartz/ValueSet.*"
+                        </Embed-Dependency>
+                        <_plugin>org.apache.felix.scrplugin.bnd.SCRDescriptorBndPlugin;destdir=${project.build.outputDirectory};</_plugin>
+                    </instructions>
+                </configuration>
+                <dependencies>
+                    <dependency>
+                        <groupId>org.apache.felix</groupId>
+                        <artifactId>org.apache.felix.scr.bnd</artifactId>
+                        <version>1.7.2</version>
+                    </dependency>
+                </dependencies>
+            </plugin>
+           <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>derby.log</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+            <!-- plain unit tests -->
+            <plugin>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>**/it/**</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>build-helper-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>reserve-network-port</id>
+                        <goals>
+                            <goal>reserve-network-port</goal>
+                        </goals>
+                        <phase>pre-integration-test</phase>
+                        <configuration>
+                            <portNames>
+                                <portName>http.port</portName>
+                            </portNames>
+                            <minPortNumber>${min.port}</minPortNumber>
+                            <maxPortNumber>${max.port}</maxPortNumber>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <!-- integration tests run with pax-exam -->
+            <plugin>
+                <artifactId>maven-failsafe-plugin</artifactId>
+                <executions>
+                  <execution>
+                    <goals>
+                      <goal>integration-test</goal>
+                      <goal>verify</goal>
+                    </goals>
+                  </execution>
+                </executions>
+                <configuration>
+                    <systemProperties>
+                        <property>
+                            <name>project.bundle.file</name>
+                            <value>${bundle.file.name}</value>
+                        </property>
+                        <property>
+                            <name>bundle.build.dir</name>
+                            <value>${bundle.build.dir}</value>
+                        </property>
+                        <property>
+                            <name>org.osgi.service.http.port</name>
+                            <value>${http.port}</value>
+                        </property>
+                    </systemProperties>
+                    <argLine>
+                        -Xmx2048m -XX:MaxPermSize=512m
+                    </argLine>
+                    <includes>
+                        <include>**/it/*</include>
+                    </includes>
+                </configuration>
+            </plugin>
+            <plugin>
+                <artifactId>maven-clean-plugin</artifactId>
+                <configuration>
+                    <filesets>
+                        <fileset>
+                            <directory>${basedir}</directory>
+                            <includes>
+                                <include>derby.log</include>
+                            </includes>
+                        </fileset>
+                        <fileset>
+                            <directory>jackrabbit</directory>
+                        </fileset>
+                    </filesets>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <configuration>
+                    <excludePackageNames>org.apache.sling.event.impl:org.apache.sling.event.impl.jobs:org.apache.sling.event.impl.jobs.config:org.apache.sling.event.impl.jobs.console:org.apache.sling.event.impl.jobs.jmx:org.apache.sling.event.impl.jobs.notifications:org.apache.sling.event.impl.jobs.queues:org.apache.sling.event.impl.jobs.scheduling:org.apache.sling.event.impl.jobs.stats:org.apache.sling.event.impl.jobs.tasks:org.apache.sling.event.impl.support</excludePackageNames>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+    <profiles>
+        <profile>
+            <id>port-java8</id>
+            <activation>
+                <activeByDefault>false</activeByDefault>
+                <jdk>1.8</jdk>
+            </activation>
+            <properties>
+                <min.port>38000</min.port>
+                <max.port>38999</max.port>
+            </properties>
+        </profile>
+    </profiles>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.discovery.api</artifactId>
+            <version>1.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.inventory</artifactId>
+            <version>1.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.core</artifactId>
+            <version>6.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.event</artifactId>
+            <version>1.3.1</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.scr.annotations</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.settings</artifactId>
+            <version>1.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.api</artifactId>
+            <version>2.11.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.osgi</artifactId>
+            <version>2.1.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.scheduler</artifactId>
+            <version>2.4.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.threads</artifactId>
+            <version>3.1.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+        	<groupId>org.apache.sling</groupId>
+        	<artifactId>org.apache.sling.discovery.commons</artifactId>
+        	<version>1.0.12</version>
+        	<scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.serviceusermapper</artifactId>
+            <version>1.2.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jackrabbit</groupId>
+            <artifactId>jackrabbit-jcr-commons</artifactId>
+            <version>2.11.2</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.quartz-scheduler</groupId>
+            <artifactId>quartz</artifactId>
+            <version>2.2.0</version>
+            <scope>provided</scope>
+        </dependency>
+      <!-- Webconsole -->
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+        </dependency>
+      <!-- Testing -->
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.cm</artifactId>
+            <version>1.5.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <version>1.10.19</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.testing.tools</artifactId>
+            <version>1.0.6</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.sling</groupId>
+          <artifactId>org.apache.sling.testing.sling-mock</artifactId>
+          <version>1.6.0</version>
+          <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-container-forked</artifactId>
+            <version>${exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-junit4</artifactId>
+            <version>${exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-link-mvn</artifactId>
+            <version>${exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-cm</artifactId>
+            <version>${exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.ops4j.pax.url</groupId>
+            <artifactId>pax-url-aether</artifactId>
+            <version>${url.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.framework</artifactId>
+            <version>5.4.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.inject</groupId>
+            <artifactId>javax.inject</artifactId>
+            <version>1</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/src/main/java/org/apache/sling/event/impl/EnvironmentComponent.java b/src/main/java/org/apache/sling/event/impl/EnvironmentComponent.java
new file mode 100644
index 0000000..b3f7929
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/EnvironmentComponent.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.commons.threads.ThreadPool;
+import org.apache.sling.event.impl.support.Environment;
+import org.apache.sling.settings.SlingSettingsService;
+
+/**
+ * Environment component. This component provides "global settings"
+ * to all services, like the application id and the thread pool.
+ * @since 3.0
+ *
+ * This component needs to be immediate to set the global variables
+ * (application id and thread pool).
+ */
+@Component(immediate=true)
+@Service(value=EnvironmentComponent.class)
+public class EnvironmentComponent {
+
+    /**
+     * Our thread pool.
+     */
+    @Reference(referenceInterface=EventingThreadPool.class)
+    private ThreadPool threadPool;
+
+    /** Sling settings service. */
+    @Reference
+    private SlingSettingsService settingsService;
+
+    /**
+     * Activate this component.
+     */
+    @Activate
+    protected void activate() {
+        // Set the application id and the thread pool
+        Environment.APPLICATION_ID = this.settingsService.getSlingId();
+        Environment.THREAD_POOL = this.threadPool;
+    }
+
+    /**
+     * Deactivate this component.
+     */
+    @Deactivate
+    protected void deactivate() {
+        // Unset the thread pool
+        if ( Environment.THREAD_POOL == this.threadPool ) {
+            Environment.THREAD_POOL = null;
+        }
+    }
+
+    protected void bindThreadPool(final EventingThreadPool etp) {
+        this.threadPool = etp;
+    }
+
+    protected void unbindThreadPool(final EventingThreadPool etp) {
+        if ( this.threadPool == etp ) {
+            this.threadPool = null;
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/EventingThreadPool.java b/src/main/java/org/apache/sling/event/impl/EventingThreadPool.java
new file mode 100644
index 0000000..0821866
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/EventingThreadPool.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl;
+
+import java.util.Map;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Modified;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.commons.osgi.PropertiesUtil;
+import org.apache.sling.commons.threads.ModifiableThreadPoolConfig;
+import org.apache.sling.commons.threads.ThreadPool;
+import org.apache.sling.commons.threads.ThreadPoolConfig;
+import org.apache.sling.commons.threads.ThreadPoolConfig.ThreadPriority;
+import org.apache.sling.commons.threads.ThreadPoolManager;
+
+
+/**
+ * The configurable eventing thread pool.
+ */
+@Component(label="Apache Sling Job Thread Pool",
+        description="This is the thread pool used by the Apache Sling job handling. The "
+                  + "threads from this pool are merely used for executing jobs. By limiting this pool, it is "
+                  + "possible to limit the maximum number of parallel processed jobs - regardless of the queue "
+                  + "configuration.",
+        metatype=true)
+@Service(value=EventingThreadPool.class)
+public class EventingThreadPool implements ThreadPool {
+
+    @Reference
+    private ThreadPoolManager threadPoolManager;
+
+    /** The real thread pool used. */
+    private org.apache.sling.commons.threads.ThreadPool threadPool;
+
+    private static final int DEFAULT_POOL_SIZE = 35;
+
+    @Property(intValue=DEFAULT_POOL_SIZE,
+              label="Pool Size",
+              description="The size of the thread pool. This pool is used to execute jobs and therefore "
+                        + "limits the maximum number of jobs executed in parallel.")
+    private static final String PROPERTY_POOL_SIZE = "minPoolSize";
+
+    public EventingThreadPool() {
+        // default constructor
+    }
+
+    public EventingThreadPool(final ThreadPoolManager tpm, final int poolSize) {
+        this.threadPoolManager = tpm;
+        this.configure(poolSize);
+    }
+
+    public void release() {
+        this.deactivate();
+    }
+
+    /**
+     * Activate this component.
+     */
+    @Activate
+    @Modified
+    protected void activate(final Map<String, Object> props) {
+        final int maxPoolSize = PropertiesUtil.toInteger(props.get(PROPERTY_POOL_SIZE), DEFAULT_POOL_SIZE);
+        this.configure(maxPoolSize);
+    }
+
+    private void configure(final int maxPoolSize) {
+        final ModifiableThreadPoolConfig config = new ModifiableThreadPoolConfig();
+        config.setMinPoolSize(maxPoolSize);
+        config.setMaxPoolSize(config.getMinPoolSize());
+        config.setQueueSize(-1); // unlimited
+        config.setShutdownGraceful(true);
+        config.setPriority(ThreadPriority.NORM);
+        config.setDaemon(true);
+        this.threadPool = threadPoolManager.create(config, "Apache Sling Job Thread Pool");
+    }
+
+    /**
+     * Deactivate this component.
+     */
+    @Deactivate
+    protected void deactivate() {
+        this.threadPoolManager.release(this.threadPool);
+    }
+
+    /**
+     * @see org.apache.sling.commons.threads.ThreadPool#execute(java.lang.Runnable)
+     */
+    @Override
+    public void execute(final Runnable runnable) {
+        threadPool.execute(runnable);
+    }
+
+    /**
+     * @see org.apache.sling.commons.threads.ThreadPool#getConfiguration()
+     */
+    @Override
+    public ThreadPoolConfig getConfiguration() {
+        return threadPool.getConfiguration();
+    }
+
+    /**
+     * @see org.apache.sling.commons.threads.ThreadPool#getName()
+     */
+    @Override
+    public String getName() {
+        return threadPool.getName();
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/InternalJobState.java b/src/main/java/org/apache/sling/event/impl/jobs/InternalJobState.java
new file mode 100644
index 0000000..6fe1994
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/InternalJobState.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs;
+
+import org.apache.sling.event.jobs.NotificationConstants;
+import org.apache.sling.event.jobs.consumer.JobExecutor;
+
+/**
+ * The state of the job after it has been processed by a {@link JobExecutor}.
+ */
+public enum InternalJobState {
+
+    SUCCEEDED(NotificationConstants.TOPIC_JOB_FINISHED),    // processing finished successfully
+    FAILED(NotificationConstants.TOPIC_JOB_FAILED),         // processing failed, can be retried
+    CANCELLED(NotificationConstants.TOPIC_JOB_CANCELLED);   // processing failed permanently
+
+    private final String topic;
+
+    InternalJobState(final String topic) {
+        this.topic = topic;
+    }
+
+    public String getTopic() {
+        return this.topic;
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/JobBuilderImpl.java b/src/main/java/org/apache/sling/event/impl/jobs/JobBuilderImpl.java
new file mode 100644
index 0000000..a662066
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/JobBuilderImpl.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.sling.event.impl.jobs.scheduling.JobScheduleBuilderImpl;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.JobBuilder;
+
+/**
+ * Fluent builder API
+ */
+public class JobBuilderImpl implements JobBuilder {
+
+    private final String topic;
+
+    private final JobManagerImpl jobManager;
+
+    private Map<String, Object> properties;
+
+    public JobBuilderImpl(final JobManagerImpl manager, final String topic) {
+        this.jobManager = manager;
+        this.topic = topic;
+    }
+
+
+    @Override
+    public JobBuilder properties(final Map<String, Object> props) {
+        this.properties = props;
+        return this;
+    }
+
+    @Override
+    public Job add() {
+        return this.add(null);
+    }
+
+    @Override
+    public Job add(final List<String> errors) {
+        return this.jobManager.addJob(this.topic, this.properties, errors);
+    }
+
+    @Override
+    public ScheduleBuilder schedule() {
+        return new JobScheduleBuilderImpl(
+                this.topic,
+                this.properties,
+                UUID.randomUUID().toString(),
+                this.jobManager.getJobScheduler());
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/JobConsumerManager.java b/src/main/java/org/apache/sling/event/impl/jobs/JobConsumerManager.java
new file mode 100644
index 0000000..08660e8
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/JobConsumerManager.java
@@ -0,0 +1,511 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Modified;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.PropertyUnbounded;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.ReferencePolicy;
+import org.apache.felix.scr.annotations.References;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.commons.osgi.PropertiesUtil;
+import org.apache.sling.discovery.PropertyProvider;
+import org.apache.sling.event.impl.jobs.config.TopologyCapabilities;
+import org.apache.sling.event.impl.support.TopicMatcher;
+import org.apache.sling.event.impl.support.TopicMatcherHelper;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.consumer.JobConsumer;
+import org.apache.sling.event.jobs.consumer.JobConsumer.JobResult;
+import org.apache.sling.event.jobs.consumer.JobExecutionContext;
+import org.apache.sling.event.jobs.consumer.JobExecutionResult;
+import org.apache.sling.event.jobs.consumer.JobExecutor;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This component manages/keeps track of all job consumer services.
+ */
+@Component(label="Apache Sling Job Consumer Manager",
+           description="The consumer manager controls the job consumer (= processors). "
+                     + "It can be used to temporarily disable job processing on the current instance. Other instances "
+                     + "in a cluster are not affected.",
+           metatype=true)
+@Service(value=JobConsumerManager.class)
+@References({
+    @Reference(referenceInterface=JobConsumer.class,
+            cardinality=ReferenceCardinality.OPTIONAL_MULTIPLE,
+            policy=ReferencePolicy.DYNAMIC),
+    @Reference(referenceInterface=JobExecutor.class,
+            cardinality=ReferenceCardinality.OPTIONAL_MULTIPLE,
+            policy=ReferencePolicy.DYNAMIC)
+})
+@Property(name="org.apache.sling.installer.configuration.persist", boolValue=false,
+          label="Distribute config",
+          description="If this is disabled, the configuration is not persisted on save in the cluster and is "
+                    + "only used on the current instance. This option should always be disabled!")
+public class JobConsumerManager {
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    @Property(unbounded=PropertyUnbounded.ARRAY, value = "*",
+              label="Topic Whitelist",
+              description="This is a list of topics which currently should be "
+                        + "processed by this instance. Leaving it empty, all job consumers are disabled. Putting a '*' as "
+                        + "one entry, enables all job consumers. Adding separate topics enables job consumers for exactly "
+                        + "this topic.")
+    private static final String PROPERTY_WHITELIST = "job.consumermanager.whitelist";
+
+    @Property(unbounded=PropertyUnbounded.ARRAY,
+              label="Topic Blacklist",
+              description="This is a list of topics which currently shouldn't be "
+                        + "processed by this instance. Leaving it empty, all job consumers are enabled. Putting a '*' as "
+                        + "one entry, disables all job consumers. Adding separate topics disables job consumers for exactly "
+                        + "this topic.")
+    private static final String PROPERTY_BLACKLIST = "job.consumermanager.blacklist";
+
+    /** The map with the consumers, keyed by topic, sorted by service ranking. */
+    private final Map<String, List<ConsumerInfo>> topicToConsumerMap = new HashMap<String, List<ConsumerInfo>>();
+
+    /** ServiceRegistration for propagation. */
+    private ServiceRegistration propagationService;
+
+    private String topics;
+
+    private TopicMatcher[] whitelistMatchers;
+
+    private TopicMatcher[] blacklistMatchers;
+
+    private volatile long changeCount;
+
+    private BundleContext bundleContext;
+
+    private final Map<String, Object[]> listenerMap = new HashMap<String, Object[]>();
+
+    private Dictionary<String, Object> getRegistrationProperties() {
+        final Dictionary<String, Object> serviceProps = new Hashtable<String, Object>();
+        serviceProps.put(PropertyProvider.PROPERTY_PROPERTIES, TopologyCapabilities.PROPERTY_TOPICS);
+        // we add a changing property to the service registration
+        // to make sure a modification event is really sent
+        synchronized ( this ) {
+            serviceProps.put("changeCount", this.changeCount++);
+        }
+        return serviceProps;
+    }
+
+    @Activate
+    protected void activate(final BundleContext bc, final Map<String, Object> props) {
+        this.bundleContext = bc;
+        this.modified(bc, props);
+    }
+
+    @Modified
+    protected void modified(final BundleContext bc, final Map<String, Object> props) {
+        final boolean wasEnabled = this.propagationService != null;
+        this.whitelistMatchers = TopicMatcherHelper.buildMatchers(PropertiesUtil.toStringArray(props.get(PROPERTY_WHITELIST)));
+        this.blacklistMatchers = TopicMatcherHelper.buildMatchers(PropertiesUtil.toStringArray(props.get(PROPERTY_BLACKLIST)));
+
+        final boolean enable = this.whitelistMatchers != null && this.blacklistMatchers != TopicMatcherHelper.MATCH_ALL;
+        if ( wasEnabled != enable ) {
+            synchronized ( this.topicToConsumerMap ) {
+                this.calculateTopics(enable);
+            }
+            if ( enable ) {
+                logger.debug("Registering property provider with: {}", this.topics);
+                this.propagationService = bc.registerService(PropertyProvider.class.getName(),
+                        new PropertyProvider() {
+
+                            @Override
+                            public String getProperty(final String name) {
+                                if ( TopologyCapabilities.PROPERTY_TOPICS.equals(name) ) {
+                                    return topics;
+                                }
+                                return null;
+                            }
+                        }, this.getRegistrationProperties());
+            } else {
+                logger.debug("Unregistering property provider with");
+                this.propagationService.unregister();
+                this.propagationService = null;
+            }
+        } else if ( enable ) {
+            // update properties
+            synchronized ( this.topicToConsumerMap ) {
+                this.calculateTopics(true);
+            }
+            logger.debug("Updating property provider with: {}", this.topics);
+            this.propagationService.setProperties(this.getRegistrationProperties());
+        }
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        if ( this.propagationService != null ) {
+            this.propagationService.unregister();
+            this.propagationService = null;
+        }
+        this.bundleContext = null;
+        synchronized ( this.topicToConsumerMap ) {
+            this.topicToConsumerMap.clear();
+            this.listenerMap.clear();
+        }
+    }
+
+    /**
+     * Get the executor for the topic.
+     * @param topic The job topic
+     * @return A consumer or <code>null</code>
+     */
+    public JobExecutor getExecutor(final String topic) {
+        synchronized ( this.topicToConsumerMap ) {
+            final List<ConsumerInfo> consumers = this.topicToConsumerMap.get(topic);
+            if ( consumers != null ) {
+                return consumers.get(0).getExecutor(this.bundleContext);
+            }
+            int pos = topic.lastIndexOf('/');
+            if ( pos > 0 ) {
+                final String category = topic.substring(0, pos + 1).concat("*");
+                final List<ConsumerInfo> categoryConsumers = this.topicToConsumerMap.get(category);
+                if ( categoryConsumers != null ) {
+                    return categoryConsumers.get(0).getExecutor(this.bundleContext);
+                }
+
+                // search deep consumers (since 1.2 of the consumer package)
+                do {
+                    final String subCategory = topic.substring(0, pos + 1).concat("**");
+                    final List<ConsumerInfo> subCategoryConsumers = this.topicToConsumerMap.get(subCategory);
+                    if ( subCategoryConsumers != null ) {
+                        return subCategoryConsumers.get(0).getExecutor(this.bundleContext);
+                    }
+                    pos = topic.lastIndexOf('/', pos - 1);
+                } while ( pos > 0 );
+            }
+        }
+        return null;
+    }
+
+    public void registerListener(final String key, final JobExecutor consumer, final JobExecutionContext handler) {
+        synchronized ( this.topicToConsumerMap ) {
+            this.listenerMap.put(key, new Object[] {consumer, handler});
+        }
+    }
+
+    public void unregisterListener(final String key) {
+        synchronized ( this.topicToConsumerMap ) {
+            this.listenerMap.remove(key);
+        }
+    }
+
+    /**
+     * Return the topics information of this instance.
+     */
+    public String getTopics() {
+        return this.topics;
+    }
+
+    /**
+     * Bind a new consumer
+     * @param serviceReference The service reference to the consumer.
+     */
+    protected void bindJobConsumer(final ServiceReference serviceReference) {
+        this.bindService(serviceReference, true);
+    }
+
+    /**
+     * Unbind a consumer
+     * @param serviceReference The service reference to the consumer.
+     */
+    protected void unbindJobConsumer(final ServiceReference serviceReference) {
+        this.unbindService(serviceReference, true);
+    }
+
+    /**
+     * Bind a new executor
+     * @param serviceReference The service reference to the executor.
+     */
+    protected void bindJobExecutor(final ServiceReference serviceReference) {
+        this.bindService(serviceReference, false);
+    }
+
+    /**
+     * Unbind a executor
+     * @param serviceReference The service reference to the executor.
+     */
+    protected void unbindJobExecutor(final ServiceReference serviceReference) {
+        this.unbindService(serviceReference, false);
+    }
+
+    /**
+     * Bind a consumer or executor
+     * @param serviceReference The service reference to the consumer or executor.
+     * @param isConsumer Indicating whether this is a JobConsumer or JobExecutor
+     */
+    private void bindService(final ServiceReference serviceReference, final boolean isConsumer) {
+        final String[] topics = PropertiesUtil.toStringArray(serviceReference.getProperty(JobConsumer.PROPERTY_TOPICS));
+        if ( topics != null && topics.length > 0 ) {
+            final ConsumerInfo info = new ConsumerInfo(serviceReference, isConsumer);
+            boolean changed = false;
+            synchronized ( this.topicToConsumerMap ) {
+                for(final String t : topics) {
+                    if ( t != null ) {
+                        final String topic = t.trim();
+                        if ( topic.length() > 0 ) {
+                            List<ConsumerInfo> consumers = this.topicToConsumerMap.get(topic);
+                            if ( consumers == null ) {
+                                consumers = new ArrayList<JobConsumerManager.ConsumerInfo>();
+                                this.topicToConsumerMap.put(topic, consumers);
+                                changed = true;
+                            }
+                            consumers.add(info);
+                            Collections.sort(consumers);
+                        }
+                    }
+                }
+                if ( changed ) {
+                    this.calculateTopics(this.propagationService != null);
+                }
+            }
+            if ( changed && this.propagationService != null ) {
+                logger.debug("Updating property provider with: {}", this.topics);
+                this.propagationService.setProperties(this.getRegistrationProperties());
+            }
+        }
+    }
+
+    /**
+     * Unbind a consumer or executor
+     * @param serviceReference The service reference to the consumer or executor.
+     * @param isConsumer Indicating whether this is a JobConsumer or JobExecutor
+     */
+    private void unbindService(final ServiceReference serviceReference, final boolean isConsumer) {
+        final String[] topics = PropertiesUtil.toStringArray(serviceReference.getProperty(JobConsumer.PROPERTY_TOPICS));
+        if ( topics != null && topics.length > 0 ) {
+            final ConsumerInfo info = new ConsumerInfo(serviceReference, isConsumer);
+            boolean changed = false;
+            synchronized ( this.topicToConsumerMap ) {
+                for(final String t : topics) {
+                    if ( t != null ) {
+                        final String topic = t.trim();
+                        if ( topic.length() > 0 ) {
+                            final List<ConsumerInfo> consumers = this.topicToConsumerMap.get(topic);
+                            if ( consumers != null ) { // sanity check
+                                for(final ConsumerInfo oldConsumer : consumers) {
+                                    if ( oldConsumer.equals(info) && oldConsumer.executor != null ) {
+                                        // notify listener
+                                        for(final Object[] listenerObjects : this.listenerMap.values()) {
+                                            if ( listenerObjects[0] == oldConsumer.executor ) {
+                                                final JobExecutionContext context = (JobExecutionContext)listenerObjects[1];
+                                                context.asyncProcessingFinished(context.result().failed());
+                                                break;
+                                            }
+                                        }
+                                    }
+                                }
+                                consumers.remove(info);
+                                if ( consumers.size() == 0 ) {
+                                    this.topicToConsumerMap.remove(topic);
+                                    changed = true;
+                                }
+                            }
+                        }
+                    }
+                }
+                if ( changed ) {
+                    this.calculateTopics(this.propagationService != null);
+                }
+            }
+            if ( changed && this.propagationService != null ) {
+                logger.debug("Updating property provider with: {}", this.topics);
+                this.propagationService.setProperties(this.getRegistrationProperties());
+            }
+        }
+    }
+
+    private boolean match(final String topic, final TopicMatcher[] matchers) {
+        for(final TopicMatcher m : matchers) {
+            if ( m.match(topic) != null ) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void calculateTopics(final boolean enabled) {
+        if ( enabled ) {
+            // create a sorted list - this ensures that the property value
+            // is always the same for the same topics.
+            final List<String> topicList = new ArrayList<String>();
+            for(final String topic : this.topicToConsumerMap.keySet() ) {
+                // check whitelist
+                if ( this.match(topic, this.whitelistMatchers) ) {
+                    // and blacklist
+                    if ( this.blacklistMatchers == null || !this.match(topic, this.blacklistMatchers) ) {
+                        topicList.add(topic);
+                    }
+                }
+            }
+            Collections.sort(topicList);
+
+            final StringBuilder sb = new StringBuilder();
+            boolean first = true;
+            for(final String topic : topicList ) {
+                if ( first ) {
+                    first = false;
+                } else {
+                    sb.append(',');
+                }
+                sb.append(topic);
+            }
+            this.topics = sb.toString();
+        } else {
+            this.topics = null;
+        }
+    }
+
+    /**
+     * Internal class caching some consumer infos like service id and ranking.
+     */
+    private final static class ConsumerInfo implements Comparable<ConsumerInfo> {
+
+        public final ServiceReference serviceReference;
+        private final boolean isConsumer;
+        public JobExecutor executor;
+        public final int ranking;
+        public final long serviceId;
+
+        public ConsumerInfo(final ServiceReference serviceReference, final boolean isConsumer) {
+            this.serviceReference = serviceReference;
+            this.isConsumer = isConsumer;
+            final Object sr = serviceReference.getProperty(Constants.SERVICE_RANKING);
+            if ( sr == null || !(sr instanceof Integer)) {
+                this.ranking = 0;
+            } else {
+                this.ranking = (Integer)sr;
+            }
+            this.serviceId = (Long)serviceReference.getProperty(Constants.SERVICE_ID);
+        }
+
+        @Override
+        public int compareTo(final ConsumerInfo o) {
+            if ( this.ranking < o.ranking ) {
+                return 1;
+            } else if (this.ranking > o.ranking ) {
+                return -1;
+            }
+            // If ranks are equal, then sort by service id in descending order.
+            return (this.serviceId < o.serviceId) ? -1 : 1;
+        }
+
+        @Override
+        public boolean equals(final Object obj) {
+            if ( obj instanceof ConsumerInfo ) {
+                return ((ConsumerInfo)obj).serviceId == this.serviceId;
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return serviceReference.hashCode();
+        }
+
+        public JobExecutor getExecutor(final BundleContext bundleContext) {
+            if ( executor == null ) {
+                if ( this.isConsumer ) {
+                    executor = new JobConsumerWrapper((JobConsumer) bundleContext.getService(this.serviceReference));
+                } else {
+                    executor = (JobExecutor) bundleContext.getService(this.serviceReference);
+                }
+            }
+            return executor;
+        }
+    }
+
+    private final static class JobConsumerWrapper implements JobExecutor {
+
+        private final JobConsumer consumer;
+
+        public JobConsumerWrapper(final JobConsumer consumer) {
+            this.consumer = consumer;
+        }
+
+        @Override
+        public JobExecutionResult process(final Job job, final JobExecutionContext context) {
+            final JobConsumer.AsyncHandler asyncHandler =
+                    new JobConsumer.AsyncHandler() {
+
+                        final Object asyncLock = new Object();
+                        final AtomicBoolean asyncDone = new AtomicBoolean(false);
+
+                        private void check(final JobExecutionResult result) {
+                            synchronized ( asyncLock ) {
+                                if ( !asyncDone.get() ) {
+                                    asyncDone.set(true);
+                                    context.asyncProcessingFinished(result);
+                                } else {
+                                    throw new IllegalStateException("Job is already marked as processed");
+                                }
+                            }
+                        }
+
+                        @Override
+                        public void ok() {
+                            this.check(context.result().succeeded());
+                        }
+
+                        @Override
+                        public void failed() {
+                            this.check(context.result().failed());
+                        }
+
+                        @Override
+                        public void cancel() {
+                            this.check(context.result().cancelled());
+                        }
+                    };
+            ((JobImpl)job).setProperty(JobConsumer.PROPERTY_JOB_ASYNC_HANDLER, asyncHandler);
+            final JobConsumer.JobResult result = this.consumer.process(job);
+            if ( result == JobResult.ASYNC ) {
+                return null;
+            } else if ( result == JobResult.FAILED) {
+                return context.result().failed();
+            } else if ( result == JobResult.OK) {
+                return context.result().succeeded();
+            }
+            return context.result().cancelled();
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/JobHandler.java b/src/main/java/org/apache/sling/event/impl/jobs/JobHandler.java
new file mode 100644
index 0000000..6337b6c
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/JobHandler.java
@@ -0,0 +1,284 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs;
+
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.api.resource.ModifiableValueMap;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.event.impl.jobs.config.JobManagerConfiguration;
+import org.apache.sling.event.impl.jobs.config.QueueConfigurationManager.QueueInfo;
+import org.apache.sling.event.impl.jobs.config.TopologyCapabilities;
+import org.apache.sling.event.impl.support.ResourceHelper;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.Queue;
+import org.apache.sling.event.jobs.consumer.JobExecutor;
+
+
+/**
+ * This object adds actions to a {@link JobImpl}.
+ */
+public class JobHandler {
+
+    private final JobImpl job;
+
+    public volatile long started = -1;
+
+    private volatile boolean isStopped = false;
+
+    private final JobManagerConfiguration configuration;
+
+    private final JobExecutor consumer;
+
+    public JobHandler(final JobImpl job,
+            final JobExecutor consumer,
+            final JobManagerConfiguration configuration) {
+        this.job = job;
+        this.consumer = consumer;
+        this.configuration = configuration;
+    }
+
+    public JobImpl getJob() {
+        return this.job;
+    }
+
+    public JobExecutor getConsumer() {
+        return this.consumer;
+    }
+
+    public boolean startProcessing(final Queue queue) {
+        this.isStopped = false;
+        return this.persistJobProperties(this.job.prepare(queue));
+    }
+
+    /**
+     * Reschedule the job
+     * Update the retry count and remove the started time.
+     * @return <code>true</code> if rescheduling was successful, <code>false</code> otherwise.
+     */
+    public boolean reschedule() {
+        final ResourceResolver resolver = this.configuration.createResourceResolver();
+        try {
+            final Resource jobResource = resolver.getResource(job.getResourcePath());
+            if ( jobResource != null ) {
+                final ModifiableValueMap mvm = jobResource.adaptTo(ModifiableValueMap.class);
+                mvm.put(Job.PROPERTY_JOB_RETRY_COUNT, job.getProperty(Job.PROPERTY_JOB_RETRY_COUNT, Integer.class));
+                if ( job.getProperty(Job.PROPERTY_RESULT_MESSAGE) != null ) {
+                    mvm.put(Job.PROPERTY_RESULT_MESSAGE, job.getProperty(Job.PROPERTY_RESULT_MESSAGE));
+                }
+                mvm.remove(Job.PROPERTY_JOB_STARTED_TIME);
+                mvm.put(JobImpl.PROPERTY_JOB_QUEUED, Calendar.getInstance());
+                try {
+                    resolver.commit();
+                    return true;
+                } catch ( final PersistenceException pe ) {
+                    this.configuration.getMainLogger().debug("Unable to update reschedule properties for job " + job.getId(), pe);
+                }
+            }
+        } finally {
+            resolver.close();
+        }
+
+        return false;
+    }
+
+    /**
+     * Finish a job.
+     * @param state The state of the processing
+     * @param keepJobInHistory whether to keep the job in the job history.
+     * @param duration the duration of the processing.
+     */
+    public void finished(final Job.JobState state,
+                          final boolean keepJobInHistory,
+                          final Long duration) {
+        final boolean isSuccess = (state == Job.JobState.SUCCEEDED);
+        final ResourceResolver resolver = this.configuration.createResourceResolver();
+        try {
+            final Resource jobResource = resolver.getResource(job.getResourcePath());
+            if ( jobResource != null ) {
+                try {
+                    String newPath = null;
+                    if ( keepJobInHistory ) {
+                        final ValueMap vm = ResourceHelper.getValueMap(jobResource);
+                        newPath = this.configuration.getStoragePath(job.getTopic(), job.getId(), isSuccess);
+                        final Map<String, Object> props = new HashMap<String, Object>(vm);
+                        props.put(JobImpl.PROPERTY_FINISHED_STATE, state.name());
+                        if ( isSuccess ) {
+                            // we set the finish date to start date + duration
+                            final Date finishDate = new Date();
+                            finishDate.setTime(job.getProcessingStarted().getTime().getTime() + duration);
+                            final Calendar finishCal = Calendar.getInstance();
+                            finishCal.setTime(finishDate);
+                            props.put(JobImpl.PROPERTY_FINISHED_DATE, finishCal);
+                        } else {
+                            // current time is good enough
+                            props.put(JobImpl.PROPERTY_FINISHED_DATE, Calendar.getInstance());
+                        }
+                        if ( job.getProperty(Job.PROPERTY_RESULT_MESSAGE) != null ) {
+                            props.put(Job.PROPERTY_RESULT_MESSAGE, job.getProperty(Job.PROPERTY_RESULT_MESSAGE));
+                        }
+                        ResourceHelper.getOrCreateResource(resolver, newPath, props);
+                    }
+                    resolver.delete(jobResource);
+                    resolver.commit();
+
+                    if ( keepJobInHistory && configuration.getMainLogger().isDebugEnabled() ) {
+                        if ( isSuccess ) {
+                            configuration.getMainLogger().debug("Kept successful job {} at {}", Utility.toString(job), newPath);
+                        } else {
+                            configuration.getMainLogger().debug("Moved cancelled job {} to {}", Utility.toString(job), newPath);
+                        }
+                    }
+                } catch ( final PersistenceException pe ) {
+                    this.configuration.getMainLogger().warn("Unable to finish job " + job.getId(), pe);
+                } catch (final InstantiationException ie) {
+                    // something happened with the resource in the meantime
+                    this.configuration.getMainLogger().debug("Unable to instantiate job", ie);
+                }
+            }
+        } finally {
+            resolver.close();
+        }
+    }
+
+    /**
+     * Reassign to a new instance.
+     */
+    public void reassign() {
+        final QueueInfo queueInfo = this.configuration.getQueueConfigurationManager().getQueueInfo(job.getTopic());
+        // Sanity check if queue configuration has changed
+        final TopologyCapabilities caps = this.configuration.getTopologyCapabilities();
+        final String targetId = (caps == null ? null : caps.detectTarget(job.getTopic(), job.getProperties(), queueInfo));
+
+        final ResourceResolver resolver = this.configuration.createResourceResolver();
+        try {
+            final Resource jobResource = resolver.getResource(job.getResourcePath());
+            if ( jobResource != null ) {
+                try {
+                    final ValueMap vm = ResourceHelper.getValueMap(jobResource);
+                    final String newPath = this.configuration.getUniquePath(targetId, job.getTopic(), job.getId(), job.getProperties());
+
+                    final Map<String, Object> props = new HashMap<String, Object>(vm);
+                    props.remove(Job.PROPERTY_JOB_QUEUE_NAME);
+                    if ( targetId == null ) {
+                        props.remove(Job.PROPERTY_JOB_TARGET_INSTANCE);
+                    } else {
+                        props.put(Job.PROPERTY_JOB_TARGET_INSTANCE, targetId);
+                    }
+                    props.remove(Job.PROPERTY_JOB_STARTED_TIME);
+
+                    try {
+                        ResourceHelper.getOrCreateResource(resolver, newPath, props);
+                        resolver.delete(jobResource);
+                        resolver.commit();
+                    } catch ( final PersistenceException pe ) {
+                        this.configuration.getMainLogger().warn("Unable to reassign job " + job.getId(), pe);
+                    }
+                } catch (final InstantiationException ie) {
+                    // something happened with the resource in the meantime
+                    this.configuration.getMainLogger().debug("Unable to instantiate job", ie);
+                }
+            }
+        } finally {
+            resolver.close();
+        }
+    }
+
+    /**
+     * Update the property of a job in the resource tree
+     * @param propNames the property names to update
+     * @return {@code true} if the update was successful.
+     */
+    public boolean persistJobProperties(final String... propNames) {
+        if ( propNames != null ) {
+            final ResourceResolver resolver = this.configuration.createResourceResolver();
+            try {
+                final Resource jobResource = resolver.getResource(job.getResourcePath());
+                if ( jobResource != null ) {
+                    final ModifiableValueMap mvm = jobResource.adaptTo(ModifiableValueMap.class);
+                    for(final String propName : propNames) {
+                        final Object val = job.getProperty(propName);
+                        if ( val != null ) {
+                            if ( val.getClass().isEnum() ) {
+                                mvm.put(propName, val.toString());
+                            } else {
+                                mvm.put(propName, val);
+                            }
+                        } else {
+                            mvm.remove(propName);
+                        }
+                    }
+                    resolver.commit();
+
+                    return true;
+                } else {
+                    this.configuration.getMainLogger().debug("No job resource found at {}", job.getResourcePath());
+                }
+            } catch ( final PersistenceException ignore ) {
+                this.configuration.getMainLogger().debug("Unable to persist properties", ignore);
+            } finally {
+                resolver.close();
+            }
+            return false;
+        }
+        return true;
+    }
+
+    public boolean isStopped() {
+        return this.isStopped;
+    }
+
+    public void stop() {
+        this.isStopped = true;
+    }
+
+    public void addToRetryList() {
+        this.configuration.addJobToRetryList(this.job);
+
+    }
+
+    public boolean removeFromRetryList() {
+        return this.configuration.removeJobFromRetryList(this.job);
+    }
+
+    @Override
+    public int hashCode() {
+        return this.job.getId().hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if ( ! (obj instanceof JobHandler) ) {
+            return false;
+        }
+        return this.job.getId().equals(((JobHandler)obj).job.getId());
+    }
+
+    @Override
+    public String toString() {
+        return "JobHandler(" + this.job.getId() + ")";
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/JobImpl.java b/src/main/java/org/apache/sling/event/impl/jobs/JobImpl.java
new file mode 100644
index 0000000..5514ebf
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/JobImpl.java
@@ -0,0 +1,411 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs;
+
+import java.text.MessageFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+import org.apache.sling.event.impl.support.ResourceHelper;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.NotificationConstants;
+import org.apache.sling.event.jobs.Queue;
+
+/**
+ * This object encapsulates all information about a job.
+ */
+public class JobImpl implements Job, Comparable<JobImpl> {
+
+    /** Internal job property containing the resource path. */
+    public static final String PROPERTY_RESOURCE_PATH = "slingevent:path";
+
+    /** Internal job property containing optional delay override. */
+    public static final String PROPERTY_DELAY_OVERRIDE = ":slingevent:delayOverride";
+
+    /**
+     * Internal job property specifying when the job was put into the queue.
+     */
+    public static final String PROPERTY_JOB_QUEUED = "event.job.queued.time";
+
+    /**
+     * This property contains the finished state of a job once it's marked as finished.
+     * The value is either "CANCELLED" or "SUCCEEDED".
+     * This property is read-only and can't be specified when the job is created.
+     */
+    public static final String PROPERTY_FINISHED_STATE = "slingevent:finishedState";
+
+    private final ValueMap properties;
+
+    private final String topic;
+
+    private final String path;
+
+    private final String jobId;
+
+    private final List<Exception> readErrorList;
+
+    private final long counter;
+
+    /**
+     * Create a new job instance
+     *
+     * @param topic The job topic
+     * @param name  The unique job name (optional)
+     * @param jobId The unique (internal) job id
+     * @param properties Non-null map of properties, at least containing {@link #PROPERTY_RESOURCE_PATH}
+     */
+    @SuppressWarnings("unchecked")
+    public JobImpl(final String topic,
+                   final String jobId,
+                   final Map<String, Object> properties) {
+        this.topic = topic;
+        this.jobId = jobId;
+        this.path = (String)properties.remove(PROPERTY_RESOURCE_PATH);
+        this.readErrorList = (List<Exception>) properties.remove(ResourceHelper.PROPERTY_MARKER_READ_ERROR_LIST);
+
+        this.properties = new ValueMapDecorator(properties);
+        this.properties.put(NotificationConstants.NOTIFICATION_PROPERTY_JOB_ID, jobId);
+        final int lastPos = jobId.lastIndexOf('_');
+        this.counter = Long.valueOf(jobId.substring(lastPos + 1));
+    }
+
+    /**
+     * Get the full resource path.
+     */
+    public String getResourcePath() {
+        return this.path;
+    }
+
+    /**
+     * Did we have read errors?
+     */
+    public boolean hasReadErrors() {
+        return this.readErrorList != null;
+    }
+
+    /**
+     * Is the error recoverable?
+     */
+    public boolean isReadErrorRecoverable() {
+        boolean result = true;
+        if ( this.readErrorList != null ) {
+            for(final Exception e : this.readErrorList) {
+                if ( e instanceof RuntimeException ) {
+                    result = false;
+                    break;
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Get all properties
+     */
+    public Map<String, Object> getProperties() {
+        return this.properties;
+    }
+
+    /**
+     * Update the information for a retry
+     */
+    public void retry() {
+        final int retries = this.getProperty(Job.PROPERTY_JOB_RETRY_COUNT, Integer.class);
+        this.properties.put(Job.PROPERTY_JOB_RETRY_COUNT, retries + 1);
+        this.properties.remove(Job.PROPERTY_JOB_STARTED_TIME);
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Job#getTopic()
+     */
+    @Override
+    public String getTopic() {
+        return this.topic;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Job#getId()
+     */
+    @Override
+    public String getId() {
+        return this.jobId;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Job#getProperty(java.lang.String)
+     */
+    @Override
+    public Object getProperty(final String name) {
+        return this.properties.get(name);
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Job#getProperty(java.lang.String, java.lang.Class)
+     */
+    @Override
+    public <T> T getProperty(final String name, final Class<T> type) {
+        return this.properties.get(name, type);
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Job#getProperty(java.lang.String, java.lang.Object)
+     */
+    @Override
+    public <T> T getProperty(final String name, final T defaultValue) {
+        return this.properties.get(name, defaultValue);
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Job#getPropertyNames()
+     */
+    @Override
+    public Set<String> getPropertyNames() {
+        return this.properties.keySet();
+    }
+
+    @Override
+    public int getRetryCount() {
+        return this.getProperty(Job.PROPERTY_JOB_RETRY_COUNT, Integer.class);
+    }
+
+    @Override
+    public int getNumberOfRetries() {
+        return this.getProperty(Job.PROPERTY_JOB_RETRIES, Integer.class);
+    }
+
+    @Override
+    public String getQueueName() {
+        return this.getProperty(Job.PROPERTY_JOB_QUEUE_NAME, String.class);
+    }
+
+    @Override
+    public String getTargetInstance() {
+        return this.getProperty(Job.PROPERTY_JOB_TARGET_INSTANCE, String.class);
+    }
+
+    @Override
+    public Calendar getProcessingStarted() {
+        return this.getProperty(Job.PROPERTY_JOB_STARTED_TIME, Calendar.class);
+    }
+
+    @Override
+    public Calendar getCreated() {
+        return this.getProperty(Job.PROPERTY_JOB_CREATED, Calendar.class);
+    }
+
+    @Override
+    public String getCreatedInstance() {
+        return this.getProperty(Job.PROPERTY_JOB_CREATED_INSTANCE, String.class);
+    }
+
+    /**
+     * Update information about the queue.
+     */
+    public void updateQueueInfo(final Queue queue) {
+        this.properties.put(Job.PROPERTY_JOB_QUEUE_NAME, queue.getName());
+        this.properties.put(Job.PROPERTY_JOB_RETRIES, queue.getConfiguration().getMaxRetries());
+    }
+
+    public void setProperty(final String name, final Object value) {
+        if ( value == null ) {
+            this.properties.remove(name);
+        } else {
+            this.properties.put(name, value);
+        }
+    }
+
+    /**
+     * Prepare a new job execution
+     */
+    public String[] prepare(final Queue queue) {
+        this.updateQueueInfo(queue);
+        this.properties.remove(JobImpl.PROPERTY_DELAY_OVERRIDE);
+        this.properties.remove(Job.PROPERTY_JOB_PROGRESS_LOG);
+        this.properties.remove(Job.PROPERTY_JOB_PROGRESS_ETA);
+        this.properties.remove(Job.PROPERTY_JOB_PROGRESS_STEPS);
+        this.properties.remove(Job.PROPERTY_JOB_PROGRESS_STEP);
+        this.properties.remove(Job.PROPERTY_RESULT_MESSAGE);
+        this.properties.put(Job.PROPERTY_JOB_STARTED_TIME, Calendar.getInstance());
+        return new String[] {Job.PROPERTY_JOB_QUEUE_NAME, Job.PROPERTY_JOB_RETRIES,
+                Job.PROPERTY_JOB_PROGRESS_LOG, Job.PROPERTY_JOB_PROGRESS_ETA, PROPERTY_JOB_PROGRESS_STEPS,
+                PROPERTY_JOB_PROGRESS_STEP, Job.PROPERTY_RESULT_MESSAGE, Job.PROPERTY_JOB_STARTED_TIME};
+    }
+
+    public String[] startProgress(final int steps, final long eta) {
+        if ( steps > 0 ) {
+            this.setProperty(Job.PROPERTY_JOB_PROGRESS_STEPS, steps);
+        }
+        if ( eta > 0 ) {
+            final Date finishDate = new Date(System.currentTimeMillis() + eta * 1000);
+            final Calendar finishCal = Calendar.getInstance();
+            finishCal.setTime(finishDate);
+            this.setProperty(Job.PROPERTY_JOB_PROGRESS_ETA, finishCal);
+        }
+        return new String[] {Job.PROPERTY_JOB_PROGRESS_ETA, PROPERTY_JOB_PROGRESS_STEPS};
+    }
+
+    public String[] setProgress(final int step) {
+        final int steps = this.getProperty(Job.PROPERTY_JOB_PROGRESS_STEPS, -1);
+        if ( steps > 0 && step > 0 ) {
+            int current = this.getProperty(Job.PROPERTY_JOB_PROGRESS_STEP, 0);
+            current += step;
+            if ( current > steps ) {
+                current = steps;
+            }
+            this.setProperty(Job.PROPERTY_JOB_PROGRESS_STEP, current);
+
+            final Calendar now = Calendar.getInstance();
+            final long elapsed = now.getTimeInMillis() - this.getProcessingStarted().getTimeInMillis();
+
+            final long eta = elapsed * steps / step;
+            now.setTimeInMillis(eta);
+            this.setProperty(Job.PROPERTY_JOB_PROGRESS_ETA, now);
+            return new String[] {Job.PROPERTY_JOB_PROGRESS_STEP, Job.PROPERTY_JOB_PROGRESS_ETA};
+        }
+        return null;
+    }
+
+    public String update(final long eta) {
+        if ( eta > 0 ) {
+            final Date finishDate = new Date(System.currentTimeMillis() + eta * 1000);
+            final Calendar finishCal = Calendar.getInstance();
+            finishCal.setTime(finishDate);
+            this.setProperty(Job.PROPERTY_JOB_PROGRESS_ETA, eta);
+        } else {
+            this.properties.remove(Job.PROPERTY_JOB_PROGRESS_ETA);
+        }
+        return Job.PROPERTY_JOB_PROGRESS_ETA;
+    }
+
+    public String log(final String message, final Object... args) {
+        final String logEntry = MessageFormat.format(message, args);
+        final String[] entries = this.getProperty(Job.PROPERTY_JOB_PROGRESS_LOG, String[].class);
+        if ( entries == null ) {
+            this.setProperty(Job.PROPERTY_JOB_PROGRESS_LOG, new String[] {logEntry});
+        } else {
+            final String[] newEntries = new String[entries.length + 1];
+            System.arraycopy(entries, 0, newEntries, 0, entries.length);
+            newEntries[entries.length] = logEntry;
+            this.setProperty(Job.PROPERTY_JOB_PROGRESS_LOG, newEntries);
+        }
+        return Job.PROPERTY_JOB_PROGRESS_LOG;
+    }
+
+    @Override
+    public JobState getJobState() {
+        final String enumValue = this.getProperty(JobImpl.PROPERTY_FINISHED_STATE, String.class);
+        if ( enumValue == null ) {
+            if ( this.getProcessingStarted() != null ) {
+                return JobState.ACTIVE;
+            }
+            return JobState.QUEUED;
+        }
+        return JobState.valueOf(enumValue);
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Job#getFinishedDate()
+     */
+    @Override
+    public Calendar getFinishedDate() {
+        return this.getProperty(Job.PROPERTY_FINISHED_DATE, Calendar.class);
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Job#getResultMessage()
+     */
+    @Override
+    public String getResultMessage() {
+        return this.getProperty(Job.PROPERTY_RESULT_MESSAGE, String.class);
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Job#getProgressLog()
+     */
+    @Override
+    public String[] getProgressLog() {
+        return this.getProperty(Job.PROPERTY_JOB_PROGRESS_LOG, String[].class);
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Job#getProgressStepCount()
+     */
+    @Override
+    public int getProgressStepCount() {
+        return this.getProperty(Job.PROPERTY_JOB_PROGRESS_STEPS, -1);
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Job#getFinishedProgressStep()
+     */
+    @Override
+    public int getFinishedProgressStep() {
+        return this.getProperty(Job.PROPERTY_JOB_PROGRESS_STEP, 0);
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Job#getProgressETA()
+     */
+    @Override
+    public Calendar getProgressETA() {
+        return this.getProperty(Job.PROPERTY_JOB_PROGRESS_ETA, Calendar.class);
+    }
+
+    @Override
+    public int compareTo(final JobImpl o) {
+        int result = this.getCreated().compareTo(o.getCreated());
+        if ( result == 0 ) {
+            if ( this.counter < o.counter ) {
+                result = -1;
+            } else if ( this.counter > o.counter ) {
+                result = 1;
+            } else {
+                result = this.jobId.compareTo(o.jobId);
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public int hashCode() {
+        return this.jobId.hashCode();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if ( obj == this ) {
+            return true;
+        }
+        if ( obj instanceof JobImpl ) {
+            return this.jobId.equals(((JobImpl)obj).jobId);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return "JobImpl [properties=" + properties + ", topic=" + topic
+                + ", path=" + path + ", jobId=" + jobId + "]";
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/JobManagerImpl.java b/src/main/java/org/apache/sling/event/impl/jobs/JobManagerImpl.java
new file mode 100644
index 0000000..b76fda4
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/JobManagerImpl.java
@@ -0,0 +1,759 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Properties;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.jackrabbit.util.ISO9075;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.QuerySyntaxException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.observation.ResourceChange;
+import org.apache.sling.api.resource.observation.ResourceChangeListener;
+import org.apache.sling.commons.scheduler.Scheduler;
+import org.apache.sling.commons.threads.ThreadPoolManager;
+import org.apache.sling.event.impl.jobs.config.JobManagerConfiguration;
+import org.apache.sling.event.impl.jobs.config.QueueConfigurationManager.QueueInfo;
+import org.apache.sling.event.impl.jobs.config.TopologyCapabilities;
+import org.apache.sling.event.impl.jobs.notifications.NotificationUtility;
+import org.apache.sling.event.impl.jobs.queues.JobQueueImpl;
+import org.apache.sling.event.impl.jobs.queues.QueueManager;
+import org.apache.sling.event.impl.jobs.scheduling.JobSchedulerImpl;
+import org.apache.sling.event.impl.jobs.stats.StatisticsManager;
+import org.apache.sling.event.impl.jobs.tasks.CleanUpTask;
+import org.apache.sling.event.impl.support.Environment;
+import org.apache.sling.event.impl.support.ResourceHelper;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.Job.JobState;
+import org.apache.sling.event.jobs.JobBuilder;
+import org.apache.sling.event.jobs.JobManager;
+import org.apache.sling.event.jobs.NotificationConstants;
+import org.apache.sling.event.jobs.Queue;
+import org.apache.sling.event.jobs.ScheduledJobInfo;
+import org.apache.sling.event.jobs.Statistics;
+import org.apache.sling.event.jobs.TopicStatistics;
+import org.apache.sling.event.jobs.jmx.QueuesMBean;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.service.event.EventConstants;
+import org.osgi.service.event.EventHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Implementation of the job manager.
+ */
+@Component(immediate=true)
+@Service(value={JobManager.class, EventHandler.class, Runnable.class})
+@Properties({
+    @Property(name="scheduler.period", longValue=60),
+    @Property(name="scheduler.concurrent", boolValue=false),
+    @Property(name=EventConstants.EVENT_TOPIC,
+              value={ResourceHelper.BUNDLE_EVENT_STARTED,
+                     ResourceHelper.BUNDLE_EVENT_UPDATED})
+})
+public class JobManagerImpl
+    implements JobManager, EventHandler, Runnable {
+
+    /** Default logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    @Reference
+    private EventAdmin eventAdmin;
+
+    @Reference
+    private Scheduler scheduler;
+
+    @Reference
+    private JobConsumerManager jobConsumerManager;
+
+    @Reference
+    private QueuesMBean queuesMBean;
+
+    @Reference
+    private ThreadPoolManager threadPoolManager;
+
+    /** The job manager configuration. */
+    @Reference
+    private JobManagerConfiguration configuration;
+
+    @Reference
+    private StatisticsManager statisticsManager;
+
+    @Reference
+    private QueueManager qManager;
+
+    private volatile CleanUpTask maintenanceTask;
+
+    /** Job Scheduler. */
+    private org.apache.sling.event.impl.jobs.scheduling.JobSchedulerImpl jobScheduler;
+
+    private volatile ServiceRegistration<ResourceChangeListener> changeListenerReg;
+
+    /**
+     * Activate this component.
+     * @param props Configuration properties
+     */
+    @Activate
+    protected void activate(final BundleContext ctx, final Map<String, Object> props) throws LoginException {
+        this.jobScheduler = new org.apache.sling.event.impl.jobs.scheduling.JobSchedulerImpl(this.configuration, this.scheduler, this);
+        this.maintenanceTask = new CleanUpTask(this.configuration, this.jobScheduler);
+
+        final Dictionary<String, Object> regProps = new Hashtable<>();
+        regProps.put(ResourceChangeListener.PATHS, this.configuration.getScheduledJobsPath(false));
+        regProps.put(ResourceChangeListener.CHANGES, new String[] {
+            ResourceChange.ChangeType.ADDED.name(),
+            ResourceChange.ChangeType.CHANGED.name(),
+            ResourceChange.ChangeType.REMOVED.name()
+        });
+        regProps.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");
+        regProps.put(Constants.SERVICE_DESCRIPTION, "Resource change listener for scheduled jobs");
+        this.changeListenerReg = ctx.registerService(ResourceChangeListener.class, this.jobScheduler, regProps);
+        logger.info("Apache Sling Job Manager started on instance {}", Environment.APPLICATION_ID);
+    }
+
+    /**
+     * Deactivate this component.
+     */
+    @Deactivate
+    protected void deactivate() {
+        logger.debug("Apache Sling Job Manager stopping on instance {}", Environment.APPLICATION_ID);
+
+        if ( this.changeListenerReg != null ) {
+            this.changeListenerReg.unregister();
+            this.changeListenerReg = null;
+        }
+
+        this.jobScheduler.deactivate();
+
+        this.maintenanceTask = null;
+        logger.info("Apache Sling Job Manager stopped on instance {}", Environment.APPLICATION_ID);
+    }
+
+    /**
+     * This method is invoked periodically by the scheduler.
+     * In the default configuration every minute
+     * @see java.lang.Runnable#run()
+     */
+    @Override
+    public void run() {
+        // invoke maintenance task
+        final CleanUpTask task = this.maintenanceTask;
+        if ( task != null ) {
+            task.run();
+        }
+    }
+
+    /**
+     * @see org.osgi.service.event.EventHandler#handleEvent(org.osgi.service.event.Event)
+     */
+    @Override
+    public void handleEvent(final Event event) {
+        this.jobScheduler.handleEvent(event);
+    }
+
+    /**
+     * Return our internal statistics object.
+     *
+     * @see org.apache.sling.event.jobs.JobManager#getStatistics()
+     */
+    @Override
+    public synchronized Statistics getStatistics() {
+        return this.statisticsManager.getGlobalStatistics();
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.JobManager#getTopicStatistics()
+     */
+    @Override
+    public Iterable<TopicStatistics> getTopicStatistics() {
+        return this.statisticsManager.getTopicStatistics().values();
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.JobManager#getQueue(java.lang.String)
+     */
+    @Override
+    public Queue getQueue(final String name) {
+        return qManager.getQueue(ResourceHelper.filterQueueName(name));
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.JobManager#getQueues()
+     */
+    @Override
+    public Iterable<Queue> getQueues() {
+        return qManager.getQueues();
+    }
+
+    /**
+     * Remove a job.
+     * If the job is already in the storage area, it's removed forever.
+     * Otherwise it's moved to the storage area.
+     */
+    private boolean internalRemoveJobById(final String jobId, final boolean forceRemove) {
+        logger.debug("Trying to remove job {}", jobId);
+        boolean result = true;
+        JobImpl job = (JobImpl)this.getJobById(jobId);
+        if ( job != null ) {
+            if ( logger.isDebugEnabled() ) {
+                logger.debug("Found removal job: {}", Utility.toString(job));
+            }
+            final JobImpl retryJob = (JobImpl)this.configuration.getJobFromRetryList(jobId);
+            if ( retryJob != null ) {
+                job = retryJob;
+            }
+            // currently running?
+            if ( !forceRemove && job.getProcessingStarted() != null ) {
+                if ( logger.isDebugEnabled() ) {
+                    logger.debug("Unable to remove job - job is started: {}", Utility.toString(job));
+                }
+                result = false;
+            } else {
+                final boolean isHistoryJob = this.configuration.isStoragePath(job.getResourcePath());
+                // if history job, simply remove - otherwise move to history!
+                if ( isHistoryJob ) {
+                    final ResourceResolver resolver = this.configuration.createResourceResolver();
+                    try {
+                        final Resource jobResource = resolver.getResource(job.getResourcePath());
+                        if ( jobResource != null ) {
+                            resolver.delete(jobResource);
+                            resolver.commit();
+                            logger.debug("Removed job with id: {}", jobId);
+                        } else {
+                            logger.debug("Unable to remove job with id - resource already removed: {}", jobId);
+                        }
+                        NotificationUtility.sendNotification(this.eventAdmin, NotificationConstants.TOPIC_JOB_REMOVED, job, null);
+                    } catch ( final PersistenceException pe) {
+                        logger.warn("Unable to remove job at " + job.getResourcePath(), pe);
+                        result = false;
+                    } finally {
+                        resolver.close();
+                    }
+                } else {
+                    final JobHandler jh = new JobHandler(job, null, this.configuration);
+                    jh.finished(Job.JobState.DROPPED, true, null);
+                }
+                this.configuration.getAuditLogger().debug("REMOVE OK : {}", jobId);
+            }
+        } else {
+            logger.debug("Job for removal does not exist (anymore): {}", jobId);
+        }
+        return result;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.JobManager#addJob(java.lang.String, java.util.Map)
+     */
+    @Override
+    public Job addJob(String topic, Map<String, Object> properties) {
+        return this.addJob(topic, properties, null);
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.JobManager#getJobById(java.lang.String)
+     */
+    @Override
+    public Job getJobById(final String id) {
+        logger.debug("Getting job by id: {}", id);
+        final ResourceResolver resolver = this.configuration.createResourceResolver();
+        final StringBuilder buf = new StringBuilder(64);
+        try {
+
+            buf.append("/jcr:root");
+            buf.append(this.configuration.getJobsBasePathWithSlash());
+            buf.append("/element(*,");
+            buf.append(ResourceHelper.RESOURCE_TYPE_JOB);
+            buf.append(")[@");
+            buf.append(ResourceHelper.PROPERTY_JOB_ID);
+            buf.append(" = '");
+            buf.append(id);
+            buf.append("']");
+            if ( logger.isDebugEnabled() ) {
+                logger.debug("Exceuting query: {}", buf.toString());
+            }
+            final Iterator<Resource> result = resolver.findResources(buf.toString(), "xpath");
+
+            while ( result.hasNext() ) {
+                final Resource jobResource = result.next();
+                // sanity check for the path
+                if ( this.configuration.isJob(jobResource.getPath()) ) {
+                    final JobImpl job = Utility.readJob(logger, jobResource);
+                    if ( job != null ) {
+                        if ( logger.isDebugEnabled() ) {
+                            logger.debug("Found job with id {} = {}", id, Utility.toString(job));
+                        }
+                        return job;
+                    }
+                }
+            }
+        } catch (final QuerySyntaxException qse) {
+            logger.warn("Query syntax wrong " + buf.toString(), qse);
+        } finally {
+            resolver.close();
+        }
+        logger.debug("Job not found with id: {}", id);
+        return null;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.JobManager#getJob(java.lang.String, java.util.Map)
+     */
+    @SuppressWarnings("unchecked")
+    @Override
+    public Job getJob(final String topic, final Map<String, Object> template) {
+        final Iterable<Job> iter;
+        if ( template == null ) {
+            iter = this.findJobs(QueryType.ALL, topic, 1, (Map<String, Object>[])null);
+        } else {
+            iter = this.findJobs(QueryType.ALL, topic, 1, template);
+        }
+        final Iterator<Job> i = iter.iterator();
+        if ( i.hasNext() ) {
+            return i.next();
+        }
+        return null;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.JobManager#removeJobById(java.lang.String)
+     */
+    @Override
+    public boolean removeJobById(final String jobId) {
+        return this.internalRemoveJobById(jobId, true);
+    }
+
+    private enum Operation {
+        LESS,
+        LESS_OR_EQUALS,
+        EQUALS,
+        GREATER_OR_EQUALS,
+        GREATER
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.JobManager#findJobs(org.apache.sling.event.jobs.JobManager.QueryType, java.lang.String, long, java.util.Map[])
+     */
+    @Override
+    public Collection<Job> findJobs(final QueryType type,
+            final String topic,
+            final long limit,
+            final Map<String, Object>... templates) {
+        final boolean isHistoryQuery = type == QueryType.HISTORY
+                                       || type == QueryType.SUCCEEDED
+                                       || type == QueryType.CANCELLED
+                                       || type == QueryType.DROPPED
+                                       || type == QueryType.ERROR
+                                       || type == QueryType.GIVEN_UP
+                                       || type == QueryType.STOPPED;
+        final List<Job> result = new ArrayList<Job>();
+        final ResourceResolver resolver = this.configuration.createResourceResolver();
+        final StringBuilder buf = new StringBuilder(64);
+        try {
+
+            buf.append("/jcr:root");
+            buf.append(this.configuration.getJobsBasePathWithSlash());
+            buf.append("/element(*,");
+            buf.append(ResourceHelper.RESOURCE_TYPE_JOB);
+            buf.append(")[@");
+            buf.append(ISO9075.encode(ResourceHelper.PROPERTY_JOB_TOPIC));
+            if (topic != null) {
+                buf.append(" = '");
+                buf.append(topic);
+                buf.append("'");
+            }
+
+            // restricting on the type - history or unfinished
+            if ( isHistoryQuery ) {
+                buf.append(" and @");
+                buf.append(ISO9075.encode(JobImpl.PROPERTY_FINISHED_STATE));
+                if ( type == QueryType.SUCCEEDED || type == QueryType.DROPPED || type == QueryType.ERROR || type == QueryType.GIVEN_UP || type == QueryType.STOPPED ) {
+                    buf.append(" = '");
+                    buf.append(type.name());
+                    buf.append("'");
+                } else if ( type == QueryType.CANCELLED ) {
+                    buf.append(" and (@");
+                    buf.append(ISO9075.encode(JobImpl.PROPERTY_FINISHED_STATE));
+                    buf.append(" = '");
+                    buf.append(QueryType.DROPPED.name());
+                    buf.append("' or @");
+                    buf.append(ISO9075.encode(JobImpl.PROPERTY_FINISHED_STATE));
+                    buf.append(" = '");
+                    buf.append(QueryType.ERROR.name());
+                    buf.append("' or @");
+                    buf.append(ISO9075.encode(JobImpl.PROPERTY_FINISHED_STATE));
+                    buf.append(" = '");
+                    buf.append(QueryType.GIVEN_UP.name());
+                    buf.append("' or @");
+                    buf.append(ISO9075.encode(JobImpl.PROPERTY_FINISHED_STATE));
+                    buf.append(" = '");
+                    buf.append(QueryType.STOPPED.name());
+                    buf.append("')");
+                }
+            } else {
+                buf.append(" and not(@");
+                buf.append(ISO9075.encode(JobImpl.PROPERTY_FINISHED_STATE));
+                buf.append(")");
+                if ( type == QueryType.ACTIVE ) {
+                    buf.append(" and @");
+                    buf.append(ISO9075.encode(Job.PROPERTY_JOB_STARTED_TIME));
+                } else if ( type == QueryType.QUEUED ) {
+                    buf.append(" and not(@");
+                    buf.append(ISO9075.encode(Job.PROPERTY_JOB_STARTED_TIME));
+                    buf.append(")");
+                }
+            }
+
+            if ( templates != null && templates.length > 0 ) {
+                int index = 0;
+                for (final Map<String,Object> template : templates) {
+                    // skip empty templates
+                    if ( template.size() == 0 ) {
+                        continue;
+                    }
+                    if ( index == 0 ) {
+                        buf.append(" and (");
+                    } else {
+                        buf.append(" or ");
+                    }
+                    buf.append('(');
+                    final Iterator<Map.Entry<String, Object>> i = template.entrySet().iterator();
+                    boolean first = true;
+                    while ( i.hasNext() ) {
+                        final Map.Entry<String, Object> current = i.next();
+                        final String key = ISO9075.encode(current.getKey());
+                        final char firstChar = key.length() > 0 ? key.charAt(0) : 0;
+                        final String propName;
+                        final Operation op;
+                        if ( firstChar == '=' ) {
+                            propName = key.substring(1);
+                            op  = Operation.EQUALS;
+                        } else if ( firstChar == '<' ) {
+                            final char secondChar = key.length() > 1 ? key.charAt(1) : 0;
+                            if ( secondChar == '=' ) {
+                                op = Operation.LESS_OR_EQUALS;
+                                propName = key.substring(2);
+                            } else {
+                                op = Operation.LESS;
+                                propName = key.substring(1);
+                            }
+                        } else if ( firstChar == '>' ) {
+                            final char secondChar = key.length() > 1 ? key.charAt(1) : 0;
+                            if ( secondChar == '=' ) {
+                                op = Operation.GREATER_OR_EQUALS;
+                                propName = key.substring(2);
+                            } else {
+                                op = Operation.GREATER;
+                                propName = key.substring(1);
+                            }
+                        } else {
+                            propName = key;
+                            op  = Operation.EQUALS;
+                        }
+
+                        if ( first ) {
+                            first = false;
+                            buf.append('@');
+                        } else {
+                            buf.append(" and @");
+                        }
+                        buf.append(propName);
+                        buf.append(' ');
+                        switch ( op ) {
+                            case EQUALS : buf.append('=');break;
+                            case LESS : buf.append('<'); break;
+                            case LESS_OR_EQUALS : buf.append("<="); break;
+                            case GREATER : buf.append('>'); break;
+                            case GREATER_OR_EQUALS : buf.append(">="); break;
+                        }
+                        buf.append(" '");
+                        buf.append(current.getValue());
+                        buf.append("'");
+                    }
+                    buf.append(')');
+                    index++;
+                }
+                if ( index > 0 ) {
+                    buf.append(')');
+                }
+            }
+            buf.append("] order by @");
+            if ( isHistoryQuery ) {
+                buf.append(JobImpl.PROPERTY_FINISHED_DATE);
+                buf.append(" descending");
+            } else {
+                buf.append(Job.PROPERTY_JOB_CREATED);
+                buf.append(" ascending");
+            }
+            final Iterator<Resource> iter = resolver.findResources(buf.toString(), "xpath");
+            long count = 0;
+
+            while ( iter.hasNext() && (limit < 1 || count < limit) ) {
+                final Resource jobResource = iter.next();
+                // sanity check for the path
+                if ( this.configuration.isJob(jobResource.getPath()) ) {
+                    final JobImpl job = Utility.readJob(logger, jobResource);
+                    if ( job != null ) {
+                        count++;
+                        result.add(job);
+                    }
+                }
+             }
+        } catch (final QuerySyntaxException qse) {
+            logger.warn("Query syntax wrong " + buf.toString(), qse);
+        } finally {
+            resolver.close();
+        }
+        return result;
+    }
+
+    /**
+     * Persist the job in the resource tree
+     * @param jobTopic The required job topic
+     * @param jobName The optional job name
+     * @param passedJobProperties The optional job properties
+     * @return The persisted job or <code>null</code>.
+     */
+    private Job addJobInternal(final String jobTopic,
+            final Map<String, Object> jobProperties,
+            final List<String> errors) {
+        final QueueInfo info = this.configuration.getQueueConfigurationManager().getQueueInfo(jobTopic);
+
+        final TopologyCapabilities caps = this.configuration.getTopologyCapabilities();
+        info.targetId = (caps == null ? null : caps.detectTarget(jobTopic, jobProperties, info));
+
+        if ( logger.isDebugEnabled() ) {
+            if ( info.targetId != null ) {
+                logger.debug("Persisting job {} into queue {}, target={}", new Object[] {Utility.toString(jobTopic, jobProperties), info.queueName, info.targetId});
+            } else {
+                logger.debug("Persisting job {} into queue {}", Utility.toString(jobTopic, jobProperties), info.queueName);
+            }
+        }
+        final ResourceResolver resolver = this.configuration.createResourceResolver();
+        try {
+            final JobImpl job = this.writeJob(resolver,
+                    jobTopic,
+                    jobProperties,
+                    info);
+            if ( info.targetId != null ) {
+                this.configuration.getAuditLogger().debug("ASSIGN OK {} : {}",
+                        info.targetId, job.getId());
+            } else {
+                this.configuration.getAuditLogger().debug("UNASSIGN OK : {}",
+                        job.getId());
+            }
+            return job;
+        } catch (final PersistenceException re ) {
+            // something went wrong, so let's log it
+            this.logger.error("Exception during persisting new job '" + Utility.toString(jobTopic, jobProperties) + "'", re);
+        } finally {
+            resolver.close();
+        }
+        if ( errors != null ) {
+            errors.add("Unable to persist new job.");
+        }
+
+        return null;
+    }
+
+    /**
+     * Write a job to the resource tree.
+     * @param resolver The resolver resolver
+     * @param event The event
+     * @param info The queue information (queue name etc.)
+     * @throws PersistenceException
+     */
+    private JobImpl writeJob(final ResourceResolver resolver,
+            final String jobTopic,
+            final Map<String, Object> jobProperties,
+            final QueueInfo info)
+    throws PersistenceException {
+        final String jobId = this.configuration.getUniqueId(jobTopic);
+        final String path = this.configuration.getUniquePath(info.targetId, jobTopic, jobId, jobProperties);
+
+        // create properties
+        final Map<String, Object> properties = new HashMap<String, Object>();
+
+        if ( jobProperties != null ) {
+            for(final Map.Entry<String, Object> entry : jobProperties.entrySet() ) {
+                final String propName = entry.getKey();
+                if ( !ResourceHelper.ignoreProperty(propName) ) {
+                    properties.put(propName, entry.getValue());
+                }
+            }
+        }
+
+        properties.put(ResourceHelper.PROPERTY_JOB_ID, jobId);
+        properties.put(ResourceHelper.PROPERTY_JOB_TOPIC, jobTopic);
+        properties.put(Job.PROPERTY_JOB_QUEUE_NAME, info.queueConfiguration.getName());
+        properties.put(Job.PROPERTY_JOB_RETRY_COUNT, 0);
+        properties.put(Job.PROPERTY_JOB_RETRIES, info.queueConfiguration.getMaxRetries());
+
+        properties.put(Job.PROPERTY_JOB_CREATED, Calendar.getInstance());
+        properties.put(JobImpl.PROPERTY_JOB_QUEUED, Calendar.getInstance());
+        properties.put(Job.PROPERTY_JOB_CREATED_INSTANCE, Environment.APPLICATION_ID);
+        if ( info.targetId != null ) {
+            properties.put(Job.PROPERTY_JOB_TARGET_INSTANCE, info.targetId);
+        } else {
+            properties.remove(Job.PROPERTY_JOB_TARGET_INSTANCE);
+        }
+
+        // create path and resource
+        properties.put(ResourceResolver.PROPERTY_RESOURCE_TYPE, ResourceHelper.RESOURCE_TYPE_JOB);
+        if ( logger.isDebugEnabled() ) {
+            logger.debug("Storing new job {} at {}", Utility.toString(jobTopic, properties), path);
+        }
+        ResourceHelper.getOrCreateResource(resolver,
+                path,
+                properties);
+
+        // update property types - priority, add path and create job
+        properties.put(JobImpl.PROPERTY_RESOURCE_PATH, path);
+        return new JobImpl(jobTopic, jobId, properties);
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.JobManager#stopJobById(java.lang.String)
+     */
+    @Override
+    public void stopJobById(final String jobId) {
+        this.stopJobById(jobId, true);
+    }
+
+    private void stopJobById(final String jobId, final boolean forward) {
+        final JobImpl job = (JobImpl)this.getJobById(jobId);
+        if ( job != null && !this.configuration.isStoragePath(job.getResourcePath()) ) {
+            // get the queue configuration
+            final QueueInfo queueInfo = this.configuration.getQueueConfigurationManager().getQueueInfo(job.getTopic());
+            final JobQueueImpl queue = (JobQueueImpl)this.qManager.getQueue(queueInfo.queueName);
+
+            boolean stopped = false;
+            if ( queue != null ) {
+                stopped = queue.stopJob(job);
+            }
+            if ( forward && !stopped ) {
+                // mark the job as stopped
+                final JobHandler jh = new JobHandler(job, null, this.configuration);
+                jh.finished(JobState.STOPPED, true, null);
+            }
+        }
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.JobManager#createJob(java.lang.String)
+     */
+    @Override
+    public JobBuilder createJob(final String topic) {
+        return new JobBuilderImpl(this, topic);
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.JobManager#getScheduledJobs()
+     */
+    @Override
+    public Collection<ScheduledJobInfo> getScheduledJobs() {
+        return this.jobScheduler.getScheduledJobs(null, -1, (Map<String, Object>[])null);
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.JobManager#getScheduledJobs()
+     */
+    @Override
+    public Collection<ScheduledJobInfo> getScheduledJobs(final String topic,
+            final long limit,
+            final Map<String, Object>... templates) {
+        return this.jobScheduler.getScheduledJobs(topic, limit, templates);
+    }
+
+    /**
+     * Internal method to add a job
+     */
+    public Job addJob(final String topic,
+            final Map<String, Object> properties,
+            final List<String> errors) {
+        final String errorMessage = Utility.checkJob(topic, properties);
+        if ( errorMessage != null ) {
+            logger.warn("{}", errorMessage);
+            if ( errors != null ) {
+                errors.add(errorMessage);
+            }
+            this.configuration.getAuditLogger().debug("ADD FAILED topic={}, properties={} : {}",
+                    new Object[] {topic,
+                                  properties,
+                                  errorMessage});
+            return null;
+        }
+        final List<String> errorList = new ArrayList<String>();
+        Job result = this.addJobInternal(topic, properties, errorList);
+        if ( errors != null ) {
+            errors.addAll(errorList);
+        }
+        if ( result == null ) {
+            this.configuration.getAuditLogger().debug("ADD FAILED topic={}, properties={} : {}",
+                    new Object[] {topic,
+                                  properties,
+                                  errorList});
+        } else {
+            this.configuration.getAuditLogger().debug("ADD OK topic={}, properties={} : {}",
+                    new Object[] {topic,
+                                  properties,
+                                  result.getId()});
+        }
+
+        return result;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.JobManager#retryJobById(java.lang.String)
+     */
+    @Override
+    public Job retryJobById(final String jobId) {
+        final JobImpl job = (JobImpl)this.getJobById(jobId);
+        if ( job != null && this.configuration.isStoragePath(job.getResourcePath()) ) {
+            this.internalRemoveJobById(jobId, true);
+            return this.addJob(job.getTopic(), job.getProperties());
+        }
+        return null;
+    }
+
+    public JobSchedulerImpl getJobScheduler() {
+        return this.jobScheduler;
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/JobTopicTraverser.java b/src/main/java/org/apache/sling/event/impl/jobs/JobTopicTraverser.java
new file mode 100644
index 0000000..1981e77
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/JobTopicTraverser.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.sling.api.resource.Resource;
+import org.slf4j.Logger;
+
+/**
+ * The job topic traverser is an utility class to traverse all jobs
+ * of a specific topic in order of creation.
+ *
+ * The traverser can be used with two different callbacks,
+ * the resource callback is called with a resource object,
+ * the job callback with a job object created from the
+ * resource.
+ */
+public class JobTopicTraverser {
+
+    /**
+     * Callback called for each found job.
+     */
+    public interface JobCallback {
+
+        /**
+         * Callback handle for a job.
+         * If the callback signals to stop traversing, the current minute is still
+         * processed completely (to ensure correct ordering of jobs).
+         * @param job The job to handle
+         * @return <code>true</code> If processing should continue, <code>false</code> otherwise.
+         */
+        boolean handle(final JobImpl job);
+    }
+
+    /**
+     * Callback called for each found resource.
+     */
+    public interface ResourceCallback {
+
+        /**
+         * Callback handle for a resource.
+         * The callback is called in sorted order on a minute base, all resources within a minute
+         * are not necessarily called in correct time order!
+         * If the callback signals to stop traversing, the traversal is stopped
+         * immediately.
+         * @param rsrc The resource to handle
+         * @return <code>true</code> If processing should continue, <code>false</code> otherwise.
+         */
+        boolean handle(final Resource rsrc);
+    }
+
+    /**
+     * Traverse the topic and call the callback for each found job.
+     *
+     * Once the callback notifies to stop traversing by returning false, the current minute
+     * will be processed completely (to ensure correct ordering of jobs) and then the
+     * traversal stops.
+     *
+     * @param logger        The logger to use for debug logging
+     * @param topicResource The topic resource
+     * @param handler       The callback
+     */
+    public static void traverse(final Logger logger,
+            final Resource topicResource,
+            final JobCallback handler) {
+        traverse(logger, topicResource, handler, null);
+    }
+
+    /**
+     * Traverse the topic and call the callback for each found resource.
+     *
+     * Once the callback notifies to stop traversing by returning false, the
+     * traversal stops.
+     *
+     * @param logger        The logger to use for debug logging
+     * @param topicResource The topic resource
+     * @param handler       The callback
+     */
+    public static void traverse(final Logger logger,
+            final Resource topicResource,
+            final ResourceCallback handler) {
+        traverse(logger, topicResource, null, handler);
+    }
+
+    /**
+     * Internal method for traversal
+     * @param logger        The logger to use for debug logging
+     * @param topicResource The topic resource
+     * @param jobHandler    The job callback
+     * @param resourceHandler    The resource callback
+     */
+    private static void traverse(final Logger logger,
+            final Resource topicResource,
+            final JobCallback jobHandler,
+            final ResourceCallback resourceHandler) {
+        logger.debug("Processing topic {}", topicResource.getName().replace('.', '/'));
+        // now years
+        for(final Resource yearResource: Utility.getSortedChildren(logger, "year", topicResource)) {
+            logger.debug("Processing year {}", yearResource.getName());
+
+            // now months
+            for(final Resource monthResource: Utility.getSortedChildren(logger, "month", yearResource)) {
+                logger.debug("Processing month {}", monthResource.getName());
+
+                // now days
+                for(final Resource dayResource: Utility.getSortedChildren(logger, "day", monthResource)) {
+                    logger.debug("Processing day {}", dayResource.getName());
+
+                    // now hours
+                    for(final Resource hourResource: Utility.getSortedChildren(logger, "hour", dayResource)) {
+                        logger.debug("Processing hour {}", hourResource.getName());
+
+                        // now minutes
+                        for(final Resource minuteResource: Utility.getSortedChildren(logger, "minute", hourResource)) {
+                            logger.debug("Processing minute {}", minuteResource.getName());
+
+                            // now jobs
+                            final List<JobImpl> jobs = new ArrayList<JobImpl>();
+                            // we use an iterator to skip removed entries
+                            // see SLING-4073
+                            final Iterator<Resource> jobIter = minuteResource.listChildren();
+                            while ( jobIter.hasNext() ) {
+                                final Resource jobResource = jobIter.next();
+                                if ( resourceHandler != null ) {
+                                    if ( !resourceHandler.handle(jobResource) ) {
+                                        return;
+                                    }
+                                } else {
+                                    final JobImpl job = Utility.readJob(logger, jobResource);
+                                    if ( job != null ) {
+                                        logger.debug("Found job {}", jobResource.getName());
+                                        jobs.add(job);
+                                    }
+                                }
+                            }
+
+                            if ( jobHandler != null ) {
+                                Collections.sort(jobs);
+
+                                boolean stop = false;
+                                for(final JobImpl job : jobs) {
+                                    if ( !jobHandler.handle(job) ) {
+                                        stop = true;
+                                    }
+                                }
+                                if ( stop ) {
+                                    return;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/Utility.java b/src/main/java/org/apache/sling/event/impl/jobs/Utility.java
new file mode 100644
index 0000000..c93fe51
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/Utility.java
@@ -0,0 +1,281 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.event.impl.support.ResourceHelper;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.consumer.JobConsumer;
+import org.osgi.service.event.Event;
+import org.slf4j.Logger;
+
+public abstract class Utility {
+
+    public static volatile boolean LOG_DEPRECATION_WARNINGS = true;
+
+    /**
+     * Check if the job topic is a valid OSGI event name (see 113.3.1 of the OSGI spec)
+     * @return <code>null</code> if the topic is syntactically correct otherwise an error description is returned
+     */
+    public static String checkJobTopic(final Object jobTopic) {
+        String message = null;
+        if ( jobTopic != null ) {
+            if ( jobTopic instanceof String ) {
+                try {
+                    new Event((String)jobTopic, (Dictionary<String, Object>)null);
+                } catch (final IllegalArgumentException iae) {
+                	message = String.format("Discarding job - job has an illegal job topic '%s'",jobTopic);
+                }
+                
+            } else {
+                message = "Discarding job - job topic is not of type string";
+            }
+        } else {
+            message = "Discarding job - job topic is missing";
+        }
+        return message;
+    }
+
+    /**
+     * Check the job.
+     * @return <code>null</code> if the topic topic is correct and all properties are serializable,
+     *                           otherwise an error description is returned
+     */
+    public static String checkJob(final Object jobTopic, final Map<String, Object> properties) {
+        final String msg = checkJobTopic(jobTopic);
+        if ( msg == null ) {
+            if ( properties != null ) {
+                for(final Object val : properties.values()) {
+                    if ( val != null && !(val instanceof Serializable) ) {
+                        return "Discarding job - properties must be serializable: " + jobTopic + " : " + properties;
+                    }
+                }
+            }
+        }
+        return msg;
+    }
+
+    /**
+     * Create an event from a job
+     * @param job The job
+     * @return New event object.
+     */
+    public static Event toEvent(final Job job) {
+        final Map<String, Object> eventProps = new HashMap<String, Object>();
+        eventProps.putAll(((JobImpl)job).getProperties());
+        eventProps.put(ResourceHelper.PROPERTY_JOB_ID, job.getId());
+        eventProps.remove(JobConsumer.PROPERTY_JOB_ASYNC_HANDLER);
+        return new Event(job.getTopic(), eventProps);
+    }
+
+    /**
+     * Append properties to the string builder
+     */
+    private static void appendProperties(final StringBuilder sb,
+            final Map<String, Object> properties) {
+        if ( properties != null ) {
+            sb.append(", properties=");
+            boolean first = true;
+            for(final String propName : properties.keySet()) {
+                if ( propName.equals(ResourceHelper.PROPERTY_JOB_ID)
+                    || propName.equals(ResourceHelper.PROPERTY_JOB_TOPIC) ) {
+                   continue;
+                }
+                if ( first ) {
+                    first = false;
+                } else {
+                    sb.append(",");
+                }
+                sb.append(propName);
+                sb.append('=');
+                final Object value = properties.get(propName);
+                // the toString() method of Calendar is very verbose
+                // therefore we do a toString for these objects based
+                // on a date
+                if ( value instanceof Calendar ) {
+                    sb.append(value.getClass().getName());
+                    sb.append('(');
+                    sb.append(((Calendar)value).getTime());
+                    sb.append(')');
+                } else {
+                    sb.append(value);
+                }
+            }
+        }
+    }
+
+    /**
+     * Improved toString method for a job.
+     * This method prints out the job topic and all of the properties.
+     */
+    public static String toString(final String jobTopic,
+            final Map<String, Object> properties) {
+        final StringBuilder sb = new StringBuilder("Sling Job ");
+        sb.append("[topic=");
+        sb.append(jobTopic);
+        appendProperties(sb, properties);
+
+        sb.append("]");
+        return sb.toString();
+    }
+
+    /**
+     * Improved toString method for a job.
+     * This method prints out the job topic and all of the properties.
+     */
+    public static String toString(final Job job) {
+        if ( job != null ) {
+            final StringBuilder sb = new StringBuilder("Sling Job ");
+            sb.append("[topic=");
+            sb.append(job.getTopic());
+            sb.append(", id=");
+            sb.append(job.getId());
+            appendProperties(sb, ((JobImpl)job).getProperties());
+            sb.append("]");
+            return sb.toString();
+        }
+        return "<null>";
+    }
+
+    /**
+     * Read a job
+     */
+    public static JobImpl readJob(final Logger logger, final Resource resource) {
+        JobImpl job = null;
+        if ( resource != null ) {
+            try {
+                final ValueMap vm = ResourceHelper.getValueMap(resource);
+
+                // check job topic and job id
+                final String errorMessage = Utility.checkJobTopic(vm.get(ResourceHelper.PROPERTY_JOB_TOPIC));
+                final String jobId = vm.get(ResourceHelper.PROPERTY_JOB_ID, String.class);
+                if ( errorMessage == null && jobId != null ) {
+                    final String topic = vm.get(ResourceHelper.PROPERTY_JOB_TOPIC, String.class);
+                    final Map<String, Object> jobProperties = ResourceHelper.cloneValueMap(vm);
+
+                    jobProperties.put(JobImpl.PROPERTY_RESOURCE_PATH, resource.getPath());
+                    // convert to integers (JCR supports only long...)
+                    jobProperties.put(Job.PROPERTY_JOB_RETRIES, vm.get(Job.PROPERTY_JOB_RETRIES, Integer.class));
+                    jobProperties.put(Job.PROPERTY_JOB_RETRY_COUNT, vm.get(Job.PROPERTY_JOB_RETRY_COUNT, Integer.class));
+                    if ( vm.get(Job.PROPERTY_JOB_PROGRESS_STEPS) != null ) {
+                        jobProperties.put(Job.PROPERTY_JOB_PROGRESS_STEPS, vm.get(Job.PROPERTY_JOB_PROGRESS_STEPS, Integer.class));
+                    }
+                    if ( vm.get(Job.PROPERTY_JOB_PROGRESS_STEP) != null ) {
+                        jobProperties.put(Job.PROPERTY_JOB_PROGRESS_STEP, vm.get(Job.PROPERTY_JOB_PROGRESS_STEP, Integer.class));
+                    }
+                    @SuppressWarnings("unchecked")
+                    final List<Exception> readErrorList = (List<Exception>) jobProperties.get(ResourceHelper.PROPERTY_MARKER_READ_ERROR_LIST);
+                    if ( readErrorList != null ) {
+                        for(final Exception e : readErrorList) {
+                            logger.warn("Unable to read job from " + resource.getPath(), e);
+                        }
+                    }
+                    job = new JobImpl(topic,
+                            jobId,
+                            jobProperties);
+                } else {
+                    if ( errorMessage != null ) {
+                        logger.warn("{} : {}", errorMessage, resource.getPath());
+                    } else if ( jobId == null ) {
+                        logger.warn("Discarding job - no job id found : {}", resource.getPath());
+                    }
+                    // remove the job as the topic is invalid anyway
+                    try {
+                        resource.getResourceResolver().delete(resource);
+                        resource.getResourceResolver().commit();
+                    } catch ( final PersistenceException ignore) {
+                        logger.debug("Unable to remove job resource.", ignore);
+                    }
+                }
+            } catch (final InstantiationException ie) {
+                // something happened with the resource in the meantime
+                logger.debug("Unable to instantiate resource.", ie);
+            } catch (final RuntimeException re) {
+                logger.debug("Unable to read resource.", re);
+            }
+
+        }
+        return job;
+    }
+
+    private static final Comparator<Resource> RESOURCE_COMPARATOR = new Comparator<Resource>() {
+
+        @Override
+        public int compare(final Resource o1, final Resource o2) {
+            Integer value1 = null;
+            try {
+                value1 = Integer.valueOf(o1.getName());
+            } catch ( final NumberFormatException nfe) {
+                // ignore
+            }
+            Integer value2 = null;
+            try {
+                value2 = Integer.valueOf(o2.getName());
+            } catch ( final NumberFormatException nfe) {
+                // ignore
+            }
+            if ( value1 != null && value2 != null ) {
+                return value1.compareTo(value2);
+            }
+            return o1.getName().compareTo(o2.getName());
+        }
+    };
+
+    /**
+     * Helper method to read all children of a resource and sort them by name
+     * @param type The type of resources (for debugging)
+     * @param rsrc The parent resource
+     * @return Sorted list of children.
+     */
+    public static List<Resource> getSortedChildren(final Logger logger, final String type, final Resource rsrc) {
+        final List<Resource> children = new ArrayList<Resource>();
+        final Iterator<Resource> monthIter = rsrc.listChildren();
+        while ( monthIter.hasNext() ) {
+            final Resource monthResource = monthIter.next();
+            children.add(monthResource);
+            logger.debug("Found {} : {}",  type, monthResource.getName());
+        }
+        Collections.sort(children, RESOURCE_COMPARATOR);
+        return children;
+    }
+
+    /**
+     * Log a deprecation warning on level info into the log
+     * @param logger The logger to use
+     * @param message The message.
+     */
+    public static void logDeprecated(final Logger logger, final String message) {
+        if ( LOG_DEPRECATION_WARNINGS && logger.isInfoEnabled() ) {
+            logger.info("DEPRECATION-WARNING: " + message, new Exception(message));
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/config/ConfigurationChangeListener.java b/src/main/java/org/apache/sling/event/impl/jobs/config/ConfigurationChangeListener.java
new file mode 100644
index 0000000..71b6fe6
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/config/ConfigurationChangeListener.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.config;
+
+/**
+ * Listener interface to get topology / queue changes.
+ * Components interested in configuration changes can subscribe
+ * themselves using the {@link JobManagerConfiguration}.
+ */
+public interface ConfigurationChangeListener {
+
+    /**
+     * Notify about a configuration change.
+     * @param active {@code true} if job processing is active, otherwise {@code false}
+     */
+    void configurationChanged(final boolean active);
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/config/ConfigurationConstants.java b/src/main/java/org/apache/sling/event/impl/jobs/config/ConfigurationConstants.java
new file mode 100644
index 0000000..18f4886
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/config/ConfigurationConstants.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.config;
+
+/**
+ * Constants for the queue configuration.
+ */
+public abstract class ConfigurationConstants {
+
+    public static final int NUMBER_OF_PROCESSORS = Runtime.getRuntime().availableProcessors();
+
+    public static final String DEFAULT_TYPE = "UNORDERED";
+    public static final String DEFAULT_PRIORITY = "NORM";
+    public static final int DEFAULT_RETRIES = 10;
+    public static final long DEFAULT_RETRY_DELAY = 2000;
+    public static final int DEFAULT_MAX_PARALLEL = 15;
+    public static final boolean DEFAULT_KEEP_JOBS = false;
+    public static final int DEFAULT_THREAD_POOL_SIZE = 0;
+    public static final boolean DEFAULT_PREFER_RUN_ON_CREATION_INSTANCE = false;
+
+    public static final String PROP_NAME = "queue.name";
+    public static final String PROP_TYPE = "queue.type";
+    public static final String PROP_TOPICS = "queue.topics";
+    public static final String PROP_MAX_PARALLEL = "queue.maxparallel";
+    public static final String PROP_RETRIES = "queue.retries";
+    public static final String PROP_RETRY_DELAY = "queue.retrydelay";
+    public static final String PROP_PRIORITY = "queue.priority";
+    public static final String PROP_KEEP_JOBS = "queue.keepJobs";
+    public static final String PROP_THREAD_POOL_SIZE = "queue.threadPoolSize";
+    public static final String PROP_PREFER_RUN_ON_CREATION_INSTANCE = "queue.preferRunOnCreationInstance";
+
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/config/InternalQueueConfiguration.java b/src/main/java/org/apache/sling/event/impl/jobs/config/InternalQueueConfiguration.java
new file mode 100644
index 0000000..c8457a2
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/config/InternalQueueConfiguration.java
@@ -0,0 +1,402 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.config;
+
+import java.util.Arrays;
+import java.util.Map;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.ConfigurationPolicy;
+import org.apache.felix.scr.annotations.Properties;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.PropertyOption;
+import org.apache.felix.scr.annotations.PropertyUnbounded;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.commons.osgi.PropertiesUtil;
+import org.apache.sling.event.impl.support.TopicMatcher;
+import org.apache.sling.event.impl.support.TopicMatcherHelper;
+import org.apache.sling.event.jobs.QueueConfiguration;
+import org.osgi.framework.Constants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component(metatype=true,
+           name="org.apache.sling.event.jobs.QueueConfiguration",
+           label="Apache Sling Job Queue Configuration",
+           description="The configuration of a job processing queue.",
+           configurationFactory=true, policy=ConfigurationPolicy.REQUIRE)
+@Service(value={InternalQueueConfiguration.class})
+@Properties({
+    @Property(name=ConfigurationConstants.PROP_NAME,
+              label="Name",
+              description="The name of the queue. If matching is used the token {0} can be used to substitute the real value."),
+    @Property(name=ConfigurationConstants.PROP_TOPICS,
+              unbounded=PropertyUnbounded.ARRAY,
+              label="Topics",
+              description="This value is required and lists the topics processed by "
+                        + "this queue. The value is a list of strings. If a string ends with a dot, "
+                        + "all topics in exactly this package match. If the string ends with a star, "
+                        + "all topics in this package and all subpackages match. If the string neither "
+                        + "ends with a dot nor with a star, this is assumed to define an exact topic."),
+    @Property(name=ConfigurationConstants.PROP_TYPE,
+              value=ConfigurationConstants.DEFAULT_TYPE,
+              options={@PropertyOption(name="UNORDERED",value="Parallel"),
+                       @PropertyOption(name="ORDERED",value="Ordered"),
+                       @PropertyOption(name="TOPIC_ROUND_ROBIN",value="Topic Round Robin")},
+              label="Type",
+              description="The queue type."),
+    @Property(name=ConfigurationConstants.PROP_MAX_PARALLEL,
+              doubleValue=ConfigurationConstants.DEFAULT_MAX_PARALLEL,
+              label="Maximum Parallel Jobs",
+              description="The maximum number of parallel jobs started for this queue. "
+                        + "A value of -1 is substituted with the number of available processors. "
+                        + "Positive integer values specify number of processors to use.  Can be greater than number of processors. "
+                        + "A decimal number between 0.0 and 1.0 is treated as a fraction of available processors. "
+                        + "For example 0.5 means half of the available processors. For ordered queue types this value is ignored (always enforced to be 1)."),
+    @Property(name=ConfigurationConstants.PROP_RETRIES,
+              intValue=ConfigurationConstants.DEFAULT_RETRIES,
+              label="Maximum Retries",
+              description="The maximum number of times a failed job slated "
+                        + "for retries is actually retried. If a job has been retried this number of "
+                        + "times and still fails, it is not rescheduled and assumed to have failed. The "
+                        + "default value is 10."),
+    @Property(name=ConfigurationConstants.PROP_RETRY_DELAY,
+              longValue=ConfigurationConstants.DEFAULT_RETRY_DELAY,
+              label="Retry Delay",
+              description="The number of milliseconds to sleep between two "
+                        + "consecutive retries of a job which failed and was set to be retried. The "
+                        + "default value is 2 seconds. This value is only relevant if there is a single "
+                        + "failed job in the queue. If there are multiple failed jobs, each job is "
+                        + "retried in turn without an intervening delay."),
+    @Property(name=ConfigurationConstants.PROP_PRIORITY,
+              value=ConfigurationConstants.DEFAULT_PRIORITY,
+              options={@PropertyOption(name="NORM",value="Norm"),
+                       @PropertyOption(name="MIN",value="Min"),
+                       @PropertyOption(name="MAX",value="Max")},
+              label="Priority",
+              description="The priority for the threads used by this queue. Default is norm."),
+    @Property(name=ConfigurationConstants.PROP_KEEP_JOBS,
+              boolValue=ConfigurationConstants.DEFAULT_KEEP_JOBS,
+              label="Keep History",
+              description="If this option is enabled, successful finished jobs are kept "
+                        + "to provide a complete history."),
+    @Property(name=ConfigurationConstants.PROP_PREFER_RUN_ON_CREATION_INSTANCE,
+              boolValue=ConfigurationConstants.DEFAULT_PREFER_RUN_ON_CREATION_INSTANCE,
+              label="Prefer Creation Instance",
+              description="If this option is enabled, the jobs are tried to "
+                        + "be run on the instance where the job was created."),
+    @Property(name=ConfigurationConstants.PROP_THREAD_POOL_SIZE,
+              intValue=ConfigurationConstants.DEFAULT_THREAD_POOL_SIZE,
+              label="Thread Pool Size",
+              description="Optional configuration value for a thread pool to be used by "
+                        + "this queue. If this is value has a positive number of threads configuration, this queue uses "
+                        + "an own thread pool with the configured number of threads."),
+    @Property(name=Constants.SERVICE_RANKING,
+              intValue=0,
+              propertyPrivate=false,
+              label="Ranking",
+              description="Integer value defining the ranking of this queue configuration. "
+                        + "If more than one queue matches a job topic, the one with the highest ranking is used."),
+    @Property(name="webconsole.configurationFactory.nameHint", value="Queue: {" + ConfigurationConstants.PROP_NAME + "}")
+})
+public class InternalQueueConfiguration
+    implements QueueConfiguration, Comparable<InternalQueueConfiguration> {
+
+    /** Logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** The name of the queue. */
+    private String name;
+
+    /** The queue type. */
+    private Type type;
+
+    /** Number of retries. */
+    private int retries;
+
+    /** Retry delay. */
+    private long retryDelay;
+
+    /** Thread priority. */
+    private ThreadPriority priority;
+
+    /** The maximum number of parallel processes (for non ordered queues) */
+    private int maxParallelProcesses;
+
+    /** The ordering. */
+    private int serviceRanking;
+
+    /** The matchers for topics. */
+    private TopicMatcher[] matchers;
+
+    /** The configured topics. */
+    private String[] topics;
+
+    /** Keep jobs. */
+    private boolean keepJobs;
+
+    /** Valid flag. */
+    private boolean valid = false;
+
+    /** Optional thread pool size. */
+    private int ownThreadPoolSize;
+
+    /** Prefer creation instance. */
+    private boolean preferCreationInstance;
+
+    private String pid;
+
+    /**
+     * Create a new configuration from a config
+     */
+    public static InternalQueueConfiguration fromConfiguration(final Map<String, Object> params) {
+        final InternalQueueConfiguration c = new InternalQueueConfiguration();
+        c.activate(params);
+        return c;
+    }
+
+    public InternalQueueConfiguration() {
+        // nothing to do, see activate
+    }
+
+    /**
+     * Create a new queue configuration
+     */
+    @Activate
+    protected void activate(final Map<String, Object> params) {
+        this.name = PropertiesUtil.toString(params.get(ConfigurationConstants.PROP_NAME), null);
+        try {
+            this.priority = ThreadPriority.valueOf(PropertiesUtil.toString(params.get(ConfigurationConstants.PROP_PRIORITY), ConfigurationConstants.DEFAULT_PRIORITY));
+        } catch ( final IllegalArgumentException iae) {
+            logger.warn("Invalid value for queue priority. Using default instead of : {}", params.get(ConfigurationConstants.PROP_PRIORITY));
+            this.priority = ThreadPriority.valueOf(ConfigurationConstants.DEFAULT_PRIORITY);
+        }
+        try {
+            this.type = Type.valueOf(PropertiesUtil.toString(params.get(ConfigurationConstants.PROP_TYPE), ConfigurationConstants.DEFAULT_TYPE));
+        } catch ( final IllegalArgumentException iae) {
+            logger.error("Invalid value for queue type configuration: {}", params.get(ConfigurationConstants.PROP_TYPE));
+            this.type = null;
+        }
+        this.retries = PropertiesUtil.toInteger(params.get(ConfigurationConstants.PROP_RETRIES), ConfigurationConstants.DEFAULT_RETRIES);
+        this.retryDelay = PropertiesUtil.toLong(params.get(ConfigurationConstants.PROP_RETRY_DELAY), ConfigurationConstants.DEFAULT_RETRY_DELAY);
+
+        // Float values are treated as percentage.  int values are treated as number of cores, -1 == all available
+        // Note: the value is based on the core count at startup.  It will not change dynamically if core count changes.
+        int cores = ConfigurationConstants.NUMBER_OF_PROCESSORS;
+        final double inMaxParallel = PropertiesUtil.toDouble(params.get(ConfigurationConstants.PROP_MAX_PARALLEL),
+                ConfigurationConstants.DEFAULT_MAX_PARALLEL);
+        logger.debug("Max parallel for queue {} is {}", this.name, inMaxParallel);
+        if ((inMaxParallel == Math.floor(inMaxParallel)) && !Double.isInfinite(inMaxParallel)) {
+            // integral type
+            if ((int) inMaxParallel == 0) {
+                logger.warn("Max threads property for {} set to zero.", this.name);
+            }
+            this.maxParallelProcesses = (inMaxParallel <= -1 ? cores : (int) inMaxParallel);
+        } else {
+            // percentage (rounded)
+            if ((inMaxParallel > 0.0) && (inMaxParallel < 1.0)) {
+                this.maxParallelProcesses = (int) Math.round(cores * inMaxParallel);
+            } else {
+                logger.warn("Invalid queue max parallel value for queue {}. Using {}", this.name, cores);
+                this.maxParallelProcesses =  cores;
+            }
+        }
+        logger.debug("Thread pool size for {} was set to {}", this.name, this.maxParallelProcesses);
+
+        // ignore parallel setting for ordered queues
+        if ( this.type == Type.ORDERED ) {
+            this.maxParallelProcesses = 1;
+        }
+        final String[] topicsParam = PropertiesUtil.toStringArray(params.get(ConfigurationConstants.PROP_TOPICS));
+        this.matchers = TopicMatcherHelper.buildMatchers(topicsParam);
+        if ( this.matchers == null ) {
+            this.topics = null;
+        } else {
+            this.topics = topicsParam;
+        }
+        this.keepJobs = PropertiesUtil.toBoolean(params.get(ConfigurationConstants.PROP_KEEP_JOBS), ConfigurationConstants.DEFAULT_KEEP_JOBS);
+        this.serviceRanking = PropertiesUtil.toInteger(params.get(Constants.SERVICE_RANKING), 0);
+        this.ownThreadPoolSize = PropertiesUtil.toInteger(params.get(ConfigurationConstants.PROP_THREAD_POOL_SIZE), ConfigurationConstants.DEFAULT_THREAD_POOL_SIZE);
+        this.preferCreationInstance = PropertiesUtil.toBoolean(params.get(ConfigurationConstants.PROP_PREFER_RUN_ON_CREATION_INSTANCE), ConfigurationConstants.DEFAULT_PREFER_RUN_ON_CREATION_INSTANCE);
+        this.pid = (String)params.get(Constants.SERVICE_PID);
+        this.valid = this.checkIsValid();
+    }
+
+    /**
+     * Check if this configuration is valid,
+     * If it is invalid, it is ignored.
+     */
+    private boolean checkIsValid() {
+        if ( type == null ) {
+            return false;
+        }
+        boolean hasMatchers = false;
+        if ( this.matchers != null ) {
+            for(final TopicMatcher m : this.matchers ) {
+                if ( m != null ) {
+                    hasMatchers = true;
+                    break;
+                }
+            }
+        }
+        if ( !hasMatchers ) {
+            return false;
+        }
+        if ( name == null || name.length() == 0 ) {
+            return false;
+        }
+        if ( retries < -1 ) {
+            return false;
+        }
+        if ( maxParallelProcesses < 1 ) {
+            return false;
+        }
+        return true;
+    }
+
+    public boolean isValid() {
+        return this.valid;
+    }
+
+    /**
+     * Check if the queue processes the event.
+     * @param topic The topic of the event
+     * @return The queue name or <code>null</code>
+     */
+    public String match(final String topic) {
+        if ( this.matchers != null ) {
+            for(final TopicMatcher m : this.matchers ) {
+                if ( m != null ) {
+                    final String rep = m.match(topic);
+                    if ( rep != null ) {
+                        return this.name.replace("{0}", rep);
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Return the name of the queue.
+     */
+    public String getName() {
+        return this.name;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.QueueConfiguration#getRetryDelayInMs()
+     */
+    @Override
+    public long getRetryDelayInMs() {
+        return this.retryDelay;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.QueueConfiguration#getMaxRetries()
+     */
+    @Override
+    public int getMaxRetries() {
+        return this.retries;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.QueueConfiguration#getType()
+     */
+    @Override
+    public Type getType() {
+        return this.type;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.QueueConfiguration#getMaxParallel()
+     */
+    @Override
+    public int getMaxParallel() {
+        return this.maxParallelProcesses;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.QueueConfiguration#getTopics()
+     */
+    @Override
+    public String[] getTopics() {
+        return this.topics;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.QueueConfiguration#getRanking()
+     */
+    @Override
+    public int getRanking() {
+        return this.serviceRanking;
+    }
+
+    public String getPid() {
+        return this.pid;
+    }
+
+    @Override
+    public boolean isKeepJobs() {
+        return this.keepJobs;
+    }
+
+    @Override
+    public int getOwnThreadPoolSize() {
+        return this.ownThreadPoolSize;
+    }
+
+    @Override
+    public boolean isPreferRunOnCreationInstance() {
+        return this.preferCreationInstance;
+    }
+
+    @Override
+    public String toString() {
+        return "Queue-Configuration(" + this.hashCode() + ") : {" +
+            "name=" + this.name +
+            ", type=" + this.type +
+            ", topics=" + (this.matchers == null ? "[]" : Arrays.toString(this.matchers)) +
+            ", maxParallelProcesses=" + this.maxParallelProcesses +
+            ", retries=" + this.retries +
+            ", retryDelayInMs=" + this.retryDelay +
+            ", keepJobs=" + this.keepJobs +
+            ", preferRunOnCreationInstance=" + this.preferCreationInstance +
+            ", ownThreadPoolSize=" + this.ownThreadPoolSize +
+            ", serviceRanking=" + this.serviceRanking +
+            ", pid=" + this.pid +
+            ", isValid=" + this.isValid() + "}";
+    }
+
+    @Override
+    public int compareTo(final InternalQueueConfiguration other) {
+        if ( this.serviceRanking < other.serviceRanking ) {
+            return 1;
+        } else if ( this.serviceRanking > other.serviceRanking ) {
+            return -1;
+        }
+        return 0;
+    }
+
+    @Override
+    public ThreadPriority getThreadPriority() {
+        return this.priority;
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/config/JobManagerConfiguration.java b/src/main/java/org/apache/sling/event/impl/jobs/config/JobManagerConfiguration.java
new file mode 100644
index 0000000..3cc8b72
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/config/JobManagerConfiguration.java
@@ -0,0 +1,661 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.config;
+
+import java.sql.Date;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Modified;
+import org.apache.felix.scr.annotations.Properties;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.commons.osgi.PropertiesUtil;
+import org.apache.sling.commons.scheduler.Scheduler;
+import org.apache.sling.discovery.TopologyEvent;
+import org.apache.sling.discovery.TopologyEvent.Type;
+import org.apache.sling.discovery.TopologyEventListener;
+import org.apache.sling.discovery.commons.InitDelayingTopologyEventListener;
+import org.apache.sling.event.impl.EnvironmentComponent;
+import org.apache.sling.event.impl.jobs.Utility;
+import org.apache.sling.event.impl.jobs.tasks.CheckTopologyTask;
+import org.apache.sling.event.impl.jobs.tasks.FindUnfinishedJobsTask;
+import org.apache.sling.event.impl.jobs.tasks.UpgradeTask;
+import org.apache.sling.event.impl.support.Environment;
+import org.apache.sling.event.impl.support.ResourceHelper;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.serviceusermapping.ServiceUserMapped;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Configuration of the job handling
+ *
+ */
+@Component(immediate=true, metatype=true,
+           label="Apache Sling Job Manager",
+           description="This is the central service of the job handling.",
+           name="org.apache.sling.event.impl.jobs.jcr.PersistenceHandler")
+@Service(value={JobManagerConfiguration.class})
+@Properties({
+    @Property(name=JobManagerConfiguration.PROPERTY_DISABLE_DISTRIBUTION,
+              boolValue=JobManagerConfiguration.DEFAULT_DISABLE_DISTRIBUTION,
+              label="Disable Distribution",
+              description="If the distribution is disabled, all jobs will be processed on the leader only! "
+                        + "Please use this switch with care."),
+    @Property(name=JobManagerConfiguration.PROPERTY_LOG_DEPRECATION_WARNINGS,
+              boolValue=JobManagerConfiguration.DEFAULT_LOG_DEPRECATION_WARNINGS,
+              label="Deprecation Warnings",
+              description="If this switch is enabled, deprecation warnings will be logged with the INFO level."),
+    @Property(name=JobManagerConfiguration.PROPERTY_STARTUP_DELAY,
+              longValue=JobManagerConfiguration.DEFAULT_STARTUP_DELAY,
+              label="Startup Delay",
+              description="Specify amount in seconds that job manager waits on startup before starting with job handling. "
+                        + "This can be used to allow enough time to restart a cluster before jobs are eventually reassigned."),
+    @Property(name=JobManagerConfiguration.PROPERTY_REPOSITORY_PATH,
+              value=JobManagerConfiguration.DEFAULT_REPOSITORY_PATH, propertyPrivate=true),
+    @Property(name=JobManagerConfiguration.PROPERTY_SCHEDULED_JOBS_PATH,
+              value=JobManagerConfiguration.DEFAULT_SCHEDULED_JOBS_PATH, propertyPrivate=true),
+    @Property(name=JobManagerConfiguration.PROPERTY_BACKGROUND_LOAD_DELAY,
+              longValue=JobManagerConfiguration.DEFAULT_BACKGROUND_LOAD_DELAY, propertyPrivate=true),
+})
+public class JobManagerConfiguration {
+
+    /** Logger. */
+    private final Logger logger = LoggerFactory.getLogger("org.apache.sling.event.impl.jobs");
+
+    /** Audit Logger. */
+    private final Logger auditLogger = LoggerFactory.getLogger("org.apache.sling.event.jobs.audit");
+
+    /** Default resource path for jobs. */
+    public static final String DEFAULT_REPOSITORY_PATH = "/var/eventing/jobs";
+
+    /** Default background load delay. */
+    public static final long DEFAULT_BACKGROUND_LOAD_DELAY = 10;
+
+    /** Default startup delay. */
+    public static final long DEFAULT_STARTUP_DELAY = 30;
+
+    /** Default for disabling the distribution. */
+    public static final boolean DEFAULT_DISABLE_DISTRIBUTION = false;
+
+    /** Default resource path for scheduled jobs. */
+    public static final String DEFAULT_SCHEDULED_JOBS_PATH = "/var/eventing/scheduled-jobs";
+
+    /** The path where all jobs are stored. */
+    public static final String PROPERTY_REPOSITORY_PATH = "repository.path";
+
+    /** The background loader waits this time of seconds after startup before loading events from the repository. (in secs) */
+    public static final String PROPERTY_BACKGROUND_LOAD_DELAY = "load.delay";
+
+    /** The entire job handling waits time amount of seconds until it starts - to allow avoiding reassign on restart of a cluster */
+    public static final String PROPERTY_STARTUP_DELAY = "startup.delay";
+
+    /** Configuration switch for distributing the jobs. */
+    public static final String PROPERTY_DISABLE_DISTRIBUTION = "job.consumermanager.disableDistribution";
+
+    /** Configuration property for the scheduled jobs path. */
+    public static final String PROPERTY_SCHEDULED_JOBS_PATH = "job.scheduled.jobs.path";
+
+    /** Default value for background loading. */
+    public static final boolean DEFAULT_BACKGROUND_LOAD_SEARCH = true;
+
+    /** Configuration property for deprecation warnings. */
+    public static final String PROPERTY_LOG_DEPRECATION_WARNINGS = "job.log.deprecation";
+
+    /** Default value for deprecation warnings. */
+    public static final boolean DEFAULT_LOG_DEPRECATION_WARNINGS = true;
+
+    /** The jobs base path with a slash. */
+    private String jobsBasePathWithSlash;
+
+    /** The base path for assigned jobs. */
+    private String assignedJobsPath;
+
+    /** The base path for unassigned jobs. */
+    private String unassignedJobsPath;
+
+    /** The base path for assigned jobs to the current instance. */
+    private String localJobsPath;
+
+    /** The base path for assigned jobs to the current instance - ending with a slash. */
+    private String localJobsPathWithSlash;
+
+    private String previousVersionAnonPath;
+
+    private String previousVersionIdentifiedPath;
+
+    private volatile long backgroundLoadDelay;
+
+    private volatile long startupDelay;
+
+    private volatile InitDelayingTopologyEventListener startupDelayListener;
+
+    private volatile boolean disabledDistribution;
+
+    private String storedCancelledJobsPath;
+
+    private String storedSuccessfulJobsPath;
+
+    /** The resource path where scheduled jobs are stored. */
+    private String scheduledJobsPath;
+
+    /** The resource path where scheduled jobs are stored - ending with a slash. */
+    private String scheduledJobsPathWithSlash;
+
+    /** List of topology awares. */
+    private final List<ConfigurationChangeListener> listeners = new ArrayList<ConfigurationChangeListener>();
+
+    /** The environment component. */
+    @Reference
+    private EnvironmentComponent environment;
+
+    @Reference
+    private ResourceResolverFactory resourceResolverFactory;
+
+    @Reference
+    private QueueConfigurationManager queueConfigManager;
+
+    @Reference
+    private Scheduler scheduler;
+
+    @Reference
+    private ServiceUserMapped serviceUserMapped;
+
+    /** Is this still active? */
+    private final AtomicBoolean active = new AtomicBoolean(false);
+
+    /** The topology capabilities. */
+    private volatile TopologyCapabilities topologyCapabilities;
+
+    /**
+     * Activate this component.
+     * @param props Configuration properties
+     * @throws RuntimeException If the default paths can't be created
+     */
+    @Activate
+    protected void activate(final Map<String, Object> props) {
+        this.update(props);
+        this.jobsBasePathWithSlash = PropertiesUtil.toString(props.get(PROPERTY_REPOSITORY_PATH),
+                DEFAULT_REPOSITORY_PATH) + '/';
+
+        // create initial resources
+        this.assignedJobsPath = this.jobsBasePathWithSlash + "assigned";
+        this.unassignedJobsPath = this.jobsBasePathWithSlash + "unassigned";
+
+        this.localJobsPath = this.assignedJobsPath.concat("/").concat(Environment.APPLICATION_ID);
+        this.localJobsPathWithSlash = this.localJobsPath.concat("/");
+
+        this.previousVersionAnonPath = this.jobsBasePathWithSlash + "anon";
+        this.previousVersionIdentifiedPath = this.jobsBasePathWithSlash + "identified";
+
+        this.storedCancelledJobsPath = this.jobsBasePathWithSlash + "cancelled";
+        this.storedSuccessfulJobsPath = this.jobsBasePathWithSlash + "finished";
+
+        this.scheduledJobsPath = PropertiesUtil.toString(props.get(PROPERTY_SCHEDULED_JOBS_PATH),
+            DEFAULT_SCHEDULED_JOBS_PATH);
+        this.scheduledJobsPathWithSlash = this.scheduledJobsPath + "/";
+
+        // create initial resources
+        final ResourceResolver resolver = this.createResourceResolver();
+        try {
+            ResourceHelper.getOrCreateBasePath(resolver, this.getLocalJobsPath());
+            ResourceHelper.getOrCreateBasePath(resolver, this.getUnassignedJobsPath());
+        } catch ( final PersistenceException pe ) {
+            logger.error("Unable to create default paths: " + pe.getMessage(), pe);
+            throw new RuntimeException(pe);
+        } finally {
+            resolver.close();
+        }
+        this.active.set(true);
+
+        // SLING-5560 : use an InitDelayingTopologyEventListener
+        if (this.startupDelay > 0) {
+            logger.debug("activate: job manager will start in {} sec. ({})", this.startupDelay, PROPERTY_STARTUP_DELAY);
+            this.startupDelayListener = new InitDelayingTopologyEventListener(startupDelay, new TopologyEventListener() {
+
+                @Override
+                public void handleTopologyEvent(TopologyEvent event) {
+                    doHandleTopologyEvent(event);
+                }
+            }, this.scheduler, logger);
+        } else {
+            logger.debug("activate: job manager will start without delay. ({}:{})", PROPERTY_STARTUP_DELAY, this.startupDelay);
+        }
+    }
+
+    /**
+     * Update with a new configuration
+     */
+    @Modified
+    protected void update(final Map<String, Object> props) {
+        this.disabledDistribution = PropertiesUtil.toBoolean(props.get(PROPERTY_DISABLE_DISTRIBUTION), DEFAULT_DISABLE_DISTRIBUTION);
+        this.backgroundLoadDelay = PropertiesUtil.toLong(props.get(PROPERTY_BACKGROUND_LOAD_DELAY), DEFAULT_BACKGROUND_LOAD_DELAY);
+        // SLING-5560: note that currently you can't change the startupDelay to have
+        // an immediate effect - it will only have an effect on next activation.
+        // (as 'startup delay runnable' is already scheduled in activate)
+        this.startupDelay = PropertiesUtil.toLong(props.get(PROPERTY_STARTUP_DELAY), DEFAULT_STARTUP_DELAY);
+        Utility.LOG_DEPRECATION_WARNINGS = PropertiesUtil.toBoolean(props.get(PROPERTY_LOG_DEPRECATION_WARNINGS), DEFAULT_LOG_DEPRECATION_WARNINGS);
+    }
+
+    /**
+     * Deactivate
+     */
+    @Deactivate
+    protected void deactivate() {
+        this.active.set(false);
+        if ( this.startupDelayListener != null) {
+            this.startupDelayListener.dispose();
+            this.startupDelayListener = null;
+        }
+        this.stopProcessing();
+    }
+
+    /**
+     * Is this component still active?
+     * @return Active?
+     */
+    public boolean isActive() {
+        return this.active.get();
+    }
+
+    /**
+     * Create a new resource resolver for reading and writing the resource tree.
+     * The resolver needs to be closed by the client.
+     * @return A resource resolver or {@code null} if the component is already deactivated.
+     * @throws RuntimeException if the resolver can't be created.
+     */
+    public ResourceResolver createResourceResolver() {
+        ResourceResolver resolver = null;
+        final ResourceResolverFactory factory = this.resourceResolverFactory;
+        if ( factory != null ) {
+            try {
+                resolver = this.resourceResolverFactory.getServiceResourceResolver(null);
+            } catch ( final LoginException le) {
+                logger.error("Unable to create new resource resolver: " + le.getMessage(), le);
+                throw new RuntimeException(le);
+            }
+        }
+        return resolver;
+    }
+
+    /**
+     * Get the current topology capabilities.
+     * @return The capabilities or {@code null}
+     */
+    public TopologyCapabilities getTopologyCapabilities() {
+        return this.topologyCapabilities;
+    }
+
+    public QueueConfigurationManager getQueueConfigurationManager() {
+        return this.queueConfigManager;
+    }
+
+    /**
+     * Get main logger.
+     * @return The main logger.
+     */
+    public Logger getMainLogger() {
+        return this.logger;
+    }
+
+    /**
+     * Get the resource path for all assigned jobs.
+     * @return The path - does not end with a slash.
+     */
+    public String getAssginedJobsPath() {
+        return this.assignedJobsPath;
+    }
+
+    /**
+     * Get the resource path for all unassigned jobs.
+     * @return The path - does not end with a slash.
+     */
+    public String getUnassignedJobsPath() {
+        return this.unassignedJobsPath;
+    }
+
+    /**
+     * Get the resource path for all jobs assigned to the current instance
+     * @return The path - does not end with a slash
+     */
+    public String getLocalJobsPath() {
+        return this.localJobsPath;
+    }
+
+    /** Counter for jobs without an id. */
+    private final AtomicLong jobCounter = new AtomicLong(0);
+
+    /**
+     * Create a unique job path (folder and name) for the job.
+     */
+    public String getUniquePath(final String targetId,
+            final String topic,
+            final String jobId,
+            final Map<String, Object> jobProperties) {
+        final String topicName = topic.replace('/', '.');
+        final StringBuilder sb = new StringBuilder();
+        if ( targetId != null ) {
+            sb.append(this.assignedJobsPath);
+            sb.append('/');
+            sb.append(targetId);
+        } else {
+            sb.append(this.unassignedJobsPath);
+        }
+        sb.append('/');
+        sb.append(topicName);
+        sb.append('/');
+        sb.append(jobId);
+
+        return sb.toString();
+    }
+
+    /**
+     * Get the unique job id
+     */
+    public String getUniqueId(final String jobTopic) {
+        final Calendar now = Calendar.getInstance();
+        final StringBuilder sb = new StringBuilder();
+        sb.append(now.get(Calendar.YEAR));
+        sb.append('/');
+        sb.append(now.get(Calendar.MONTH) + 1);
+        sb.append('/');
+        sb.append(now.get(Calendar.DAY_OF_MONTH));
+        sb.append('/');
+        sb.append(now.get(Calendar.HOUR_OF_DAY));
+        sb.append('/');
+        sb.append(now.get(Calendar.MINUTE));
+        sb.append('/');
+        sb.append(Environment.APPLICATION_ID);
+        sb.append('_');
+        sb.append(jobCounter.getAndIncrement());
+
+        return sb.toString();
+    }
+
+    public boolean isLocalJob(final String jobPath) {
+        return jobPath != null && jobPath.startsWith(this.localJobsPathWithSlash);
+    }
+
+    public boolean isJob(final String jobPath) {
+        return jobPath.startsWith(this.jobsBasePathWithSlash);
+    }
+
+    public String getJobsBasePathWithSlash() {
+        return this.jobsBasePathWithSlash;
+    }
+
+    public String getPreviousVersionAnonPath() {
+        return this.previousVersionAnonPath;
+    }
+
+    public String getPreviousVersionIdentifiedPath() {
+        return this.previousVersionIdentifiedPath;
+    }
+
+    public boolean disableDistribution() {
+        return this.disabledDistribution;
+    }
+
+    public String getStoredCancelledJobsPath() {
+        return this.storedCancelledJobsPath;
+    }
+
+    public String getStoredSuccessfulJobsPath() {
+        return this.storedSuccessfulJobsPath;
+    }
+
+    /**
+     * Get the storage path for finished jobs.
+     * @param topic Topic of the finished job
+     * @param jobId The job id of the finished job.
+     * @param isSuccess Whether processing was successful or not
+     * @return The complete storage path
+     */
+    public String getStoragePath(final String topic, final String jobId, final boolean isSuccess) {
+        final String topicName = topic.replace('/', '.');
+        final StringBuilder sb = new StringBuilder();
+        if ( isSuccess ) {
+            sb.append(this.storedSuccessfulJobsPath);
+        } else {
+            sb.append(this.storedCancelledJobsPath);
+        }
+        sb.append('/');
+        sb.append(topicName);
+        sb.append('/');
+        sb.append(jobId);
+
+        return sb.toString();
+
+    }
+
+    /**
+     * Check whether this is a storage path.
+     */
+    public boolean isStoragePath(final String path) {
+        return path.startsWith(this.storedCancelledJobsPath) || path.startsWith(this.storedSuccessfulJobsPath);
+    }
+
+    /**
+     * Get the scheduled jobs path
+     * @param slash If {@code false} the path is returned, if {@code true} the path appended with a slash is returned.
+     * @return The path for the scheduled jobs
+     */
+    public String getScheduledJobsPath(final boolean slash) {
+        return (slash ? this.scheduledJobsPathWithSlash : this.scheduledJobsPath);
+    }
+
+    /**
+     * Stop processing
+     * @param deactivate Whether to deactivate the capabilities
+     */
+    private void stopProcessing() {
+        logger.debug("Stopping job processing...");
+        final TopologyCapabilities caps = this.topologyCapabilities;
+
+        if ( caps != null ) {
+            // deactivate old capabilities - this stops all background processes
+            caps.deactivate();
+            this.topologyCapabilities = null;
+
+            // stop all listeners
+            this.notifiyListeners();
+        }
+        logger.debug("Job processing stopped");
+    }
+
+    /**
+     * Start processing
+     * @param eventType The event type
+     * @param newCaps The new capabilities
+     */
+    private void startProcessing(final Type eventType, final TopologyCapabilities newCaps) {
+        logger.debug("Starting job processing...");
+        // create new capabilities and update view
+        this.topologyCapabilities = newCaps;
+
+        // before we propagate the new topology we do some maintenance
+        if ( eventType == Type.TOPOLOGY_INIT ) {
+            final UpgradeTask task = new UpgradeTask(this);
+            task.run();
+
+            final FindUnfinishedJobsTask rt = new FindUnfinishedJobsTask(this);
+            rt.run();
+
+            final CheckTopologyTask mt = new CheckTopologyTask(this);
+            mt.fullRun();
+
+            notifiyListeners();
+        } else {
+            // and run checker again in some seconds (if leader)
+            // notify listeners afterwards
+            final Scheduler local = this.scheduler;
+            if ( local != null ) {
+                final Runnable r = new Runnable() {
+
+                    @Override
+                    public void run() {
+                        if ( newCaps == topologyCapabilities && newCaps.isActive()) {
+                            // start listeners
+                            notifiyListeners();
+                            if ( newCaps.isLeader() && newCaps.isActive() ) {
+                                final CheckTopologyTask mt = new CheckTopologyTask(JobManagerConfiguration.this);
+                                mt.fullRun();
+                            }
+                        }
+                    }
+                };
+                if ( !local.schedule(r, local.AT(new Date(System.currentTimeMillis() + this.backgroundLoadDelay * 1000))) ) {
+                    // if for whatever reason scheduling doesn't work, let's run now
+                    r.run();
+                }
+            }
+        }
+        logger.debug("Job processing started");
+    }
+
+    /**
+     * Notify all listeners
+     */
+    private void notifiyListeners() {
+        synchronized ( this.listeners ) {
+            final TopologyCapabilities caps = this.topologyCapabilities;
+            for(final ConfigurationChangeListener l : this.listeners) {
+                l.configurationChanged(caps != null);
+            }
+        }
+    }
+
+    /**
+     * This method is invoked asynchronously from the TopologyHandler.
+     * Therefore this method can't be invoked concurrently
+     * @see org.apache.sling.discovery.TopologyEventListener#handleTopologyEvent(org.apache.sling.discovery.TopologyEvent)
+     */
+    public void handleTopologyEvent(TopologyEvent event) {
+        if ( this.startupDelayListener != null ) {
+            // with startup.delay > 0
+            this.startupDelayListener.handleTopologyEvent(event);
+        } else {
+            // classic (startup.delay <= 0)
+            this.logger.debug("Received topology event {}", event);
+            doHandleTopologyEvent(event);
+        }
+    }
+
+    void doHandleTopologyEvent(final TopologyEvent event) {
+
+        // check if there is a change of properties which doesn't affect us
+        // but we need to use the new view !
+        boolean stopProcessing = true;
+        if ( event.getType() == Type.PROPERTIES_CHANGED ) {
+            final Map<String, String> newAllInstances = TopologyCapabilities.getAllInstancesMap(event.getNewView());
+            if ( this.topologyCapabilities != null && this.topologyCapabilities.isSame(newAllInstances) ) {
+                logger.debug("No changes in capabilities - updating topology capabilities with new view");
+                stopProcessing = false;
+            }
+        }
+
+        final TopologyEvent.Type eventType = event.getType();
+
+        if ( eventType == Type.TOPOLOGY_CHANGING ) {
+           this.stopProcessing();
+
+        } else if ( eventType == Type.TOPOLOGY_INIT
+            || event.getType() == Type.TOPOLOGY_CHANGED
+            || event.getType() == Type.PROPERTIES_CHANGED ) {
+
+            if ( stopProcessing ) {
+                this.stopProcessing();
+            }
+
+            this.startProcessing(eventType, new TopologyCapabilities(event.getNewView(), this));
+        }
+    }
+
+    /**
+     * Add a topology aware listener
+     * @param service Listener to notify about changes.
+     */
+    public void addListener(final ConfigurationChangeListener service) {
+        synchronized ( this.listeners ) {
+            this.listeners.add(service);
+            service.configurationChanged(this.topologyCapabilities != null);
+        }
+    }
+
+    /**
+     * Remove a topology aware listener
+     * @param service Listener to notify about changes.
+     */
+    public void removeListener(final ConfigurationChangeListener service) {
+        synchronized ( this.listeners )  {
+            this.listeners.remove(service);
+        }
+    }
+
+    private final Map<String, Job> retryList = new HashMap<String, Job>();
+
+    public void addJobToRetryList(final Job job) {
+        synchronized ( retryList ) {
+            retryList.put(job.getId(), job);
+        }
+    }
+
+    public List<Job> clearJobRetryList() {
+        final List<Job> result = new ArrayList<Job>();
+        synchronized ( this.retryList ) {
+            result.addAll(retryList.values());
+            retryList.clear();
+        }
+        return result;
+    }
+
+    public boolean removeJobFromRetryList(final Job job) {
+        synchronized ( retryList ) {
+            return retryList.remove(job.getId()) != null;
+        }
+    }
+
+    public Job getJobFromRetryList(final String jobId) {
+        synchronized ( retryList ) {
+            return retryList.get(jobId);
+        }
+    }
+
+    /**
+     * The audit logger is logging actions for auditing.
+     * @return The logger
+     */
+    public Logger getAuditLogger() {
+        return this.auditLogger;
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/config/MainQueueConfiguration.java b/src/main/java/org/apache/sling/event/impl/jobs/config/MainQueueConfiguration.java
new file mode 100644
index 0000000..9287e6b
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/config/MainQueueConfiguration.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.config;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Modified;
+import org.apache.felix.scr.annotations.Properties;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.PropertyOption;
+import org.apache.felix.scr.annotations.Service;
+import org.osgi.framework.Constants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * This is the configuration for the main queue.
+ *
+ */
+@Component(label="Apache Sling Job Default Queue",
+           description="The configuration of the default job queue.",
+           name="org.apache.sling.event.impl.jobs.DefaultJobManager",
+           metatype=true)
+@Service(value=MainQueueConfiguration.class)
+@Properties({
+    @Property(name=ConfigurationConstants.PROP_PRIORITY,
+              value=ConfigurationConstants.DEFAULT_PRIORITY,
+              options={@PropertyOption(name="NORM",value="Norm"),
+                       @PropertyOption(name="MIN",value="Min"),
+                       @PropertyOption(name="MAX",value="Max")},
+              label="Priority",
+              description="The priority for the threads used by this queue. Default is norm."),
+    @Property(name=ConfigurationConstants.PROP_RETRIES,
+            intValue=ConfigurationConstants.DEFAULT_RETRIES,
+            label="Maximum Retries",
+            description="The maximum number of times a failed job slated "
+                      + "for retries is actually retried. If a job has been retried this number of "
+                      + "times and still fails, it is not rescheduled and assumed to have failed. The "
+                      + "default value is 10."),
+    @Property(name=ConfigurationConstants.PROP_RETRY_DELAY,
+            longValue=ConfigurationConstants.DEFAULT_RETRY_DELAY,
+            label="Retry Delay",
+            description="The number of milliseconds to sleep between two "
+                      + "consecutive retries of a job which failed and was set to be retried. The "
+                      + "default value is 2 seconds. This value is only relevant if there is a single "
+                      + "failed job in the queue. If there are multiple failed jobs, each job is "
+                      + "retried in turn without an intervening delay."),
+    @Property(name=ConfigurationConstants.PROP_MAX_PARALLEL,
+            intValue=ConfigurationConstants.DEFAULT_MAX_PARALLEL,
+            label="Maximum Parallel Jobs",
+            description="The maximum number of parallel jobs started for this queue. "
+                      + "A value of -1 is substituted with the number of available processors."),
+})
+public class MainQueueConfiguration {
+
+    public static final String MAIN_QUEUE_NAME = "<main queue>";
+
+    /** Default logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private InternalQueueConfiguration mainConfiguration;
+
+    /**
+     * Activate this component.
+     * @param props Configuration properties
+     */
+    @Activate
+    protected void activate(final Map<String, Object> props) {
+        this.update(props);
+    }
+
+    /**
+     * Configure this component.
+     * @param props Configuration properties
+     */
+    @Modified
+    protected void update(final Map<String, Object> props) {
+        // create a new dictionary with the missing info and do some sanity puts
+        final Map<String, Object> queueProps = new HashMap<String, Object>(props);
+        queueProps.put(ConfigurationConstants.PROP_TOPICS, "*");
+        queueProps.put(ConfigurationConstants.PROP_NAME, MAIN_QUEUE_NAME);
+        queueProps.put(ConfigurationConstants.PROP_TYPE, InternalQueueConfiguration.Type.UNORDERED);
+        queueProps.put(Constants.SERVICE_PID, "org.apache.sling.event.impl.jobs.DefaultJobManager");
+        logger.debug("properties for queue {}: {}", MAIN_QUEUE_NAME, queueProps);
+        this.mainConfiguration = InternalQueueConfiguration.fromConfiguration(queueProps);
+    }
+
+    /**
+     * Return the main queue configuration object.
+     * @return The main queue configuration object.
+     */
+    public InternalQueueConfiguration getMainConfiguration() {
+        return this.mainConfiguration;
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/config/QueueConfigurationManager.java b/src/main/java/org/apache/sling/event/impl/jobs/config/QueueConfigurationManager.java
new file mode 100644
index 0000000..ce2b79a
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/config/QueueConfigurationManager.java
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.config;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.ReferencePolicy;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.event.impl.support.ResourceHelper;
+
+
+/**
+ * The queue manager manages queue configurations.
+ */
+@Component
+@Service(value=QueueConfigurationManager.class)
+@Reference(referenceInterface=InternalQueueConfiguration.class, policy=ReferencePolicy.DYNAMIC,
+           cardinality=ReferenceCardinality.OPTIONAL_MULTIPLE,
+           bind="bindConfig", unbind="unbindConfig", updated="updateConfig")
+public class QueueConfigurationManager {
+
+    /** Empty configuration array. */
+    private static final InternalQueueConfiguration[] EMPTY_CONFIGS = new InternalQueueConfiguration[0];
+
+    /** Configurations - ordered by service ranking. */
+    private volatile InternalQueueConfiguration[] orderedConfigs = EMPTY_CONFIGS;
+
+    /** All configurations. */
+    private final List<InternalQueueConfiguration> configurations = new ArrayList<InternalQueueConfiguration>();
+
+    /** The main queue configuration. */
+    @Reference
+    private MainQueueConfiguration mainQueueConfiguration;
+
+    /**
+     * Add a new queue configuration.
+     * @param config A new queue configuration.
+     */
+    protected void bindConfig(final InternalQueueConfiguration config) {
+        synchronized ( configurations ) {
+            configurations.add(config);
+            this.createConfigurationCache();
+        }
+    }
+
+    /**
+     * Remove a queue configuration.
+     * @param config The queue configuration.
+     */
+    protected void unbindConfig(final InternalQueueConfiguration config) {
+        synchronized ( configurations ) {
+            configurations.remove(config);
+            this.createConfigurationCache();
+        }
+    }
+
+    /**
+     * Update a queue configuration.
+     * @param config The queue configuration.
+     */
+    protected void updateConfig(final InternalQueueConfiguration config) {
+        // InternalQueueConfiguration does not implement modified atm,
+        // but we handle this case anyway
+        synchronized ( configurations ) {
+            this.createConfigurationCache();
+        }
+    }
+
+    /**
+     * Create the configurations cache used by clients.
+     */
+    private void createConfigurationCache() {
+        if ( this.configurations.isEmpty() ) {
+            this.orderedConfigs = EMPTY_CONFIGS;
+        } else {
+            Collections.sort(configurations);
+            orderedConfigs = configurations.toArray(new InternalQueueConfiguration[configurations.size()]);
+        }
+    }
+
+    /**
+     * Return all configurations.
+     * @return An array with all queue configurations except the main queue. Array might be empty.
+     */
+    public InternalQueueConfiguration[] getConfigurations() {
+        return orderedConfigs;
+    }
+
+    /**
+     * Get the configuration for the main queue.
+     * @return The configuration for the main queue.
+     */
+    public InternalQueueConfiguration getMainQueueConfiguration() {
+        return this.mainQueueConfiguration.getMainConfiguration();
+    }
+
+    public static final class QueueInfo {
+        public InternalQueueConfiguration queueConfiguration;
+        public String queueName;
+        public String targetId;
+
+        @Override
+        public String toString() {
+            return queueName;
+        }
+
+        @Override
+        public int hashCode() {
+            return queueName.hashCode();
+        }
+
+        @Override
+        public boolean equals(final Object obj) {
+            if ( obj == this ) {
+                return true;
+            }
+            if ( obj instanceof QueueInfo ) {
+                return ((QueueInfo)obj).queueName.equals(this.queueName);
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Find the queue configuration for the job.
+     * This method only returns a configuration if one matches.
+     */
+    public QueueInfo getQueueInfo(final String topic) {
+        final InternalQueueConfiguration[] configurations = this.getConfigurations();
+        for(final InternalQueueConfiguration config : configurations) {
+            if ( config.isValid() ) {
+                final String qn = config.match(topic);
+                if ( qn != null ) {
+                    final QueueInfo result = new QueueInfo();
+                    result.queueConfiguration = config;
+                    result.queueName = ResourceHelper.filterName(qn);
+
+                    return result;
+                }
+            }
+        }
+        final QueueInfo result = new QueueInfo();
+        result.queueConfiguration = this.mainQueueConfiguration.getMainConfiguration();
+        result.queueName = result.queueConfiguration.getName();
+
+        return result;
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/config/TopologyCapabilities.java b/src/main/java/org/apache/sling/event/impl/jobs/config/TopologyCapabilities.java
new file mode 100644
index 0000000..75532fb
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/config/TopologyCapabilities.java
@@ -0,0 +1,324 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.config;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.TopologyView;
+import org.apache.sling.event.impl.jobs.config.QueueConfigurationManager.QueueInfo;
+import org.apache.sling.event.impl.support.Environment;
+import org.apache.sling.event.jobs.QueueConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The capabilities of a topology.
+ */
+public class TopologyCapabilities {
+
+    public static final String PROPERTY_TOPICS = "org.apache.sling.event.jobs.consumer.topics";
+
+    /** Logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** Map: key: topic, value: sling IDs */
+    private final Map<String, List<InstanceDescription>> instanceCapabilities;
+
+    /** Round robin map. */
+    private final Map<String, Integer> roundRobinMap = new HashMap<String, Integer>();
+
+    /** Instance map. */
+    private final Map<String, InstanceDescription> instanceMap = new HashMap<String, InstanceDescription>();
+
+    /** Is this the leader of the cluster? */
+    private final boolean isLeader;
+
+    /** Is this still active? */
+    private volatile boolean active = true;
+
+    /** All instances. */
+    private final Map<String, String> allInstances;
+
+    /** Instance comparator. */
+    private final InstanceDescriptionComparator instanceComparator;
+
+    /** JobManagerConfiguration. */
+    private final JobManagerConfiguration jobManagerConfiguration;
+
+    /** Topology view. */
+    private final TopologyView view;
+
+    public static final class InstanceDescriptionComparator implements Comparator<InstanceDescription> {
+
+        private final String localClusterId;
+
+
+        public InstanceDescriptionComparator(final String clusterId) {
+            this.localClusterId = clusterId;
+        }
+
+        @Override
+        public int compare(final InstanceDescription o1, final InstanceDescription o2) {
+            if ( o1.getSlingId().equals(o2.getSlingId()) ) {
+                return 0;
+            }
+            final boolean o1IsLocalCluster = localClusterId.equals(o1.getClusterView().getId());
+            final boolean o2IsLocalCluster = localClusterId.equals(o2.getClusterView().getId());
+            if ( o1IsLocalCluster && !o2IsLocalCluster ) {
+                return -1;
+            }
+            if ( !o1IsLocalCluster && o2IsLocalCluster ) {
+                return 1;
+            }
+            if ( o1IsLocalCluster ) {
+                if ( o1.isLeader() && !o2.isLeader() ) {
+                    return -1;
+                } else if ( o2.isLeader() && !o1.isLeader() ) {
+                    return 1;
+                }
+            }
+            return o1.getSlingId().compareTo(o2.getSlingId());
+        }
+    }
+
+    public static Map<String, String> getAllInstancesMap(final TopologyView view) {
+        final Map<String, String> allInstances = new TreeMap<String, String>();
+
+        for(final InstanceDescription desc : view.getInstances() ) {
+            final String topics = desc.getProperty(PROPERTY_TOPICS);
+            if ( topics != null && topics.length() > 0 ) {
+                allInstances.put(desc.getSlingId(), topics);
+            } else {
+                allInstances.put(desc.getSlingId(), "");
+            }
+        }
+        return allInstances;
+    }
+
+    /**
+     * Create a new instance
+     * @param view The new view
+     * @param config The current job manager configuration.
+     */
+    public TopologyCapabilities(final TopologyView view,
+            final JobManagerConfiguration config) {
+        this.jobManagerConfiguration = config;
+        this.instanceComparator = new InstanceDescriptionComparator(view.getLocalInstance().getClusterView().getId());
+        this.isLeader = view.getLocalInstance().isLeader();
+        this.allInstances = getAllInstancesMap(view);
+        final Map<String, List<InstanceDescription>> newCaps = new HashMap<String, List<InstanceDescription>>();
+        for(final InstanceDescription desc : view.getInstances() ) {
+            final String topics = desc.getProperty(PROPERTY_TOPICS);
+            if ( topics != null && topics.length() > 0 ) {
+                this.logger.debug("Detected capabilities of {} : {}", desc.getSlingId(), topics);
+                for(final String topic : topics.split(",") ) {
+                    List<InstanceDescription> list = newCaps.get(topic);
+                    if ( list == null ) {
+                        list = new ArrayList<InstanceDescription>();
+                        newCaps.put(topic, list);
+                    }
+                    list.add(desc);
+                    Collections.sort(list, this.instanceComparator);
+                }
+            }
+            this.instanceMap.put(desc.getSlingId(), desc);
+        }
+        this.instanceCapabilities = newCaps;
+        this.view = view;
+    }
+
+    /**
+     * Is this capabilities the same as represented by the provided instance map?
+     * @param newAllInstancesMap The instance map
+     * @return {@code true} if they represent the same state.
+     */
+    public boolean isSame(final Map<String, String> newAllInstancesMap) {
+        return this.allInstances.equals(newAllInstancesMap);
+    }
+
+    /**
+     * Deactivate this object.
+     */
+    public void deactivate() {
+        this.active = false;
+    }
+
+    /**
+     * Is this object still active?
+     * If it is not active anymore it should not be used!
+     * @return {@code true} if still active.
+     */
+    public boolean isActive() {
+        return this.active && this.jobManagerConfiguration.isActive() && this.view.isCurrent();
+    }
+
+    /**
+     * Is this instance still active?
+     * @param instanceId The instance id
+     * @return {@code true} if the instance is active.
+     */
+    public boolean isActive(final String instanceId) {
+        return this.allInstances.containsKey(instanceId);
+    }
+    /**
+     * Is the current instance the leader?
+     */
+    public boolean isLeader() {
+        return this.isLeader;
+    }
+
+    /**
+     * Add instances to the list if not already included
+     */
+    private void addAll(final List<InstanceDescription> potentialTargets, final List<InstanceDescription> newTargets) {
+        if ( newTargets != null ) {
+            for(final InstanceDescription desc : newTargets) {
+                boolean found = false;
+                for(final InstanceDescription existingDesc : potentialTargets) {
+                    if ( desc.getSlingId().equals(existingDesc.getSlingId()) ) {
+                        found = true;
+                        break;
+                    }
+                }
+                if ( !found ) {
+                    potentialTargets.add(desc);
+                }
+            }
+        }
+    }
+
+    /**
+     * Return the potential targets (Sling IDs) sorted by ID
+     * @return A list of instance descriptions. The list might be empty.
+     */
+    public List<InstanceDescription> getPotentialTargets(final String jobTopic) {
+        // calculate potential targets
+        final List<InstanceDescription> potentialTargets = new ArrayList<InstanceDescription>();
+
+        // first: topic targets - directly handling the topic
+        addAll(potentialTargets, this.instanceCapabilities.get(jobTopic));
+
+        // second: category targets - handling the topic category
+        int pos = jobTopic.lastIndexOf('/');
+        if ( pos > 0 ) {
+            final String category = jobTopic.substring(0, pos + 1).concat("*");
+            addAll(potentialTargets, this.instanceCapabilities.get(category));
+
+            // search deep consumers (since 1.2 of the consumer package)
+            do {
+                final String subCategory = jobTopic.substring(0, pos + 1).concat("**");
+                addAll(potentialTargets, this.instanceCapabilities.get(subCategory));
+
+                pos = jobTopic.lastIndexOf('/', pos - 1);
+            } while ( pos > 0 );
+        }
+        Collections.sort(potentialTargets, this.instanceComparator);
+
+        return potentialTargets;
+    }
+
+    /**
+     * Detect the target instance.
+     */
+    public String detectTarget(final String jobTopic, final Map<String, Object> jobProperties,
+            final QueueInfo queueInfo) {
+        final List<InstanceDescription> potentialTargets = this.getPotentialTargets(jobTopic);
+        logger.debug("Potential targets for {} : {}", jobTopic, potentialTargets);
+        String createdOn = null;
+        if ( jobProperties != null ) {
+            createdOn = (String) jobProperties.get(org.apache.sling.event.jobs.Job.PROPERTY_JOB_CREATED_INSTANCE);
+        }
+        if ( createdOn == null ) {
+            createdOn = Environment.APPLICATION_ID;
+        }
+        final InstanceDescription createdOnInstance = this.instanceMap.get(createdOn);
+
+        if ( potentialTargets != null && potentialTargets.size() > 0 ) {
+            if ( createdOnInstance != null ) {
+                // create a list with local targets first.
+                final List<InstanceDescription> localTargets = new ArrayList<InstanceDescription>();
+                for(final InstanceDescription desc : potentialTargets) {
+                    if ( desc.getClusterView().getId().equals(createdOnInstance.getClusterView().getId()) ) {
+                        if ( !this.jobManagerConfiguration.disableDistribution() || desc.isLeader() ) {
+                            localTargets.add(desc);
+                        }
+                    }
+                }
+                if ( localTargets.size() > 0 ) {
+                    potentialTargets.clear();
+                    potentialTargets.addAll(localTargets);
+                    logger.debug("Potential targets filtered for {} : {}", jobTopic, potentialTargets);
+                }
+            }
+            // check prefer run on creation instance
+            if ( queueInfo.queueConfiguration.isPreferRunOnCreationInstance() ) {
+                InstanceDescription creationDesc = null;
+                for(final InstanceDescription desc : potentialTargets) {
+                    if ( desc.getSlingId().equals(createdOn) ) {
+                        creationDesc = desc;
+                        break;
+                    }
+                }
+                if ( creationDesc != null ) {
+                    potentialTargets.clear();
+                    potentialTargets.add(creationDesc);
+                    logger.debug("Potential targets reduced to creation instance for {} : {}", jobTopic, potentialTargets);
+                }
+            }
+            if ( queueInfo.queueConfiguration.getType() == QueueConfiguration.Type.ORDERED ) {
+                // for ordered queues we always pick the first as we have to pick the same target on each cluster view
+                // on all instances (TODO - we could try to do some round robin of the whole queue)
+                final String result = potentialTargets.get(0).getSlingId();
+                logger.debug("Target for {} : {}", jobTopic, result);
+
+                return result;
+            }
+            // TODO - this is a simple round robin which is not based on the actual load
+            //        of the instances
+            Integer index = this.roundRobinMap.get(jobTopic);
+            if ( index == null ) {
+                index = 0;
+            }
+            if ( index >= potentialTargets.size() ) {
+                index = 0;
+            }
+            this.roundRobinMap.put(jobTopic, index + 1);
+            final String result = potentialTargets.get(index).getSlingId();
+            logger.debug("Target for {} : {}", jobTopic, result);
+            return result;
+        }
+
+        return null;
+    }
+
+    /**
+     * Get the instance capabilities.
+     * @return The map of instance capabilities.
+     */
+    public Map<String, List<InstanceDescription>> getInstanceCapabilities() {
+        return this.instanceCapabilities;
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/config/TopologyHandler.java b/src/main/java/org/apache/sling/event/impl/jobs/config/TopologyHandler.java
new file mode 100644
index 0000000..2c49ca2
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/config/TopologyHandler.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.config;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.discovery.TopologyEvent;
+import org.apache.sling.discovery.TopologyEventListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This topology handler is handling the topology change events asynchronously
+ * and processes them by queuing them.
+ */
+@Component
+@Service(value = TopologyEventListener.class)
+public class TopologyHandler implements TopologyEventListener, Runnable {
+
+    /** The logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass().getName());
+
+    @Reference
+    private JobManagerConfiguration configuration;
+
+    /** A local queue for async handling of the events */
+    private final BlockingQueue<QueueItem> queue = new LinkedBlockingQueue<QueueItem>();
+
+    /** Active flag. */
+    private final AtomicBoolean isActive = new AtomicBoolean(false);
+
+    @Activate
+    protected void activate() {
+        this.isActive.set(true);
+        final Thread thread = new Thread(this, "Apache Sling Job Topology Listener Thread");
+        thread.setDaemon(true);
+
+        thread.start();
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        this.isActive.set(false);
+        this.queue.clear();
+        try {
+            this.queue.put(new QueueItem());
+        } catch ( final InterruptedException ie) {
+            logger.warn("Thread got interrupted.", ie);
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    @Override
+    public void handleTopologyEvent(final TopologyEvent event) {
+        final QueueItem item = new QueueItem();
+        item.event = event;
+        try {
+            this.queue.put(item);
+        } catch ( final InterruptedException ie) {
+            logger.warn("Thread got interrupted.", ie);
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    @Override
+    public void run() {
+        while ( isActive.get() ) {
+            QueueItem item = null;
+            try {
+                item = this.queue.take();
+            } catch ( final InterruptedException ie) {
+                logger.warn("Thread got interrupted.", ie);
+                Thread.currentThread().interrupt();
+                isActive.set(false);
+            }
+            if ( isActive.get() && item != null && item.event != null ) {
+                final JobManagerConfiguration config = this.configuration;
+                if ( config != null ) {
+                    config.handleTopologyEvent(item.event);
+                }
+            }
+        }
+    }
+
+    /**
+     * We need a holder class to be able to put something into the queue to stop it.
+     */
+    public static final class QueueItem {
+        public TopologyEvent event;
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/console/InventoryPlugin.java b/src/main/java/org/apache/sling/event/impl/jobs/console/InventoryPlugin.java
new file mode 100644
index 0000000..619136c
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/console/InventoryPlugin.java
@@ -0,0 +1,471 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.console;
+
+import java.io.PrintWriter;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.felix.inventory.Format;
+import org.apache.felix.inventory.InventoryPrinter;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Properties;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.event.impl.jobs.JobConsumerManager;
+import org.apache.sling.event.impl.jobs.config.InternalQueueConfiguration;
+import org.apache.sling.event.impl.jobs.config.JobManagerConfiguration;
+import org.apache.sling.event.impl.jobs.config.TopologyCapabilities;
+import org.apache.sling.event.jobs.JobManager;
+import org.apache.sling.event.jobs.Queue;
+import org.apache.sling.event.jobs.QueueConfiguration;
+import org.apache.sling.event.jobs.ScheduleInfo;
+import org.apache.sling.event.jobs.ScheduledJobInfo;
+import org.apache.sling.event.jobs.Statistics;
+import org.apache.sling.event.jobs.TopicStatistics;
+
+/**
+ * This is a inventory plugin displaying the active queues, some statistics
+ * and the configurations.
+ * @since 3.2
+ */
+@Component
+@Service(value={InventoryPrinter.class})
+@Properties({
+    @Property(name=InventoryPrinter.NAME, value="slingjobs"),
+    @Property(name=InventoryPrinter.TITLE, value="Sling Jobs"),
+    @Property(name=InventoryPrinter.FORMAT, value={"TEXT", "JSON"}),
+    @Property(name=InventoryPrinter.WEBCONSOLE, boolValue=false)
+})
+public class InventoryPlugin implements InventoryPrinter {
+
+    @Reference
+    private JobManager jobManager;
+
+    @Reference
+    private JobManagerConfiguration configuration;
+
+    @Reference
+    private JobConsumerManager jobConsumerManager;
+
+    /**
+     * Format an array.
+     */
+    private String formatArrayAsText(final String[] array) {
+        if ( array == null || array.length == 0 ) {
+            return "";
+        }
+        return Arrays.toString(array);
+    }
+
+    private String formatType(final QueueConfiguration.Type type) {
+        switch ( type ) {
+            case ORDERED : return "Ordered";
+            case TOPIC_ROUND_ROBIN : return "Topic Round Robin";
+            case UNORDERED : return "Parallel";
+        }
+        return type.toString();
+    }
+
+    /** Default date format used. */
+    private final DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss:SSS yyyy-MMM-dd");
+
+    /**
+     * Format a date
+     */
+    private synchronized String formatDate(final long time) {
+        if ( time == -1 ) {
+            return "-";
+        }
+        final Date d = new Date(time);
+        return dateFormat.format(d);
+    }
+
+    /**
+     * Format time (= duration)
+     */
+    private String formatTime(final long time) {
+        if ( time == 0 ) {
+            return "-";
+        }
+        if ( time < 1000 ) {
+            return time + " ms";
+        } else if ( time < 1000 * 60 ) {
+            return time / 1000 + " secs";
+        }
+        final long min = time / 1000 / 60;
+        final long secs = (time - min * 1000 * 60);
+        return min + " min " + secs / 1000 + " secs";
+    }
+
+    /**
+     * @see org.apache.felix.inventory.InventoryPrinter#print(java.io.PrintWriter, org.apache.felix.inventory.Format, boolean)
+     */
+    @Override
+    public void print(final PrintWriter pw, final Format format, final boolean isZip) {
+        if ( format.equals(Format.TEXT) ) {
+            printText(pw);
+        } else if ( format.equals(Format.JSON) ) {
+            printJson(pw);
+        }
+    }
+
+    private void printText(final PrintWriter pw) {
+        pw.println("Apache Sling Job Handling");
+        pw.println("-------------------------");
+
+        String topics = this.jobConsumerManager.getTopics();
+        if ( topics == null ) {
+            topics = "";
+        }
+
+        Statistics s = this.jobManager.getStatistics();
+        pw.println("Overall Statistics");
+        pw.printf("Start Time : %s%n", formatDate(s.getStartTime()));
+        pw.printf("Local topic consumers: %s%n", topics);
+        pw.printf("Last Activated : %s%n", formatDate(s.getLastActivatedJobTime()));
+        pw.printf("Last Finished : %s%n", formatDate(s.getLastFinishedJobTime()));
+        pw.printf("Queued Jobs : %s%n", s.getNumberOfQueuedJobs());
+        pw.printf("Active Jobs : %s%n", s.getNumberOfActiveJobs());
+        pw.printf("Jobs : %s%n", s.getNumberOfJobs());
+        pw.printf("Finished Jobs : %s%n", s.getNumberOfFinishedJobs());
+        pw.printf("Failed Jobs : %s%n", s.getNumberOfFailedJobs());
+        pw.printf("Cancelled Jobs : %s%n", s.getNumberOfCancelledJobs());
+        pw.printf("Processed Jobs : %s%n", s.getNumberOfProcessedJobs());
+        pw.printf("Average Processing Time : %s%n", formatTime(s.getAverageProcessingTime()));
+        pw.printf("Average Waiting Time : %s%n", formatTime(s.getAverageWaitingTime()));
+        pw.println();
+
+        pw.println("Topology Capabilities");
+        final TopologyCapabilities cap = this.configuration.getTopologyCapabilities();
+        if ( cap == null ) {
+            pw.print("No topology information available !");
+        } else {
+            final Map<String, List<InstanceDescription>> instanceCaps = cap.getInstanceCapabilities();
+            for(final Map.Entry<String, List<InstanceDescription>> entry : instanceCaps.entrySet()) {
+                final StringBuilder sb = new StringBuilder();
+                for(final InstanceDescription id : entry.getValue()) {
+                    if ( sb.length() > 0 ) {
+                        sb.append(", ");
+                    }
+                    if ( id.isLocal() ) {
+                        sb.append("local");
+                    } else {
+                        sb.append(id.getSlingId());
+                    }
+                }
+                pw.printf("%s : %s%n", entry.getKey(), sb.toString());
+            }
+        }
+        pw.println();
+
+        pw.println("Scheduled Jobs");
+        final Collection<ScheduledJobInfo> infos = this.jobManager.getScheduledJobs();
+        if ( infos.size() == 0 ) {
+            pw.print("No jobs currently scheduled");
+        } else {
+            for(final ScheduledJobInfo info : infos) {
+                pw.println("Schedule");
+                pw.printf("Job Topic< : %s%n", info.getJobTopic());
+                pw.print("Schedules : ");
+                boolean first = true;
+                for(final ScheduleInfo si : info.getSchedules() ) {
+                    if ( !first ) {
+                        pw.print(", ");
+                    }
+                    first = false;
+                    switch ( si.getType() ) {
+                    case YEARLY : pw.printf("YEARLY %s %s : %s:%s", si.getMonthOfYear(), si.getDayOfMonth(), si.getHourOfDay(), si.getMinuteOfHour());
+                                  break;
+                    case MONTHLY : pw.printf("MONTHLY %s : %s:%s", si.getDayOfMonth(), si.getHourOfDay(), si.getMinuteOfHour());
+                                  break;
+                    case WEEKLY : pw.printf("WEEKLY %s : %s:%s", si.getDayOfWeek(), si.getHourOfDay(), si.getMinuteOfHour());
+                                  break;
+                    case DAILY : pw.printf("DAILY %s:%s", si.getHourOfDay(), si.getMinuteOfHour());
+                                 break;
+                    case HOURLY : pw.printf("HOURLY %s", si.getMinuteOfHour());
+                                 break;
+                    case CRON : pw.printf("CRON %s", si.getExpression());
+                                 break;
+                    default : pw.printf("AT %s", si.getAt());
+                    }
+                }
+                pw.println();
+                pw.println();
+            }
+        }
+        pw.println();
+
+        boolean isEmpty = true;
+        for(final Queue q : this.jobManager.getQueues()) {
+            isEmpty = false;
+            pw.printf("Active JobQueue: %s %s%n", q.getName(),
+                    q.isSuspended() ? "(SUSPENDED)" : "");
+
+            s = q.getStatistics();
+            final QueueConfiguration c = q.getConfiguration();
+            pw.println("Statistics");
+            pw.printf("Start Time : %s%n", formatDate(s.getStartTime()));
+            pw.printf("Last Activated : %s%n", formatDate(s.getLastActivatedJobTime()));
+            pw.printf("Last Finished : %s%n", formatDate(s.getLastFinishedJobTime()));
+            pw.printf("Queued Jobs : %s%n", s.getNumberOfQueuedJobs());
+            pw.printf("Active Jobs : %s%n", s.getNumberOfActiveJobs());
+            pw.printf("Jobs : %s%n", s.getNumberOfJobs());
+            pw.printf("Finished Jobs : %s%n", s.getNumberOfFinishedJobs());
+            pw.printf("Failed Jobs : %s%n", s.getNumberOfFailedJobs());
+            pw.printf("Cancelled Jobs : %s%n", s.getNumberOfCancelledJobs());
+            pw.printf("Processed Jobs : %s%n", s.getNumberOfProcessedJobs());
+            pw.printf("Average Processing Time : %s%n", formatTime(s.getAverageProcessingTime()));
+            pw.printf("Average Waiting Time : %s%n", formatTime(s.getAverageWaitingTime()));
+            pw.printf("Status Info : %s%n", q.getStateInfo());
+            pw.println("Configuration");
+            pw.printf("Type : %s%n", formatType(c.getType()));
+            pw.printf("Topics : %s%n", formatArrayAsText(c.getTopics()));
+            pw.printf("Max Parallel : %s%n", c.getMaxParallel());
+            pw.printf("Max Retries : %s%n", c.getMaxRetries());
+            pw.printf("Retry Delay : %s ms%n", c.getRetryDelayInMs());
+            pw.printf("Priority : %s%n", c.getThreadPriority());
+            pw.println();
+        }
+        if ( isEmpty ) {
+            pw.println("No active queues.");
+            pw.println();
+        }
+
+        for(final TopicStatistics ts : this.jobManager.getTopicStatistics()) {
+            pw.printf("Topic Statistics - %s%n", ts.getTopic());
+            pw.printf("Last Activated : %s%n", formatDate(ts.getLastActivatedJobTime()));
+            pw.printf("Last Finished : %s%n", formatDate(ts.getLastFinishedJobTime()));
+            pw.printf("Finished Jobs : %s%n", ts.getNumberOfFinishedJobs());
+            pw.printf("Failed Jobs : %s%n", ts.getNumberOfFailedJobs());
+            pw.printf("Cancelled Jobs : %s%n", ts.getNumberOfCancelledJobs());
+            pw.printf("Processed Jobs : %s%n", ts.getNumberOfProcessedJobs());
+            pw.printf("Average Processing Time : %s%n", formatTime(ts.getAverageProcessingTime()));
+            pw.printf("Average Waiting Time : %s%n", formatTime(ts.getAverageWaitingTime()));
+            pw.println();
+        }
+
+        pw.println("Apache Sling Job Handling - Job Queue Configurations");
+        pw.println("----------------------------------------------------");
+        this.printQueueConfiguration(pw, this.configuration.getQueueConfigurationManager().getMainQueueConfiguration());
+        final InternalQueueConfiguration[] configs = this.configuration.getQueueConfigurationManager().getConfigurations();
+        for(final InternalQueueConfiguration c : configs ) {
+            this.printQueueConfiguration(pw, c);
+        }
+    }
+
+    private void printQueueConfiguration(final PrintWriter pw, final InternalQueueConfiguration c) {
+        pw.printf("Job Queue Configuration: %s%n",
+                c.getName());
+        pw.printf("Valid : %s%n", c.isValid());
+        pw.printf("Type : %s%n", formatType(c.getType()));
+        pw.printf("Topics : %s%n", formatArrayAsText(c.getTopics()));
+        pw.printf("Max Parallel : %s%n", c.getMaxParallel());
+        pw.printf("Max Retries : %s%n", c.getMaxRetries());
+        pw.printf("Retry Delay : %s ms%n", c.getRetryDelayInMs());
+        pw.printf("Priority : %s%n", c.getThreadPriority());
+        pw.printf("Ranking : %s%n", c.getRanking());
+
+        pw.println();
+    }
+
+    private void printJson(final PrintWriter pw) {
+        pw.println("{");
+        Statistics s = this.jobManager.getStatistics();
+        pw.println("  \"statistics\" : {");
+        pw.printf("    \"startTime\" : %s,%n", s.getStartTime());
+        pw.printf("    \"startTimeText\" : \"%s\",%n", formatDate(s.getStartTime()));
+        pw.printf("    \"lastActivatedJobTime\" : %s,%n", s.getLastActivatedJobTime());
+        pw.printf("    \"lastActivatedJobTimeText\" : \"%s\",%n", formatDate(s.getLastActivatedJobTime()));
+        pw.printf("    \"lastFinishedJobTime\" : %s,%n", s.getLastFinishedJobTime());
+        pw.printf("    \"lastFinishedJobTimeText\" : \"%s\",%n", formatDate(s.getLastFinishedJobTime()));
+        pw.printf("    \"numberOfQueuedJobs\" : %s,%n", s.getNumberOfQueuedJobs());
+        pw.printf("    \"numberOfActiveJobs\" : %s,%n", s.getNumberOfActiveJobs());
+        pw.printf("    \"numberOfJobs\" : %s,%n", s.getNumberOfJobs());
+        pw.printf("    \"numberOfFinishedJobs\" : %s,%n", s.getNumberOfFinishedJobs());
+        pw.printf("    \"numberOfFailedJobs\" : %s,%n", s.getNumberOfFailedJobs());
+        pw.printf("    \"numberOfCancelledJobs\" : %s,%n", s.getNumberOfCancelledJobs());
+        pw.printf("    \"numberOfProcessedJobs\" : %s,%n", s.getNumberOfProcessedJobs());
+        pw.printf("    \"averageProcessingTime\" : %s,%n", s.getAverageProcessingTime());
+        pw.printf("    \"averageProcessingTimeText\" : \"%s\",%n", formatTime(s.getAverageProcessingTime()));
+        pw.printf("    \"averageWaitingTime\" : %s,%n", s.getAverageWaitingTime());
+        pw.printf("    \"averageWaitingTimeText\" : \"%s\"%n", formatTime(s.getAverageWaitingTime()));
+        pw.print("  }");
+
+        final TopologyCapabilities cap = this.configuration.getTopologyCapabilities();
+        if ( cap != null ) {
+            pw.println(",");
+            pw.println("  \"capabilities\" : [");
+            final Map<String, List<InstanceDescription>> instanceCaps = cap.getInstanceCapabilities();
+            final Iterator<Map.Entry<String, List<InstanceDescription>>> iter = instanceCaps.entrySet().iterator();
+            while ( iter.hasNext() ) {
+                final Map.Entry<String, List<InstanceDescription>> entry = iter.next();
+                final List<String> instances = new ArrayList<String>();
+                for(final InstanceDescription id : entry.getValue()) {
+                    if ( id.isLocal() ) {
+                        instances.add("local");
+                    } else {
+                        instances.add(id.getSlingId());
+                    }
+                }
+                pw.println("    {");
+                pw.printf("       \"topic\" : \"%s\",%n", entry.getKey());
+                pw.printf("       \"instances\" : %s%n", formatArrayAsJson(instances.toArray(new String[instances.size()])));
+                if ( iter.hasNext() ) {
+                    pw.println("    },");
+                } else {
+                    pw.println("    }");
+                }
+            }
+            pw.print("  ]");
+        }
+
+        boolean first = true;
+        for(final Queue q : this.jobManager.getQueues()) {
+            pw.println(",");
+            if ( first ) {
+                pw.println("  \"queues\" : [");
+                first = false;
+            }
+            pw.println("    {");
+            pw.printf("      \"name\" : \"%s\",%n", q.getName());
+            pw.printf("      \"suspended\" : %s,%n", q.isSuspended());
+
+            s = q.getStatistics();
+            pw.println("      \"statistics\" : {");
+            pw.printf("        \"startTime\" : %s,%n", s.getStartTime());
+            pw.printf("        \"startTimeText\" : \"%s\",%n", formatDate(s.getStartTime()));
+            pw.printf("        \"lastActivatedJobTime\" : %s,%n", s.getLastActivatedJobTime());
+            pw.printf("        \"lastActivatedJobTimeText\" : \"%s\",%n", formatDate(s.getLastActivatedJobTime()));
+            pw.printf("        \"lastFinishedJobTime\" : %s,%n", s.getLastFinishedJobTime());
+            pw.printf("        \"lastFinishedJobTimeText\" : \"%s\",%n", formatDate(s.getLastFinishedJobTime()));
+            pw.printf("        \"numberOfQueuedJobs\" : %s,%n", s.getNumberOfQueuedJobs());
+            pw.printf("        \"numberOfActiveJobs\" : %s,%n", s.getNumberOfActiveJobs());
+            pw.printf("        \"numberOfJobs\" : %s,%n", s.getNumberOfJobs());
+            pw.printf("        \"numberOfFinishedJobs\" : %s,%n", s.getNumberOfFinishedJobs());
+            pw.printf("        \"numberOfFailedJobs\" : %s,%n", s.getNumberOfFailedJobs());
+            pw.printf("        \"numberOfCancelledJobs\" : %s,%n", s.getNumberOfCancelledJobs());
+            pw.printf("        \"numberOfProcessedJobs\" : %s,%n", s.getNumberOfProcessedJobs());
+            pw.printf("        \"averageProcessingTime\" : %s,%n", s.getAverageProcessingTime());
+            pw.printf("        \"averageProcessingTimeText\" : \"%s\",%n", formatTime(s.getAverageProcessingTime()));
+            pw.printf("        \"averageWaitingTime\" : %s,%n", s.getAverageWaitingTime());
+            pw.printf("        \"averageWaitingTimeText\" : \"%s\"%n", formatTime(s.getAverageWaitingTime()));
+            pw.print("      },");
+
+            final QueueConfiguration c = q.getConfiguration();
+            pw.printf("      \"stateInfo\" : \"%s\",%n", q.getStateInfo());
+            pw.println("      \"configuration\" : {");
+            pw.printf("        \"type\" : \"%s\",%n", c.getType());
+            pw.printf("        \"topics\" : \"%s\",%n", formatArrayAsJson(c.getTopics()));
+            pw.printf("        \"maxParallel\" : %s,%n", c.getMaxParallel());
+            pw.printf("        \"maxRetries\" : %s,%n", c.getMaxRetries());
+            pw.printf("        \"retryDelayInMs\" : %s,%n", c.getRetryDelayInMs());
+            pw.printf("        \"priority\" : \"%s\"%n", c.getThreadPriority());
+            pw.println("      }");
+            pw.print("    }");
+        }
+        if ( !first ) {
+            pw.print("  ]");
+        }
+
+        first = true;
+        for(final TopicStatistics ts : this.jobManager.getTopicStatistics()) {
+            pw.println(",");
+            if ( first ) {
+                pw.println("  \"topicStatistics\" : [");
+                first = false;
+            }
+            pw.println("    {");
+            pw.printf("      \"topic\" : \"%s\",%n", ts.getTopic());
+            pw.printf("      \"lastActivatedJobTime\" : %s,%n", ts.getLastActivatedJobTime());
+            pw.printf("      \"lastActivatedJobTimeText\" : \"%s\",%n", formatDate(ts.getLastActivatedJobTime()));
+            pw.printf("      \"lastFinishedJobTime\" : %s,%n", ts.getLastFinishedJobTime());
+            pw.printf("      \"lastFinishedJobTimeText\" : \"%s\",%n", formatDate(ts.getLastFinishedJobTime()));
+            pw.printf("      \"numberOfFinishedJobs\" : %s,%n", ts.getNumberOfFinishedJobs());
+            pw.printf("      \"numberOfFailedJobs\" : %s,%n", ts.getNumberOfFailedJobs());
+            pw.printf("      \"numberOfCancelledJobs\" : %s,%n", ts.getNumberOfCancelledJobs());
+            pw.printf("      \"numberOfProcessedJobs\" : %s,%n", ts.getNumberOfProcessedJobs());
+            pw.printf("      \"averageProcessingTime\" : %s,%n", ts.getAverageProcessingTime());
+            pw.printf("      \"averageProcessingTimeText\" : \"%s\",%n", formatTime(ts.getAverageProcessingTime()));
+            pw.printf("      \"averageWaitingTime\" : %s,%n", ts.getAverageWaitingTime());
+            pw.printf("      \"averageWaitingTimeText\" : \"%s\"%n", formatTime(ts.getAverageWaitingTime()));
+            pw.print("    }");
+        }
+        if ( !first ) {
+            pw.print("  ]");
+        }
+
+        pw.println(",");
+        pw.println("  \"configurations\" : [");
+        this.printQueueConfigurationJson(pw, this.configuration.getQueueConfigurationManager().getMainQueueConfiguration());
+        final InternalQueueConfiguration[] configs = this.configuration.getQueueConfigurationManager().getConfigurations();
+        for(final InternalQueueConfiguration c : configs ) {
+            pw.println(",");
+            this.printQueueConfigurationJson(pw, c);
+        }
+        pw.println();
+        pw.println("  ]");
+        pw.println("}");
+    }
+
+    private void printQueueConfigurationJson(final PrintWriter pw, final InternalQueueConfiguration c) {
+        pw.println("    {");
+        pw.printf("      \"name\" : \"%s\",%n", c.getName());
+        pw.printf("      \"valid\" : %s,%n", c.isValid());
+        pw.printf("      \"type\" : \"%s\",%n", c.getType());
+        pw.printf("      \"topics\" : %s,%n", formatArrayAsJson(c.getTopics()));
+        pw.printf("      \"maxParallel\" : %s,%n", c.getMaxParallel());
+        pw.printf("      \"maxRetries\" : %s,%n", c.getMaxRetries());
+        pw.printf("      \"retryDelayInMs\" : %s,%n", c.getRetryDelayInMs());
+        pw.printf("      \"priority\" : \"%s\",%n", c.getThreadPriority());
+        pw.printf("      \"ranking\" : %s%n", c.getRanking());
+        pw.print("    }");
+    }
+
+    /**
+     * Format an array.
+     */
+    private String formatArrayAsJson(final String[] array) {
+        if ( array == null || array.length == 0 ) {
+            return "[]";
+        }
+        final StringBuilder sb = new StringBuilder("[");
+        boolean first = true;
+        for(final String s : array) {
+            if ( !first ) {
+                sb.append(", ");
+            }
+            first = false;
+            sb.append("\"");
+            sb.append(s);
+            sb.append("\"");
+        }
+        sb.append("]");
+        return sb.toString();
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/console/WebConsolePlugin.java b/src/main/java/org/apache/sling/event/impl/jobs/console/WebConsolePlugin.java
new file mode 100644
index 0000000..ffe94b5
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/console/WebConsolePlugin.java
@@ -0,0 +1,453 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.console;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.URLEncoder;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Properties;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.api.request.ResponseUtil;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.event.impl.jobs.JobConsumerManager;
+import org.apache.sling.event.impl.jobs.config.InternalQueueConfiguration;
+import org.apache.sling.event.impl.jobs.config.JobManagerConfiguration;
+import org.apache.sling.event.impl.jobs.config.TopologyCapabilities;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.JobManager;
+import org.apache.sling.event.jobs.Queue;
+import org.apache.sling.event.jobs.QueueConfiguration;
+import org.apache.sling.event.jobs.ScheduleInfo;
+import org.apache.sling.event.jobs.ScheduledJobInfo;
+import org.apache.sling.event.jobs.Statistics;
+import org.apache.sling.event.jobs.TopicStatistics;
+import org.apache.sling.event.jobs.consumer.JobConsumer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This is a web console plugin displaying the active queues, some statistics
+ * and the configurations.
+ * @since 3.0
+ */
+@Component
+@Service(value={javax.servlet.Servlet.class, JobConsumer.class})
+@Properties({
+    @Property(name="felix.webconsole.label", value="slingevent"),
+    @Property(name="felix.webconsole.title", value="Jobs"),
+    @Property(name="felix.webconsole.category", value="Sling"),
+    @Property(name=JobConsumer.PROPERTY_TOPICS, value={"sling/webconsole/test"})
+})
+public class WebConsolePlugin extends HttpServlet implements JobConsumer {
+
+    private static final String SLING_WEBCONSOLE_TEST_JOB_TOPIC = "sling/webconsole/test";
+
+    private static final long serialVersionUID = -6983227434841706385L;
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    @Reference
+    private JobManager jobManager;
+
+    @Reference
+    private JobManagerConfiguration configuration;
+
+    @Reference
+    private JobConsumerManager jobConsumerManager;
+
+    private static final String PAR_QUEUE = "queue";
+
+    private Queue getQueue(final HttpServletRequest req) {
+        final String name = req.getParameter(PAR_QUEUE);
+        if ( name != null ) {
+            for(final Queue q : this.jobManager.getQueues()) {
+                if ( name.equals(q.getName()) ) {
+                    return q;
+                }
+            }
+        }
+        return null;
+    }
+
+    private String getQueueErrorMessage(final HttpServletRequest req, final String command) {
+        final String name = req.getParameter(PAR_QUEUE);
+        if ( name == null || name.length() == 0 ) {
+            return "Queue parameter missing for opertation " + command;
+        }
+        return "Queue with name '" + name + "' not found for operation " + command;
+    }
+
+    @Override
+    protected void doPost(final HttpServletRequest req, final HttpServletResponse resp)
+    throws ServletException, IOException {
+        String msg = null;
+        final String cmd = req.getParameter("action");
+        if ( "suspend".equals(cmd) ) {
+            final Queue q = this.getQueue(req);
+            if ( q != null ) {
+                q.suspend();
+            } else {
+                msg = this.getQueueErrorMessage(req, "suspend");
+            }
+        } else if ( "resume".equals(cmd) ) {
+            final Queue q = this.getQueue(req);
+            if ( q != null ) {
+                q.resume();
+            } else {
+                msg = this.getQueueErrorMessage(req, "resume");
+            }
+        } else if ( "reset".equals(cmd) ) {
+            if ( req.getParameter(PAR_QUEUE) == null || req.getParameter(PAR_QUEUE).length() == 0 ) {
+                this.jobManager.getStatistics().reset();
+            } else {
+                final Queue q = this.getQueue(req);
+                if ( q != null ) {
+                    q.getStatistics().reset();
+                } else {
+                    msg = this.getQueueErrorMessage(req, "reset");
+                }
+            }
+        } else if ( "test".equals(cmd) ) {
+            this.startTestJob();
+        } else if ( "dropall".equals(cmd) ) {
+            final Queue q = this.getQueue(req);
+            if ( q != null ) {
+                q.removeAll();
+            } else {
+                msg = this.getQueueErrorMessage(req, "drop all");
+            }
+        } else {
+            msg = "Unknown command";
+        }
+        final String path = req.getContextPath() + req.getServletPath() + req.getPathInfo();
+        final String redirectTo;
+        if ( msg == null ) {
+            redirectTo = path;
+        } else {
+            redirectTo = path + "?message=" + URLEncoder.encode(msg, "UTF-8");
+        }
+        resp.sendRedirect(resp.encodeRedirectURL(redirectTo));
+    }
+
+    private void startTestJob() {
+        logger.info("Adding test job: {}", SLING_WEBCONSOLE_TEST_JOB_TOPIC);
+        this.jobManager.addJob(SLING_WEBCONSOLE_TEST_JOB_TOPIC, null);
+    }
+
+    @Override
+    protected void doGet(final HttpServletRequest req, final HttpServletResponse res)
+     throws ServletException, IOException {
+        final String msg = req.getParameter("message");
+        final PrintWriter pw = res.getWriter();
+
+        pw.println("<form method='POST' name='eventingcmd'>" +
+        		     "<input type='hidden' name='action' value=''/>"+
+                     "<input type='hidden' name='queue' value=''/>" +
+                   "</form>");
+        pw.println("<script type='text/javascript'>");
+        pw.println("function eventingsubmit(action, queue) {" +
+                   " document.forms['eventingcmd'].action.value = action;" +
+                   " document.forms['eventingcmd'].queue.value = queue;" +
+                   " document.forms['eventingcmd'].submit();" +
+                   "} </script>");
+
+        pw.printf("<p class='statline ui-state-highlight'>Apache Sling Job Handling%s%n</p>",
+                msg != null ? " : " + ResponseUtil.escapeXml(msg) : "");
+        pw.println("<div class='ui-widget-header ui-corner-top buttonGroup'>");
+        pw.println("<span style='float: left; margin-left: 1em'>Apache Sling Job Handling: Overall Statistics</span>");
+        this.printForm(pw, null, "Reset Stats", "reset");
+        pw.println("</div>");
+
+        pw.println("<table class='nicetable'><tbody>");
+        String topics = this.jobConsumerManager.getTopics();
+        if ( topics == null ) {
+            topics = "";
+        } else {
+            final String[] allTopics = topics.split(",");
+            final StringBuilder sb = new StringBuilder();
+            boolean first = true;
+            for(final String t : allTopics) {
+                if ( first) {
+                    first = false;
+                } else {
+                    sb.append("<br/>");
+                }
+                sb.append(ResponseUtil.escapeXml(t));
+            }
+            topics = sb.toString();
+        }
+        Statistics s = this.jobManager.getStatistics();
+        pw.printf("<tr><td>Start Time</td><td>%s</td></tr>", formatDate(s.getStartTime()));
+        pw.printf("<tr><td>Local topic consumers: </td><td>%s</td></tr>", topics);
+        pw.printf("<tr><td>Last Activated</td><td>%s</td></tr>", formatDate(s.getLastActivatedJobTime()));
+        pw.printf("<tr><td>Last Finished</td><td>%s</td></tr>", formatDate(s.getLastFinishedJobTime()));
+        pw.printf("<tr><td>Queued Jobs</td><td>%s</td></tr>", s.getNumberOfQueuedJobs());
+        pw.printf("<tr><td>Active Jobs</td><td>%s</td></tr>", s.getNumberOfActiveJobs());
+        pw.printf("<tr><td>Jobs</td><td>%s</td></tr>", s.getNumberOfJobs());
+        pw.printf("<tr><td>Finished Jobs</td><td>%s</td></tr>", s.getNumberOfFinishedJobs());
+        pw.printf("<tr><td>Failed Jobs</td><td>%s</td></tr>", s.getNumberOfFailedJobs());
+        pw.printf("<tr><td>Cancelled Jobs</td><td>%s</td></tr>", s.getNumberOfCancelledJobs());
+        pw.printf("<tr><td>Processed Jobs</td><td>%s</td></tr>", s.getNumberOfProcessedJobs());
+        pw.printf("<tr><td>Average Processing Time</td><td>%s</td></tr>", formatTime(s.getAverageProcessingTime()));
+        pw.printf("<tr><td>Average Waiting Time</td><td>%s</td></tr>", formatTime(s.getAverageWaitingTime()));
+        pw.println("</tbody></table>");
+        pw.println("<br/>");
+
+        pw.println("<table class='nicetable'><tbody>");
+        pw.println("<tr><th colspan='2'>Topology Capabilities</th></tr>");
+        final TopologyCapabilities cap = this.configuration.getTopologyCapabilities();
+        if ( cap == null ) {
+            pw.print("<tr><td colspan='2'>No topology information available !</td></tr>");
+        } else {
+            final Map<String, List<InstanceDescription>> instanceCaps = cap.getInstanceCapabilities();
+            for(final Map.Entry<String, List<InstanceDescription>> entry : instanceCaps.entrySet()) {
+                final StringBuilder sb = new StringBuilder();
+                for(final InstanceDescription id : entry.getValue()) {
+                    if ( sb.length() > 0 ) {
+                        sb.append("<br/>");
+                    }
+                    if ( id.isLocal() ) {
+                        sb.append("<b>local</b>");
+                    } else {
+                        sb.append(ResponseUtil.escapeXml(id.getSlingId()));
+                    }
+                }
+                pw.printf("<tr><td>%s</td><td>%s</td></tr>", ResponseUtil.escapeXml(entry.getKey()), sb.toString());
+            }
+        }
+        pw.println("</tbody></table>");
+        pw.println("<br/>");
+
+        pw.println("<p class='statline'>Scheduled Jobs</p>");
+        pw.println("<table class='nicetable'><tbody>");
+        final Collection<ScheduledJobInfo> infos = this.jobManager.getScheduledJobs();
+        if ( infos.size() == 0 ) {
+            pw.print("<tr><td colspan='5'>No jobs currently scheduled.</td></tr>");
+        } else {
+            pw.println("<tr><th>Schedule</th><th>Job Topic</th><th>Schedules</th></tr>");
+            int index = 1;
+            for(final ScheduledJobInfo info : infos) {
+                pw.printf("<tr><td><b>%s</b></td><td>%s</td><td>",
+                        String.valueOf(index), ResponseUtil.escapeXml(info.getJobTopic()));
+                boolean first = true;
+                for(final ScheduleInfo si : info.getSchedules() ) {
+                    if ( !first ) {
+                        pw.print("<br/>");
+                    }
+                    first = false;
+                    switch ( si.getType() ) {
+                    case YEARLY : pw.printf("YEARLY %s %s : %s:%s", si.getMonthOfYear(), si.getDayOfMonth(), si.getHourOfDay(), si.getMinuteOfHour());
+                                  break;
+                    case MONTHLY : pw.printf("MONTHLY %s : %s:%s", si.getDayOfMonth(), si.getHourOfDay(), si.getMinuteOfHour());
+                                  break;
+                    case WEEKLY : pw.printf("WEEKLY %s : %s:%s", si.getDayOfWeek(), si.getHourOfDay(), si.getMinuteOfHour());
+                                  break;
+                    case DAILY : pw.printf("DAILY %s:%s", si.getHourOfDay(), si.getMinuteOfHour());
+                                 break;
+                    case HOURLY : pw.printf("HOURLY %s", si.getMinuteOfHour());
+                                 break;
+                    case CRON : pw.printf("CRON %s", ResponseUtil.escapeXml(si.getExpression()));
+                                  break;
+                    default : pw.printf("AT %s", si.getAt());
+                    }
+                }
+                pw.print("</td></tr>");
+                index++;
+            }
+        }
+        pw.println("</tbody></table>");
+        pw.println("<br/>");
+
+        boolean isEmpty = true;
+        for(final Queue q : this.jobManager.getQueues()) {
+            isEmpty = false;
+            final String queueName = q.getName();
+            pw.println("<div class='ui-widget-header ui-corner-top buttonGroup'>");
+            pw.printf("<span style='float: left; margin-left: 1em'>Active JobQueue: %s %s</span>", ResponseUtil.escapeXml(queueName),
+                    q.isSuspended() ? "(SUSPENDED)" : "");
+            this.printForm(pw, queueName, "Reset Stats", "reset");
+            if ( q.isSuspended() ) {
+                this.printForm(pw, queueName, "Resume", "resume");
+            } else {
+                this.printForm(pw, queueName, "Suspend", "suspend");
+            }
+            this.printForm(pw, queueName, "Test", "test");
+            this.printForm(pw, queueName, "Drop All", "dropall");
+            pw.println("</div>");
+            pw.println("<table class='nicetable'><tbody>");
+
+            s = q.getStatistics();
+            final QueueConfiguration c = q.getConfiguration();
+            pw.println("<tr><th colspan='2'>Statistics</th><th colspan='2'>Configuration</th></tr>");
+            pw.printf("<tr><td>Start Time</td><td>%s</td><td>Type</td><td>%s</td></tr>", formatDate(s.getStartTime()), formatType(c.getType()));
+            pw.printf("<tr><td>Last Activated</td><td>%s</td><td>Topics</td><td>%s</td></tr>", formatDate(s.getLastActivatedJobTime()), formatArray(c.getTopics()));
+            pw.printf("<tr><td>Last Finished</td><td>%s</td><td>Max Parallel</td><td>%s</td></tr>", formatDate(s.getLastFinishedJobTime()), c.getMaxParallel());
+            pw.printf("<tr><td>Queued Jobs</td><td>%s</td><td>Max Retries</td><td>%s</td></tr>", s.getNumberOfQueuedJobs(), c.getMaxRetries());
+            pw.printf("<tr><td>Active Jobs</td><td>%s</td><td>Retry Delay</td><td>%s ms</td></tr>", s.getNumberOfActiveJobs(), c.getRetryDelayInMs());
+            pw.printf("<tr><td>Jobs</td><td>%s</td><td>Priority</td><td>%s</td></tr>", s.getNumberOfJobs(), c.getThreadPriority());
+            pw.printf("<tr><td>Finished Jobs</td><td>%s</td><td colspan='2'>&nbsp</td></tr>", s.getNumberOfFinishedJobs());
+            pw.printf("<tr><td>Failed Jobs</td><td>%s</td><td colspan='2'>&nbsp</td></tr>", s.getNumberOfFailedJobs());
+            pw.printf("<tr><td>Cancelled Jobs</td><td>%s</td><td colspan='2'>&nbsp</td></tr>", s.getNumberOfCancelledJobs());
+            pw.printf("<tr><td>Processed Jobs</td><td>%s</td><td colspan='2'>&nbsp</td></tr>", s.getNumberOfProcessedJobs());
+            pw.printf("<tr><td>Average Processing Time</td><td>%s</td><td colspan='2'>&nbsp</td></tr>", formatTime(s.getAverageProcessingTime()));
+            pw.printf("<tr><td>Average Waiting Time</td><td>%s</td><td colspan='2'>&nbsp</td></tr>", formatTime(s.getAverageWaitingTime()));
+            pw.printf("<tr><td>Status Info</td><td colspan='3'>%s</td></tr>", ResponseUtil.escapeXml(q.getStateInfo()));
+            pw.println("</tbody></table>");
+            pw.println("<br/>");
+        }
+        if ( isEmpty ) {
+            pw.println("<p>No active queues.</p>");
+            pw.println("<br/>");
+        }
+
+        for(final TopicStatistics ts : this.jobManager.getTopicStatistics()) {
+            pw.println("<table class='nicetable'><tbody>");
+            pw.printf("<tr><th colspan='2'>Topic Statistics: %s</th></tr>", ResponseUtil.escapeXml(ts.getTopic()));
+
+            pw.printf("<tr><td>Last Activated</td><td>%s</td></tr>", formatDate(ts.getLastActivatedJobTime()));
+            pw.printf("<tr><td>Last Finished</td><td>%s</td></tr>", formatDate(ts.getLastFinishedJobTime()));
+            pw.printf("<tr><td>Finished Jobs</td><td>%s</td></tr>", ts.getNumberOfFinishedJobs());
+            pw.printf("<tr><td>Failed Jobs</td><td>%s</td></tr>", ts.getNumberOfFailedJobs());
+            pw.printf("<tr><td>Cancelled Jobs</td><td>%s</td></tr>", ts.getNumberOfCancelledJobs());
+            pw.printf("<tr><td>Processed Jobs</td><td>%s</td></tr>", ts.getNumberOfProcessedJobs());
+            pw.printf("<tr><td>Average Processing Time</td><td>%s</td></tr>", formatTime(ts.getAverageProcessingTime()));
+            pw.printf("<tr><td>Average Waiting Time</td><td>%s</td></tr>", formatTime(ts.getAverageWaitingTime()));
+            pw.println("</tbody></table>");
+            pw.println("<br/>");
+        }
+
+        pw.println("<p class='statline'>Apache Sling Job Handling - Job Queue Configurations</p>");
+        this.printQueueConfiguration(req, pw, this.configuration.getQueueConfigurationManager().getMainQueueConfiguration());
+        final InternalQueueConfiguration[] configs = this.configuration.getQueueConfigurationManager().getConfigurations();
+        for(final InternalQueueConfiguration c : configs ) {
+            this.printQueueConfiguration(req, pw, c);
+        }
+    }
+
+    private void printQueueConfiguration(final HttpServletRequest req, final PrintWriter pw, final InternalQueueConfiguration c) {
+        pw.println("<div class='ui-widget-header ui-corner-top buttonGroup'>");
+        pw.printf("<span style='float: left; margin-left: 1em'>Job Queue Configuration: %s</span>%n",
+                ResponseUtil.escapeXml(c.getName()));
+        pw.printf("<button id='edit' class='ui-state-default ui-corner-all' onclick='javascript:window.location=\"%s%s/configMgr/%s\";'>Edit</button>",
+                req.getContextPath(), req.getServletPath(), c.getPid());
+        this.printForm(pw, c.getName(), "Test", "test");
+
+        pw.println("</div>");
+        pw.println("<table class='nicetable'><tbody>");
+        pw.println("<tr><th colspan='2'>Configuration</th></tr>");
+        pw.printf("<tr><td>Valid</td><td>%s</td></tr>", c.isValid());
+        pw.printf("<tr><td>Type</td><td>%s</td></tr>", formatType(c.getType()));
+        pw.printf("<tr><td>Topics</td><td>%s</td></tr>", formatArray(c.getTopics()));
+        pw.printf("<tr><td>Max Parallel</td><td>%s</td></tr>", c.getMaxParallel());
+        pw.printf("<tr><td>Max Retries</td><td>%s</td></tr>", c.getMaxRetries());
+        pw.printf("<tr><td>Retry Delay</td><td>%s ms</td></tr>", c.getRetryDelayInMs());
+        pw.printf("<tr><td>Priority</td><td>%s</td></tr>", c.getThreadPriority());
+        pw.printf("<tr><td>Ranking</td><td>%s</td></tr>", c.getRanking());
+
+        pw.println("</tbody></table>");
+        pw.println("<br/>");
+    }
+
+    /**
+     * Format an array for html rendering.
+     */
+    private String formatArray(final String[] array) {
+        if ( array == null || array.length == 0 ) {
+            return "";
+        }
+        final StringBuilder sb = new StringBuilder();
+        boolean first = true;
+        for(final String s : array ) {
+            if ( !first ) {
+                sb.append('\n');
+            }
+            first = false;
+            sb.append(s);
+        }
+        return ResponseUtil.escapeXml(sb.toString());
+    }
+
+    private String formatType(final QueueConfiguration.Type type) {
+        switch ( type ) {
+            case ORDERED : return "Ordered";
+            case TOPIC_ROUND_ROBIN : return "Topic Round Robin";
+            case UNORDERED : return "Parallel";
+        }
+        return type.toString();
+    }
+    /** Default date format used. */
+    private final DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss:SSS yyyy-MMM-dd");
+
+    /**
+     * Format a date
+     */
+    private synchronized String formatDate(final long time) {
+        if ( time == -1 ) {
+            return "-";
+        }
+        final Date d = new Date(time);
+        return dateFormat.format(d);
+    }
+
+    /**
+     * Format time (= duration)
+     */
+    private String formatTime(final long time) {
+        if ( time == 0 ) {
+            return "-";
+        }
+        if ( time < 1000 ) {
+            return time + " ms";
+        } else if ( time < 1000 * 60 ) {
+            return time / 1000 + " secs";
+        }
+        final long min = time / 1000 / 60;
+        final long secs = (time - min * 1000 * 60);
+        return min + " min " + secs / 1000 + " secs";
+    }
+
+    private void printForm(final PrintWriter pw,
+            final String qeueName,
+            final String buttonLabel,
+            final String cmd) {
+        pw.printf("<button class='ui-state-default ui-corner-all' onclick='javascript:eventingsubmit(\"%s\", \"%s\");'>" +
+                "%s</button>", ResponseUtil.escapeXml(cmd), (qeueName != null ? ResponseUtil.escapeXml(qeueName) : ""), ResponseUtil.escapeXml(buttonLabel));
+    }
+
+    @Override
+    public JobResult process(final Job job) {
+        logger.info("Received test job {}", job.getTopic());
+        return JobResult.OK;
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/jmx/AbstractJobStatistics.java b/src/main/java/org/apache/sling/event/impl/jobs/jmx/AbstractJobStatistics.java
new file mode 100644
index 0000000..8bf4033
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/jmx/AbstractJobStatistics.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.sling.event.impl.jobs.jmx;
+
+import java.util.Date;
+
+import javax.management.StandardMBean;
+
+import org.apache.sling.event.jobs.Statistics;
+import org.apache.sling.event.jobs.jmx.StatisticsMBean;
+
+public abstract class AbstractJobStatistics extends StandardMBean implements
+        StatisticsMBean {
+
+    public AbstractJobStatistics() {
+        super(StatisticsMBean.class, false);
+    }
+
+    protected abstract Statistics getStatistics();
+
+    public long getAverageProcessingTime() {
+        return getStatistics().getAverageProcessingTime();
+    }
+
+    public long getAverageWaitingTime() {
+        return getStatistics().getAverageWaitingTime();
+    }
+
+    public long getLastActivatedJobTime() {
+        return getStatistics().getLastActivatedJobTime();
+    }
+
+    public long getLastFinishedJobTime() {
+        return getStatistics().getLastFinishedJobTime();
+    }
+
+    public long getNumberOfActiveJobs() {
+        return getStatistics().getNumberOfActiveJobs();
+    }
+
+    public long getNumberOfCancelledJobs() {
+        return getStatistics().getNumberOfCancelledJobs();
+    }
+
+    public long getStartTime() {
+        return getStatistics().getStartTime();
+    }
+
+    public Date getStartDate() {
+        return new Date(getStartTime());
+    }
+
+    public long getNumberOfFinishedJobs() {
+        return getStatistics().getNumberOfFinishedJobs();
+    }
+
+    public long getNumberOfFailedJobs() {
+        return getStatistics().getNumberOfFailedJobs();
+    }
+
+    public long getNumberOfProcessedJobs() {
+        return getStatistics().getNumberOfProcessedJobs();
+    }
+
+    public long getNumberOfQueuedJobs() {
+        return getStatistics().getNumberOfQueuedJobs();
+    }
+
+    public long getNumberOfJobs() {
+        return getStatistics().getNumberOfJobs();
+    }
+
+    public void reset() {
+        getStatistics().reset();
+    }
+
+    public Date getLastActivatedJobDate() {
+        return new Date(getStatistics().getLastActivatedJobTime());
+    }
+
+    public Date getLastFinishedJobDate() {
+        return new Date(getStatistics().getLastFinishedJobTime());
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/jmx/AllJobStatisticsMBean.java b/src/main/java/org/apache/sling/event/impl/jobs/jmx/AllJobStatisticsMBean.java
new file mode 100644
index 0000000..0e696d6
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/jmx/AllJobStatisticsMBean.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.sling.event.impl.jobs.jmx;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Properties;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.event.jobs.JobManager;
+import org.apache.sling.event.jobs.Statistics;
+import org.apache.sling.event.jobs.jmx.StatisticsMBean;
+
+@Component
+@Service(value = StatisticsMBean.class)
+@Properties(@Property(name = "jmx.objectname", value = "org.apache.sling:type=queues,name=AllQueues"))
+public class AllJobStatisticsMBean extends AbstractJobStatistics {
+
+    private static final long TTL = 1000L;
+    private long agregateStatisticsTTL = 0L;
+    private Statistics aggregateStatistics;
+    @Reference
+    private JobManager jobManager;
+
+    /**
+     * @return the aggregate stats from the job manager.
+     */
+    @Override
+    protected Statistics getStatistics() {
+        if (System.currentTimeMillis() > agregateStatisticsTTL) {
+            aggregateStatistics = jobManager.getStatistics();
+            agregateStatisticsTTL = System.currentTimeMillis() + TTL;
+        }
+        return aggregateStatistics;
+    }
+
+    @Override
+    public String getName() {
+        return "All Queues";
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/jmx/EmptyStatistics.java b/src/main/java/org/apache/sling/event/impl/jobs/jmx/EmptyStatistics.java
new file mode 100644
index 0000000..35772ba
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/jmx/EmptyStatistics.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.sling.event.impl.jobs.jmx;
+
+import org.apache.sling.event.jobs.Statistics;
+
+/**
+ * Dummy stats that just returns 0 for all info, used where the queue doesnt
+ * implement the Statistics interface.
+ */
+public class EmptyStatistics implements Statistics {
+
+    public long getStartTime() {
+        return 0;
+    }
+
+    public long getNumberOfFinishedJobs() {
+        return 0;
+    }
+
+    public long getNumberOfCancelledJobs() {
+        return 0;
+    }
+
+    public long getNumberOfFailedJobs() {
+        return 0;
+    }
+
+    public long getNumberOfProcessedJobs() {
+        return 0;
+    }
+
+    public long getNumberOfActiveJobs() {
+        return 0;
+    }
+
+    public long getNumberOfQueuedJobs() {
+        return 0;
+    }
+
+    public long getNumberOfJobs() {
+        return 0;
+    }
+
+    public long getLastActivatedJobTime() {
+        return 0;
+    }
+
+    public long getLastFinishedJobTime() {
+        return 0;
+    }
+
+    public long getAverageWaitingTime() {
+        return 0;
+    }
+
+    public long getAverageProcessingTime() {
+        return 0;
+    }
+
+    public void reset() {
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/jmx/QueueMBeanImpl.java b/src/main/java/org/apache/sling/event/impl/jobs/jmx/QueueMBeanImpl.java
new file mode 100644
index 0000000..615ac90
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/jmx/QueueMBeanImpl.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.sling.event.impl.jobs.jmx;
+
+import org.apache.sling.event.jobs.Queue;
+import org.apache.sling.event.jobs.Statistics;
+
+/**
+ * An MBean that provides statistics from
+ */
+public class QueueMBeanImpl extends AbstractJobStatistics {
+
+    private final String name;
+
+    private final Statistics statistics;
+
+    public QueueMBeanImpl(Queue queue) {
+        this.name = queue.getName();
+        if (queue instanceof Statistics) {
+            this.statistics = (Statistics) queue;
+        } else {
+            this.statistics = new EmptyStatistics();
+        }
+    }
+
+    @Override
+    protected Statistics getStatistics() {
+        return this.statistics;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/jmx/QueueStatusEvent.java b/src/main/java/org/apache/sling/event/impl/jobs/jmx/QueueStatusEvent.java
new file mode 100644
index 0000000..dca3e33
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/jmx/QueueStatusEvent.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.sling.event.impl.jobs.jmx;
+
+import org.apache.sling.event.jobs.Queue;
+
+public class QueueStatusEvent {
+
+    private final Queue queue;
+    private final Queue oldqueue;
+
+
+    public QueueStatusEvent(final Queue queue, final Queue oldqueue) {
+        this.queue = queue;
+        this.oldqueue = oldqueue;
+    }
+    public boolean isNew() {
+        return this.oldqueue == null;
+    }
+
+    public boolean isUpdate() {
+        return this.queue == this.oldqueue;
+    }
+
+    public boolean isRemoved() {
+        return this.queue == null;
+    }
+
+    public Queue getQueue() {
+        return queue;
+    }
+
+    public Queue getOldQueue() {
+        return oldqueue;
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/jmx/QueuesMBeanImpl.java b/src/main/java/org/apache/sling/event/impl/jobs/jmx/QueuesMBeanImpl.java
new file mode 100644
index 0000000..83a56e9
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/jmx/QueuesMBeanImpl.java
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.sling.event.impl.jobs.jmx;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+import javax.management.AttributeChangeNotification;
+import javax.management.MBeanNotificationInfo;
+import javax.management.Notification;
+import javax.management.NotificationBroadcasterSupport;
+import javax.management.StandardEmitterMBean;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.event.jobs.Queue;
+import org.apache.sling.event.jobs.jmx.QueuesMBean;
+import org.apache.sling.event.jobs.jmx.StatisticsMBean;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+
+@Component
+@Service(value = { QueuesMBean.class })
+@Property(name = "jmx.objectname", value = "org.apache.sling:type=queues,name=QueueNames")
+public class QueuesMBeanImpl extends StandardEmitterMBean implements QueuesMBean {
+
+    private static final String QUEUE_NOTIFICATION = "org.apache.sling.event.queue";
+    private static final String[] NOTIFICATION_TYPES = { QUEUE_NOTIFICATION };
+    private Map<String, QueueMBeanHolder> queues = new ConcurrentHashMap<String, QueueMBeanHolder>();
+    private String[] names;
+    private AtomicLong sequence = new AtomicLong(System.currentTimeMillis());
+    private BundleContext bundleContext;
+
+    class QueueMBeanHolder {
+
+        QueueMBeanHolder(String name, QueueMBeanImpl queueMBean,
+                ServiceRegistration registration) {
+            this.name = name;
+            this.queueMBean = queueMBean;
+            this.registration = registration;
+        }
+
+        QueueMBeanImpl queueMBean;
+        ServiceRegistration registration;
+        String name;
+
+    }
+
+    public QueuesMBeanImpl() {
+        super(QueuesMBean.class, false, new NotificationBroadcasterSupport(
+                new MBeanNotificationInfo(NOTIFICATION_TYPES,
+                        Notification.class.getName(),
+                        "Notifications about queues")));
+    }
+
+    @Activate
+    public void activate(final BundleContext bc) {
+        bundleContext = bc;
+    }
+
+    @Deactivate
+    public void deactivate() {
+        bundleContext = null;
+    }
+
+    public void sendEvent(final QueueStatusEvent e) {
+        if (e.isNew()) {
+            bindQueueMBean(e);
+        } else if (e.isRemoved()) {
+            unbindQueueMBean(e);
+        } else {
+            updateQueueMBean(e);
+        }
+    }
+
+    private void updateQueueMBean(QueueStatusEvent e) {
+        QueueMBeanHolder queueMBeanHolder = queues.get(e.getQueue().getName());
+        if (queueMBeanHolder != null) {
+            String[] oldQueue = getQueueNames();
+            names = null;
+            this.sendNotification(new AttributeChangeNotification(this,
+                    sequence.incrementAndGet(), System.currentTimeMillis(),
+                    "Queue " + e.getQueue().getName() + " updated ",
+                    "queueNames", "String[]", oldQueue, getQueueNames()));
+        }
+    }
+
+    private void unbindQueueMBean(QueueStatusEvent e) {
+        QueueMBeanHolder queueMBeanHolder = queues.get(e.getOldQueue().getName());
+        if (queueMBeanHolder != null) {
+            removeAndNotify(queueMBeanHolder);
+        }
+    }
+
+    private void bindQueueMBean(QueueStatusEvent e) {
+        QueueMBeanHolder queueMBeanHolder = queues.get(e.getQueue().getName());
+        if (queueMBeanHolder != null) {
+            removeAndNotify(queueMBeanHolder);
+        }
+        addAndNotify(e.getQueue());
+    }
+
+    private void addAndNotify(Queue queue) {
+        String[] oldQueue = getQueueNames();
+        QueueMBeanHolder queueMBeanHolder = add(queue);
+        names = null;
+        this.sendNotification(new AttributeChangeNotification(this, sequence
+                .incrementAndGet(), System.currentTimeMillis(), "Queue "
+                + queueMBeanHolder.name + " added ", "queueNames", "String[]",
+                oldQueue, getQueueNames()));
+    }
+
+    private void removeAndNotify(QueueMBeanHolder queueMBeanHolder) {
+        String[] oldQueue = getQueueNames();
+        remove(queueMBeanHolder);
+        names = null;
+        this.sendNotification(new AttributeChangeNotification(this, sequence
+                .incrementAndGet(), System.currentTimeMillis(), "Queue "
+                + queueMBeanHolder.name + " removed ", "queueNames",
+                "String[]", oldQueue, getQueueNames()));
+    }
+
+    private QueueMBeanHolder add(Queue queue) {
+        QueueMBeanImpl queueMBean = new QueueMBeanImpl(queue);
+        ServiceRegistration serviceRegistration = bundleContext
+                .registerService(StatisticsMBean.class.getName(), queueMBean,
+                        createProperties(
+                                "jmx.objectname","org.apache.sling:type=queues,name="+queue.getName(),
+                                Constants.SERVICE_DESCRIPTION, "QueueMBean for queue "+queue.getName(),
+                                Constants.SERVICE_VENDOR, "The Apache Software Foundation"));
+        QueueMBeanHolder queueMBeanHolder = new QueueMBeanHolder(
+                queue.getName(), queueMBean, serviceRegistration);
+        queues.put(queueMBeanHolder.name, queueMBeanHolder);
+        return queueMBeanHolder;
+    }
+
+    private Dictionary<String, Object> createProperties(Object ... values) {
+        Dictionary<String, Object> props = new Hashtable<String, Object>();
+        for ( int i = 0; i < values.length; i+=2) {
+            props.put((String) values[i], values[i+1]);
+        }
+        return props;
+    }
+
+    private void remove(QueueMBeanHolder queueMBeanHolder) {
+        queueMBeanHolder.registration.unregister();
+        queues.remove(queueMBeanHolder.name);
+    }
+
+    @Override
+    public String[] getQueueNames() {
+        if (names == null) {
+            List<String> lnames = new ArrayList<String>(queues.keySet());
+            Collections.sort(lnames);
+            names = lnames.toArray(new String[lnames.size()]);
+        }
+        return names;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/notifications/NewJobSender.java b/src/main/java/org/apache/sling/event/impl/jobs/notifications/NewJobSender.java
new file mode 100644
index 0000000..590026f
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/notifications/NewJobSender.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.notifications;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.sling.api.resource.observation.ExternalResourceChangeListener;
+import org.apache.sling.api.resource.observation.ResourceChange;
+import org.apache.sling.api.resource.observation.ResourceChange.ChangeType;
+import org.apache.sling.api.resource.observation.ResourceChangeListener;
+import org.apache.sling.event.impl.jobs.config.JobManagerConfiguration;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.NotificationConstants;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This component receives resource added events and sends a job
+ * created event.
+ */
+@Component
+public class NewJobSender implements ResourceChangeListener, ExternalResourceChangeListener {
+
+    /** Logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** The job manager configuration. */
+    @Reference
+    private JobManagerConfiguration configuration;
+
+    /** The event admin. */
+    @Reference
+    private EventAdmin eventAdmin;
+
+    /** Service registration for the event handler. */
+    private volatile ServiceRegistration<ResourceChangeListener> listenerRegistration;
+
+    /**
+     * Activate this component.
+     * Register an event handler.
+     */
+    @Activate
+    protected void activate(final BundleContext bundleContext) {
+        final Dictionary<String, Object> properties = new Hashtable<String, Object>();
+        properties.put(Constants.SERVICE_DESCRIPTION, "Apache Sling Job Topic Manager Event Handler");
+        properties.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");
+        properties.put(ResourceChangeListener.CHANGES, ChangeType.ADDED.toString());
+        properties.put(ResourceChangeListener.PATHS, this.configuration.getLocalJobsPath());
+
+        this.listenerRegistration = bundleContext.registerService(ResourceChangeListener.class, this, properties);
+    }
+
+    /**
+     * Deactivate this component.
+     * Unregister the event handler.
+     */
+    @Deactivate
+    protected void deactivate() {
+        if ( this.listenerRegistration != null ) {
+            this.listenerRegistration.unregister();
+            this.listenerRegistration = null;
+        }
+    }
+
+    @Override
+	public void onChange(final List<ResourceChange> resourceChanges) {
+    	for(final ResourceChange resourceChange : resourceChanges) {
+    		logger.debug("Received event {}", resourceChange);
+
+    		final String path = resourceChange.getPath();
+
+    		final int topicStart = this.configuration.getLocalJobsPath().length() + 1;
+    		final int topicEnd = path.indexOf('/', topicStart);
+    		if ( topicEnd != -1 ) {
+    			final String topic = path.substring(topicStart, topicEnd).replace('.', '/');
+                final String jobId = path.substring(topicEnd + 1);
+
+                if ( path.indexOf("_", topicEnd + 1) != -1 ) {
+                	// only job id and topic are guaranteed
+                	final Dictionary<String, Object> properties = new Hashtable<String, Object>();
+                	properties.put(NotificationConstants.NOTIFICATION_PROPERTY_JOB_ID, jobId);
+                    properties.put(NotificationConstants.NOTIFICATION_PROPERTY_JOB_TOPIC, topic);
+
+                 // we also set internally the queue name
+                    final String queueName = this.configuration.getQueueConfigurationManager().getQueueInfo(topic).queueName;
+                    properties.put(Job.PROPERTY_JOB_QUEUE_NAME, queueName);
+
+                    final Event jobEvent = new Event(NotificationConstants.TOPIC_JOB_ADDED, properties);
+                    // as this is send within handling an event, we do sync call
+                    this.eventAdmin.sendEvent(jobEvent);
+                }
+    		}
+    	}
+
+	}
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/notifications/NotificationUtility.java b/src/main/java/org/apache/sling/event/impl/jobs/notifications/NotificationUtility.java
new file mode 100644
index 0000000..d45d818
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/notifications/NotificationUtility.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.notifications;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import org.apache.sling.event.impl.jobs.JobImpl;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.NotificationConstants;
+import org.apache.sling.event.jobs.consumer.JobConsumer;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.service.event.EventConstants;
+
+public abstract class NotificationUtility {
+
+    /** Event property containing the time for job start and job finished events. */
+    public static final String PROPERTY_TIME = ":time";
+
+    /**
+     * Helper method for sending the notification events.
+     */
+    public static void sendNotification(final EventAdmin eventAdmin,
+            final String eventTopic,
+            final Job job,
+            final Long time) {
+        if ( eventAdmin != null ) {
+            // create new copy of job object
+            final Job jobCopy = new JobImpl(job.getTopic(), job.getId(), ((JobImpl)job).getProperties());
+            sendNotificationInternal(eventAdmin, eventTopic, jobCopy, time);
+        }
+    }
+
+    /**
+     * Helper method for sending the notification events.
+     */
+    private static void sendNotificationInternal(final EventAdmin eventAdmin,
+            final String eventTopic,
+            final Job job,
+            final Long time) {
+        final Dictionary<String, Object> eventProps = new Hashtable<String, Object>();
+        // add basic job properties
+        eventProps.put(NotificationConstants.NOTIFICATION_PROPERTY_JOB_ID, job.getId());
+        eventProps.put(NotificationConstants.NOTIFICATION_PROPERTY_JOB_TOPIC, job.getTopic());
+        // copy payload
+        for(final String name : job.getPropertyNames()) {
+            eventProps.put(name, job.getProperty(name));
+        }
+        // remove async handler
+        eventProps.remove(JobConsumer.PROPERTY_JOB_ASYNC_HANDLER);
+        // add timestamp
+        eventProps.put(EventConstants.TIMESTAMP, System.currentTimeMillis());
+        // add internal time information
+        if ( time != null ) {
+            eventProps.put(PROPERTY_TIME, time);
+        }
+        eventAdmin.postEvent(new Event(eventTopic, eventProps));
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/queues/JobExecutionContextImpl.java b/src/main/java/org/apache/sling/event/impl/jobs/queues/JobExecutionContextImpl.java
new file mode 100644
index 0000000..d2c2286
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/queues/JobExecutionContextImpl.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.queues;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.sling.event.impl.jobs.JobHandler;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.consumer.JobExecutionContext;
+import org.apache.sling.event.jobs.consumer.JobExecutionResult;
+
+/**
+ * Implementation of the job execution context passed to
+ * job executors.
+ */
+public class JobExecutionContextImpl implements JobExecutionContext {
+
+    /**
+     * Call back interface to the queue.
+     */
+    public interface ASyncHandler {
+        void finished(Job.JobState state);
+    }
+
+    /**
+     * Boolean to check whether init progress has been called.
+     */
+    private final AtomicBoolean initProgressIsCalled = new AtomicBoolean(false);
+
+    /**
+     * Flag to indicate whether this is async processing
+     */
+    private final AtomicBoolean isAsync = new AtomicBoolean(false);
+
+    private final ASyncHandler asyncHandler;
+
+    private final JobHandler handler;
+
+    public JobExecutionContextImpl(final JobHandler handler,
+            final ASyncHandler asyncHandler) {
+        this.handler = handler;
+        this.asyncHandler = asyncHandler;
+    }
+
+    public void markAsync() {
+        this.isAsync.set(true);
+    }
+
+    @Override
+    public void initProgress(final int steps,
+            final long eta) {
+        if ( initProgressIsCalled.compareAndSet(false, true) ) {
+            handler.persistJobProperties(handler.getJob().startProgress(steps, eta));
+        }
+    }
+
+    @Override
+    public void incrementProgressCount(final int steps) {
+        if ( initProgressIsCalled.get() ) {
+            handler.persistJobProperties(handler.getJob().setProgress(steps));
+        }
+    }
+
+    @Override
+    public void updateProgress(final long eta) {
+        if ( initProgressIsCalled.get() ) {
+            handler.persistJobProperties(handler.getJob().update(eta));
+        }
+    }
+
+    @Override
+    public void log(final String message, Object... args) {
+        handler.persistJobProperties(handler.getJob().log(message, args));
+    }
+
+    @Override
+    public boolean isStopped() {
+        return handler.isStopped();
+    }
+
+    @Override
+    public void asyncProcessingFinished(final JobExecutionResult result) {
+        synchronized ( this ) {
+            if ( isAsync.compareAndSet(true, false) ) {
+                Job.JobState state = null;
+                if ( result.succeeded() ) {
+                    state = Job.JobState.SUCCEEDED;
+                } else if ( result.failed() ) {
+                    state = Job.JobState.QUEUED;
+                } else if ( result.cancelled() ) {
+                    if ( handler.isStopped() ) {
+                        state = Job.JobState.STOPPED;
+                    } else {
+                        state = Job.JobState.ERROR;
+                    }
+                }
+                asyncHandler.finished(state);
+            } else {
+                throw new IllegalStateException("Job is not processed async or is already finished: " + handler.getJob().getId());
+            }
+        }
+    }
+
+    @Override
+    public ResultBuilder result() {
+        return new ResultBuilderImpl();
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/queues/JobExecutionResultImpl.java b/src/main/java/org/apache/sling/event/impl/jobs/queues/JobExecutionResultImpl.java
new file mode 100644
index 0000000..28083e4
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/queues/JobExecutionResultImpl.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.queues;
+
+import org.apache.sling.event.impl.jobs.InternalJobState;
+import org.apache.sling.event.jobs.consumer.JobExecutionResult;
+
+/**
+ * The job execution result.
+ */
+public class JobExecutionResultImpl implements JobExecutionResult {
+
+    /** Constant object for the success case. */
+    public static final JobExecutionResultImpl SUCCEEDED = new JobExecutionResultImpl(InternalJobState.SUCCEEDED, null, null);
+    /** Constant object for the cancelled case. */
+    public static final JobExecutionResultImpl CANCELLED = new JobExecutionResultImpl(InternalJobState.CANCELLED, null, null);
+    /** Constant object for the failed case. */
+    public static final JobExecutionResultImpl FAILED = new JobExecutionResultImpl(InternalJobState.FAILED, null, null);
+
+    /** The state of the execution. */
+    private final InternalJobState state;
+
+    /** Optional message. */
+    private final String message;
+
+    /** Optional retry delay. */
+    private final Long retryDelayInMs;
+
+    /**
+     * Create a new result
+     * @param state The result state
+     * @param message Optional Message
+     * @param retryDelayInMs Optional retry delay
+     */
+    public JobExecutionResultImpl(final InternalJobState state,
+            final String message,
+            final Long retryDelayInMs) {
+        this.state = state;
+        this.message = message;
+        this.retryDelayInMs = retryDelayInMs;
+    }
+
+    /**
+     * Get the internal state
+     * @return The state.
+     */
+    public InternalJobState getState() {
+        return this.state;
+    }
+
+    @Override
+    public boolean succeeded() {
+        return this.state == InternalJobState.SUCCEEDED;
+    }
+
+    @Override
+    public boolean cancelled() {
+        return this.state == InternalJobState.CANCELLED;
+    }
+
+    @Override
+    public boolean failed() {
+        return this.state == InternalJobState.FAILED;
+    }
+
+    @Override
+    public String getMessage() {
+        return this.message;
+    }
+
+    @Override
+    public Long getRetryDelayInMs() {
+        return this.retryDelayInMs;
+    }
+
+    @Override
+    public String toString() {
+        return "JobExecutionResultImpl [state=" + state + ", message="
+                + message + ", retryDelayInMs=" + retryDelayInMs + "]";
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/queues/JobQueueImpl.java b/src/main/java/org/apache/sling/event/impl/jobs/queues/JobQueueImpl.java
new file mode 100644
index 0000000..0ab1a6b
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/queues/JobQueueImpl.java
@@ -0,0 +1,708 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.queues;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.commons.threads.ThreadPool;
+import org.apache.sling.event.impl.EventingThreadPool;
+import org.apache.sling.event.impl.jobs.InternalJobState;
+import org.apache.sling.event.impl.jobs.JobHandler;
+import org.apache.sling.event.impl.jobs.JobImpl;
+import org.apache.sling.event.impl.jobs.JobTopicTraverser;
+import org.apache.sling.event.impl.jobs.Utility;
+import org.apache.sling.event.impl.jobs.config.InternalQueueConfiguration;
+import org.apache.sling.event.impl.jobs.notifications.NotificationUtility;
+import org.apache.sling.event.impl.support.BatchResourceRemover;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.Job.JobState;
+import org.apache.sling.event.jobs.NotificationConstants;
+import org.apache.sling.event.jobs.Queue;
+import org.apache.sling.event.jobs.QueueConfiguration.Type;
+import org.apache.sling.event.jobs.Statistics;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The job blocking queue extends the blocking queue by some
+ * functionality for the job event handling.
+ */
+public class JobQueueImpl
+    implements Queue {
+
+    /** Default timeout for suspend. */
+    private static final long MAX_SUSPEND_TIME = 1000 * 60 * 60; // 60 mins
+
+    /** The logger. */
+    private final Logger logger;
+
+    /** Configuration. */
+    private final InternalQueueConfiguration configuration;
+
+    /** The queue name. */
+    private volatile String queueName;
+
+    /** Are we still running? */
+    private volatile boolean running;
+
+    /** Suspended since. */
+    private final AtomicLong suspendedSince = new AtomicLong(-1);
+
+    /** Services used by the queues. */
+    private final QueueServices services;
+
+    /** The map of events we're processing. */
+    private final Map<String, JobHandler> processingJobsLists = new HashMap<String, JobHandler>();
+
+    private final ThreadPool threadPool;
+
+    /** Async counter. */
+    private final AtomicInteger asyncCounter = new AtomicInteger();
+
+    /** Flag for outdated. */
+    private final AtomicBoolean isOutdated = new AtomicBoolean(false);
+
+    /** A marker for closing the queue. */
+    private final AtomicBoolean closeMarker = new AtomicBoolean(false);
+
+    /** A marker for doing a full cache search. */
+    private final AtomicBoolean doFullCacheSearch = new AtomicBoolean(false);
+
+    /** A counter for rescheduling. */
+    private final AtomicInteger waitCounter = new AtomicInteger();
+
+    /** The job cache. */
+    private final QueueJobCache cache;
+
+    /** Semaphore for handling the max number of jobs. */
+    private final Semaphore available;
+
+    /** Guard for having only one thread executing start jobs. */
+    private final AtomicBoolean startJobsGuard = new AtomicBoolean(false);
+
+    /** Lock for close/start. */
+    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+
+    /** Sleeping until is only set for ordered queues if a job is rescheduled. */
+    private volatile long isSleepingUntil = -1;
+
+    /**
+     * Create a new queue.
+     *
+     * @param name The queue name
+     * @param config The queue configuration
+     * @param services The queue services
+     * @param topics The topics handled by this queue
+     *
+     * @return {@code JobQueueImpl} if there are jobs to process, {@code null} otherwise.
+     */
+    public static JobQueueImpl createQueue(final String name,
+                        final InternalQueueConfiguration config,
+                        final QueueServices services,
+                        final Set<String> topics) {
+        final QueueJobCache cache = new QueueJobCache(services.configuration, name, services.statisticsManager, config.getType(), topics);
+        if ( cache.isEmpty() ) {
+            return null;
+        }
+        return new JobQueueImpl(name, config, services, cache);
+    }
+
+    /**
+     * Create a new queue.
+     *
+     * @param name The queue name
+     * @param config The queue configuration
+     * @param services The queue services
+     * @param cache The job cache
+     */
+    private JobQueueImpl(final String name,
+                        final InternalQueueConfiguration config,
+                        final QueueServices services,
+                        final QueueJobCache cache) {
+        if ( config.getOwnThreadPoolSize() > 0 ) {
+            this.threadPool = new EventingThreadPool(services.threadPoolManager, config.getOwnThreadPoolSize());
+        } else {
+            this.threadPool = services.eventingThreadPool;
+        }
+        this.queueName = name;
+        this.configuration = config;
+        this.services = services;
+        this.logger = LoggerFactory.getLogger(this.getClass().getName() + '.' + name);
+        this.running = true;
+        this.cache = cache;
+        this.available = new Semaphore(config.getMaxParallel(), true);
+        logger.info("Starting job queue {}", queueName);
+        logger.debug("Configuration for job queue={}", configuration);
+    }
+
+    /**
+     * Return the queue configuration
+     */
+    @Override
+    public InternalQueueConfiguration getConfiguration() {
+        return this.configuration;
+    }
+
+    /**
+     * Get the name of the job queue.
+     */
+    @Override
+    public String getName() {
+        return this.queueName;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Queue#getStatistics()
+     */
+    @Override
+    public Statistics getStatistics() {
+        return this.services.statisticsManager.getQueueStatistics(this.queueName);
+    }
+
+    /**
+     * Start the job queue.
+     * This method might be called concurrently, therefore we use a guard
+     */
+    public void startJobs() {
+        if ( this.startJobsGuard.compareAndSet(false, true) ) {
+            // we start as many jobs in parallel as possible
+            while ( this.running && !this.isOutdated.get() && !this.isSuspended() && this.available.tryAcquire() ) {
+                boolean started = false;
+                this.lock.writeLock().lock();
+                try {
+                    final JobHandler handler = this.cache.getNextJob(this.services.jobConsumerManager,
+                            this.services.statisticsManager, this, this.doFullCacheSearch.getAndSet(false));
+                    if ( handler != null ) {
+                        started = true;
+                        this.threadPool.execute(new Runnable() {
+
+                            @Override
+                            public void run() {
+                                // update thread priority and name
+                                final Thread currentThread = Thread.currentThread();
+                                final String oldName = currentThread.getName();
+                                final int oldPriority = currentThread.getPriority();
+
+                                currentThread.setName(oldName + "-" + handler.getJob().getQueueName() + "(" + handler.getJob().getTopic() + ")");
+                                if ( configuration.getThreadPriority() != null ) {
+                                    switch ( configuration.getThreadPriority() ) {
+                                        case NORM : currentThread.setPriority(Thread.NORM_PRIORITY);
+                                                    break;
+                                        case MIN  : currentThread.setPriority(Thread.MIN_PRIORITY);
+                                                    break;
+                                        case MAX  : currentThread.setPriority(Thread.MAX_PRIORITY);
+                                                    break;
+                                    }
+                                }
+
+                                try {
+                                    startJob(handler);
+                                } finally {
+                                    currentThread.setPriority(oldPriority);
+                                    currentThread.setName(oldName);
+                                }
+                                // and try to launch another job
+                                startJobs();
+                            }
+                        });
+                    } else {
+                        // no job available, stop look
+                        break;
+                    }
+
+                } finally {
+                    if ( !started ) {
+                        this.available.release();
+                    }
+                    this.lock.writeLock().unlock();
+                }
+            }
+            this.startJobsGuard.set(false);
+        }
+    }
+
+    private void startJob(final JobHandler handler) {
+        try {
+            this.closeMarker.set(false);
+            try {
+                final JobImpl job = handler.getJob();
+                handler.started = System.currentTimeMillis();
+
+                this.services.configuration.getAuditLogger().debug("START OK : {}", job.getId());
+                // sanity check for the queued property
+                Calendar queued = job.getProperty(JobImpl.PROPERTY_JOB_QUEUED, Calendar.class);
+                if ( queued == null ) {
+                    // we simply use a date of ten seconds ago
+                    queued = Calendar.getInstance();
+                    queued.setTimeInMillis(System.currentTimeMillis() - 10000);
+                }
+                final long queueTime = handler.started - queued.getTimeInMillis();
+                // update statistics
+                this.services.statisticsManager.jobStarted(this.queueName, job.getTopic(), queueTime);
+                // send notification
+                NotificationUtility.sendNotification(this.services.eventAdmin, NotificationConstants.TOPIC_JOB_STARTED, job, queueTime);
+
+                synchronized ( this.processingJobsLists ) {
+                    this.processingJobsLists.put(job.getId(), handler);
+                }
+
+                JobExecutionResultImpl result = JobExecutionResultImpl.CANCELLED;
+                Job.JobState resultState = Job.JobState.ERROR;
+                final JobExecutionContextImpl ctx = new JobExecutionContextImpl(handler, new JobExecutionContextImpl.ASyncHandler() {
+
+                    @Override
+                    public void finished(final JobState state) {
+                        services.jobConsumerManager.unregisterListener(job.getId());
+                        finishedJob(job.getId(), state, true);
+                        asyncCounter.decrementAndGet();
+                    }
+                });
+
+                try {
+                    synchronized ( ctx ) {
+                        result = (JobExecutionResultImpl)handler.getConsumer().process(job, ctx);
+                        if ( result == null ) { // ASYNC processing
+                            services.jobConsumerManager.registerListener(job.getId(), handler.getConsumer(), ctx);
+                            asyncCounter.incrementAndGet();
+                            ctx.markAsync();
+                        } else {
+                            if ( result.succeeded() ) {
+                                resultState = Job.JobState.SUCCEEDED;
+                            } else if ( result.failed() ) {
+                                resultState = Job.JobState.QUEUED;
+                            } else if ( result.cancelled() ) {
+                                if ( handler.isStopped() ) {
+                                    resultState = Job.JobState.STOPPED;
+                                } else {
+                                    resultState = Job.JobState.ERROR;
+                                }
+                            }
+                        }
+                    }
+                } catch (final Throwable t) { //NOSONAR
+                    logger.error("Unhandled error occured in job processor " + t.getMessage() + " while processing job " + Utility.toString(job), t);
+                    // we don't reschedule if an exception occurs
+                    result = JobExecutionResultImpl.CANCELLED;
+                    resultState = Job.JobState.ERROR;
+                } finally {
+                    if ( result != null ) {
+                        if ( result.getRetryDelayInMs() != null ) {
+                            job.setProperty(JobImpl.PROPERTY_DELAY_OVERRIDE, result.getRetryDelayInMs());
+                        }
+                        if ( result.getMessage() != null ) {
+                           job.setProperty(Job.PROPERTY_RESULT_MESSAGE, result.getMessage());
+                        }
+                        this.finishedJob(job.getId(), resultState, false);
+                    }
+                }
+            } catch (final Exception re) {
+                // if an exception occurs, we just log
+                this.logger.error("Exception during job processing.", re);
+            }
+        } finally {
+            this.available.release();
+        }
+    }
+
+    /**
+     * Outdate this queue.
+     */
+    public void outdate() {
+        if ( this.isOutdated.compareAndSet(false, true) ) {
+            final String name = this.getName() + "<outdated>(" + this.hashCode() + ")";
+            this.logger.info("Outdating queue {}, renaming to {}.", this.queueName, name);
+            this.queueName = name;
+        }
+    }
+
+    /**
+     * Check if the queue can be closed
+     */
+    public boolean tryToClose() {
+        // resume the queue as we want to close it!
+        this.resume();
+        this.lock.writeLock().lock();
+        try {
+            // check if possible
+            if ( this.canBeClosed() ) {
+                if ( this.closeMarker.get() ) {
+                    this.close();
+                    return true;
+                }
+                this.closeMarker.set(true);
+            }
+        } finally {
+            this.lock.writeLock().unlock();
+        }
+        return false;
+    }
+
+    /**
+     * Check whether this queue can be closed
+     */
+    private boolean canBeClosed() {
+        return !this.isSuspended()
+            && this.asyncCounter.get() == 0
+            && this.waitCounter.get() == 0
+            && this.available.availablePermits() == this.configuration.getMaxParallel();
+    }
+
+    /**
+     * Close this queue.
+     */
+    public void close() {
+        this.running = false;
+        this.logger.debug("Shutting down job queue {}", queueName);
+        this.resume();
+
+        synchronized ( this.processingJobsLists ) {
+            this.processingJobsLists.clear();
+        }
+        if ( this.configuration.getOwnThreadPoolSize() > 0 ) {
+            ((EventingThreadPool)this.threadPool).release();
+        }
+
+        this.logger.info("Stopped job queue {}", this.queueName);
+    }
+
+    /**
+     * Periodic maintenance
+     */
+    public void maintain() {
+        // check suspended
+        final long since = this.suspendedSince.get();
+        if ( since != -1 && since + MAX_SUSPEND_TIME < System.currentTimeMillis() ) {
+            logger.info("Waking up suspended queue. It has been suspended for more than {}ms", MAX_SUSPEND_TIME);
+            this.resume();
+        }
+
+        // set full cache search
+        this.doFullCacheSearch.set(true);
+
+        this.startJobs();
+    }
+
+    /**
+     * Inform the queue about new job for the given topics.
+     * @param topics the new topics
+     */
+    public void wakeUpQueue(final Set<String> topics) {
+        this.cache.handleNewTopics(topics);
+    }
+
+    /**
+     * Put a job back in the queue
+     * @param handler The job handler
+     */
+    private void requeue(final JobHandler handler) {
+        this.cache.reschedule(this.queueName, handler, this.services.statisticsManager);
+        this.startJobs();
+    }
+
+    private static final class RescheduleInfo {
+        public boolean      reschedule = false;
+        // processing time is only set of state is SUCCEEDED
+        public long         processingTime;
+        public Job.JobState state;
+        public InternalJobState       finalState;
+    }
+
+    private RescheduleInfo handleReschedule(final JobHandler handler, final Job.JobState resultState) {
+        final RescheduleInfo info = new RescheduleInfo();
+        info.state = resultState;
+        switch ( resultState ) {
+            case SUCCEEDED : // job is finished
+                if ( this.logger.isDebugEnabled() ) {
+                    this.logger.debug("Finished job {}", Utility.toString(handler.getJob()));
+                }
+                info.processingTime = System.currentTimeMillis() - handler.started;
+                info.finalState = InternalJobState.SUCCEEDED;
+                break;
+            case QUEUED : // check if we exceeded the number of retries
+                final int retries = handler.getJob().getProperty(Job.PROPERTY_JOB_RETRIES, 0);
+                int retryCount = handler.getJob().getProperty(Job.PROPERTY_JOB_RETRY_COUNT, 0);
+
+                retryCount++;
+                if ( retries != -1 && retryCount > retries ) {
+                    if ( this.logger.isDebugEnabled() ) {
+                        this.logger.debug("Cancelled job {}", Utility.toString(handler.getJob()));
+                    }
+                    info.finalState = InternalJobState.CANCELLED;
+                } else {
+                    info.reschedule = true;
+                    handler.getJob().retry();
+                    if ( this.logger.isDebugEnabled() ) {
+                        this.logger.debug("Failed job {}", Utility.toString(handler.getJob()));
+                    }
+                    info.finalState = InternalJobState.FAILED;
+                }
+                break;
+            default : // consumer cancelled the job (STOPPED, GIVEN_UP, ERROR)
+                if ( this.logger.isDebugEnabled() ) {
+                    this.logger.debug("Cancelled job {}", Utility.toString(handler.getJob()));
+                }
+                info.finalState = InternalJobState.CANCELLED;
+                break;
+        }
+
+        if ( info.state == Job.JobState.QUEUED && !info.reschedule ) {
+            info.state = Job.JobState.GIVEN_UP;
+        }
+        return info;
+    }
+
+    /**
+     * Handle job finish and determine whether to reschedule or cancel the job
+     */
+    private boolean finishedJob(final String jobId,
+                                final Job.JobState resultState,
+                                final boolean isAsync) {
+        this.services.configuration.getAuditLogger().debug("FINISHED {} : {}", resultState, jobId);
+        this.logger.debug("Received finish for job {}, resultState={}", jobId, resultState);
+
+        // get job handler
+        final JobHandler handler;
+        // let's remove the event from our processing list
+        synchronized ( this.processingJobsLists ) {
+            handler = this.processingJobsLists.remove(jobId);
+        }
+
+        if ( !this.running ) {
+            this.logger.warn("Queue is not running anymore. Discarding finish for {}", jobId);
+            return false;
+        }
+
+        if ( handler == null ) {
+            this.logger.warn("This job has never been started by this queue: {}", jobId);
+            return false;
+        }
+
+        // handle the rescheduling of the job
+        final RescheduleInfo rescheduleInfo = this.handleReschedule(handler, resultState);
+
+        if ( !rescheduleInfo.reschedule ) {
+            // we keep cancelled jobs and succeeded jobs if the queue is configured like this.
+            final boolean keepJobs = rescheduleInfo.state != Job.JobState.SUCCEEDED || this.configuration.isKeepJobs();
+            handler.finished(rescheduleInfo.state, keepJobs, rescheduleInfo.processingTime);
+        } else {
+            this.reschedule(handler);
+        }
+        // update statistics
+        this.services.statisticsManager.jobEnded(this.queueName,
+                handler.getJob().getTopic(),
+                rescheduleInfo.finalState,
+                rescheduleInfo.processingTime);
+        // send notification
+        NotificationUtility.sendNotification(this.services.eventAdmin,
+                rescheduleInfo.finalState.getTopic(),
+                handler.getJob(), rescheduleInfo.processingTime);
+
+        return rescheduleInfo.reschedule;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Queue#resume()
+     */
+    @Override
+    public void resume() {
+        if ( this.suspendedSince.getAndSet(-1) != -1 ) {
+            this.logger.debug("Waking up suspended queue {}", queueName);
+            this.startJobs();
+        }
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Queue#suspend()
+     */
+    @Override
+    public void suspend() {
+        if ( this.suspendedSince.compareAndSet(-1, System.currentTimeMillis()) ) {
+            this.logger.debug("Suspending queue {}", queueName);
+        }
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Queue#isSuspended()
+     */
+    @Override
+    public boolean isSuspended() {
+        return this.suspendedSince.get() != -1;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Queue#removeAll()
+     */
+    @Override
+    public synchronized void removeAll() {
+        final Set<String> topics = this.cache.getTopics();
+        logger.debug("Removing all jobs for queue {} : {}", queueName, topics);
+
+        if ( !topics.isEmpty() ) {
+
+            final ResourceResolver resolver = this.services.configuration.createResourceResolver();
+            try {
+                final Resource baseResource = resolver.getResource(this.services.configuration.getLocalJobsPath());
+
+                // sanity check - should never be null
+                if ( baseResource != null ) {
+                    final BatchResourceRemover brr = new BatchResourceRemover();
+
+                    for(final String t : topics) {
+                        final Resource topicResource = baseResource.getChild(t.replace('/', '.'));
+                        if ( topicResource != null ) {
+                            JobTopicTraverser.traverse(logger, topicResource, new JobTopicTraverser.JobCallback() {
+
+                                @Override
+                                public boolean handle(final JobImpl job) {
+                                    final Resource jobResource = topicResource.getResourceResolver().getResource(job.getResourcePath());
+                                    // sanity check
+                                    if ( jobResource != null ) {
+                                        try {
+                                            brr.delete(jobResource);
+                                        } catch ( final PersistenceException ignore) {
+                                            logger.error("Unable to remove job " + job, ignore);
+                                            topicResource.getResourceResolver().revert();
+                                            topicResource.getResourceResolver().refresh();
+                                        }
+                                    }
+                                    return true;
+                                }
+                            });
+                        }
+                    }
+                    try {
+                        resolver.commit();
+                    } catch ( final PersistenceException ignore) {
+                        logger.error("Unable to remove jobs", ignore);
+                    }
+                }
+            } finally {
+                resolver.close();
+            }
+        }
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Queue#getState(java.lang.String)
+     */
+    @Override
+    public Object getState(final String key) {
+        if ( this.configuration.getType() == Type.ORDERED ) {
+            if ( "isSleepingUntil".equals(key) ) {
+                return this.isSleepingUntil;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Queue#getStateInfo()
+     */
+    @Override
+    public String getStateInfo() {
+        return "outdated=" + this.isOutdated.get() +
+                ", suspendedSince=" + this.suspendedSince.get() +
+                ", asyncJobs=" + this.asyncCounter.get() +
+                ", waitCount=" + this.waitCounter.get() +
+                ", jobCount=" + String.valueOf(this.configuration.getMaxParallel() - this.available.availablePermits() +
+                (this.configuration.getType() == Type.ORDERED ? ", isSleepingUntil=" + this.isSleepingUntil : ""));
+    }
+
+    /**
+     * Get the retry delay for a job.
+     * @param handler The job handler.
+     * @return The retry delay
+     */
+    private long getRetryDelay(final JobHandler handler) {
+        long delay = this.configuration.getRetryDelayInMs();
+        if ( handler.getJob().getProperty(JobImpl.PROPERTY_DELAY_OVERRIDE) != null ) {
+            delay = handler.getJob().getProperty(JobImpl.PROPERTY_DELAY_OVERRIDE, Long.class);
+        } else  if ( handler.getJob().getProperty(Job.PROPERTY_JOB_RETRY_DELAY) != null ) {
+            delay = handler.getJob().getProperty(Job.PROPERTY_JOB_RETRY_DELAY, Long.class);
+        }
+        return delay;
+    }
+
+    public boolean stopJob(final JobImpl job) {
+        final JobHandler handler;
+        synchronized ( this.processingJobsLists ) {
+            handler = this.processingJobsLists.get(job.getId());
+        }
+        if ( handler != null ) {
+            handler.stop();
+        }
+        return handler != null;
+    }
+
+    private void reschedule(final JobHandler handler) {
+        // we delay putting back the job until the retry delay is over
+        final long delay = this.getRetryDelay(handler);
+        if ( delay > 0 ) {
+            if ( this.configuration.getType() == Type.ORDERED ) {
+                this.cache.setIsBlocked(true);
+            }
+            handler.addToRetryList();
+            final Date fireDate = new Date();
+            fireDate.setTime(System.currentTimeMillis() + delay);
+            if ( this.configuration.getType() == Type.ORDERED ) {
+                this.isSleepingUntil = fireDate.getTime();
+            }
+
+            final String jobName = "Waiting:" + queueName + ":" + handler.hashCode();
+            final Runnable t = new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        if ( handler.removeFromRetryList() ) {
+                            requeue(handler);
+                        }
+                        waitCounter.decrementAndGet();
+                    } finally {
+                        if ( configuration.getType() == Type.ORDERED ) {
+                            isSleepingUntil = -1;
+                            cache.setIsBlocked(false);
+                            startJobs();
+                        }
+                    }
+                }
+            };
+            this.waitCounter.incrementAndGet();
+            if ( !services.scheduler.schedule(t, services.scheduler.AT(fireDate).name(jobName)) ) {
+                // if scheduling fails run the thread directly
+                t.run();
+            }
+        } else {
+            // put directly into queue
+            this.requeue(handler);
+        }
+    }
+}
+
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/queues/QueueJobCache.java b/src/main/java/org/apache/sling/event/impl/jobs/queues/QueueJobCache.java
new file mode 100644
index 0000000..ebcc92d
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/queues/QueueJobCache.java
@@ -0,0 +1,344 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.queues;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentSkipListSet;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.event.impl.jobs.JobConsumerManager;
+import org.apache.sling.event.impl.jobs.JobHandler;
+import org.apache.sling.event.impl.jobs.JobImpl;
+import org.apache.sling.event.impl.jobs.JobTopicTraverser;
+import org.apache.sling.event.impl.jobs.Utility;
+import org.apache.sling.event.impl.jobs.config.JobManagerConfiguration;
+import org.apache.sling.event.impl.jobs.stats.StatisticsManager;
+import org.apache.sling.event.jobs.Job.JobState;
+import org.apache.sling.event.jobs.Queue;
+import org.apache.sling.event.jobs.QueueConfiguration;
+import org.apache.sling.event.jobs.QueueConfiguration.Type;
+import org.apache.sling.event.jobs.consumer.JobExecutor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The queue job cache caches jobs per queue based on the topics the queue is actively
+ * processing.
+ */
+public class QueueJobCache {
+
+    /** Logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** The maximum of pre loaded jobs for a topic. */
+    private final int maxPreloadLimit = 10;
+
+    /** The job manager configuration. */
+    private final JobManagerConfiguration configuration;
+
+    /** The set of topics handled by this queue. */
+    private final Set<String> topics;
+
+    /** The set of new topics to scan. */
+    private final Set<String> topicsWithNewJobs = new HashSet<String>();
+
+    /** The cache of current objects. */
+    private final List<JobImpl> cache = new ArrayList<JobImpl>();
+
+    /** The queue type. */
+    private final QueueConfiguration.Type queueType;
+
+    /** Block the cache - for ordered queues only. */
+    private final AtomicBoolean queueIsBlocked = new AtomicBoolean(false);
+
+    /**
+     * Create a new queue job cache
+     * @param configuration Current job manager configuration
+     * @param queueName The queue name
+     * @param statisticsManager The statistics manager
+     * @param queueType The queue type
+     * @param topics The topics handled by this queue.
+     */
+    public QueueJobCache(final JobManagerConfiguration configuration,
+            final String queueName,
+            final StatisticsManager statisticsManager,
+            final QueueConfiguration.Type queueType,
+            final Set<String> topics) {
+        this.configuration = configuration;
+        this.queueType = queueType;
+        this.topics = new ConcurrentSkipListSet<String>(topics);
+        this.fillCache(queueName, statisticsManager);
+    }
+
+    /**
+     * All topics of this queue.
+     * @return The topics.
+     */
+    public Set<String> getTopics() {
+        return this.topics;
+    }
+
+    /**
+     * Check whether there are jobs for this queue
+     * @return {@code true} if there is any job outstanding.
+     */
+    public boolean isEmpty() {
+        boolean result = true;
+        synchronized ( this.cache ) {
+            result = this.cache.isEmpty();
+        }
+        if ( result ) {
+            synchronized ( this.topicsWithNewJobs ) {
+                result = this.topicsWithNewJobs.isEmpty();
+            }
+        }
+        return result;
+    }
+
+    public void setIsBlocked(final boolean value) {
+        this.queueIsBlocked.set(value);
+    }
+
+    /**
+     * Fill the cache.
+     * No need to sync as this is called from the constructor.
+     */
+    private void fillCache(final String queueName, final StatisticsManager statisticsManager) {
+        final Set<String> checkingTopics = new HashSet<String>();
+        checkingTopics.addAll(this.topics);
+        if ( !checkingTopics.isEmpty() ) {
+            this.loadJobs(queueName, checkingTopics, statisticsManager);
+        }
+    }
+
+    /**
+     * Get the next job.
+     * This method is potentially called concurrently, and
+     * {@link #reschedule(String, JobHandler, StatisticsManager)} and {@link #handleNewTopics(Set)}
+     * can be called concurrently.
+     * @param jobConsumerManager The job consumer manager
+     * @param statisticsManager The statistics manager
+     * @param queue The queue
+     * @param doFull Whether to do a full scan
+     * @return The job handler or {@code null}.
+     */
+    public JobHandler getNextJob(final JobConsumerManager jobConsumerManager,
+            final StatisticsManager statisticsManager,
+            final Queue queue,
+            final boolean doFull) {
+        JobHandler handler = null;
+
+        if ( !this.queueIsBlocked.get() ) {
+            synchronized ( this.cache ) {
+                boolean retry;
+                do {
+                    retry = false;
+                    if ( this.cache.isEmpty() ) {
+                        final Set<String> checkingTopics = new HashSet<String>();
+                        synchronized ( this.topicsWithNewJobs ) {
+                            checkingTopics.addAll(this.topicsWithNewJobs);
+                            this.topicsWithNewJobs.clear();
+                        }
+                        if ( doFull ) {
+                            checkingTopics.addAll(this.topics);
+                        }
+                        if ( !checkingTopics.isEmpty() ) {
+                            this.loadJobs(queue.getName(), checkingTopics, statisticsManager);
+                        }
+                    }
+
+                    if ( !this.cache.isEmpty() ) {
+                        final JobImpl job = this.cache.remove(0);
+                        final JobExecutor consumer = jobConsumerManager.getExecutor(job.getTopic());
+
+                        handler = new JobHandler(job, consumer, this.configuration);
+                        if ( consumer != null ) {
+                            if ( !handler.startProcessing(queue) ) {
+                                statisticsManager.jobDequeued(queue.getName(), handler.getJob().getTopic());
+                                if ( logger.isDebugEnabled() ) {
+                                    logger.debug("Discarding removed job {}", Utility.toString(job));
+                                }
+                                handler = null;
+                                retry = true;
+                            }
+                        } else {
+                            statisticsManager.jobDequeued(queue.getName(), handler.getJob().getTopic());
+                            // no consumer on this instance, assign to another instance
+                            handler.reassign();
+
+                            handler = null;
+                            retry = true;
+                        }
+
+                    }
+                } while ( handler == null && retry);
+            }
+        }
+        return handler;
+    }
+
+    /**
+     * Load the next N x numberOf(topics) jobs
+     * @param checkingTopics The set of topics to check.
+     */
+    private void loadJobs( final String queueName, final Set<String> checkingTopics,
+            final StatisticsManager statisticsManager) {
+        logger.debug("Starting jobs loading from {}...", checkingTopics);
+
+        final Map<String, List<JobImpl>> topicCache = new HashMap<String, List<JobImpl>>();
+
+        final ResourceResolver resolver = this.configuration.createResourceResolver();
+        try {
+            final Resource baseResource = resolver.getResource(this.configuration.getLocalJobsPath());
+            // sanity check - should never be null
+            if ( baseResource != null ) {
+                for(final String topic : checkingTopics) {
+
+                    final Resource topicResource = baseResource.getChild(topic.replace('/', '.'));
+                    if ( topicResource != null ) {
+                        topicCache.put(topic, loadJobs(queueName, topic, topicResource, statisticsManager));
+                    }
+                }
+            }
+        } finally {
+            resolver.close();
+        }
+        orderTopics(topicCache);
+
+        logger.debug("Finished jobs loading {}", this.cache.size());
+    }
+
+    /**
+     * Order the topics based on the queue type and put them in the cache.
+     * @param topicCache The topic based cache
+     */
+    private void orderTopics(final Map<String, List<JobImpl>> topicCache) {
+        if ( this.queueType == Type.ORDERED
+             || this.queueType == Type.UNORDERED) {
+            for(final List<JobImpl> list : topicCache.values()) {
+                this.cache.addAll(list);
+            }
+            Collections.sort(this.cache);
+        } else {
+            // topic round robin
+            boolean done = true;
+            do {
+                done = true;
+                for(final Map.Entry<String, List<JobImpl>> entry : topicCache.entrySet()) {
+                    if ( !entry.getValue().isEmpty() ) {
+                        this.cache.add(entry.getValue().remove(0));
+                        if ( !entry.getValue().isEmpty() ) {
+                            done = false;
+                        }
+                    }
+                }
+            } while ( !done ) ;
+        }
+    }
+
+    /**
+     * Load the next N x numberOf(topics) jobs.
+     * @param topic The topic
+     * @param topicResource The parent resource of the jobs
+     * @return The cache which will be filled with the jobs.
+     */
+    private List<JobImpl> loadJobs(final String queueName, final String topic,
+            final Resource topicResource,
+            final StatisticsManager statisticsManager) {
+        logger.debug("Loading jobs from topic {}", topic);
+        final List<JobImpl> list = new ArrayList<JobImpl>();
+
+        final AtomicBoolean scanTopic = new AtomicBoolean(false);
+
+        JobTopicTraverser.traverse(logger, topicResource, new JobTopicTraverser.JobCallback() {
+
+            @Override
+            public boolean handle(final JobImpl job) {
+                if ( job.getProcessingStarted() == null && !job.hasReadErrors() ) {
+                    list.add(job);
+                    statisticsManager.jobQueued(queueName, topic);
+                    if ( list.size() == maxPreloadLimit ) {
+                        scanTopic.set(true);
+                    }
+                } else if ( job.getProcessingStarted() != null ) {
+                    logger.debug("Ignoring job {} - processing already started.", job);
+                } else {
+                    // error reading job
+                    scanTopic.set(true);
+                    if ( job.isReadErrorRecoverable() ) {
+                        logger.debug("Ignoring job {} due to recoverable read errors.", job);
+                    } else {
+                        logger.debug("Failing job {} due to unrecoverable read errors.", job);
+                        final JobHandler handler = new JobHandler(job, null, configuration);
+                        handler.finished(JobState.ERROR, true, null);
+                    }
+                }
+                return list.size() < maxPreloadLimit;
+            }
+        });
+        if ( scanTopic.get() ) {
+            synchronized ( this.topicsWithNewJobs ) {
+                this.topicsWithNewJobs.add(topic);
+            }
+        }
+        logger.debug("Caching {} jobs for topic {}", list.size(), topic);
+
+        return list;
+    }
+
+    /**
+     * Inform the queue cache about topics containing new jobs
+     * @param topics The set of topics to scan
+     */
+    public void handleNewTopics(final Set<String> topics) {
+        logger.debug("Update cache to handle new event for topics {}", topics);
+        synchronized ( this.topicsWithNewJobs ) {
+            this.topicsWithNewJobs.addAll(topics);
+        }
+        this.topics.addAll(topics);
+    }
+
+    /**
+     * Reschedule a job
+     * Reschedule the job and add it back into the cache.
+     * @param queueName The queue name
+     * @param handler The job handler
+     * @param statisticsManager The statistics manager
+     */
+    public void reschedule(final String queueName, final JobHandler handler, final StatisticsManager statisticsManager) {
+        synchronized ( this.cache ) {
+            if ( handler.reschedule() ) {
+                if ( this.queueType == Type.ORDERED ) {
+                    this.cache.add(0, handler.getJob());
+                } else {
+                    this.cache.add(handler.getJob());
+                }
+                statisticsManager.jobQueued(queueName, handler.getJob().getTopic());
+            }
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/queues/QueueManager.java b/src/main/java/org/apache/sling/event/impl/jobs/queues/QueueManager.java
new file mode 100644
index 0000000..fd7fcf8
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/queues/QueueManager.java
@@ -0,0 +1,447 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.queues;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Properties;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.commons.scheduler.Scheduler;
+import org.apache.sling.commons.threads.ThreadPool;
+import org.apache.sling.commons.threads.ThreadPoolManager;
+import org.apache.sling.event.impl.EventingThreadPool;
+import org.apache.sling.event.impl.jobs.JobConsumerManager;
+import org.apache.sling.event.impl.jobs.JobHandler;
+import org.apache.sling.event.impl.jobs.JobImpl;
+import org.apache.sling.event.impl.jobs.config.ConfigurationChangeListener;
+import org.apache.sling.event.impl.jobs.config.InternalQueueConfiguration;
+import org.apache.sling.event.impl.jobs.config.JobManagerConfiguration;
+import org.apache.sling.event.impl.jobs.config.QueueConfigurationManager;
+import org.apache.sling.event.impl.jobs.config.QueueConfigurationManager.QueueInfo;
+import org.apache.sling.event.impl.jobs.jmx.QueueStatusEvent;
+import org.apache.sling.event.impl.jobs.jmx.QueuesMBeanImpl;
+import org.apache.sling.event.impl.jobs.stats.StatisticsManager;
+import org.apache.sling.event.impl.support.Environment;
+import org.apache.sling.event.impl.support.ResourceHelper;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.NotificationConstants;
+import org.apache.sling.event.jobs.Queue;
+import org.apache.sling.event.jobs.jmx.QueuesMBean;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.service.event.EventConstants;
+import org.osgi.service.event.EventHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Implementation of the queue manager.
+ */
+@Component(immediate=true)
+@Service(value={Runnable.class, QueueManager.class, EventHandler.class})
+@Properties({
+    @Property(name=Scheduler.PROPERTY_SCHEDULER_PERIOD, longValue=60),
+    @Property(name=Scheduler.PROPERTY_SCHEDULER_CONCURRENT, boolValue=false),
+    @Property(name=EventConstants.EVENT_TOPIC, value=NotificationConstants.TOPIC_JOB_ADDED)
+})
+public class QueueManager
+    implements Runnable, EventHandler, ConfigurationChangeListener {
+
+    /** Default logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    @Reference
+    private EventAdmin eventAdmin;
+
+    @Reference
+    private Scheduler scheduler;
+
+    @Reference
+    private JobConsumerManager jobConsumerManager;
+
+    @Reference
+    private QueuesMBean queuesMBean;
+
+    @Reference
+    private ThreadPoolManager threadPoolManager;
+
+    /**
+     * Our thread pool.
+     */
+    @Reference(referenceInterface=EventingThreadPool.class)
+    private ThreadPool threadPool;
+
+    /** The job manager configuration. */
+    @Reference
+    private JobManagerConfiguration configuration;
+
+    @Reference
+    private StatisticsManager statisticsManager;
+
+    /** Lock object for the queues map - we don't want to sync directly on the concurrent map. */
+    private final Object queuesLock = new Object();
+
+    /** All active queues. */
+    private final Map<String, JobQueueImpl> queues = new ConcurrentHashMap<String, JobQueueImpl>();
+
+    /** We count the scheduler runs. */
+    private volatile long schedulerRuns;
+
+    /** Flag whether the manager is active or suspended. */
+    private final AtomicBoolean isActive = new AtomicBoolean(false);
+
+    /** The queue services. */
+    private volatile QueueServices queueServices;
+
+    /**
+     * Activate this component.
+     * @param props Configuration properties
+     */
+    @Activate
+    protected void activate(final Map<String, Object> props) {
+        logger.info("Apache Sling Queue Manager starting on instance {}", Environment.APPLICATION_ID);
+        this.queueServices = new QueueServices();
+        queueServices.configuration = this.configuration;
+        queueServices.eventAdmin = this.eventAdmin;
+        queueServices.jobConsumerManager = this.jobConsumerManager;
+        queueServices.scheduler = this.scheduler;
+        queueServices.threadPoolManager = this.threadPoolManager;
+        queueServices.statisticsManager = statisticsManager;
+        queueServices.eventingThreadPool = this.threadPool;
+        this.configuration.addListener(this);
+        logger.info("Apache Sling Queue Manager started on instance {}", Environment.APPLICATION_ID);
+    }
+
+    /**
+     * Deactivate this component.
+     */
+    @Deactivate
+    protected void deactivate() {
+        logger.debug("Apache Sling Queue Manager stopping on instance {}", Environment.APPLICATION_ID);
+
+        this.configuration.removeListener(this);
+        final Iterator<JobQueueImpl> i = this.queues.values().iterator();
+        while ( i.hasNext() ) {
+            final JobQueueImpl jbq = i.next();
+            jbq.close();
+            // update mbeans
+            ((QueuesMBeanImpl)queuesMBean).sendEvent(new QueueStatusEvent(null, jbq));
+        }
+        this.queues.clear();
+        this.queueServices = null;
+        logger.info("Apache Sling Queue Manager stopped on instance {}", Environment.APPLICATION_ID);
+    }
+
+    /**
+     * This method is invoked periodically by the scheduler.
+     * It searches for idle queues and stops them after a timeout. If a queue
+     * is idle for two consecutive clean up calls, it is removed.
+     * @see java.lang.Runnable#run()
+     */
+    private void maintain() {
+        this.schedulerRuns++;
+        logger.debug("Queue manager maintenance: Starting #{}", this.schedulerRuns);
+
+        // queue maintenance
+        if ( this.isActive.get() ) {
+            for(final JobQueueImpl jbq : this.queues.values() ) {
+                jbq.maintain();
+            }
+        }
+
+        // full topic scan is done every third run
+        if ( schedulerRuns % 3 == 0 && this.isActive.get() ) {
+            this.fullTopicScan();
+        }
+
+        // we only do a full clean up on every fifth run
+        final boolean doFullCleanUp = (schedulerRuns % 5 == 0);
+
+        if ( doFullCleanUp ) {
+            // check for idle queue
+            logger.debug("Checking for idle queues...");
+
+           // we synchronize to avoid creating a queue which is about to be removed during cleanup
+            synchronized ( queuesLock ) {
+                final Iterator<Map.Entry<String, JobQueueImpl>> i = this.queues.entrySet().iterator();
+                while ( i.hasNext() ) {
+                    final Map.Entry<String, JobQueueImpl> current = i.next();
+                    final JobQueueImpl jbq = current.getValue();
+                    if ( jbq.tryToClose() ) {
+                        logger.debug("Removing idle job queue {}", jbq);
+                        // remove
+                        i.remove();
+                        // update mbeans
+                        ((QueuesMBeanImpl)queuesMBean).sendEvent(new QueueStatusEvent(null, jbq));
+                    }
+                }
+            }
+        }
+        logger.debug("Queue manager maintenance: Finished #{}", this.schedulerRuns);
+    }
+
+    /**
+     * Start a new queue
+     * This method first searches the corresponding queue - if such a queue
+     * does not exist yet, it is created and started.
+     *
+     * @param queueInfo The queue info
+     * @param topics The topics
+     */
+    private void start(final QueueInfo queueInfo,
+                       final Set<String> topics) {
+        final InternalQueueConfiguration config = queueInfo.queueConfiguration;
+        // get or create queue
+        boolean isNewQueue = false;
+        JobQueueImpl queue = null;
+        // we synchronize to avoid creating a queue which is about to be removed during cleanup
+        synchronized ( queuesLock ) {
+            queue = this.queues.get(queueInfo.queueName);
+            // check for reconfiguration, we really do an identity check here(!)
+            if ( queue != null && queue.getConfiguration() != config ) {
+                this.outdateQueue(queue);
+                // we use a new queue with the configuration
+                queue = null;
+            }
+            if ( queue == null ) {
+                queue = JobQueueImpl.createQueue(queueInfo.queueName, config, queueServices, topics);
+                // on startup the queue might be empty and we get null back from createQueue
+                if ( queue != null ) {
+                    isNewQueue = true;
+                    queues.put(queueInfo.queueName, queue);
+                    ((QueuesMBeanImpl)queuesMBean).sendEvent(new QueueStatusEvent(queue, null));
+                }
+            }
+        }
+        if ( queue != null ) {
+            if ( !isNewQueue ) {
+                queue.wakeUpQueue(topics);
+            }
+            queue.startJobs();
+        }
+    }
+
+    /**
+     * This method is invoked periodically by the scheduler.
+     * In the default configuration every minute
+     * @see java.lang.Runnable#run()
+     */
+    @Override
+    public void run() {
+        this.maintain();
+    }
+
+    private void outdateQueue(final JobQueueImpl queue) {
+        // remove the queue with the old name
+        // check for main queue
+        final String oldName = ResourceHelper.filterQueueName(queue.getName());
+        this.queues.remove(oldName);
+        // check if we can close or have to rename
+        if ( queue.tryToClose() ) {
+            // copy statistics
+            // update mbeans
+            ((QueuesMBeanImpl)queuesMBean).sendEvent(new QueueStatusEvent(null, queue));
+        } else {
+            queue.outdate();
+            // readd with new name
+            String newName = ResourceHelper.filterName(queue.getName());
+            int index = 0;
+            while ( this.queues.containsKey(newName) ) {
+                newName = ResourceHelper.filterName(queue.getName()) + '$' + String.valueOf(index++);
+            }
+            this.queues.put(newName, queue);
+            // update mbeans
+            ((QueuesMBeanImpl)queuesMBean).sendEvent(new QueueStatusEvent(queue, queue));
+        }
+    }
+
+    /**
+     * Outdate all queues.
+     */
+    private void restart() {
+        // let's rename/close all queues and clear them
+        synchronized ( queuesLock ) {
+            final List<JobQueueImpl> queues = new ArrayList<JobQueueImpl>(this.queues.values());
+            for(final JobQueueImpl queue : queues ) {
+                this.outdateQueue(queue);
+            }
+        }
+        // check if we're still active
+        final JobManagerConfiguration config = this.configuration;
+        if ( config != null ) {
+            final List<Job> rescheduleList = this.configuration.clearJobRetryList();
+            for(final Job j : rescheduleList) {
+                final JobHandler jh = new JobHandler((JobImpl)j, null, this.configuration);
+                jh.reschedule();
+            }
+        }
+    }
+
+    /**
+     * @param name The queue name
+     * @return The queue or {@code null}.
+     * @see org.apache.sling.event.jobs.JobManager#getQueue(java.lang.String)
+     */
+    public Queue getQueue(final String name) {
+        return this.queues.get(name);
+    }
+
+    /**
+     * @return An iterator for the available queues.
+     * @see org.apache.sling.event.jobs.JobManager#getQueues()
+     */
+    public Iterable<Queue> getQueues() {
+        final Iterator<JobQueueImpl> jqI = this.queues.values().iterator();
+        return new Iterable<Queue>() {
+
+            @Override
+            public Iterator<Queue> iterator() {
+                return new Iterator<Queue>() {
+
+                    @Override
+                    public boolean hasNext() {
+                        return jqI.hasNext();
+                    }
+
+                    @Override
+                    public Queue next() {
+                        return jqI.next();
+                    }
+
+                    @Override
+                    public void remove() {
+                        throw new UnsupportedOperationException();
+                    }
+                };
+            }
+        };
+    }
+
+    /**
+     * This method is called whenever the topology or queue configurations change.
+     * @param active Whether the job handling is active atm.
+     */
+    @Override
+    public void configurationChanged(final boolean active) {
+        // are we still active?
+        if ( this.configuration != null ) {
+            logger.debug("Topology changed {}", active);
+            this.isActive.set(active);
+            if ( active ) {
+                fullTopicScan();
+            } else {
+                this.restart();
+            }
+        }
+    }
+
+    private void fullTopicScan() {
+        logger.debug("Scanning repository for existing topics...");
+        final Set<String> topics = this.scanTopics();
+        final Map<QueueInfo, Set<String>> mapping = this.updateTopicMapping(topics);
+        // start queues
+        for(final Map.Entry<QueueInfo, Set<String>> entry : mapping.entrySet() ) {
+            this.start(entry.getKey(), entry.getValue());
+        }
+    }
+
+    /**
+     * Scan the resource tree for topics.
+     */
+    private Set<String> scanTopics() {
+        final Set<String> topics = new HashSet<String>();
+
+        final ResourceResolver resolver = this.configuration.createResourceResolver();
+        try {
+            final Resource baseResource = resolver.getResource(this.configuration.getLocalJobsPath());
+
+            // sanity check - should never be null
+            if ( baseResource != null ) {
+                final Iterator<Resource> topicIter = baseResource.listChildren();
+                while ( topicIter.hasNext() ) {
+                    final Resource topicResource = topicIter.next();
+                    final String topic = topicResource.getName().replace('.', '/');
+                    logger.debug("Found topic {}", topic);
+                    topics.add(topic);
+                }
+            }
+        } finally {
+            resolver.close();
+        }
+        return topics;
+    }
+
+    /**
+     * @see org.osgi.service.event.EventHandler#handleEvent(org.osgi.service.event.Event)
+     */
+    @Override
+    public void handleEvent(final Event event) {
+        final String topic = (String)event.getProperty(NotificationConstants.NOTIFICATION_PROPERTY_JOB_TOPIC);
+        if ( this.isActive.get() && topic != null ) {
+            final QueueInfo info = this.configuration.getQueueConfigurationManager().getQueueInfo(topic);
+            this.start(info, Collections.singleton(topic));
+        }
+    }
+
+    /**
+     * Get the latest mapping from queue name to topics
+     */
+    private Map<QueueInfo, Set<String>> updateTopicMapping(final Set<String> topics) {
+        final Map<QueueInfo, Set<String>> mapping = new HashMap<QueueConfigurationManager.QueueInfo, Set<String>>();
+        for(final String topic : topics) {
+            final QueueInfo queueInfo = this.configuration.getQueueConfigurationManager().getQueueInfo(topic);
+            Set<String> queueTopics = mapping.get(queueInfo);
+            if ( queueTopics == null ) {
+                queueTopics = new HashSet<String>();
+                mapping.put(queueInfo, queueTopics);
+            }
+            queueTopics.add(topic);
+        }
+
+        this.logger.debug("Established new topic mapping: {}", mapping);
+        return mapping;
+    }
+
+    protected void bindThreadPool(final EventingThreadPool etp) {
+        this.threadPool = etp;
+    }
+
+    protected void unbindThreadPool(final EventingThreadPool etp) {
+        if ( this.threadPool == etp ) {
+            this.threadPool = null;
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/queues/QueueServices.java b/src/main/java/org/apache/sling/event/impl/jobs/queues/QueueServices.java
new file mode 100644
index 0000000..e39235f
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/queues/QueueServices.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.queues;
+
+import org.apache.sling.commons.scheduler.Scheduler;
+import org.apache.sling.commons.threads.ThreadPool;
+import org.apache.sling.commons.threads.ThreadPoolManager;
+import org.apache.sling.event.impl.jobs.JobConsumerManager;
+import org.apache.sling.event.impl.jobs.config.JobManagerConfiguration;
+import org.apache.sling.event.impl.jobs.stats.StatisticsManager;
+import org.osgi.service.event.EventAdmin;
+
+/**
+ * The queue services class is a helper class containing all
+ * services used by the queue implementations.
+ * This avoids passing a set of separate objects.
+ */
+public class QueueServices {
+
+    public JobManagerConfiguration configuration;
+
+    public JobConsumerManager jobConsumerManager;
+
+    public EventAdmin eventAdmin;
+
+    public ThreadPoolManager threadPoolManager;
+
+    public Scheduler scheduler;
+
+    public StatisticsManager statisticsManager;
+
+    public ThreadPool eventingThreadPool;
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/queues/ResultBuilderImpl.java b/src/main/java/org/apache/sling/event/impl/jobs/queues/ResultBuilderImpl.java
new file mode 100644
index 0000000..93684d9
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/queues/ResultBuilderImpl.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.queues;
+
+import org.apache.sling.event.impl.jobs.InternalJobState;
+import org.apache.sling.event.jobs.consumer.JobExecutionContext.ResultBuilder;
+import org.apache.sling.event.jobs.consumer.JobExecutionResult;
+
+public class ResultBuilderImpl implements ResultBuilder {
+
+    private volatile String message;
+
+    private volatile Long retryDelayInMs;
+
+    @Override
+    public JobExecutionResult failed(final long retryDelayInMs) {
+        this.retryDelayInMs = retryDelayInMs;
+        return new JobExecutionResultImpl(InternalJobState.FAILED, message, retryDelayInMs);
+    }
+
+    @Override
+    public ResultBuilder message(final String message) {
+        this.message = message;
+        return this;
+    }
+
+    @Override
+    public JobExecutionResult succeeded() {
+        return new JobExecutionResultImpl(InternalJobState.SUCCEEDED, message, retryDelayInMs);
+    }
+
+    @Override
+    public JobExecutionResult failed() {
+        return new JobExecutionResultImpl(InternalJobState.FAILED, message, retryDelayInMs);
+    }
+
+    @Override
+    public JobExecutionResult cancelled() {
+        return new JobExecutionResultImpl(InternalJobState.CANCELLED, message, retryDelayInMs);
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/scheduling/JobScheduleBuilderImpl.java b/src/main/java/org/apache/sling/event/impl/jobs/scheduling/JobScheduleBuilderImpl.java
new file mode 100644
index 0000000..7e46985
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/scheduling/JobScheduleBuilderImpl.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.scheduling;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.event.impl.support.ScheduleInfoImpl;
+import org.apache.sling.event.jobs.JobBuilder.ScheduleBuilder;
+import org.apache.sling.event.jobs.ScheduledJobInfo;
+
+/**
+ * The builder implementation for scheduled jobs.
+ */
+public final class JobScheduleBuilderImpl implements ScheduleBuilder {
+
+    private final String topic;
+
+    private final Map<String, Object> properties;
+
+    private final String scheduleName;
+
+    private final JobSchedulerImpl jobScheduler;
+
+    private volatile boolean suspend = false;
+
+    private final List<ScheduleInfoImpl> schedules = new ArrayList<ScheduleInfoImpl>();
+
+    public JobScheduleBuilderImpl(
+            final String topic,
+            final Map<String, Object> properties,
+            final String name,
+            final JobSchedulerImpl jobScheduler) {
+        this.topic = topic;
+        this.properties = properties;
+        this.scheduleName = name;
+        this.jobScheduler = jobScheduler;
+    }
+
+    @Override
+    public ScheduleBuilder weekly(final int day, final int hour, final int minute) {
+        schedules.add(ScheduleInfoImpl.WEEKLY(day, hour, minute));
+        return this;
+    }
+
+    @Override
+    public ScheduleBuilder daily(final int hour, final int minute) {
+        schedules.add(ScheduleInfoImpl.DAILY(hour, minute));
+        return this;
+    }
+
+    @Override
+    public ScheduleBuilder hourly(final int minute) {
+        schedules.add(ScheduleInfoImpl.HOURLY(minute));
+        return this;
+    }
+
+    @Override
+    public ScheduleBuilder at(final Date date) {
+        schedules.add(ScheduleInfoImpl.AT(date));
+        return this;
+    }
+
+    @Override
+    public ScheduleBuilder monthly(final int day, final int hour, final int minute) {
+        schedules.add(ScheduleInfoImpl.MONTHLY(day, hour, minute));
+        return this;
+    }
+
+    @Override
+    public ScheduleBuilder yearly(final int month, final int day, final int hour, final int minute) {
+        schedules.add(ScheduleInfoImpl.YEARLY(month, day, hour, minute));
+        return this;
+    }
+
+    @Override
+    public ScheduleBuilder cron(final String expression) {
+        schedules.add(ScheduleInfoImpl.CRON(expression));
+        return this;
+    }
+
+    @Override
+    public ScheduledJobInfo add() {
+        return this.add(null);
+    }
+
+    @Override
+    public ScheduledJobInfo add(final List<String> errors) {
+        return this.jobScheduler.addScheduledJob(topic,
+                properties,
+                scheduleName,
+                suspend,
+                schedules,
+                errors);
+    }
+
+    @Override
+    public ScheduleBuilder suspend() {
+        this.suspend = true;
+        return this;
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/scheduling/JobSchedulerImpl.java b/src/main/java/org/apache/sling/event/impl/jobs/scheduling/JobSchedulerImpl.java
new file mode 100644
index 0000000..b4b7612
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/scheduling/JobSchedulerImpl.java
@@ -0,0 +1,566 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.scheduling;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.sling.api.resource.ModifiableValueMap;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.observation.ExternalResourceChangeListener;
+import org.apache.sling.api.resource.observation.ResourceChange;
+import org.apache.sling.api.resource.observation.ResourceChangeListener;
+import org.apache.sling.commons.scheduler.JobContext;
+import org.apache.sling.commons.scheduler.ScheduleOptions;
+import org.apache.sling.commons.scheduler.Scheduler;
+import org.apache.sling.event.impl.jobs.JobManagerImpl;
+import org.apache.sling.event.impl.jobs.Utility;
+import org.apache.sling.event.impl.jobs.config.ConfigurationChangeListener;
+import org.apache.sling.event.impl.jobs.config.JobManagerConfiguration;
+import org.apache.sling.event.impl.jobs.config.TopologyCapabilities;
+import org.apache.sling.event.impl.support.ResourceHelper;
+import org.apache.sling.event.impl.support.ScheduleInfoImpl;
+import org.apache.sling.event.jobs.JobBuilder;
+import org.apache.sling.event.jobs.ScheduleInfo;
+import org.apache.sling.event.jobs.ScheduleInfo.ScheduleType;
+import org.apache.sling.event.jobs.ScheduledJobInfo;
+import org.osgi.service.event.Event;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * The scheduler for managing scheduled jobs.
+ *
+ * This is not a component by itself, it's directly created from the job manager.
+ * The job manager is also registering itself as an event handler and forwards
+ * the events to this service.
+ */
+public class JobSchedulerImpl
+    implements ConfigurationChangeListener,
+               ResourceChangeListener, ExternalResourceChangeListener,
+               org.apache.sling.commons.scheduler.Job {
+
+    private static final String PROPERTY_READ_JOB = "properties";
+
+    private static final String PROPERTY_SCHEDULE_INDEX = "index";
+
+    /** Default logger */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** Is this active? */
+    private final AtomicBoolean active = new AtomicBoolean(false);
+
+    /** Central job handling configuration. */
+    private final JobManagerConfiguration configuration;
+
+    /** Scheduler service. */
+    private final Scheduler scheduler;
+
+    /** Job manager. */
+    private final JobManagerImpl jobManager;
+
+    /** Scheduled job handler. */
+    private final ScheduledJobHandler scheduledJobHandler;
+
+    /** All scheduled jobs, by scheduler name */
+    private final Map<String, ScheduledJobInfoImpl> scheduledJobs = new HashMap<String, ScheduledJobInfoImpl>();
+
+    /**
+     * Create the scheduler
+     * @param configuration Central job manager configuration
+     * @param scheduler The scheduler service
+     * @param jobManager The job manager
+     */
+    public JobSchedulerImpl(final JobManagerConfiguration configuration,
+            final Scheduler scheduler,
+            final JobManagerImpl jobManager) {
+        this.configuration = configuration;
+        this.scheduler = scheduler;
+        this.jobManager = jobManager;
+
+        this.configuration.addListener(this);
+
+        this.scheduledJobHandler = new ScheduledJobHandler(configuration, this);
+    }
+
+    /**
+     * Deactivate this component.
+     */
+    public void deactivate() {
+        this.configuration.removeListener(this);
+
+        this.scheduledJobHandler.deactivate();
+
+        if ( this.active.compareAndSet(true, false) ) {
+            this.stopScheduling();
+        }
+        synchronized ( this.scheduledJobs ) {
+            this.scheduledJobs.clear();
+        }
+    }
+
+    /**
+     * @see org.apache.sling.event.impl.jobs.config.ConfigurationChangeListener#configurationChanged(boolean)
+     */
+    @Override
+    public void configurationChanged(final boolean processingActive) {
+        // scheduling is only active if
+        // - processing is active and
+        // - configuration is still available and active
+        // - and current instance is leader
+        final boolean schedulingActive;
+        if ( processingActive ) {
+            final TopologyCapabilities caps = this.configuration.getTopologyCapabilities();
+            if ( caps != null && caps.isActive() ) {
+                schedulingActive = caps.isLeader();
+            } else {
+                schedulingActive = false;
+            }
+        } else {
+            schedulingActive = false;
+        }
+
+        // switch activation based on current state and new state
+        if ( schedulingActive ) {
+            // activate if inactive
+            if ( this.active.compareAndSet(false, true) ) {
+                this.startScheduling();
+            }
+        } else {
+            // deactivate if active
+            if ( this.active.compareAndSet(true, false) ) {
+                this.stopScheduling();
+            }
+        }
+    }
+
+    /**
+     * Start all scheduled jobs
+     */
+    private void startScheduling() {
+        synchronized ( this.scheduledJobs ) {
+            for(final ScheduledJobInfo info : this.scheduledJobs.values()) {
+                this.startScheduledJob(((ScheduledJobInfoImpl)info));
+            }
+        }
+    }
+
+    /**
+     * Stop all scheduled jobs.
+     */
+    private void stopScheduling() {
+        synchronized ( this.scheduledJobs ) {
+            for(final ScheduledJobInfo info : this.scheduledJobs.values()) {
+                this.stopScheduledJob((ScheduledJobInfoImpl)info);
+            }
+        }
+    }
+
+    /**
+     * Add a scheduled job
+     */
+    public void scheduleJob(final ScheduledJobInfoImpl info) {
+        synchronized ( this.scheduledJobs ) {
+            this.scheduledJobs.put(info.getName(), info);
+            this.startScheduledJob(info);
+        }
+    }
+
+    /**
+     * Unschedule a scheduled job
+     */
+    public void unscheduleJob(final ScheduledJobInfoImpl info) {
+        synchronized ( this.scheduledJobs ) {
+            if ( this.scheduledJobs.remove(info.getName()) != null ) {
+                this.stopScheduledJob(info);
+            }
+        }
+    }
+
+    /**
+     * Remove a scheduled job
+     */
+    public void removeJob(final ScheduledJobInfoImpl info) {
+        this.unscheduleJob(info);
+        this.scheduledJobHandler.remove(info);
+    }
+
+    /**
+     * Start a scheduled job
+     * @param info The scheduling info
+     */
+    private void startScheduledJob(final ScheduledJobInfoImpl info) {
+        if ( this.active.get() ) {
+            if ( !info.isSuspended() ) {
+                this.configuration.getAuditLogger().debug("SCHEDULED OK name={}, topic={}, properties={} : {}",
+                        new Object[] {info.getName(),
+                                      info.getJobTopic(),
+                                      info.getJobProperties()},
+                                      info.getSchedules());
+                int index = 0;
+                for(final ScheduleInfo si : info.getSchedules()) {
+                    final String name = info.getSchedulerJobId() + "-" + String.valueOf(index);
+                    ScheduleOptions options = null;
+                    switch ( si.getType() ) {
+                        case DAILY:
+                        case WEEKLY:
+                        case HOURLY:
+                        case MONTHLY:
+                        case YEARLY:
+                        case CRON:
+                            options = this.scheduler.EXPR(((ScheduleInfoImpl)si).getCronExpression());
+
+                            break;
+                        case DATE:
+                            options = this.scheduler.AT(((ScheduleInfoImpl)si).getNextScheduledExecution());
+                            break;
+                    }
+                    // Create configuration for scheduled job
+                    final Map<String, Serializable> config = new HashMap<String, Serializable>();
+                    config.put(PROPERTY_READ_JOB, info);
+                    config.put(PROPERTY_SCHEDULE_INDEX, index);
+                    this.scheduler.schedule(this, options.name(name).config(config).canRunConcurrently(false));
+                    index++;
+                }
+            } else {
+                this.configuration.getAuditLogger().debug("SCHEDULED SUSPENDED name={}, topic={}, properties={} : {}",
+                        new Object[] {info.getName(),
+                                      info.getJobTopic(),
+                                      info.getJobProperties(),
+                                      info.getSchedules()});
+            }
+        }
+    }
+
+    /**
+     * Stop a scheduled job
+     * @param info The scheduling info
+     */
+    private void stopScheduledJob(final ScheduledJobInfoImpl info) {
+        final Scheduler localScheduler = this.scheduler;
+        if ( localScheduler != null ) {
+            this.configuration.getAuditLogger().debug("SCHEDULED STOP name={}, topic={}, properties={} : {}",
+                    new Object[] {info.getName(),
+                                  info.getJobTopic(),
+                                  info.getJobProperties(),
+                                  info.getSchedules()});
+            for(int index = 0; index<info.getSchedules().size(); index++) {
+                final String name = info.getSchedulerJobId() + "-" + String.valueOf(index);
+                localScheduler.unschedule(name);
+            }
+        }
+    }
+
+    /**
+     * @see org.apache.sling.commons.scheduler.Job#execute(org.apache.sling.commons.scheduler.JobContext)
+     */
+    @Override
+    public void execute(final JobContext context) {
+        if ( !active.get() ) {
+            // not active anymore, simply return
+            return;
+        }
+        final ScheduledJobInfoImpl info = (ScheduledJobInfoImpl) context.getConfiguration().get(PROPERTY_READ_JOB);
+
+        if ( info.isSuspended() ) {
+            return;
+        }
+
+        this.jobManager.addJob(info.getJobTopic(), info.getJobProperties());
+        final int index = (Integer)context.getConfiguration().get(PROPERTY_SCHEDULE_INDEX);
+        final Iterator<ScheduleInfo> iter = info.getSchedules().iterator();
+        ScheduleInfo si = iter.next();
+        for(int i=0; i<index; i++) {
+            si = iter.next();
+        }
+        // if scheduled once (DATE), remove from schedule
+        if ( si.getType() == ScheduleType.DATE ) {
+            if ( index == 0 && info.getSchedules().size() == 1 ) {
+                // remove
+                this.scheduledJobHandler.remove(info);
+            } else {
+                // update schedule list
+                final List<ScheduleInfo> infos = new ArrayList<ScheduleInfo>();
+                for(final ScheduleInfo i : info.getSchedules() ) {
+                    if ( i != si ) { // no need to use equals
+                        infos.add(i);
+                    }
+                }
+                info.update(infos);
+                this.scheduledJobHandler.updateSchedule(info.getName(), infos);
+            }
+        }
+    }
+
+    /**
+     * @see org.osgi.service.event.EventHandler#handleEvent(org.osgi.service.event.Event)
+     */
+    public void handleEvent(final Event event) {
+        if ( ResourceHelper.BUNDLE_EVENT_STARTED.equals(event.getTopic())
+             || ResourceHelper.BUNDLE_EVENT_UPDATED.equals(event.getTopic()) ) {
+            this.scheduledJobHandler.bundleEvent();
+        }
+    }
+
+    /**
+     * Helper method which just logs the exception in debug mode.
+     * @param e
+     */
+    private void ignoreException(final Exception e) {
+        if ( this.logger.isDebugEnabled() ) {
+            this.logger.debug("Ignored exception " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Create a schedule builder for a currently scheduled job
+     */
+    public JobBuilder.ScheduleBuilder createJobBuilder(final ScheduledJobInfoImpl info) {
+        final JobBuilder.ScheduleBuilder sb = new JobScheduleBuilderImpl(info.getJobTopic(),
+                info.getJobProperties(), info.getName(), this);
+        return (info.isSuspended() ? sb.suspend() : sb);
+    }
+
+    private enum Operation {
+        LESS,
+        LESS_OR_EQUALS,
+        EQUALS,
+        GREATER_OR_EQUALS,
+        GREATER
+    }
+
+    /**
+     * Check if the job matches the template
+     */
+    private boolean match(final ScheduledJobInfoImpl job, final Map<String, Object> template) {
+        if ( template != null ) {
+            for(final Map.Entry<String, Object> current : template.entrySet()) {
+                final String key = current.getKey();
+                final char firstChar = key.length() > 0 ? key.charAt(0) : 0;
+                final String propName;
+                final Operation op;
+                if ( firstChar == '=' ) {
+                    propName = key.substring(1);
+                    op  = Operation.EQUALS;
+                } else if ( firstChar == '<' ) {
+                    final char secondChar = key.length() > 1 ? key.charAt(1) : 0;
+                    if ( secondChar == '=' ) {
+                        op = Operation.LESS_OR_EQUALS;
+                        propName = key.substring(2);
+                    } else {
+                        op = Operation.LESS;
+                        propName = key.substring(1);
+                    }
+                } else if ( firstChar == '>' ) {
+                    final char secondChar = key.length() > 1 ? key.charAt(1) : 0;
+                    if ( secondChar == '=' ) {
+                        op = Operation.GREATER_OR_EQUALS;
+                        propName = key.substring(2);
+                    } else {
+                        op = Operation.GREATER;
+                        propName = key.substring(1);
+                    }
+                } else {
+                    propName = key;
+                    op  = Operation.EQUALS;
+                }
+                final Object value = current.getValue();
+
+                if ( op == Operation.EQUALS ) {
+                    if ( !value.equals(job.getJobProperties().get(propName)) ) {
+                        return false;
+                    }
+                } else {
+                    if ( value instanceof Comparable ) {
+                        @SuppressWarnings({ "unchecked", "rawtypes" })
+                        final int result = ((Comparable)value).compareTo(job.getJobProperties().get(propName));
+                        if ( op == Operation.LESS && result > -1 ) {
+                            return false;
+                        } else if ( op == Operation.LESS_OR_EQUALS && result > 0 ) {
+                            return false;
+                        } else if ( op == Operation.GREATER_OR_EQUALS && result < 0 ) {
+                            return false;
+                        } else if ( op == Operation.GREATER && result < 1 ) {
+                            return false;
+                        }
+                    } else {
+                        // if the value is not comparable we simply don't match
+                        return false;
+                    }
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Get all scheduled jobs
+     */
+    public Collection<ScheduledJobInfo> getScheduledJobs(final String topic,
+            final long limit,
+            final Map<String, Object>... templates) {
+        final List<ScheduledJobInfo> jobs = new ArrayList<ScheduledJobInfo>();
+        long count = 0;
+        synchronized ( this.scheduledJobs ) {
+            for(final ScheduledJobInfoImpl job : this.scheduledJobs.values() ) {
+                boolean add = true;
+                if ( topic != null && !topic.equals(job.getJobTopic()) ) {
+                    add = false;
+                }
+                if ( add && templates != null && templates.length != 0 ) {
+                    add = false;
+                    for (Map<String,Object> template : templates) {
+                        add = this.match(job, template);
+                        if ( add ) {
+                            break;
+                        }
+                    }
+                }
+                if ( add ) {
+                    jobs.add(job);
+                    count++;
+                    if ( limit > 0 && count == limit ) {
+                        break;
+                    }
+                }
+            }
+        }
+        return jobs;
+    }
+
+    /**
+     * Change the suspended flag for a scheduled job
+     * @param info The schedule info
+     * @param flag The corresponding flag
+     */
+    public void setSuspended(final ScheduledJobInfoImpl info, final boolean flag) {
+        final ResourceResolver resolver = configuration.createResourceResolver();
+        try {
+            final StringBuilder sb = new StringBuilder(this.configuration.getScheduledJobsPath(true));
+            sb.append(ResourceHelper.filterName(info.getName()));
+            final String path = sb.toString();
+
+            final Resource eventResource = resolver.getResource(path);
+            if ( eventResource != null ) {
+                final ModifiableValueMap mvm = eventResource.adaptTo(ModifiableValueMap.class);
+                if ( flag ) {
+                    mvm.put(ResourceHelper.PROPERTY_SCHEDULE_SUSPENDED, Boolean.TRUE);
+                } else {
+                    mvm.remove(ResourceHelper.PROPERTY_SCHEDULE_SUSPENDED);
+                }
+                resolver.commit();
+            }
+            if ( flag ) {
+                this.stopScheduledJob(info);
+            } else {
+                this.startScheduledJob(info);
+            }
+        } catch (final PersistenceException pe) {
+            // we ignore the exception if removing fails
+            ignoreException(pe);
+        } finally {
+            resolver.close();
+        }
+    }
+
+    /**
+     * Add a scheduled job
+     * @param topic The job topic
+     * @param properties The job properties
+     * @param scheduleName The schedule name
+     * @param isSuspended Whether it is suspended
+     * @param scheduleInfos The scheduling information
+     * @param errors Optional list to contain potential errors
+     * @return A new job info or {@code null}
+     */
+    public ScheduledJobInfo addScheduledJob(final String topic,
+            final Map<String, Object> properties,
+            final String scheduleName,
+            final boolean isSuspended,
+            final List<ScheduleInfoImpl> scheduleInfos,
+            final List<String> errors) {
+        final List<String> msgs = new ArrayList<String>();
+        if ( scheduleName == null || scheduleName.length() == 0 ) {
+            msgs.add("Schedule name not specified");
+        }
+        final String errorMessage = Utility.checkJob(topic, properties);
+        if ( errorMessage != null ) {
+            msgs.add(errorMessage);
+        }
+        if ( scheduleInfos.size() == 0 ) {
+            msgs.add("No schedule defined for " + scheduleName);
+        }
+        for(final ScheduleInfoImpl info : scheduleInfos) {
+            info.check(msgs);
+        }
+        if ( msgs.size() == 0 ) {
+            try {
+                final ScheduledJobInfo info = this.scheduledJobHandler.addOrUpdateJob(topic, properties, scheduleName, isSuspended, scheduleInfos);
+                if ( info != null ) {
+                    return info;
+                }
+                msgs.add("Unable to persist scheduled job.");
+            } catch ( final PersistenceException pe) {
+                msgs.add("Unable to persist scheduled job: " + scheduleName);
+                logger.warn("Unable to persist scheduled job", pe);
+            }
+        } else {
+            for(final String msg : msgs) {
+                logger.warn(msg);
+            }
+        }
+        if ( errors != null ) {
+            errors.addAll(msgs);
+        }
+        return null;
+    }
+
+    public void maintenance() {
+        this.scheduledJobHandler.maintenance();
+    }
+
+    /**
+     * @see org.apache.sling.api.resource.observation.ResourceChangeListener#onChange(java.util.List)
+     */
+    @Override
+    public void onChange(List<ResourceChange> changes) {
+        for(final ResourceChange change : changes ) {
+            if ( change.getPath() != null && change.getPath().startsWith(this.configuration.getScheduledJobsPath(true)) ) {
+                if ( change.getType() == ResourceChange.ChangeType.REMOVED ) {
+                    // removal
+                    logger.debug("Remove scheduled job {}", change.getPath());
+                    this.scheduledJobHandler.handleRemove(change.getPath());
+                } else {
+                    // add or update
+                    logger.debug("Add or update scheduled job {}, event {}", change.getPath(), change.getType());
+                    this.scheduledJobHandler.handleAddUpdate(change.getPath());
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/scheduling/ScheduledJobHandler.java b/src/main/java/org/apache/sling/event/impl/jobs/scheduling/ScheduledJobHandler.java
new file mode 100644
index 0000000..deb3955
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/scheduling/ScheduledJobHandler.java
@@ -0,0 +1,545 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.scheduling;
+
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.sling.api.resource.ModifiableValueMap;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.event.impl.jobs.config.JobManagerConfiguration;
+import org.apache.sling.event.impl.support.Environment;
+import org.apache.sling.event.impl.support.ResourceHelper;
+import org.apache.sling.event.impl.support.ScheduleInfoImpl;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.ScheduleInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ */
+public class ScheduledJobHandler implements Runnable {
+
+    public static final class Holder {
+        public Calendar created;
+        public ScheduledJobInfoImpl info;
+        public long read;
+    }
+
+    /** Logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** The job manager configuration. */
+    private final JobManagerConfiguration configuration;
+
+    /** The job scheduler. */
+    private final JobSchedulerImpl jobScheduler;
+
+    /** The map of all scheduled jobs, key is the filtered schedule name */
+    private final Map<String, Holder> scheduledJobs = new HashMap<String, Holder>();
+
+    private final AtomicLong lastBundleActivity = new AtomicLong();
+
+    private final AtomicBoolean isRunning = new AtomicBoolean(true);
+
+    /** A local queue for serializing the event processing. */
+    private final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>();
+
+    /**
+     * @param configuration Current job manager configuration
+     */
+    public ScheduledJobHandler(final JobManagerConfiguration configuration,
+            final JobSchedulerImpl jobScheduler) {
+        this.configuration = configuration;
+        this.jobScheduler = jobScheduler;
+        final Thread t = new Thread(this, "Apache Sling Scheduled Job Handler Thread");
+        t.setDaemon(true);
+        t.start();
+
+        this.addFullScan();
+    }
+
+    /**
+     * Add a task/runnable to the queue
+     */
+    private void addTask(final Runnable r) {
+        try {
+            this.queue.put(r);
+        } catch (final InterruptedException e) {
+            this.ignoreException(e);
+            Thread.currentThread().interrupt();
+        }
+    }
+    /**
+     * Add a full scan to the task queue
+     */
+    private void addFullScan() {
+        this.addTask(new Runnable() {
+            @Override
+            public void run() {
+                scan();
+            }
+        });
+    }
+
+    public void deactivate() {
+        this.isRunning.set(false);
+        this.queue.clear();
+        // put a NOP runnable to wake up the queue
+        this.addTask(new Runnable() {
+            @Override
+            public  void run() {
+                // do nothing
+            }
+        });
+    }
+
+    @Override
+    public void run() {
+        while ( this.isRunning.get() ) {
+            Runnable r = null;
+            try {
+                r = this.queue.take();
+            } catch (final InterruptedException e) {
+                this.ignoreException(e);
+                Thread.currentThread().interrupt();
+                this.isRunning.set(false);
+            }
+            if ( this.isRunning.get() && r != null) {
+                r.run();
+            }
+        }
+    }
+
+    private void scan() {
+        final ResourceResolver resolver = configuration.createResourceResolver();
+        if ( resolver != null ) {
+            try {
+                logger.debug("Scanning for scheduled jobs...");
+                final String path = this.configuration.getScheduledJobsPath(false);
+                final Resource startResource = resolver.getResource(path);
+                if ( startResource != null ) {
+                    final Map<String, Holder> newScheduledJobs = new HashMap<String, Holder>();
+                    synchronized ( this.scheduledJobs ) {
+                        for(final Resource rsrc : startResource.getChildren()) {
+                            if ( !isRunning.get() ) {
+                                break;
+                            }
+                            handleAddOrUpdate(newScheduledJobs, rsrc);
+                        }
+                        if ( isRunning.get() ) {
+                            for(final Holder h : this.scheduledJobs.values()) {
+                                if ( h.info != null ) {
+                                    this.jobScheduler.unscheduleJob(h.info);
+                                }
+                            }
+                            this.scheduledJobs.clear();
+                            this.scheduledJobs.putAll(newScheduledJobs);
+                        }
+                    }
+                }
+                logger.debug("Finished scanning for scheduled jobs...");
+            } finally {
+                resolver.close();
+            }
+        }
+    }
+
+    /**
+     * Read a scheduled job from the resource
+     * @return The job or <code>null</code>
+     */
+    private Map<String, Object> readScheduledJob(final Resource eventResource) {
+        try {
+            final ValueMap vm = ResourceHelper.getValueMap(eventResource);
+            final Map<String, Object> properties = ResourceHelper.cloneValueMap(vm);
+
+            @SuppressWarnings("unchecked")
+            final List<Exception> readErrorList = (List<Exception>) properties.remove(ResourceHelper.PROPERTY_MARKER_READ_ERROR_LIST);
+            if ( readErrorList != null ) {
+                for(final Exception e : readErrorList) {
+                    logger.warn("Unable to read scheduled job from " + eventResource.getPath(), e);
+                }
+            } else {
+                return properties;
+            }
+        } catch (final InstantiationException ie) {
+            // something happened with the resource in the meantime
+            this.ignoreException(ie);
+        }
+        return null;
+    }
+
+    /**
+     * Write a scheduled job to the resource tree.
+     * @throws PersistenceException
+     */
+    public ScheduledJobInfoImpl addOrUpdateJob(
+            final String jobTopic,
+            final Map<String, Object> jobProperties,
+            final String scheduleName,
+            final boolean suspend,
+            final List<ScheduleInfoImpl> scheduleInfos)
+    throws PersistenceException {
+        final Map<String, Object> properties = this.writeScheduledJob(jobTopic, jobProperties, scheduleName, suspend, scheduleInfos);
+
+        final String key = ResourceHelper.filterName(scheduleName);
+        synchronized ( this.scheduledJobs ) {
+            final Holder h = this.scheduledJobs.remove(key);
+            if ( h != null && h.info != null ) {
+                this.jobScheduler.unscheduleJob(h.info);
+            }
+            final Holder holder = new Holder();
+            holder.created = (Calendar) properties.get(Job.PROPERTY_JOB_CREATED);
+            holder.read = System.currentTimeMillis();
+            holder.info = this.addOrUpdateScheduledJob(properties, h == null ? null : h.info);
+
+            this.jobScheduler.scheduleJob(holder.info);
+            return holder.info;
+        }
+    }
+
+    private Map<String, Object> writeScheduledJob(final String jobTopic,
+            final Map<String, Object> jobProperties,
+            final String scheduleName,
+            final boolean suspend,
+            final List<ScheduleInfoImpl> scheduleInfos)
+    throws PersistenceException {
+        final ResourceResolver resolver = this.configuration.createResourceResolver();
+        try {
+            // create properties
+            final Map<String, Object> properties = new HashMap<String, Object>();
+
+            if ( jobProperties != null ) {
+                for(final Map.Entry<String, Object> entry : jobProperties.entrySet() ) {
+                    final String propName = entry.getKey();
+                    if ( !ResourceHelper.ignoreProperty(propName) ) {
+                        properties.put(propName, entry.getValue());
+                    }
+                }
+            }
+
+            properties.put(ResourceHelper.PROPERTY_JOB_TOPIC, jobTopic);
+            properties.put(Job.PROPERTY_JOB_CREATED, Calendar.getInstance());
+            properties.put(Job.PROPERTY_JOB_CREATED_INSTANCE, Environment.APPLICATION_ID);
+
+            // put scheduler name and scheduler info
+            properties.put(ResourceHelper.PROPERTY_SCHEDULE_NAME, scheduleName);
+            final String[] infoArray = new String[scheduleInfos.size()];
+            int index = 0;
+            for(final ScheduleInfoImpl info : scheduleInfos) {
+                infoArray[index] = info.getSerializedString();
+                index++;
+            }
+            properties.put(ResourceHelper.PROPERTY_SCHEDULE_INFO, infoArray);
+            if ( suspend ) {
+                properties.put(ResourceHelper.PROPERTY_SCHEDULE_SUSPENDED, Boolean.TRUE);
+            }
+
+            // create path and resource
+            properties.put(ResourceResolver.PROPERTY_RESOURCE_TYPE, ResourceHelper.RESOURCE_TYPE_SCHEDULED_JOB);
+
+            final String path = this.configuration.getScheduledJobsPath(true) + ResourceHelper.filterName(scheduleName);
+
+            // update existing resource
+            final Resource existingInfo = resolver.getResource(path);
+            if ( existingInfo != null ) {
+                resolver.delete(existingInfo);
+                logger.debug("Updating scheduled job {} at {}", properties, path);
+            } else {
+                logger.debug("Storing new scheduled job {} at {}", properties, path);
+            }
+            ResourceHelper.getOrCreateResource(resolver,
+                    path,
+                    properties);
+            // put back real schedule infos
+            properties.put(ResourceHelper.PROPERTY_SCHEDULE_INFO, scheduleInfos);
+
+            return properties;
+        } finally {
+            resolver.close();
+        }
+    }
+
+    private ScheduledJobInfoImpl addOrUpdateScheduledJob(
+            final Map<String, Object> properties,
+            final ScheduledJobInfoImpl oldInfo) {
+        properties.remove(ResourceResolver.PROPERTY_RESOURCE_TYPE);
+        properties.remove(Job.PROPERTY_JOB_CREATED);
+        properties.remove(Job.PROPERTY_JOB_CREATED_INSTANCE);
+
+        final String jobTopic = (String) properties.remove(ResourceHelper.PROPERTY_JOB_TOPIC);
+        final String schedulerName = (String) properties.remove(ResourceHelper.PROPERTY_SCHEDULE_NAME);
+
+        final ScheduledJobInfoImpl info;
+        if ( oldInfo == null ) {
+            info = new ScheduledJobInfoImpl(jobScheduler, schedulerName);
+        } else {
+            info = oldInfo;
+        }
+        info.update(jobTopic, properties);
+
+        return info;
+    }
+
+    /**
+     * A bundle event occurred which means we can try loading jobs that previously
+     * failed because of missing classes.
+     */
+    public void bundleEvent() {
+        this.lastBundleActivity.set(System.currentTimeMillis());
+        this.addTask(new Runnable() {
+            @Override
+            public void run() {
+                final Map<String, Holder> updateJobs = new HashMap<String, ScheduledJobHandler.Holder>();
+                synchronized ( scheduledJobs ) {
+                    for(final Map.Entry<String, Holder> entry : scheduledJobs.entrySet()) {
+                        if ( entry.getValue().info == null && entry.getValue().read < lastBundleActivity.get() ) {
+                            updateJobs.put(entry.getKey(), entry.getValue());
+                        }
+                    }
+                }
+                if ( !updateJobs.isEmpty() && isRunning.get() ) {
+                    ResourceResolver resolver = configuration.createResourceResolver();
+                    if ( resolver != null ) {
+                        try {
+                            for(final Map.Entry<String, Holder> entry : updateJobs.entrySet()) {
+                                final String path = configuration.getScheduledJobsPath(true) + entry.getKey();
+                                final Resource rsrc = resolver.getResource(path);
+                                if ( !isRunning.get() ) {
+                                    break;
+                                }
+                                if ( rsrc != null ) {
+                                    synchronized ( scheduledJobs ) {
+                                        handleAddOrUpdate(scheduledJobs, rsrc);
+                                    }
+                                }
+                            }
+                        } finally {
+                            resolver.close();
+                        }
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Handle observation event for removing a scheduled job
+     * @param path The path to the job
+     */
+    public void handleRemove(final String path) {
+        this.addTask(new Runnable() {
+            @Override
+            public void run() {
+                if ( isRunning.get() ) {
+                    final String scheduleKey = ResourceHelper.filterName(ResourceUtil.getName(path));
+                    if ( scheduleKey != null ) {
+                        synchronized ( scheduledJobs ) {
+                            final Holder h = scheduledJobs.remove(scheduleKey);
+                            if ( h != null && h.info != null ) {
+                                jobScheduler.unscheduleJob(h.info);
+                            }
+                        }
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Handle observation event for adding or updating a scheduled job
+     * @param path The path to the job
+     */
+    public void handleAddUpdate(final String path) {
+        this.addTask(new Runnable() {
+            @Override
+            public void run() {
+                if ( isRunning.get() ) {
+                    final ResourceResolver resolver = configuration.createResourceResolver();
+                    if ( resolver != null ) {
+                        try {
+                            final Resource rsrc = resolver.getResource(path);
+                            if ( rsrc != null ) {
+                                synchronized ( scheduledJobs ) {
+                                    handleAddOrUpdate(scheduledJobs, rsrc);
+                                }
+                            }
+                        } finally {
+                            resolver.close();
+                        }
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Handle add or update of a resource
+     * @param newScheduledJobs The map to store the jobs
+     * @param rsrc The resource containing the job
+     */
+    private void handleAddOrUpdate(final Map<String, Holder> newScheduledJobs, final Resource rsrc) {
+        final String id = ResourceHelper.filterName(rsrc.getName());
+        final Holder scheduled = this.scheduledJobs.remove(id);
+        boolean read = false;
+        if ( scheduled != null ) {
+            // check if loading failed and we can retry
+            if ( scheduled.info == null || scheduled.read < this.lastBundleActivity.get() ) {
+                read = true;
+            }
+            // check if this is an update
+            if ( scheduled.info != null ) {
+                final ValueMap vm = ResourceUtil.getValueMap(rsrc);
+                final Calendar changed = (Calendar) vm.get(Job.PROPERTY_JOB_CREATED);
+                if ( changed != null && scheduled.created.compareTo(changed) < 0 ) {
+                    read = true;
+                }
+            }
+            if ( !read ) {
+                // nothing changes
+                newScheduledJobs.put(id, scheduled);
+            }
+        } else {
+            read = true;
+        }
+        if ( read ) {
+            // read
+            final Holder holder = new Holder();
+            holder.read = System.currentTimeMillis();
+
+            final Map<String, Object> properties = this.readScheduledJob(rsrc);
+            if ( properties != null ) {
+                holder.created = (Calendar) properties.get(Job.PROPERTY_JOB_CREATED);
+                holder.info = this.addOrUpdateScheduledJob(properties, scheduled != null ? scheduled.info : null);
+            }
+            newScheduledJobs.put(id, holder);
+
+            if ( holder.info == null && scheduled != null && scheduled.info != null ) {
+                this.jobScheduler.unscheduleJob(scheduled.info);
+            }
+            if ( holder.info != null ) {
+                this.jobScheduler.scheduleJob(holder.info);
+            }
+        }
+    }
+
+    /**
+     * Remove a scheduled job
+     * @param info The schedule info
+     */
+    public void remove(final ScheduledJobInfoImpl info) {
+        final String scheduleKey = ResourceHelper.filterName(info.getName());
+
+        final ResourceResolver resolver = configuration.createResourceResolver();
+        try {
+            final StringBuilder sb = new StringBuilder(configuration.getScheduledJobsPath(true));
+            sb.append(scheduleKey);
+            final String path = sb.toString();
+
+            final Resource eventResource = resolver.getResource(path);
+            if ( eventResource != null ) {
+                resolver.delete(eventResource);
+                resolver.commit();
+            }
+        } catch (final PersistenceException pe) {
+            // we ignore the exception if removing fails
+            ignoreException(pe);
+        } finally {
+            resolver.close();
+        }
+
+        synchronized ( this.scheduledJobs ) {
+            final Holder h = scheduledJobs.remove(scheduleKey);
+            if ( h != null && h.info != null ) {
+                jobScheduler.unscheduleJob(h.info);
+            }
+        }
+    }
+
+    public void updateSchedule(final String scheduleName, final Collection<ScheduleInfo> scheduleInfo) {
+
+        final ResourceResolver resolver = configuration.createResourceResolver();
+        try {
+            final String scheduleKey = ResourceHelper.filterName(scheduleName);
+
+            final StringBuilder sb = new StringBuilder(configuration.getScheduledJobsPath(true));
+            sb.append(scheduleKey);
+            final String path = sb.toString();
+
+            final Resource rsrc = resolver.getResource(path);
+            // This is an update, if we can't find the resource we ignore it
+            if ( rsrc != null ) {
+                final Calendar now = Calendar.getInstance();
+
+                // update holder first
+                synchronized ( scheduledJobs ) {
+                    final Holder h = scheduledJobs.get(scheduleKey);
+                    if ( h != null ) {
+                        h.created = now;
+                    }
+                }
+
+                final ModifiableValueMap mvm = rsrc.adaptTo(ModifiableValueMap.class);
+                mvm.put(Job.PROPERTY_JOB_CREATED, now);
+                final String[] infoArray = new String[scheduleInfo.size()];
+                int index = 0;
+                for(final ScheduleInfo si : scheduleInfo) {
+                    infoArray[index] = ((ScheduleInfoImpl)si).getSerializedString();
+                    index++;
+                }
+                mvm.put(ResourceHelper.PROPERTY_SCHEDULE_INFO, infoArray);
+
+                try {
+                    resolver.commit();
+                } catch ( final PersistenceException pe) {
+                    logger.warn("Unable to update scheduled job " + scheduleName, pe);
+                }
+            }
+        } finally {
+            resolver.close();
+        }
+    }
+
+    /**
+     * Helper method which just logs the exception in debug mode.
+     * @param e The exception
+     */
+    private void ignoreException(final Exception e) {
+        if ( this.logger.isDebugEnabled() ) {
+            this.logger.debug("Ignored exception " + e.getMessage(), e);
+        }
+    }
+
+    public void maintenance() {
+        this.addFullScan();
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/scheduling/ScheduledJobInfoImpl.java b/src/main/java/org/apache/sling/event/impl/jobs/scheduling/ScheduledJobInfoImpl.java
new file mode 100644
index 0000000..99fcdb4
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/scheduling/ScheduledJobInfoImpl.java
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.scheduling;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.sling.event.impl.support.ResourceHelper;
+import org.apache.sling.event.impl.support.ScheduleInfoImpl;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.JobBuilder.ScheduleBuilder;
+import org.apache.sling.event.jobs.ScheduleInfo;
+import org.apache.sling.event.jobs.ScheduledJobInfo;
+
+/**
+ * The job schedule information.
+ * It holds all required information like
+ * - the name of the schedule
+ * - the job topic
+ * - the job properties
+ * - scheduling information
+ */
+public class ScheduledJobInfoImpl implements ScheduledJobInfo, Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private final String scheduleName;
+
+    private final JobSchedulerImpl jobScheduler;
+
+    private final AtomicBoolean isSuspended = new AtomicBoolean(false);
+
+    private volatile List<ScheduleInfo> scheduleInfos;
+
+    private volatile String jobTopic;
+
+    private volatile Map<String, Object> jobProperties;
+
+    /**
+     * Create a new info object
+     * @param jobScheduler The job scheduler
+     * @param scheduleName The unique name
+     */
+    public ScheduledJobInfoImpl(final JobSchedulerImpl jobScheduler,
+            final String scheduleName) {
+        this.jobScheduler = jobScheduler;
+        this.scheduleName = scheduleName;
+    }
+
+    /**
+     * Update/set the job related information
+     * @param jobTopic      The job topic
+     * @param jobProperties The job properties
+     */
+    public void update(final String jobTopic,
+            final Map<String, Object> jobProperties) {
+        final boolean isSuspended = jobProperties.remove(ResourceHelper.PROPERTY_SCHEDULE_SUSPENDED) != null;
+        @SuppressWarnings("unchecked")
+        final List<ScheduleInfo> scheduleInfos = (List<ScheduleInfo>) jobProperties.remove(ResourceHelper.PROPERTY_SCHEDULE_INFO);
+
+        this.jobTopic = jobTopic;
+        this.jobProperties = jobProperties;
+        this.scheduleInfos = Collections.unmodifiableList(scheduleInfos);
+
+        this.isSuspended.set(isSuspended);
+    }
+
+    /**
+     * Update the scheduling information
+     * @param scheduleInfos The new schedule
+     */
+    public void update(final List<ScheduleInfo> scheduleInfos) {
+        this.scheduleInfos =  Collections.unmodifiableList(scheduleInfos);
+    }
+
+    /**
+     * Get the schedule name
+     */
+    public String getName() {
+        return this.scheduleName;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.ScheduledJobInfo#getSchedules()
+     */
+    @Override
+    public Collection<ScheduleInfo> getSchedules() {
+        return this.scheduleInfos;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.ScheduledJobInfo#getNextScheduledExecution()
+     */
+    @Override
+    public Date getNextScheduledExecution() {
+        Date result = null;
+        for(final ScheduleInfo info : this.scheduleInfos) {
+            final Date newResult = ((ScheduleInfoImpl)info).getNextScheduledExecution();
+            if ( result == null || result.getTime() > newResult.getTime() ) {
+                result = newResult;
+            }
+        }
+        return result;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.ScheduledJobInfo#getJobTopic()
+     */
+    @Override
+    public String getJobTopic() {
+        return this.jobTopic;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.ScheduledJobInfo#getJobProperties()
+     */
+    @Override
+    public Map<String, Object> getJobProperties() {
+        return this.jobProperties;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.ScheduledJobInfo#unschedule()
+     */
+    @Override
+    public void unschedule() {
+        this.jobScheduler.removeJob(this);
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.ScheduledJobInfo#reschedule()
+     */
+    @Override
+    public ScheduleBuilder reschedule() {
+        return this.jobScheduler.createJobBuilder(this);
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.ScheduledJobInfo#suspend()
+     */
+    @Override
+    public void suspend() {
+        if ( this.isSuspended.compareAndSet(false, true) ) {
+            this.jobScheduler.setSuspended(this, true);
+        }
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.ScheduledJobInfo#resume()
+     */
+    @Override
+    public void resume() {
+        if ( this.isSuspended.compareAndSet(true, false) ) {
+            this.jobScheduler.setSuspended(this, false);
+        }
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.ScheduledJobInfo#isSuspended()
+     */
+    @Override
+    public boolean isSuspended() {
+        return this.isSuspended.get();
+    }
+
+    /**
+     * Get the scheduler job id
+     */
+    public String getSchedulerJobId() {
+        return Job.class.getName() + ":" + this.scheduleName;
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/stats/StatisticsImpl.java b/src/main/java/org/apache/sling/event/impl/jobs/stats/StatisticsImpl.java
new file mode 100644
index 0000000..00a278f
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/stats/StatisticsImpl.java
@@ -0,0 +1,319 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.stats;
+
+import org.apache.sling.event.jobs.Statistics;
+
+/**
+ * Implementation of the statistics.
+ */
+public class StatisticsImpl implements Statistics {
+
+    private volatile long startTime;
+
+    private volatile long activeJobs;
+
+    private volatile long queuedJobs;
+
+    private volatile long lastActivated = -1;
+
+    private volatile long lastFinished = -1;
+
+    private volatile long averageWaitingTime;
+
+    private volatile long averageProcessingTime;
+
+    private volatile long waitingTime;
+
+    private volatile long processingTime;
+
+    private volatile long waitingCount;
+
+    private volatile long processingCount;
+
+    private volatile long finishedJobs;
+
+    private volatile long failedJobs;
+
+    private volatile long cancelledJobs;
+
+    public StatisticsImpl() {
+        this(System.currentTimeMillis());
+    }
+
+    public StatisticsImpl(final long startTime) {
+        this.startTime = startTime;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Statistics#getStartTime()
+     */
+    @Override
+    public synchronized long getStartTime() {
+        return startTime;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Statistics#getNumberOfProcessedJobs()
+     */
+    @Override
+    public synchronized long getNumberOfProcessedJobs() {
+        return getNumberOfCancelledJobs() + getNumberOfFailedJobs() + getNumberOfFinishedJobs();
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Statistics#getNumberOfActiveJobs()
+     */
+    @Override
+    public synchronized long getNumberOfActiveJobs() {
+        return activeJobs;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Statistics#getNumberOfQueuedJobs()
+     */
+    @Override
+    public synchronized long getNumberOfQueuedJobs() {
+        return queuedJobs;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Statistics#getNumberOfJobs()
+     */
+    @Override
+    public synchronized long getNumberOfJobs() {
+        return activeJobs + queuedJobs;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Statistics#getAverageWaitingTime()
+     */
+    @Override
+    public synchronized long getAverageWaitingTime() {
+        return averageWaitingTime;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Statistics#getAverageProcessingTime()
+     */
+    @Override
+    public synchronized long getAverageProcessingTime() {
+        return averageProcessingTime;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Statistics#getNumberOfFinishedJobs()
+     */
+    @Override
+    public synchronized long getNumberOfFinishedJobs() {
+        return finishedJobs;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Statistics#getNumberOfCancelledJobs()
+     */
+    @Override
+    public synchronized long getNumberOfCancelledJobs() {
+        return cancelledJobs;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Statistics#getNumberOfFailedJobs()
+     */
+    @Override
+    public synchronized long getNumberOfFailedJobs() {
+        return failedJobs;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Statistics#getLastActivatedJobTime()
+     */
+    @Override
+    public synchronized long getLastActivatedJobTime() {
+        return this.lastActivated;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Statistics#getLastFinishedJobTime()
+     */
+    @Override
+    public synchronized long getLastFinishedJobTime() {
+        return this.lastFinished;
+    }
+
+    /**
+     * Add a finished job
+     * @param jobTime The processing time for this job.
+     */
+    public synchronized void finishedJob(final long jobTime) {
+        this.lastFinished = System.currentTimeMillis();
+        this.processingTime += jobTime;
+        this.processingCount++;
+        this.averageProcessingTime = this.processingTime / this.processingCount;
+        this.finishedJobs++;
+        this.activeJobs--;
+    }
+
+    /**
+     * Add a failed job.
+     */
+    public synchronized void failedJob() {
+        this.failedJobs++;
+        this.activeJobs--;
+    }
+
+    /**
+     * Add a cancelled job.
+     */
+    public synchronized void cancelledJob() {
+        this.cancelledJobs++;
+        this.activeJobs--;
+    }
+
+    /**
+     * New job in the queue
+     */
+    public synchronized void incQueued() {
+        this.queuedJobs++;
+    }
+
+    /**
+     * Job not processed by us
+     */
+    public synchronized void decQueued() {
+        this.queuedJobs--;
+    }
+
+    /**
+     * Clear all queued
+     */
+    public synchronized void clearQueued() {
+        this.queuedJobs = 0;
+    }
+
+    /**
+     * Add a job from the queue to status active
+     * @param queueTime The time the job stayed in the queue.
+     */
+    public synchronized void addActive(final long queueTime) {
+        this.queuedJobs--;
+        this.activeJobs++;
+        this.waitingCount++;
+        this.waitingTime += queueTime;
+        this.averageWaitingTime = this.waitingTime / this.waitingCount;
+        this.lastActivated = System.currentTimeMillis();
+    }
+
+    /**
+     * Add another statistics information.
+     */
+    public synchronized void add(final StatisticsImpl other) {
+        synchronized ( other ) {
+            if ( other.lastActivated > this.lastActivated ) {
+                this.lastActivated = other.lastActivated;
+            }
+            if ( other.lastFinished > this.lastFinished ) {
+                this.lastFinished = other.lastFinished;
+            }
+            this.queuedJobs += other.queuedJobs;
+            this.waitingTime += other.waitingTime;
+            this.waitingCount += other.waitingCount;
+            if ( this.waitingCount > 0 ) {
+                this.averageWaitingTime = this.waitingTime / this.waitingCount;
+            }
+            this.processingTime += other.processingTime;
+            this.processingCount += other.processingCount;
+            if ( this.processingCount > 0 ) {
+                this.averageProcessingTime = this.processingTime / this.processingCount;
+            }
+            this.finishedJobs += other.finishedJobs;
+            this.failedJobs += other.failedJobs;
+            this.cancelledJobs += other.cancelledJobs;
+            this.activeJobs += other.activeJobs;
+        }
+    }
+
+    /**
+     * Create a new statistics object with exactly the same values.
+     */
+    public void copyFrom(final StatisticsImpl other) {
+        final long localQueuedJobs;
+        final long localLastActivated;
+        final long localLastFinished;
+        final long localAverageWaitingTime;
+        final long localAverageProcessingTime;
+        final long localWaitingTime;
+        final long localProcessingTime;
+        final long localWaitingCount;
+        final long localProcessingCount;
+        final long localFinishedJobs;
+        final long localFailedJobs;
+        final long localCancelledJobs;
+        final long localActiveJobs;
+        synchronized ( other ) {
+            localQueuedJobs = other.queuedJobs;
+            localLastActivated = other.lastActivated;
+            localLastFinished = other.lastFinished;
+            localAverageWaitingTime = other.averageWaitingTime;
+            localAverageProcessingTime = other.averageProcessingTime;
+            localWaitingTime = other.waitingTime;
+            localProcessingTime = other.processingTime;
+            localWaitingCount = other.waitingCount;
+            localProcessingCount = other.processingCount;
+            localFinishedJobs = other.finishedJobs;
+            localFailedJobs = other.failedJobs;
+            localCancelledJobs = other.cancelledJobs;
+            localActiveJobs = other.activeJobs;
+        }
+        synchronized ( this ) {
+            this.queuedJobs = localQueuedJobs;
+            this.lastActivated = localLastActivated;
+            this.lastFinished = localLastFinished;
+            this.averageWaitingTime = localAverageWaitingTime;
+            this.averageProcessingTime = localAverageProcessingTime;
+            this.waitingTime = localWaitingTime;
+            this.processingTime = localProcessingTime;
+            this.waitingCount = localWaitingCount;
+            this.processingCount = localProcessingCount;
+            this.finishedJobs = localFinishedJobs;
+            this.failedJobs = localFailedJobs;
+            this.cancelledJobs = localCancelledJobs;
+            this.activeJobs = localActiveJobs;
+        }
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.Statistics#reset()
+     */
+    @Override
+    public synchronized void reset() {
+        this.startTime = System.currentTimeMillis();
+        this.lastActivated = -1;
+        this.lastFinished = -1;
+        this.averageWaitingTime = 0;
+        this.averageProcessingTime = 0;
+        this.waitingTime = 0;
+        this.processingTime = 0;
+        this.waitingCount = 0;
+        this.processingCount = 0;
+        this.finishedJobs = 0;
+        this.failedJobs = 0;
+        this.cancelledJobs = 0;
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/stats/StatisticsManager.java b/src/main/java/org/apache/sling/event/impl/jobs/stats/StatisticsManager.java
new file mode 100644
index 0000000..c2d3d15
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/stats/StatisticsManager.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.stats;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.event.impl.jobs.InternalJobState;
+import org.apache.sling.event.impl.jobs.config.JobManagerConfiguration;
+import org.apache.sling.event.jobs.Statistics;
+import org.apache.sling.event.jobs.TopicStatistics;
+
+/**
+ * The statistics manager keeps track of all statistics related tasks.
+ */
+@Component
+@Service(value=StatisticsManager.class)
+public class StatisticsManager {
+
+    /** The job manager configuration. */
+    @Reference
+    private JobManagerConfiguration configuration;
+
+    /** Global statistics. */
+    private final StatisticsImpl globalStatistics = new StatisticsImpl() {
+
+        @Override
+        public synchronized void reset() {
+            super.reset();
+            topicStatistics.clear();
+            for(final Statistics s : queueStatistics.values()) {
+                s.reset();
+            }
+        }
+
+    };
+
+    /** Statistics per topic. */
+    private final ConcurrentMap<String, TopicStatistics> topicStatistics = new ConcurrentHashMap<String, TopicStatistics>();
+
+    /** Statistics per queue. */
+    private final ConcurrentMap<String, Statistics> queueStatistics = new ConcurrentHashMap<String, Statistics>();
+
+    /**
+     * Get the global statistics.
+     * @return The global statistics.
+     */
+    public Statistics getGlobalStatistics() {
+        return this.globalStatistics;
+    }
+
+    /**
+     * Get all topic statistics.
+     * @return The map of topic statistics by topic.
+     */
+    public Map<String, TopicStatistics> getTopicStatistics() {
+        return topicStatistics;
+    }
+
+    /**
+     * Get a single queue statistics.
+     * @param queueName The queue name.
+     * @return The statistics for that queue.
+     */
+    public Statistics getQueueStatistics(final String queueName) {
+        Statistics queueStats = queueStatistics.get(queueName);
+        if ( queueStats == null ) {
+            queueStats = new StatisticsImpl();
+        }
+        return queueStats;
+    }
+
+    /**
+     * Internal method to get the statistics of a queue.
+     * @param queueName The queue name.
+     * @return The statistics or {@code null} if queue name is {@code null}.
+     */
+    private StatisticsImpl getStatisticsForQueue(final String queueName) {
+        if ( queueName == null ) {
+            return null;
+        }
+        StatisticsImpl queueStats = (StatisticsImpl)queueStatistics.get(queueName);
+        if ( queueStats == null ) {
+            queueStatistics.putIfAbsent(queueName, new StatisticsImpl());
+            queueStats = (StatisticsImpl)queueStatistics.get(queueName);
+        }
+        return queueStats;
+    }
+
+    public void jobEnded(final String queueName,
+            final String topic,
+            final InternalJobState state,
+            final long processingTime) {
+        final StatisticsImpl queueStats = getStatisticsForQueue(queueName);
+
+        TopicStatisticsImpl ts = (TopicStatisticsImpl)this.topicStatistics.get(topic);
+        if ( ts == null ) {
+            this.topicStatistics.putIfAbsent(topic, new TopicStatisticsImpl(topic));
+            ts = (TopicStatisticsImpl)this.topicStatistics.get(topic);
+        }
+
+        if ( state == InternalJobState.CANCELLED ) {
+            ts.addCancelled();
+            this.globalStatistics.cancelledJob();
+            if ( queueStats != null ) {
+                queueStats.cancelledJob();
+            }
+
+        } else if ( state == InternalJobState.FAILED ) {
+            ts.addFailed();
+            this.globalStatistics.failedJob();
+            if ( queueStats != null ) {
+                queueStats.failedJob();
+            }
+
+        } else if ( state == InternalJobState.SUCCEEDED ) {
+            ts.addFinished(processingTime);
+            this.globalStatistics.finishedJob(processingTime);
+            if ( queueStats != null ) {
+                queueStats.finishedJob(processingTime);
+            }
+
+        }
+    }
+
+    public void jobStarted(final String queueName,
+            final String topic,
+            final long queueTime) {
+        final StatisticsImpl queueStats = getStatisticsForQueue(queueName);
+
+        TopicStatisticsImpl ts = (TopicStatisticsImpl)this.topicStatistics.get(topic);
+        if ( ts == null ) {
+            this.topicStatistics.putIfAbsent(topic, new TopicStatisticsImpl(topic));
+            ts = (TopicStatisticsImpl)this.topicStatistics.get(topic);
+        }
+
+        ts.addActivated(queueTime);
+        this.globalStatistics.addActive(queueTime);
+        if ( queueStats != null ) {
+            queueStats.addActive(queueTime);
+        }
+    }
+
+    public void jobQueued(final String queueName,
+            final String topic) {
+        final StatisticsImpl queueStats = getStatisticsForQueue(queueName);
+
+        this.globalStatistics.incQueued();
+        if ( queueStats != null ) {
+            queueStats.incQueued();
+        }
+    }
+
+    public void jobDequeued(final String queueName, final String topic) {
+        final StatisticsImpl queueStats = getStatisticsForQueue(queueName);
+
+        this.globalStatistics.decQueued();
+        if ( queueStats != null ) {
+            queueStats.decQueued();
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/stats/TopicStatisticsImpl.java b/src/main/java/org/apache/sling/event/impl/jobs/stats/TopicStatisticsImpl.java
new file mode 100644
index 0000000..3b0a32e
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/stats/TopicStatisticsImpl.java
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.stats;
+
+import org.apache.sling.event.jobs.TopicStatistics;
+
+/**
+ * Implementation of the statistics.
+ */
+public class TopicStatisticsImpl implements TopicStatistics {
+
+    private final String topic;
+
+    private volatile long lastActivated = -1;
+
+    private volatile long lastFinished = -1;
+
+    private volatile long averageWaitingTime;
+
+    private volatile long averageProcessingTime;
+
+    private volatile long waitingTime;
+
+    private volatile long processingTime;
+
+    private volatile long waitingCount;
+
+    private volatile long processingCount;
+
+    private volatile long finishedJobs;
+
+    private volatile long failedJobs;
+
+    private volatile long cancelledJobs;
+
+    /** Constructor. */
+    public TopicStatisticsImpl(final String topic) {
+        this.topic = topic;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.TopicStatistics#getTopic()
+     */
+    @Override
+    public String getTopic() {
+        return this.topic;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.TopicStatistics#getNumberOfProcessedJobs()
+     */
+    @Override
+    public synchronized long getNumberOfProcessedJobs() {
+        return getNumberOfCancelledJobs() + getNumberOfFailedJobs() + getNumberOfFinishedJobs();
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.TopicStatistics#getAverageWaitingTime()
+     */
+    @Override
+    public synchronized long getAverageWaitingTime() {
+        return averageWaitingTime;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.TopicStatistics#getAverageProcessingTime()
+     */
+    @Override
+    public synchronized long getAverageProcessingTime() {
+        return averageProcessingTime;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.TopicStatistics#getNumberOfFinishedJobs()
+     */
+    @Override
+    public synchronized long getNumberOfFinishedJobs() {
+        return finishedJobs;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.TopicStatistics#getNumberOfCancelledJobs()
+     */
+    @Override
+    public synchronized long getNumberOfCancelledJobs() {
+        return cancelledJobs;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.TopicStatistics#getNumberOfFailedJobs()
+     */
+    @Override
+    public synchronized long getNumberOfFailedJobs() {
+        return failedJobs;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.TopicStatistics#getLastActivatedJobTime()
+     */
+    @Override
+    public synchronized long getLastActivatedJobTime() {
+        return this.lastActivated;
+    }
+
+    /**
+     * @see org.apache.sling.event.jobs.TopicStatistics#getLastFinishedJobTime()
+     */
+    @Override
+    public synchronized long getLastFinishedJobTime() {
+        return this.lastFinished;
+    }
+
+    /**
+     * Add a finished job.
+     * @param jobTime The time of the job processing.
+     */
+    public synchronized void addFinished(final long jobTime) {
+        this.finishedJobs++;
+        this.lastFinished = System.currentTimeMillis();
+        if ( jobTime > 0 ) {
+            this.processingTime += jobTime;
+            this.processingCount++;
+            this.averageProcessingTime = this.processingTime / this.processingCount;
+        }
+    }
+
+    /**
+     * Add a started job.
+     * @param queueTime The time of the job in the queue.
+     */
+    public synchronized void addActivated(final long queueTime) {
+        this.lastActivated = System.currentTimeMillis();
+        if ( queueTime > 0 ) {
+            this.waitingTime += queueTime;
+            this.waitingCount++;
+            this.averageWaitingTime = this.waitingTime / this.waitingCount;
+        }
+    }
+
+    /**
+     * Add a failed job.
+     */
+    public synchronized void addFailed() {
+        this.failedJobs++;
+    }
+
+    /**
+     * Add a cancelled job.
+     */
+    public synchronized void addCancelled() {
+        this.cancelledJobs++;
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/tasks/CheckTopologyTask.java b/src/main/java/org/apache/sling/event/impl/jobs/tasks/CheckTopologyTask.java
new file mode 100644
index 0000000..879ee2a
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/tasks/CheckTopologyTask.java
@@ -0,0 +1,333 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.tasks;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.event.impl.jobs.JobTopicTraverser;
+import org.apache.sling.event.impl.jobs.config.JobManagerConfiguration;
+import org.apache.sling.event.impl.jobs.config.QueueConfigurationManager;
+import org.apache.sling.event.impl.jobs.config.QueueConfigurationManager.QueueInfo;
+import org.apache.sling.event.impl.jobs.config.TopologyCapabilities;
+import org.apache.sling.event.impl.support.ResourceHelper;
+import org.apache.sling.event.jobs.Job;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The check topology task checks for changes in the topology and queue configuration
+ * and reassigns jobs.
+ * If the leader instance finds a dead instance it reassigns its jobs to live instances.
+ * The leader instance also checks for unassigned jobs and tries to assign them.
+ * If an instance detects jobs which it doesn't process anymore it reassigns them as
+ * well.
+ */
+public class CheckTopologyTask {
+
+    /** Logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** Job manager configuration. */
+    private final JobManagerConfiguration configuration;
+
+    /** The capabilities. */
+    private final TopologyCapabilities caps;
+
+    /**
+     * Constructor
+     * @param config The configuration
+     */
+    public CheckTopologyTask(final JobManagerConfiguration config) {
+        this.configuration = config;
+        this.caps = this.configuration.getTopologyCapabilities();
+    }
+
+    /**
+     * Reassign jobs from stopped instance.
+     */
+    private void reassignJobsFromStoppedInstances() {
+        if ( caps.isLeader() && caps.isActive() ) {
+            this.logger.debug("Checking for stopped instances...");
+            final ResourceResolver resolver = this.configuration.createResourceResolver();
+            if ( resolver != null ) {
+                try {
+                    final Resource jobsRoot = resolver.getResource(this.configuration.getAssginedJobsPath());
+                    this.logger.debug("Got jobs root {}", jobsRoot);
+
+                    // this resource should exist, but we check anyway
+                    if ( jobsRoot != null ) {
+                        final Iterator<Resource> instanceIter = jobsRoot.listChildren();
+                        while ( caps.isActive() && instanceIter.hasNext() ) {
+                            final Resource instanceResource = instanceIter.next();
+
+                            final String instanceId = instanceResource.getName();
+                            if ( !caps.isActive(instanceId) ) {
+                                logger.debug("Found stopped instance {}", instanceId);
+                                assignJobs(instanceResource, true);
+                            }
+                        }
+                    }
+                } finally {
+                    resolver.close();
+                }
+            }
+        }
+    }
+
+    /**
+     * Reassign stale jobs from this instance
+     */
+    private void reassignStaleJobs() {
+        if ( caps.isActive() ) {
+            this.logger.debug("Checking for stale jobs...");
+            final ResourceResolver resolver = this.configuration.createResourceResolver();
+            if ( resolver != null ) {
+                try {
+                    final Resource jobsRoot = resolver.getResource(this.configuration.getLocalJobsPath());
+
+                    // this resource should exist, but we check anyway
+                    if ( jobsRoot != null ) {
+                        final Iterator<Resource> topicIter = jobsRoot.listChildren();
+                        while ( caps.isActive() && topicIter.hasNext() ) {
+                            final Resource topicResource = topicIter.next();
+
+                            final String topicName = topicResource.getName().replace('.', '/');
+                            this.logger.debug("Checking topic {}..." , topicName);
+                            final List<InstanceDescription> potentialTargets = caps.getPotentialTargets(topicName);
+                            boolean reassign = true;
+                            for(final InstanceDescription desc : potentialTargets) {
+                                if ( desc.isLocal() ) {
+                                    reassign = false;
+                                    break;
+                                }
+                            }
+                            if ( reassign ) {
+                                final QueueConfigurationManager qcm = this.configuration.getQueueConfigurationManager();
+                                if ( qcm == null ) {
+                                    break;
+                                }
+                                final QueueInfo info = qcm.getQueueInfo(topicName);
+				logger.info ("Start reassigning stale jobs");
+                                JobTopicTraverser.traverse(this.logger, topicResource, new JobTopicTraverser.ResourceCallback() {
+
+                                    @Override
+                                    public boolean handle(final Resource rsrc) {
+                                        try {
+                                            final ValueMap vm = ResourceHelper.getValueMap(rsrc);
+                                            final String targetId = caps.detectTarget(topicName, vm, info);
+
+                                            final Map<String, Object> props = new HashMap<String, Object>(vm);
+                                            props.remove(Job.PROPERTY_JOB_STARTED_TIME);
+
+                                            final String newPath;
+                                            if ( targetId != null ) {
+                                                newPath = configuration.getAssginedJobsPath() + '/' + targetId + '/' + topicResource.getName() + rsrc.getPath().substring(topicResource.getPath().length());
+                                                props.put(Job.PROPERTY_JOB_QUEUE_NAME, info.queueName);
+                                                props.put(Job.PROPERTY_JOB_TARGET_INSTANCE, targetId);
+                                            } else {
+                                                newPath = configuration.getUnassignedJobsPath() + '/' + topicResource.getName() + rsrc.getPath().substring(topicResource.getPath().length());
+                                                props.remove(Job.PROPERTY_JOB_QUEUE_NAME);
+                                                props.remove(Job.PROPERTY_JOB_TARGET_INSTANCE);
+                                            }
+                                            try {
+                                                ResourceHelper.getOrCreateResource(resolver, newPath, props);
+                                                resolver.delete(rsrc);
+                                                resolver.commit();
+                                                final String jobId = vm.get(ResourceHelper.PROPERTY_JOB_ID, String.class);
+                                                if ( targetId != null ) {
+                                                    configuration.getAuditLogger().debug("REASSIGN OK {} : {}", targetId, jobId);
+                                                } else {
+                                                    configuration.getAuditLogger().debug("REUNASSIGN OK : {}", jobId);
+                                                }
+                                            } catch ( final PersistenceException pe ) {
+                                                logger.warn("Unable to move stale job from " + rsrc.getPath() + " to " + newPath, pe);
+                                                resolver.refresh();
+                                                resolver.revert();
+                                            }
+                                        } catch (final InstantiationException ie) {
+                                            // something happened with the resource in the meantime
+                                            logger.warn("Unable to move stale job from " + rsrc.getPath(), ie);
+                                            resolver.refresh();
+                                            resolver.revert();
+                                        }
+                                        return caps.isActive();
+                                    }
+                                });
+
+                            }
+                        }
+                    }
+                } finally {
+                    resolver.close();
+                }
+            }
+        }
+    }
+
+    /**
+     * Try to assign unassigned jobs as there might be changes in:
+     * - queue configurations
+     * - topology
+     * - capabilities
+     */
+    public void assignUnassignedJobs() {
+        if ( caps != null && caps.isLeader() && caps.isActive() ) {
+            logger.debug("Checking unassigned jobs...");
+            final ResourceResolver resolver = this.configuration.createResourceResolver();
+            if ( resolver != null ) {
+                try {
+                    final Resource unassignedRoot = resolver.getResource(this.configuration.getUnassignedJobsPath());
+                    logger.debug("Got unassigned root {}", unassignedRoot);
+
+                    // this resource should exist, but we check anyway
+                    if ( unassignedRoot != null ) {
+                        assignJobs(unassignedRoot, false);
+                    }
+                } finally {
+                    resolver.close();
+                }
+            }
+        }
+    }
+
+    /**
+     * Try to assign all jobs from the jobs root.
+     * The jobs are stored by topic
+     * @param jobsRoot The root of the jobs
+     * @param unassign Whether to unassign the job if no instance is found.
+     */
+    private void assignJobs(final Resource jobsRoot,
+            final boolean unassign) {
+        final ResourceResolver resolver = jobsRoot.getResourceResolver();
+
+        final Iterator<Resource> topicIter = jobsRoot.listChildren();
+        while ( caps.isActive() && topicIter.hasNext() ) {
+            final Resource topicResource = topicIter.next();
+
+            final String topicName = topicResource.getName().replace('.', '/');
+            logger.debug("Found topic {}", topicName);
+
+            // first check if there is an instance for these topics
+            final List<InstanceDescription> potentialTargets = caps.getPotentialTargets(topicName);
+            if ( potentialTargets != null && potentialTargets.size() > 0 ) {
+                final QueueConfigurationManager qcm = this.configuration.getQueueConfigurationManager();
+                if ( qcm == null ) {
+                    break;
+                }
+                final QueueInfo info = qcm.getQueueInfo(topicName);
+                logger.debug("Found queue {} for {}", info.queueConfiguration, topicName);
+
+                JobTopicTraverser.traverse(this.logger, topicResource, new JobTopicTraverser.ResourceCallback() {
+
+                    @Override
+                    public boolean handle(final Resource rsrc) {
+                        try {
+                            final ValueMap vm = ResourceHelper.getValueMap(rsrc);
+                            final String targetId = caps.detectTarget(topicName, vm, info);
+
+                            if ( targetId != null ) {
+                                final String newPath = configuration.getAssginedJobsPath() + '/' + targetId + '/' + topicResource.getName() + rsrc.getPath().substring(topicResource.getPath().length());
+                                final Map<String, Object> props = new HashMap<String, Object>(vm);
+                                props.put(Job.PROPERTY_JOB_QUEUE_NAME, info.queueName);
+                                props.put(Job.PROPERTY_JOB_TARGET_INSTANCE, targetId);
+                                props.remove(Job.PROPERTY_JOB_STARTED_TIME);
+                                try {
+                                    ResourceHelper.getOrCreateResource(resolver, newPath, props);
+                                    resolver.delete(rsrc);
+                                    resolver.commit();
+                                    final String jobId = vm.get(ResourceHelper.PROPERTY_JOB_ID, String.class);
+                                    configuration.getAuditLogger().debug("REASSIGN OK {} : {}", targetId, jobId);
+                                } catch ( final PersistenceException pe ) {
+                                    logger.warn("Unable to move unassigned job from " + rsrc.getPath() + " to " + newPath, pe);
+                                    resolver.refresh();
+                                    resolver.revert();
+                                }
+                            }
+                        } catch (final InstantiationException ie) {
+                            // something happened with the resource in the meantime
+                            logger.warn("Unable to move unassigned job from " + rsrc.getPath(), ie);
+                            resolver.refresh();
+                            resolver.revert();
+                        }
+                        return caps.isActive();
+                    }
+                });
+            }
+            // now unassign if there are still jobs
+            if ( caps.isActive() && unassign ) {
+                // we have to move everything to the unassigned area
+                JobTopicTraverser.traverse(this.logger, topicResource, new JobTopicTraverser.ResourceCallback() {
+
+                    @Override
+                    public boolean handle(final Resource rsrc) {
+                        try {
+                            final ValueMap vm = ResourceHelper.getValueMap(rsrc);
+                            final String newPath = configuration.getUnassignedJobsPath() + '/' + topicResource.getName() + rsrc.getPath().substring(topicResource.getPath().length());
+                            final Map<String, Object> props = new HashMap<String, Object>(vm);
+                            props.remove(Job.PROPERTY_JOB_QUEUE_NAME);
+                            props.remove(Job.PROPERTY_JOB_TARGET_INSTANCE);
+                            props.remove(Job.PROPERTY_JOB_STARTED_TIME);
+
+                            try {
+                                ResourceHelper.getOrCreateResource(resolver, newPath, props);
+                                resolver.delete(rsrc);
+                                resolver.commit();
+                                final String jobId = vm.get(ResourceHelper.PROPERTY_JOB_ID, String.class);
+                                configuration.getAuditLogger().debug("REUNASSIGN OK : {}", jobId);
+                            } catch ( final PersistenceException pe ) {
+                                logger.warn("Unable to unassigned job from " + rsrc.getPath() + " to " + newPath, pe);
+                                resolver.refresh();
+                                resolver.revert();
+                            }
+                        } catch (final InstantiationException ie) {
+                            // something happened with the resource in the meantime
+                            logger.warn("Unable to unassigned job from " + rsrc.getPath(), ie);
+                            resolver.refresh();
+                            resolver.revert();
+                        }
+                        return caps.isActive();
+                    }
+                });
+            }
+        }
+    }
+
+    /**
+     * One maintenance run
+     */
+    public void fullRun() {
+        if ( this.caps != null ) {
+            this.reassignJobsFromStoppedInstances();
+
+            // check for all topics
+            this.reassignStaleJobs();
+
+            // try to assign unassigned jobs
+            this.assignUnassignedJobs();
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/tasks/CleanUpTask.java b/src/main/java/org/apache/sling/event/impl/jobs/tasks/CleanUpTask.java
new file mode 100644
index 0000000..7fdcb88
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/tasks/CleanUpTask.java
@@ -0,0 +1,275 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.tasks;
+
+import java.util.Calendar;
+import java.util.Iterator;
+
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.event.impl.jobs.config.JobManagerConfiguration;
+import org.apache.sling.event.impl.jobs.config.TopologyCapabilities;
+import org.apache.sling.event.impl.jobs.scheduling.JobSchedulerImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Maintenance task...
+ *
+ * In the default configuration, this task runs every minute
+ */
+public class CleanUpTask {
+
+    /** Logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** Job manager configuration. */
+    private final JobManagerConfiguration configuration;
+
+    /** Job scheduler. */
+    private final JobSchedulerImpl jobScheduler;
+
+    /** We count the scheduler runs. */
+    private volatile long schedulerRuns;
+
+    /**
+     * Constructor
+     */
+    public CleanUpTask(final JobManagerConfiguration config, final JobSchedulerImpl jobScheduler) {
+        this.configuration = config;
+        this.jobScheduler = jobScheduler;
+    }
+
+    /**
+     * One maintenance run
+     */
+    public void run() {
+        this.schedulerRuns++;
+        logger.debug("Job manager maintenance: Starting #{}", this.schedulerRuns);
+
+        final TopologyCapabilities topologyCapabilities = configuration.getTopologyCapabilities();
+        if ( topologyCapabilities != null ) {
+            // Clean up
+            final String cleanUpUnassignedPath;;
+            if ( topologyCapabilities.isLeader() ) {
+                cleanUpUnassignedPath = this.configuration.getUnassignedJobsPath();
+            } else {
+                cleanUpUnassignedPath = null;
+            }
+
+            // job scheduler is handled every third run
+            if ( schedulerRuns % 3 == 1 ) {
+                this.jobScheduler.maintenance();
+            }
+            if ( schedulerRuns % 60 == 0 ) { // full clean up is done every hour
+                this.fullEmptyFolderCleanup(topologyCapabilities, this.configuration.getLocalJobsPath());
+                if ( cleanUpUnassignedPath != null ) {
+                    this.fullEmptyFolderCleanup(topologyCapabilities, cleanUpUnassignedPath);
+                }
+            } else if ( schedulerRuns % 5 == 0 ) { // simple clean up every 5 minutes
+                this.simpleEmptyFolderCleanup(topologyCapabilities, this.configuration.getLocalJobsPath());
+                if ( cleanUpUnassignedPath != null ) {
+                    this.simpleEmptyFolderCleanup(topologyCapabilities, cleanUpUnassignedPath);
+                }
+            }
+        }
+
+        logger.debug("Job manager maintenance: Finished #{}", this.schedulerRuns);
+    }
+
+    /**
+     * Simple empty folder removes empty folders for the last ten minutes
+     * starting five minutes ago.
+     * If folder for minute 59 is removed, we check the hour folder as well.
+     */
+    private void simpleEmptyFolderCleanup(final TopologyCapabilities caps, final String basePath) {
+        this.logger.debug("Cleaning up job resource tree: looking for empty folders");
+        final ResourceResolver resolver = this.configuration.createResourceResolver();
+        try {
+            final Calendar cleanUpDate = Calendar.getInstance();
+            // go back five minutes
+            cleanUpDate.add(Calendar.MINUTE, -5);
+
+            final Resource baseResource = resolver.getResource(basePath);
+            // sanity check - should never be null
+            if ( baseResource != null ) {
+                final Iterator<Resource> topicIter = baseResource.listChildren();
+                while ( caps.isActive() && topicIter.hasNext() ) {
+                    final Resource topicResource = topicIter.next();
+
+                    for(int i = 0; i < 10; i++) {
+                        if ( caps.isActive() ) {
+                            final StringBuilder sb = new StringBuilder(topicResource.getPath());
+                            sb.append('/');
+                            sb.append(cleanUpDate.get(Calendar.YEAR));
+                            sb.append('/');
+                            sb.append(cleanUpDate.get(Calendar.MONTH) + 1);
+                            sb.append('/');
+                            sb.append(cleanUpDate.get(Calendar.DAY_OF_MONTH));
+                            sb.append('/');
+                            sb.append(cleanUpDate.get(Calendar.HOUR_OF_DAY));
+                            sb.append('/');
+                            sb.append(cleanUpDate.get(Calendar.MINUTE));
+                            final String path = sb.toString();
+
+                            final Resource dateResource = resolver.getResource(path);
+                            if ( dateResource != null && !dateResource.listChildren().hasNext() ) {
+                                resolver.delete(dateResource);
+                                resolver.commit();
+                            }
+                            // check hour folder
+                            if ( path.endsWith("59") ) {
+                                final String hourPath = path.substring(0, path.length() - 3);
+                                final Resource hourResource = resolver.getResource(hourPath);
+                                if ( hourResource != null && !hourResource.listChildren().hasNext() ) {
+                                    resolver.delete(hourResource);
+                                    resolver.commit();
+                                }
+                            }
+                            // go back another minute in time
+                            cleanUpDate.add(Calendar.MINUTE, -1);
+                        }
+                    }
+                }
+            }
+
+        } catch (final PersistenceException pe) {
+            // in the case of an error, we just log this as a warning
+            this.logger.warn("Exception during job resource tree cleanup.", pe);
+        } finally {
+            resolver.close();
+        }
+    }
+
+    /**
+     * Full cleanup - this scans all directories!
+     */
+    private void fullEmptyFolderCleanup(final TopologyCapabilities caps, final String basePath) {
+        this.logger.debug("Cleaning up job resource tree: removing ALL empty folders");
+        final ResourceResolver resolver = this.configuration.createResourceResolver();
+        if ( resolver == null ) {
+            return;
+        }
+        try {
+            final Resource baseResource = resolver.getResource(basePath);
+            // sanity check - should never be null
+            if ( baseResource != null ) {
+                final Calendar now = Calendar.getInstance();
+                final int removeYear = now.get(Calendar.YEAR);
+                final int removeMonth = now.get(Calendar.MONTH) + 1;
+                final int removeDay = now.get(Calendar.DAY_OF_MONTH);
+                final int removeHour = now.get(Calendar.HOUR_OF_DAY);
+
+                final Iterator<Resource> topicIter = baseResource.listChildren();
+                while ( caps.isActive() && topicIter.hasNext() ) {
+                    final Resource topicResource = topicIter.next();
+
+                    // now years
+                    final Iterator<Resource> yearIter = topicResource.listChildren();
+                    while ( caps.isActive() && yearIter.hasNext() ) {
+                        final Resource yearResource = yearIter.next();
+                        final int year = Integer.valueOf(yearResource.getName());
+                        // we should not have a year higher than "now", but we test anyway
+                        if ( year > removeYear ) {
+                            continue;
+                        }
+                        final boolean oldYear = year < removeYear;
+
+                        // months
+                        final Iterator<Resource> monthIter = yearResource.listChildren();
+                        while ( caps.isActive() && monthIter.hasNext() ) {
+                            final Resource monthResource = monthIter.next();
+                            final int month = Integer.valueOf(monthResource.getName());
+                            if ( !oldYear && month > removeMonth ) {
+                                continue;
+                            }
+                            final boolean oldMonth = oldYear || month < removeMonth;
+
+                            // days
+                            final Iterator<Resource> dayIter = monthResource.listChildren();
+                            while ( caps.isActive() && dayIter.hasNext() ) {
+                                final Resource dayResource = dayIter.next();
+                                final int day = Integer.valueOf(dayResource.getName());
+                                if ( !oldMonth && day > removeDay ) {
+                                    continue;
+                                }
+                                final boolean oldDay = oldMonth || day < removeDay;
+
+                                // hours
+                                final Iterator<Resource> hourIter = dayResource.listChildren();
+                                while ( caps.isActive() && hourIter.hasNext() ) {
+                                    final Resource hourResource = hourIter.next();
+                                    final int hour = Integer.valueOf(hourResource.getName());
+                                    if ( !oldDay && hour > removeHour ) {
+                                        continue;
+                                    }
+                                    final boolean oldHour = (oldDay && (oldMonth || removeHour > 0)) || hour < (removeHour -1);
+
+                                    // we only remove minutes if the hour is old
+                                    if ( oldHour ) {
+                                        final Iterator<Resource> minuteIter = hourResource.listChildren();
+                                        while ( caps.isActive() && minuteIter.hasNext() ) {
+                                            final Resource minuteResource = minuteIter.next();
+
+                                            // check if we can delete the minute
+                                            if ( !minuteResource.listChildren().hasNext() ) {
+                                                resolver.delete(minuteResource);
+                                                resolver.commit();
+                                            }
+                                        }
+                                    }
+
+                                    // check if we can delete the hour
+                                    if ( caps.isActive() && oldHour && !hourResource.listChildren().hasNext()) {
+                                        resolver.delete(hourResource);
+                                        resolver.commit();
+                                    }
+                                }
+                                // check if we can delete the day
+                                if ( caps.isActive() && oldDay && !dayResource.listChildren().hasNext()) {
+                                    resolver.delete(dayResource);
+                                    resolver.commit();
+                                }
+                            }
+
+                            // check if we can delete the month
+                            if ( caps.isActive() && oldMonth && !monthResource.listChildren().hasNext() ) {
+                                resolver.delete(monthResource);
+                                resolver.commit();
+                            }
+                        }
+
+                        // check if we can delete the year
+                        if ( caps.isActive() && oldYear && !yearResource.listChildren().hasNext() ) {
+                            resolver.delete(yearResource);
+                            resolver.commit();
+                        }
+                    }
+                }
+            }
+
+        } catch (final PersistenceException pe) {
+            // in the case of an error, we just log this as a warning
+            this.logger.warn("Exception during job resource tree cleanup.", pe);
+        } finally {
+            resolver.close();
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/tasks/FindUnfinishedJobsTask.java b/src/main/java/org/apache/sling/event/impl/jobs/tasks/FindUnfinishedJobsTask.java
new file mode 100644
index 0000000..7bd889b
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/tasks/FindUnfinishedJobsTask.java
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.tasks;
+
+import java.util.Calendar;
+import java.util.Iterator;
+
+import org.apache.sling.api.resource.ModifiableValueMap;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.event.impl.jobs.JobImpl;
+import org.apache.sling.event.impl.jobs.JobTopicTraverser;
+import org.apache.sling.event.impl.jobs.config.JobManagerConfiguration;
+import org.apache.sling.event.jobs.Job;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This task is executed when the job handling starts.
+ * It checks for unfinished jobs from a previous start and corrects their state.
+ */
+public class FindUnfinishedJobsTask {
+
+    /** Logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** Job manager configuration. */
+    private final JobManagerConfiguration configuration;
+
+    /**
+     * Constructor
+     * @param config the configuration
+     */
+    public FindUnfinishedJobsTask(final JobManagerConfiguration config) {
+        this.configuration = config;
+    }
+
+    public void run() {
+        this.initialScan();
+    }
+
+    /**
+     * Scan the resource tree for unfinished jobs from previous runs
+     */
+    private void initialScan() {
+        logger.debug("Scanning repository for unfinished jobs...");
+        final ResourceResolver resolver = configuration.createResourceResolver();
+        if ( resolver == null ) {
+            return;
+        }
+        try {
+            final Resource baseResource = resolver.getResource(configuration.getLocalJobsPath());
+
+            // sanity check - should never be null
+            if ( baseResource != null ) {
+                final Iterator<Resource> topicIter = baseResource.listChildren();
+                while ( topicIter.hasNext() ) {
+                    final Resource topicResource = topicIter.next();
+                    logger.debug("Found topic {}", topicResource.getName());
+
+                    // init topic
+                    initTopic(topicResource);
+                }
+            }
+        } finally {
+            resolver.close();
+        }
+    }
+
+    /**
+     * Initialize a topic and update all jobs from that topic.
+     * Reset started time and increase retry count of unfinished jobs
+     * @param topicResource The topic resource
+     */
+    private void initTopic(final Resource topicResource) {
+        logger.debug("Initializing topic {}...", topicResource.getName());
+
+        JobTopicTraverser.traverse(logger, topicResource, new JobTopicTraverser.JobCallback() {
+
+            @Override
+            public boolean handle(final JobImpl job) {
+                if ( job.getProcessingStarted() != null ) {
+                    logger.debug("Found unfinished job {}", job.getId());
+                    job.retry();
+                    try {
+                        final Resource jobResource = topicResource.getResourceResolver().getResource(job.getResourcePath());
+                        // sanity check
+                        if ( jobResource != null ) {
+                            final ModifiableValueMap mvm = jobResource.adaptTo(ModifiableValueMap.class);
+                            mvm.remove(Job.PROPERTY_JOB_STARTED_TIME);
+                            mvm.put(Job.PROPERTY_JOB_RETRY_COUNT, job.getRetryCount());
+                            if ( job.getProperty(JobImpl.PROPERTY_JOB_QUEUED, Calendar.class) == null) {
+                                mvm.put(JobImpl.PROPERTY_JOB_QUEUED, Calendar.getInstance());
+                            }
+                            jobResource.getResourceResolver().commit();
+                        }
+                    } catch ( final PersistenceException ignore) {
+                        logger.error("Unable to update unfinished job " + job, ignore);
+                    }
+                } else if ( job.getProperty(JobImpl.PROPERTY_JOB_QUEUED, Calendar.class) == null) {
+                    logger.debug("Found job without queued date {}", job.getId());
+                    try {
+                        final Resource jobResource = topicResource.getResourceResolver().getResource(job.getResourcePath());
+                        // sanity check
+                        if ( jobResource != null ) {
+                            final ModifiableValueMap mvm = jobResource.adaptTo(ModifiableValueMap.class);
+                            mvm.put(JobImpl.PROPERTY_JOB_QUEUED, Calendar.getInstance());
+                            jobResource.getResourceResolver().commit();
+                        }
+                    } catch ( final PersistenceException ignore) {
+                        logger.error("Unable to update queued date for job " + job.getId(), ignore);
+                    }
+                }
+
+                return true;
+            }
+        });
+        logger.debug("Topic {} initialized", topicResource.getName());
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/tasks/HistoryCleanUpTask.java b/src/main/java/org/apache/sling/event/impl/jobs/tasks/HistoryCleanUpTask.java
new file mode 100644
index 0000000..6cb136b
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/tasks/HistoryCleanUpTask.java
@@ -0,0 +1,255 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.tasks;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.event.impl.jobs.JobImpl;
+import org.apache.sling.event.impl.jobs.config.JobManagerConfiguration;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.consumer.JobExecutionContext;
+import org.apache.sling.event.jobs.consumer.JobExecutionResult;
+import org.apache.sling.event.jobs.consumer.JobExecutor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Task to clean up the history,
+ * A clean up task can be configured with three properties:
+ * - age : only jobs older than this amount of minutes are removed (default is two days)
+ * - topic : only jobs with this topic are removed (default is no topic, meaning all jobs are removed)
+ *           The value should either be a string or an array of string
+ * - state : only jobs in this state are removed (default is no state, meaning all jobs are removed)
+ *           The value should either be a string or an array of string. Allowed values are:
+ *           SUCCEEDED, STOPPED, GIVEN_UP, ERROR, DROPPED
+ */
+@Component
+@Service(value = JobExecutor.class)
+@Property(name = JobExecutor.PROPERTY_TOPICS, value = "org/apache/sling/event/impl/jobs/tasks/HistoryCleanUpTask")
+public class HistoryCleanUpTask implements JobExecutor {
+
+    private static final String PROPERTY_AGE = "age";
+
+    private static final String PROPERTY_TOPIC = "topic";
+
+    private static final String PROPERTY_STATE = "state";
+
+    private static final int DEFAULT_AGE = 60 * 24 * 2; // older than two days
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    @Reference
+    private JobManagerConfiguration configuration;
+
+    @Override
+    public JobExecutionResult process(final Job job, final JobExecutionContext context) {
+        int age = job.getProperty(PROPERTY_AGE, DEFAULT_AGE);
+        if ( age < 1 ) {
+            age = DEFAULT_AGE;
+        }
+        final Calendar removeDate = Calendar.getInstance();
+        removeDate.add(Calendar.MINUTE, -age);
+
+        final String[] topics = job.getProperty(PROPERTY_TOPIC, String[].class);
+        final String[] states = job.getProperty(PROPERTY_STATE, String[].class);
+        final String logTopics = (topics == null ? "ALL" : Arrays.toString(topics));
+        final String logStates = (states == null ? "ALL" : Arrays.toString(states));
+        context.log("Cleaning up job history. Removing all jobs older than {0}, with topics {1} and states {2}",
+                removeDate, logTopics, logStates);
+
+        final List<String> stateList;
+        if ( states != null ) {
+            stateList = new ArrayList<String>();
+            for(final String s : states) {
+                stateList.add(s);
+            }
+        } else {
+            stateList = null;
+        }
+        final ResourceResolver resolver = this.configuration.createResourceResolver();
+        try {
+            if ( stateList == null || stateList.contains(Job.JobState.SUCCEEDED.name()) ) {
+                this.cleanup(removeDate, resolver, context, configuration.getStoredSuccessfulJobsPath(), topics, null);
+            }
+            if ( stateList == null || stateList.contains(Job.JobState.DROPPED.name())
+                 || stateList.contains(Job.JobState.ERROR.name())
+                 || stateList.contains(Job.JobState.GIVEN_UP.name())
+                 || stateList.contains(Job.JobState.STOPPED.name())) {
+                this.cleanup(removeDate, resolver, context, configuration.getStoredCancelledJobsPath(), topics, stateList);
+            }
+
+        } catch (final PersistenceException pe) {
+            // in the case of an error, we just log this as a warning
+            this.logger.warn("Exception during job resource tree cleanup.", pe);
+        } finally {
+            resolver.close();
+        }
+        return context.result().succeeded();
+    }
+
+    private void cleanup(final Calendar removeDate,
+            final ResourceResolver resolver,
+            final JobExecutionContext context,
+            final String basePath,
+            final String[] topics,
+            final List<String> stateList)
+    throws PersistenceException {
+        final Resource baseResource = resolver.getResource(basePath);
+        // sanity check - should never be null
+        if ( baseResource != null ) {
+            final Iterator<Resource> topicIter = baseResource.listChildren();
+            while ( !context.isStopped() && topicIter.hasNext() ) {
+                final Resource topicResource = topicIter.next();
+
+                // check topic
+                boolean found = topics == null;
+                int index = 0;
+                while ( !found && index < topics.length ) {
+                    if ( topicResource.getName().equals(topics[index]) ) {
+                        found = true;
+                    }
+                    index++;
+                }
+                if ( !found ) {
+                    continue;
+                }
+
+                final int removeYear = removeDate.get(Calendar.YEAR);
+                final int removeMonth = removeDate.get(Calendar.MONTH) + 1;
+                final int removeDay = removeDate.get(Calendar.DAY_OF_MONTH);
+                final int removeHour = removeDate.get(Calendar.HOUR_OF_DAY);
+                final int removeMinute = removeDate.get(Calendar.MINUTE);
+
+                // start with years
+                final Iterator<Resource> yearIter = topicResource.listChildren();
+                while ( !context.isStopped() && yearIter.hasNext() ) {
+                    final Resource yearResource = yearIter.next();
+                    final int year = Integer.valueOf(yearResource.getName());
+                    if ( year > removeYear ) {
+                        continue;
+                    }
+                    final boolean oldYear = year < removeYear;
+
+                    // months
+                    final Iterator<Resource> monthIter = yearResource.listChildren();
+                    while ( !context.isStopped() && monthIter.hasNext() ) {
+                        final Resource monthResource = monthIter.next();
+                        final int month = Integer.valueOf(monthResource.getName());
+                        if ( !oldYear && month > removeMonth) {
+                            continue;
+                        }
+                        final boolean oldMonth = oldYear || month < removeMonth;
+
+                        // days
+                        final Iterator<Resource> dayIter = monthResource.listChildren();
+                        while ( !context.isStopped() && dayIter.hasNext() ) {
+                            final Resource dayResource = dayIter.next();
+                            final int day = Integer.valueOf(dayResource.getName());
+                            if ( !oldMonth && day > removeDay) {
+                                continue;
+                            }
+                            final boolean oldDay = oldMonth || day < removeDay;
+
+                            // hours
+                            final Iterator<Resource> hourIter = dayResource.listChildren();
+                            while ( !context.isStopped() && hourIter.hasNext() ) {
+                                final Resource hourResource = hourIter.next();
+                                final int hour = Integer.valueOf(hourResource.getName());
+                                if ( !oldDay && hour > removeHour) {
+                                    continue;
+                                }
+                                final boolean oldHour = oldDay || hour < removeHour;
+
+                                // minutes
+                                final Iterator<Resource> minuteIter = hourResource.listChildren();
+                                while ( !context.isStopped() && minuteIter.hasNext() ) {
+                                    final Resource minuteResource = minuteIter.next();
+
+                                    // check if we can delete the minute
+                                    final int minute = Integer.valueOf(minuteResource.getName());
+                                    final boolean oldMinute = oldHour || minute <= removeMinute;
+
+                                    if ( oldMinute ) {
+                                        final Iterator<Resource> jobIter = minuteResource.listChildren();
+                                        while ( !context.isStopped() && jobIter.hasNext() ) {
+                                            final Resource jobResource = jobIter.next();
+                                            boolean remove = stateList == null;
+                                            if ( !remove ) {
+                                                final ValueMap vm = ResourceUtil.getValueMap(jobResource);
+                                                final String state = vm.get(JobImpl.PROPERTY_FINISHED_STATE, String.class);
+                                                if ( state != null && stateList.contains(state) ) {
+                                                    remove = true;
+                                                }
+                                            }
+                                            if ( remove ) {
+                                                resolver.delete(jobResource);
+                                                resolver.commit();
+                                            }
+                                        }
+                                        // check if we can delete the minute
+                                        if ( !context.isStopped() && !minuteResource.listChildren().hasNext()) {
+                                            resolver.delete(minuteResource);
+                                            resolver.commit();
+                                        }
+                                    }
+                                }
+
+                                // check if we can delete the hour
+                                if ( !context.isStopped() && oldHour && !hourResource.listChildren().hasNext()) {
+                                    resolver.delete(hourResource);
+                                    resolver.commit();
+                                }
+                            }
+                            // check if we can delete the day
+                            if ( !context.isStopped() && oldDay && !dayResource.listChildren().hasNext()) {
+                                resolver.delete(dayResource);
+                                resolver.commit();
+                            }
+                        }
+
+                        // check if we can delete the month
+                        if ( !context.isStopped() && oldMonth && !monthResource.listChildren().hasNext() ) {
+                            resolver.delete(monthResource);
+                            resolver.commit();
+                        }
+                    }
+
+                    // check if we can delete the year
+                    if ( !context.isStopped() && oldYear && !yearResource.listChildren().hasNext() ) {
+                        resolver.delete(yearResource);
+                        resolver.commit();
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/jobs/tasks/UpgradeTask.java b/src/main/java/org/apache/sling/event/impl/jobs/tasks/UpgradeTask.java
new file mode 100644
index 0000000..99c7b0e
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/jobs/tasks/UpgradeTask.java
@@ -0,0 +1,279 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.tasks;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.event.impl.jobs.JobTopicTraverser;
+import org.apache.sling.event.impl.jobs.config.JobManagerConfiguration;
+import org.apache.sling.event.impl.jobs.config.QueueConfigurationManager;
+import org.apache.sling.event.impl.jobs.config.QueueConfigurationManager.QueueInfo;
+import org.apache.sling.event.impl.jobs.config.TopologyCapabilities;
+import org.apache.sling.event.impl.support.Environment;
+import org.apache.sling.event.impl.support.ResourceHelper;
+import org.apache.sling.event.jobs.Job;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Upgrade task
+ *
+ * Upgrade jobs from earlier versions to the new format.
+ */
+public class UpgradeTask {
+
+    /** Logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** Job manager configuration. */
+    private final JobManagerConfiguration configuration;
+
+    /** The capabilities. */
+    private final TopologyCapabilities caps;
+
+    /**
+     * Constructor
+     * @param config the configuration
+     */
+    public UpgradeTask(final JobManagerConfiguration config) {
+        this.configuration = config;
+        this.caps = this.configuration.getTopologyCapabilities();
+    }
+
+    /**
+     * Upgrade
+     */
+    public void run() {
+        if ( caps.isLeader() ) {
+            this.processJobsFromPreviousVersions();
+        }
+        this.upgradeBridgedJobs();
+    }
+
+    /**
+     * Upgrade bridged jobs.
+     * In previous versions, bridged jobs were stored under a special topic.
+     * This has changed, the jobs are now stored with their real topic.
+     */
+    private void upgradeBridgedJobs() {
+        final String path = configuration.getLocalJobsPath() + "/slingevent:eventadmin";
+        final ResourceResolver resolver = configuration.createResourceResolver();
+        if ( resolver != null ) {
+            try {
+                final Resource rootResource = resolver.getResource(path);
+                if ( rootResource != null ) {
+                    upgradeBridgedJobs(rootResource);
+                }
+                if ( caps.isLeader() ) {
+                    final Resource unassignedRoot = resolver.getResource(configuration.getUnassignedJobsPath() + "/slingevent:eventadmin");
+                    if ( unassignedRoot != null ) {
+                        upgradeBridgedJobs(unassignedRoot);
+                    }
+                }
+            } finally {
+                resolver.close();
+            }
+        }
+    }
+
+    /**
+     * Upgrade bridged jobs
+     * @param rootResource  The root resource (topic resource)
+     */
+    private void upgradeBridgedJobs(final Resource topicResource) {
+        final String topicName = topicResource.getName().replace('.', '/');
+        final QueueConfigurationManager qcm = configuration.getQueueConfigurationManager();
+        if ( qcm == null ) {
+            return;
+        }
+        final QueueInfo info = qcm.getQueueInfo(topicName);
+        JobTopicTraverser.traverse(logger, topicResource, new JobTopicTraverser.ResourceCallback() {
+
+            @Override
+            public boolean handle(final Resource rsrc) {
+                try {
+                    final ValueMap vm = ResourceHelper.getValueMap(rsrc);
+                    final String targetId = caps.detectTarget(topicName, vm, info);
+
+                    final Map<String, Object> props = new HashMap<String, Object>(vm);
+                    final String newPath;
+                    if ( targetId != null ) {
+                        newPath = configuration.getAssginedJobsPath() + '/' + targetId + '/' + topicResource.getName() + rsrc.getPath().substring(topicResource.getPath().length());
+                        props.put(Job.PROPERTY_JOB_QUEUE_NAME, info.queueName);
+                        props.put(Job.PROPERTY_JOB_TARGET_INSTANCE, targetId);
+                    } else {
+                        newPath = configuration.getUnassignedJobsPath() + '/' + topicResource.getName() + rsrc.getPath().substring(topicResource.getPath().length());
+                        props.remove(Job.PROPERTY_JOB_QUEUE_NAME);
+                        props.remove(Job.PROPERTY_JOB_TARGET_INSTANCE);
+                    }
+                    props.remove(Job.PROPERTY_JOB_STARTED_TIME);
+                    try {
+                        ResourceHelper.getOrCreateResource(topicResource.getResourceResolver(), newPath, props);
+                        topicResource.getResourceResolver().delete(rsrc);
+                        topicResource.getResourceResolver().commit();
+                    } catch ( final PersistenceException pe ) {
+                        logger.warn("Unable to move job from previous version " + rsrc.getPath(), pe);
+                        topicResource.getResourceResolver().refresh();
+                        topicResource.getResourceResolver().revert();
+                    }
+                } catch (final InstantiationException ie) {
+                    logger.warn("Unable to move job from previous version " + rsrc.getPath(), ie);
+                    topicResource.getResourceResolver().refresh();
+                    topicResource.getResourceResolver().revert();
+                }
+                return caps.isActive();
+            }
+        });
+    }
+
+    /**
+     * Handle jobs from previous versions (<= 3.1.4) by moving them to the unassigned area
+     */
+    private void processJobsFromPreviousVersions() {
+        final ResourceResolver resolver = configuration.createResourceResolver();
+        if ( resolver != null ) {
+            try {
+                this.processJobsFromPreviousVersions(resolver.getResource(configuration.getPreviousVersionAnonPath()));
+                this.processJobsFromPreviousVersions(resolver.getResource(configuration.getPreviousVersionIdentifiedPath()));
+            } catch ( final PersistenceException pe ) {
+                this.logger.warn("Problems moving jobs from previous version.", pe);
+            } finally {
+                resolver.close();
+            }
+        }
+    }
+
+    /**
+     * Recursively find jobs and move them
+     */
+    private void processJobsFromPreviousVersions(final Resource rsrc) throws PersistenceException {
+        if ( rsrc != null && caps.isActive() ) {
+            if ( rsrc.isResourceType(ResourceHelper.RESOURCE_TYPE_JOB) ) {
+                this.moveJobFromPreviousVersion(rsrc);
+            } else {
+                for(final Resource child : rsrc.getChildren()) {
+                    this.processJobsFromPreviousVersions(child);
+                }
+                if ( caps.isActive() ) {
+                    rsrc.getResourceResolver().delete(rsrc);
+                    rsrc.getResourceResolver().commit();
+                    rsrc.getResourceResolver().refresh();
+                }
+            }
+        }
+    }
+
+    /**
+     * Move a single job
+     */
+    private void moveJobFromPreviousVersion(final Resource jobResource)
+    throws PersistenceException {
+        final ResourceResolver resolver = jobResource.getResourceResolver();
+
+        try {
+            final ValueMap vm = ResourceHelper.getValueMap(jobResource);
+            // check for binary properties
+            Map<String, Object> binaryProperties = new HashMap<String, Object>();
+            final ObjectInputStream ois = vm.get("slingevent:properties", ObjectInputStream.class);
+            if ( ois != null ) {
+                try {
+                    int length = ois.readInt();
+                    for(int i=0;i<length;i++) {
+                        final String key = (String)ois.readObject();
+                        final Object value = ois.readObject();
+                        binaryProperties.put(key, value);
+                    }
+                } catch (final ClassNotFoundException cnfe) {
+                    throw new PersistenceException("Class not found.", cnfe);
+                } catch (final java.io.InvalidClassException ice) {
+                    throw new PersistenceException("Invalid class.", ice);
+                } catch (final IOException ioe) {
+                    throw new PersistenceException("Unable to deserialize job properties.", ioe);
+                } finally {
+                    try {
+                        ois.close();
+                    } catch (final IOException ioe) {
+                        throw new PersistenceException("Unable to deserialize job properties.", ioe);
+                    }
+                }
+            }
+
+            final Map<String, Object> properties = ResourceHelper.cloneValueMap(vm);
+
+            final String topic = (String)properties.remove("slingevent:topic");
+            properties.put(ResourceHelper.PROPERTY_JOB_TOPIC, topic);
+
+            properties.remove(Job.PROPERTY_JOB_QUEUE_NAME);
+            properties.remove(Job.PROPERTY_JOB_TARGET_INSTANCE);
+            // and binary properties
+            properties.putAll(binaryProperties);
+            properties.remove("slingevent:properties");
+
+            if ( !properties.containsKey(Job.PROPERTY_JOB_RETRIES) ) {
+                properties.put(Job.PROPERTY_JOB_RETRIES, 10); // we put a dummy value here; this gets updated by the queue
+            }
+            if ( !properties.containsKey(Job.PROPERTY_JOB_RETRY_COUNT) ) {
+                properties.put(Job.PROPERTY_JOB_RETRY_COUNT, 0);
+            }
+
+            final List<InstanceDescription> potentialTargets = caps.getPotentialTargets(topic);
+            String targetId = null;
+            if ( potentialTargets != null && potentialTargets.size() > 0 ) {
+                final QueueConfigurationManager qcm = configuration.getQueueConfigurationManager();
+                if ( qcm == null ) {
+                    resolver.revert();
+                    return;
+                }
+                final QueueInfo info = qcm.getQueueInfo(topic);
+                logger.debug("Found queue {} for {}", info.queueConfiguration, topic);
+                targetId = caps.detectTarget(topic, vm, info);
+                if ( targetId != null ) {
+                    properties.put(Job.PROPERTY_JOB_QUEUE_NAME, info.queueName);
+                    properties.put(Job.PROPERTY_JOB_TARGET_INSTANCE, targetId);
+                    properties.put(Job.PROPERTY_JOB_RETRIES, info.queueConfiguration.getMaxRetries());
+                }
+            }
+
+            properties.put(Job.PROPERTY_JOB_CREATED_INSTANCE, "old:" + Environment.APPLICATION_ID);
+            properties.put(ResourceResolver.PROPERTY_RESOURCE_TYPE, ResourceHelper.RESOURCE_TYPE_JOB);
+
+            final String jobId = configuration.getUniqueId(topic);
+            properties.put(ResourceHelper.PROPERTY_JOB_ID, jobId);
+            properties.remove(Job.PROPERTY_JOB_STARTED_TIME);
+
+            final String newPath = configuration.getUniquePath(targetId, topic, jobId, vm);
+            this.logger.debug("Moving 'old' job from {} to {}", jobResource.getPath(), newPath);
+
+            ResourceHelper.getOrCreateResource(resolver, newPath, properties);
+            resolver.delete(jobResource);
+            resolver.commit();
+        } catch (final InstantiationException ie) {
+            throw new PersistenceException("Exception while reading reasource: " + ie.getMessage(), ie.getCause());
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/support/BatchResourceRemover.java b/src/main/java/org/apache/sling/event/impl/support/BatchResourceRemover.java
new file mode 100644
index 0000000..f98b65c
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/support/BatchResourceRemover.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.support;
+
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+
+/**
+ * This class can be used for batch removal of resources
+ */
+public class BatchResourceRemover {
+
+    private final int max;
+
+    private int count;
+
+    public BatchResourceRemover() {
+        this(50);
+    }
+
+    public BatchResourceRemover(final int batchSize) {
+        this.max = batchSize;
+    }
+
+    public void delete(final Resource rsrc )
+    throws PersistenceException {
+        final ResourceResolver resolver = rsrc.getResourceResolver();
+        for(final Resource child : rsrc.getChildren()) {
+            delete(child);
+        }
+        resolver.delete(rsrc);
+        count++;
+        if ( count >= max ) {
+            resolver.commit();
+            count = 0;
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/support/Environment.java b/src/main/java/org/apache/sling/event/impl/support/Environment.java
new file mode 100644
index 0000000..7e36b70
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/support/Environment.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.support;
+
+import org.apache.sling.commons.threads.ThreadPool;
+
+/**
+ * This class provides "global settings"
+ * to all services, like the application id and the thread pool.
+ * @since 3.0
+ */
+public class Environment {
+
+    /** Global application id. */
+    public static String APPLICATION_ID;
+
+    /** Global thread pool. */
+    public static volatile ThreadPool THREAD_POOL;
+}
diff --git a/src/main/java/org/apache/sling/event/impl/support/ExactTopicMatcher.java b/src/main/java/org/apache/sling/event/impl/support/ExactTopicMatcher.java
new file mode 100644
index 0000000..1938b04
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/support/ExactTopicMatcher.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.support;
+
+/**
+ * The topic must match exactly.
+ */
+public class ExactTopicMatcher implements TopicMatcher {
+
+    private final String topicName;
+
+    public ExactTopicMatcher(final String name) {
+        this.topicName = name;
+    }
+
+    /**
+     * @see org.apache.sling.event.impl.support.TopicMatcher#match(java.lang.String)
+     */
+    @Override
+    public String match(final String topic) {
+        return this.topicName.equals(topic) ? "" : null;
+    }
+
+    @Override
+    public String toString() {
+        return "ExactTopicMatcher [topic=" + topicName + "]";
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/event/impl/support/PackageTopicMatcher.java b/src/main/java/org/apache/sling/event/impl/support/PackageTopicMatcher.java
new file mode 100644
index 0000000..371fd8b
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/support/PackageTopicMatcher.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.support;
+
+
+/**
+ * Package matcher - the topic must be in the same package.
+ */
+public class PackageTopicMatcher implements TopicMatcher {
+
+    private final String packageName;
+
+    public PackageTopicMatcher(final String name) {
+        // remove last char and maybe a trailing slash
+        int lastPos = name.length() - 1;
+        if ( lastPos > 0 && name.charAt(lastPos - 1) == '/' ) {
+            lastPos--;
+        }
+        this.packageName = name.substring(0, lastPos);
+    }
+
+    /**
+     * @see org.apache.sling.event.impl.support.TopicMatcher#match(java.lang.String)
+     */
+    @Override
+    public String match(final String topic) {
+        final int pos = topic.lastIndexOf('/');
+        return pos > -1 && topic.substring(0, pos).equals(packageName) ? topic.substring(pos + 1) : null;
+    }
+
+    @Override
+    public String toString() {
+        return "PackageTopicMatcher [packageName=" + packageName + "]";
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/support/ResourceHelper.java b/src/main/java/org/apache/sling/event/impl/support/ResourceHelper.java
new file mode 100644
index 0000000..4383480
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/support/ResourceHelper.java
@@ -0,0 +1,426 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.support;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.event.impl.jobs.JobImpl;
+import org.apache.sling.event.impl.jobs.config.MainQueueConfiguration;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.ScheduleInfo;
+import org.apache.sling.event.jobs.consumer.JobConsumer;
+import org.osgi.service.event.EventConstants;
+
+public abstract class ResourceHelper {
+
+    public static final String RESOURCE_TYPE_FOLDER = "sling:Folder";
+
+    public static final String RESOURCE_TYPE_JOB = "slingevent:Job";
+
+    /** We use the same resource type as for timed events. */
+    public static final String RESOURCE_TYPE_SCHEDULED_JOB = "slingevent:TimedEvent";
+
+    public static final String BUNDLE_EVENT_UPDATED = "org/osgi/framework/BundleEvent/UPDATED";
+
+    public static final String BUNDLE_EVENT_STARTED = "org/osgi/framework/BundleEvent/STARTED";
+
+    public static final String PROPERTY_SCHEDULE_NAME = "slingevent:scheduleName";
+    public static final String PROPERTY_SCHEDULE_INFO = "slingevent:scheduleInfo";
+    public static final String PROPERTY_SCHEDULE_INFO_TYPE = "slingevent:scheduleInfoType";
+    public static final String PROPERTY_SCHEDULE_SUSPENDED = "slingevent:scheduleSuspended";
+
+    public static final String PROPERTY_JOB_ID = "slingevent:eventId";
+    public static final String PROPERTY_JOB_TOPIC = "event.job.topic";
+    public static final String PROPERTY_DISTRIBUTE = "event.distribute";
+    public static final String PROPERTY_APPLICATION = "event.application";
+
+    /** List of ignored properties to write to the repository. */
+    private static final String[] IGNORE_PROPERTIES = new String[] {
+        ResourceHelper.PROPERTY_DISTRIBUTE,
+        ResourceHelper.PROPERTY_APPLICATION,
+        EventConstants.EVENT_TOPIC,
+        ResourceHelper.PROPERTY_JOB_ID,
+        JobImpl.PROPERTY_DELAY_OVERRIDE,
+        JobConsumer.PROPERTY_JOB_ASYNC_HANDLER,
+        Job.PROPERTY_JOB_PROGRESS_LOG,
+        Job.PROPERTY_JOB_PROGRESS_ETA,
+        Job.PROPERTY_JOB_PROGRESS_STEP,
+        Job.PROPERTY_JOB_PROGRESS_STEPS,
+        Job.PROPERTY_FINISHED_DATE,
+        JobImpl.PROPERTY_FINISHED_STATE,
+        Job.PROPERTY_RESULT_MESSAGE,
+        PROPERTY_SCHEDULE_INFO,
+        PROPERTY_SCHEDULE_NAME,
+        PROPERTY_SCHEDULE_INFO_TYPE,
+        PROPERTY_SCHEDULE_SUSPENDED
+    };
+
+    /**
+     * Check if this property should be ignored
+     */
+    public static boolean ignoreProperty(final String name) {
+        for(final String prop : IGNORE_PROPERTIES) {
+            if ( prop.equals(name) ) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** Allowed characters for a node name */
+    private static final BitSet ALLOWED_CHARS;
+
+    /** Replacement characters for unallowed characters in a node name */
+    private static final char REPLACEMENT_CHAR = '_';
+
+    // Prepare the ALLOWED_CHARS bitset with bits indicating the unicode
+    // character index of allowed characters. We deliberately only support
+    // a subset of the actually allowed set of characters for nodes ...
+    static {
+        final String allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz0123456789_,.-+#!?$%&()=";
+        final BitSet allowedSet = new BitSet();
+        for (int i = 0; i < allowed.length(); i++) {
+            allowedSet.set(allowed.charAt(i));
+        }
+        ALLOWED_CHARS = allowedSet;
+    }
+
+    /**
+     * Filter the queue name for not allowed characters and replace them
+     * - with the exception of the main queue, which will not be filtered
+     * @param queueName the suggested queue name
+     * @return the filtered queue name
+     */
+    public static String filterQueueName(final String queueName) {
+        if ( queueName.equals(MainQueueConfiguration.MAIN_QUEUE_NAME) ) {
+            return queueName;
+        } else {
+            return ResourceHelper.filterName(queueName);
+        }
+    }
+
+    /**
+     * Filter the node name for not allowed characters and replace them.
+     * @param resourceName The suggested resource name.
+     * @return The filtered node name.
+     */
+    public static String filterName(final String resourceName) {
+        if ( resourceName == null ) {
+            return null;
+        }
+        final StringBuilder sb = new StringBuilder(resourceName.length());
+        char lastAdded = 0;
+
+        for(int i=0; i < resourceName.length(); i++) {
+            final char c = resourceName.charAt(i);
+            char toAdd = c;
+
+            if (!ALLOWED_CHARS.get(c)) {
+                if (lastAdded == REPLACEMENT_CHAR) {
+                    // do not add several _ in a row
+                    continue;
+                }
+                toAdd = REPLACEMENT_CHAR;
+
+            } else if(i == 0 && Character.isDigit(c)) {
+                sb.append(REPLACEMENT_CHAR);
+            }
+
+            sb.append(toAdd);
+            lastAdded = toAdd;
+        }
+
+        if (sb.length()==0) {
+            sb.append(REPLACEMENT_CHAR);
+        }
+
+        return sb.toString();
+    }
+
+    public static final String PROPERTY_MARKER_READ_ERROR_LIST = ResourceHelper.class.getName() + "/ReadErrorList";
+
+    public static Map<String, Object> cloneValueMap(final ValueMap vm) throws InstantiationException {
+        List<Exception> hasReadError = null;
+        try {
+            final Map<String, Object> result = new HashMap<String, Object>(vm);
+            for(final Map.Entry<String, Object> entry : result.entrySet()) {
+                if ( entry.getKey().equals(PROPERTY_SCHEDULE_INFO) ) {
+                    final String[] infoArray = vm.get(entry.getKey(), String[].class);
+                    if ( infoArray == null || infoArray.length == 0 ) {
+                        if ( hasReadError == null ) {
+                            hasReadError = new ArrayList<Exception>();
+                        }
+                        hasReadError.add(new Exception("Unable to deserialize property '" + entry.getKey() + "' : " + entry.getValue()));
+                    } else {
+                        final List<ScheduleInfo> infos = new ArrayList<ScheduleInfo>();
+                        for(final String i : infoArray) {
+                            final ScheduleInfoImpl info = ScheduleInfoImpl.deserialize(i);
+                            if ( info != null ) {
+                                infos.add(info);
+                            }
+                        }
+                        if ( infos.size() < infoArray.length ) {
+                            if ( hasReadError == null ) {
+                                hasReadError = new ArrayList<Exception>();
+                            }
+                            hasReadError.add(new Exception("Unable to deserialize property '" + entry.getKey() + "' : " + Arrays.toString(infoArray)));
+                        } else {
+                            entry.setValue(infos);
+                        }
+                    }
+                }
+                if ( entry.getValue() instanceof InputStream ) {
+                    final Object value = vm.get(entry.getKey(), Serializable.class);
+                    if ( value != null ) {
+                        entry.setValue(value);
+                    } else {
+                        if ( hasReadError == null ) {
+                            hasReadError = new ArrayList<Exception>();
+                        }
+                        // let's find out which class might be missing
+                        ObjectInputStream ois = null;
+                        try {
+                            ois = new ObjectInputStream((InputStream)entry.getValue());
+                            ois.readObject();
+
+                            hasReadError.add(new Exception("Unable to deserialize property '" + entry.getKey() + "'"));
+                        } catch (final ClassNotFoundException cnfe) {
+                             hasReadError.add(new Exception("Unable to deserialize property '" + entry.getKey() + "'", cnfe));
+                        } catch (final IOException ioe) {
+                            hasReadError.add(new RuntimeException("Unable to deserialize property '" + entry.getKey() + "'", ioe));
+                        } finally {
+                            if ( ois != null ) {
+                                try {
+                                    ois.close();
+                                } catch (IOException ignore) {
+                                    // ignore
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            if ( hasReadError != null ) {
+                result.put(PROPERTY_MARKER_READ_ERROR_LIST, hasReadError);
+            }
+            return result;
+        } catch ( final IllegalArgumentException iae) {
+            // the JCR implementation might throw an IAE if something goes wrong
+            throw (InstantiationException)new InstantiationException(iae.getMessage()).initCause(iae);
+        }
+    }
+
+    public static ValueMap getValueMap(final Resource resource) throws InstantiationException {
+        final ValueMap vm = ResourceUtil.getValueMap(resource);
+        // trigger full loading
+        try {
+            vm.size();
+        } catch ( final IllegalArgumentException iae) {
+            // the JCR implementation might throw an IAE if something goes wrong
+            throw (InstantiationException)new InstantiationException(iae.getMessage()).initCause(iae);
+        }
+        return vm;
+    }
+
+    public static void getOrCreateBasePath(final ResourceResolver resolver,
+            final String path)
+    throws PersistenceException {
+       getOrCreateResource(resolver,
+                        path,
+                        ResourceHelper.RESOURCE_TYPE_FOLDER,
+                        ResourceHelper.RESOURCE_TYPE_FOLDER,
+                        true);
+    }
+
+    public static Resource getOrCreateResource(final ResourceResolver resolver,
+            final String path, final Map<String, Object> props)
+    throws PersistenceException {
+       return getOrCreateResource(resolver,
+                        path,
+                        props,
+                        ResourceHelper.RESOURCE_TYPE_FOLDER,
+                        true);
+    }
+
+    /**
+     * Creates or gets the resource at the given path.
+     * This is a copy of Sling's API ResourceUtil method to avoid a dependency on the latest
+     * Sling API version! We can remove this once we update to Sling API > 2.8
+     * @param resolver The resource resolver to use for creation
+     * @param path     The full path to be created
+     * @param resourceType The optional resource type of the final resource to create
+     * @param intermediateResourceType THe optional resource type of all intermediate resources
+     * @param autoCommit If set to true, a commit is performed after each resource creation.
+     */
+    private static Resource getOrCreateResource(
+                            final ResourceResolver resolver,
+                            final String path,
+                            final String resourceType,
+                            final String intermediateResourceType,
+                            final boolean autoCommit)
+    throws PersistenceException {
+        final Map<String, Object> props;
+        if ( resourceType == null ) {
+            props = null;
+        } else {
+            props = Collections.singletonMap(ResourceResolver.PROPERTY_RESOURCE_TYPE, (Object)resourceType);
+        }
+        return getOrCreateResource(resolver, path, props, intermediateResourceType, autoCommit);
+    }
+
+    /**
+     * Creates or gets the resource at the given path.
+     * If an exception occurs, it retries the operation up to five times if autoCommit is enabled.
+     * In this case, {@link ResourceResolver#revert()} is called on the resolver before the
+     * creation is retried.
+     * This is a copy of Sling's API ResourceUtil method to avoid a dependency on the latest
+     * Sling API version! We can remove this once we update to Sling API > 2.8
+     *
+     * @param resolver The resource resolver to use for creation
+     * @param path     The full path to be created
+     * @param resourceProperties The optional resource properties of the final resource to create
+     * @param intermediateResourceType THe optional resource type of all intermediate resources
+     * @param autoCommit If set to true, a commit is performed after each resource creation.
+     */
+    private static Resource getOrCreateResource(
+            final ResourceResolver resolver,
+            final String path,
+            final Map<String, Object> resourceProperties,
+            final String intermediateResourceType,
+            final boolean autoCommit)
+    throws PersistenceException {
+        PersistenceException mostRecentPE = null;
+        for(int i=0;i<5;i++) {
+            try {
+                return getOrCreateResourceInternal(resolver,
+                        path,
+                        resourceProperties,
+                        intermediateResourceType,
+                        autoCommit);
+            } catch ( final PersistenceException pe ) {
+                if ( autoCommit ) {
+                    // in case of exception, revert to last clean state and retry
+                    resolver.revert();
+                    resolver.refresh();
+                    mostRecentPE = pe;
+                } else {
+                    throw pe;
+                }
+            }
+        }
+        throw mostRecentPE;
+    }
+
+    /**
+     * Creates or gets the resource at the given path.
+     * This is a copy of Sling's API ResourceUtil method to avoid a dependency on the latest
+     * Sling API version! We can remove this once we update to Sling API > 2.8
+     *
+     * @param resolver The resource resolver to use for creation
+     * @param path     The full path to be created
+     * @param resourceProperties The optional resource properties of the final resource to create
+     * @param intermediateResourceType THe optional resource type of all intermediate resources
+     * @param autoCommit If set to true, a commit is performed after each resource creation.
+     */
+    private static Resource getOrCreateResourceInternal(
+            final ResourceResolver resolver,
+            final String path,
+            final Map<String, Object> resourceProperties,
+            final String intermediateResourceType,
+            final boolean autoCommit)
+    throws PersistenceException {
+        Resource rsrc = resolver.getResource(path);
+        if ( rsrc == null ) {
+            final int lastPos = path.lastIndexOf('/');
+            final String name = path.substring(lastPos + 1);
+
+            final Resource parentResource;
+            if ( lastPos == 0 ) {
+                parentResource = resolver.getResource("/");
+            } else {
+                final String parentPath = path.substring(0, lastPos);
+                parentResource = getOrCreateResource(resolver,
+                        parentPath,
+                        intermediateResourceType,
+                        intermediateResourceType,
+                        autoCommit);
+            }
+            if ( autoCommit ) {
+                resolver.refresh();
+            }
+            try {
+                int retry = 5;
+                while ( retry > 0 && rsrc == null ) {
+                    rsrc = resolver.create(parentResource, name, resourceProperties);
+                    // check for SNS
+                    if ( !name.equals(rsrc.getName()) ) {
+                        resolver.refresh();
+                        resolver.delete(rsrc);
+                        rsrc = resolver.getResource(parentResource, name);
+                    }
+                    retry--;
+                }
+                if ( rsrc == null ) {
+                    throw new PersistenceException("Unable to create resource.");
+                }
+            } catch ( final PersistenceException pe ) {
+                // this could be thrown because someone else tried to create this
+                // node concurrently
+                resolver.refresh();
+                rsrc = resolver.getResource(parentResource, name);
+                if ( rsrc == null ) {
+                    throw pe;
+                }
+            }
+            if ( autoCommit ) {
+                try {
+                    resolver.commit();
+                    resolver.refresh();
+                    rsrc = resolver.getResource(parentResource, name);
+                } catch ( final PersistenceException pe ) {
+                    // try again - maybe someone else did create the resource in the meantime
+                    // or we ran into Jackrabbit's stale item exception in a clustered environment
+                    resolver.revert();
+                    resolver.refresh();
+                    rsrc = resolver.getResource(parentResource, name);
+                    if ( rsrc == null ) {
+                        rsrc = resolver.create(parentResource, name, resourceProperties);
+                        resolver.commit();
+                    }
+                }
+            }
+        }
+        return rsrc;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/event/impl/support/ScheduleInfoImpl.java b/src/main/java/org/apache/sling/event/impl/support/ScheduleInfoImpl.java
new file mode 100644
index 0000000..70b45f9
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/support/ScheduleInfoImpl.java
@@ -0,0 +1,421 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.support;
+
+import java.io.Serializable;
+import java.text.ParseException;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+import org.apache.sling.event.jobs.ScheduleInfo;
+import org.quartz.CronExpression;
+
+public class ScheduleInfoImpl implements ScheduleInfo, Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** Serialization version. */
+    private static final String VERSION = "1";
+
+    public static ScheduleInfoImpl HOURLY(final int minutes) {
+        return new ScheduleInfoImpl(ScheduleType.HOURLY, -1, -1, minutes, null, -1, null);
+    }
+
+    public static ScheduleInfoImpl CRON(final String expr) {
+        return new ScheduleInfoImpl(ScheduleType.CRON, -1, -1, -1, null, -1, expr);
+    }
+
+    public static ScheduleInfoImpl AT(final Date at) {
+        return new ScheduleInfoImpl(ScheduleType.DATE, -1, -1, -1, at, -1, null);
+    }
+
+    public static ScheduleInfoImpl YEARLY(final int month, final int day, final int hour, final int minute) {
+        return new ScheduleInfoImpl(ScheduleType.YEARLY, day, hour, minute, null, month, null);
+    }
+
+    public static ScheduleInfoImpl MONTHLY(final int day, final int hour, final int minute) {
+        return new ScheduleInfoImpl(ScheduleType.MONTHLY, day, hour, minute, null, -1, null);
+    }
+
+    public static ScheduleInfoImpl WEEKLY(final int day, final int hour, final int minute) {
+        return new ScheduleInfoImpl(ScheduleType.WEEKLY, day, hour, minute, null, -1, null);
+    }
+
+    public static ScheduleInfoImpl DAILY(final int hour, final int minute) {
+        return new ScheduleInfoImpl(ScheduleType.DAILY, -1, hour, minute, null, -1, null);
+    }
+
+    private final ScheduleType scheduleType;
+
+    private final int dayOfWeek;
+
+    private final int hourOfDay;
+
+    private final int minuteOfHour;
+
+    private final Date at;
+
+    private final int monthOfYear;
+
+    private final String expression;
+
+    private ScheduleInfoImpl(final ScheduleType scheduleType,
+            final int dayOfWeek,
+            final int hourOfDay,
+            final int minuteOfHour,
+            final Date at,
+            final int monthOfYear,
+            final String expression) {
+        this.scheduleType = scheduleType;
+        this.dayOfWeek = dayOfWeek;
+        this.hourOfDay = hourOfDay;
+        this.minuteOfHour = minuteOfHour;
+        this.at = at;
+        this.monthOfYear = monthOfYear;
+        this.expression = expression;
+    }
+
+    public static ScheduleInfoImpl deserialize(final ScheduleType scheduleType, final String s) {
+        final String[] parts = s.split("|");
+        if ( scheduleType == ScheduleType.YEARLY && parts.length == 4 ) {
+            try {
+                return new ScheduleInfoImpl(scheduleType,
+                        Integer.parseInt(parts[0]),
+                        Integer.parseInt(parts[1]),
+                        Integer.parseInt(parts[2]),
+                        null,
+                        Integer.parseInt(parts[3]),
+                        null);
+            } catch ( final IllegalArgumentException iae) {
+                // ignore and return null
+            }
+        } else if ( scheduleType == ScheduleType.MONTHLY && parts.length == 3 ) {
+            try {
+                return new ScheduleInfoImpl(scheduleType,
+                        Integer.parseInt(parts[0]),
+                        Integer.parseInt(parts[1]),
+                        Integer.parseInt(parts[2]),
+                        null,
+                        -1,
+                        null);
+            } catch ( final IllegalArgumentException iae) {
+                // ignore and return null
+            }
+        } else if ( scheduleType == ScheduleType.WEEKLY && parts.length == 3 ) {
+            try {
+                return new ScheduleInfoImpl(scheduleType,
+                        Integer.parseInt(parts[0]),
+                        Integer.parseInt(parts[1]),
+                        Integer.parseInt(parts[2]),
+                        null,
+                        -1,
+                        null);
+            } catch ( final IllegalArgumentException iae) {
+                // ignore and return null
+            }
+        } else if ( scheduleType == ScheduleType.DAILY && parts.length == 2 ) {
+            try {
+                return new ScheduleInfoImpl(scheduleType,
+                        -1,
+                        Integer.parseInt(parts[0]),
+                        Integer.parseInt(parts[1]),
+                        null,
+                        -1,
+                        null);
+            } catch ( final IllegalArgumentException iae) {
+                // ignore and return null
+            }
+        } else if ( scheduleType == ScheduleType.HOURLY && parts.length == 1 ) {
+            try {
+                return new ScheduleInfoImpl(scheduleType,
+                        -1,
+                        -1,
+                        Integer.parseInt(parts[0]),
+                        null,
+                        -1,
+                        null);
+            } catch ( final IllegalArgumentException iae) {
+                // ignore and return null
+            }
+        } else if ( scheduleType == ScheduleType.CRON && parts.length == 1 ) {
+            try {
+                return new ScheduleInfoImpl(scheduleType,
+                        -1,
+                        -1,
+                        -1,
+                        null,
+                        -1,
+                        parts[0]);
+            } catch ( final IllegalArgumentException iae) {
+                // ignore and return null
+            }
+        }
+
+        return null;
+    }
+
+    public static ScheduleInfoImpl deserialize(final String s) {
+        final String[] parts = s.split("\\|");
+        if ( parts.length == 8 && parts[0].equals(VERSION) ) {
+            try {
+                return new ScheduleInfoImpl(ScheduleType.valueOf(parts[1]),
+                        Integer.parseInt(parts[2]),
+                        Integer.parseInt(parts[3]),
+                        Integer.parseInt(parts[4]),
+                        (parts[5].equals("null") ? null : new Date(Long.parseLong(parts[5]))),
+                        Integer.parseInt(parts[6]),
+                        (parts[7].equals("null") ? null : parts[7])
+                        );
+            } catch ( final IllegalArgumentException iae) {
+                // ignore and return null
+            }
+        }
+        return null;
+    }
+
+    public String getSerializedString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(VERSION);
+        sb.append("|");
+        sb.append(this.scheduleType.name());
+        sb.append("|");
+        sb.append(String.valueOf(this.dayOfWeek));
+        sb.append("|");
+        sb.append(String.valueOf(this.hourOfDay));
+        sb.append("|");
+        sb.append(String.valueOf(this.minuteOfHour));
+        sb.append("|");
+        if ( at == null ) {
+            sb.append("null");
+        } else {
+            sb.append(String.valueOf(at.getTime()));
+        }
+        sb.append("|");
+        sb.append(String.valueOf(this.monthOfYear));
+        sb.append("|");
+        if ( expression == null ) {
+            sb.append("null");
+        } else {
+            sb.append(String.valueOf(expression));
+        }
+        return sb.toString();
+    }
+
+    @Override
+    public ScheduleType getType() {
+        return this.scheduleType;
+    }
+
+    @Override
+    public Date getAt() {
+        return this.at;
+    }
+
+    @Override
+    public int getDayOfWeek() {
+        return (this.scheduleType == ScheduleType.WEEKLY ? this.dayOfWeek : -1);
+    }
+
+    @Override
+    public int getHourOfDay() {
+        return this.hourOfDay;
+    }
+
+    @Override
+    public int getMinuteOfHour() {
+        return this.minuteOfHour;
+    }
+
+    @Override
+    public String getExpression() {
+        return this.expression;
+    }
+
+    @Override
+    public int getMonthOfYear() {
+        return this.monthOfYear;
+    }
+
+    @Override
+    public int getDayOfMonth() {
+        return (this.scheduleType == ScheduleType.MONTHLY
+                || this.scheduleType == ScheduleType.YEARLY ? this.dayOfWeek : -1);
+    }
+
+    public void check(final List<String> errors) {
+        switch ( this.scheduleType ) {
+        case DAILY : if ( hourOfDay < 0 || hourOfDay > 23 || minuteOfHour < 0 || minuteOfHour > 59 ) {
+                         errors.add("Wrong time information : " + minuteOfHour + ":" + minuteOfHour);
+                     }
+                     break;
+        case DATE :  if ( at == null || at.getTime() <= System.currentTimeMillis() + 2000 ) {
+                         errors.add("Date must be in the future : " + at);
+                     }
+                     break;
+        case HOURLY : if ( minuteOfHour < 0 || minuteOfHour > 59 ) {
+                          errors.add("Minute must be between 0 and 59 : " + minuteOfHour);
+                      }
+                      break;
+        case WEEKLY : if ( hourOfDay < 0 || hourOfDay > 23 || minuteOfHour < 0 || minuteOfHour > 59 ) {
+                          errors.add("Wrong time information : " + minuteOfHour + ":" + minuteOfHour);
+                      }
+                      if ( dayOfWeek < 1 || dayOfWeek > 7 ) {
+                          errors.add("Day must be between 1 and 7 : " + dayOfWeek);
+                      }
+                      break;
+        case MONTHLY : if ( hourOfDay < 0 || hourOfDay > 23 || minuteOfHour < 0 || minuteOfHour > 59 ) {
+                           errors.add("Wrong time information : " + minuteOfHour + ":" + minuteOfHour);
+                       }
+                       if ( dayOfWeek < 1 || dayOfWeek > 28 ) {
+                           errors.add("Day must be between 1 and 28 : " + dayOfWeek);
+                       }
+                       break;
+        case YEARLY : if ( hourOfDay < 0 || hourOfDay > 23 || minuteOfHour < 0 || minuteOfHour > 59 ) {
+                          errors.add("Wrong time information : " + minuteOfHour + ":" + minuteOfHour);
+                      }
+                      if ( dayOfWeek < 1 || dayOfWeek > 28 ) {
+                          errors.add("Day must be between 1 and 28 : " + dayOfWeek);
+                      }
+                      if ( monthOfYear < 1 || monthOfYear > 12 ) {
+                          errors.add("Month must be between 1 and 12 : " + dayOfWeek);
+                      }
+                      break;
+        case CRON : if ( expression == null ) {
+                         errors.add("Expression must be specified.");
+                    }
+                    try {
+                        new CronExpression(this.expression);
+                    } catch (final ParseException e) {
+                        errors.add("Expression must be valid: " + this.expression);
+                    }
+        }
+    }
+
+    public Date getNextScheduledExecution() {
+        final Calendar now = Calendar.getInstance();
+        switch ( this.scheduleType ) {
+            case DATE : return this.at;
+            case DAILY : final Calendar next = Calendar.getInstance();
+                         next.set(Calendar.HOUR_OF_DAY, this.hourOfDay);
+                         next.set(Calendar.MINUTE, this.minuteOfHour);
+                         if ( next.before(now) ) {
+                             next.add(Calendar.DAY_OF_WEEK, 1);
+                         }
+                         return next.getTime();
+            case WEEKLY : final Calendar nextW = Calendar.getInstance();
+                          nextW.set(Calendar.HOUR_OF_DAY, this.hourOfDay);
+                          nextW.set(Calendar.MINUTE, this.minuteOfHour);
+                          nextW.set(Calendar.DAY_OF_WEEK, this.dayOfWeek);
+                          if ( nextW.before(now) ) {
+                              nextW.add(Calendar.WEEK_OF_YEAR, 1);
+                          }
+                          return nextW.getTime();
+            case HOURLY : final Calendar nextH = Calendar.getInstance();
+                          nextH.set(Calendar.MINUTE, this.minuteOfHour);
+                          if ( nextH.before(now) ) {
+                              nextH.add(Calendar.HOUR_OF_DAY, 1);
+                          }
+                          return nextH.getTime();
+            case MONTHLY : final Calendar nextM = Calendar.getInstance();
+                           nextM.set(Calendar.HOUR_OF_DAY, this.hourOfDay);
+                           nextM.set(Calendar.MINUTE, this.minuteOfHour);
+                           nextM.set(Calendar.DAY_OF_MONTH, this.dayOfWeek);
+                           if ( nextM.before(now) ) {
+                               nextM.add(Calendar.MONTH, 1);
+                           }
+                           return nextM.getTime();
+            case YEARLY : final Calendar nextY = Calendar.getInstance();
+                          nextY.set(Calendar.HOUR_OF_DAY, this.hourOfDay);
+                          nextY.set(Calendar.MINUTE, this.minuteOfHour);
+                          nextY.set(Calendar.DAY_OF_MONTH, this.dayOfWeek);
+                          nextY.set(Calendar.MONTH, this.monthOfYear - 1);
+                          if ( nextY.before(now) ) {
+                              nextY.add(Calendar.YEAR, 1);
+                          }
+                          return nextY.getTime();
+            case CRON : try {
+                            final CronExpression exp = new CronExpression(this.expression);
+                            return exp.getNextValidTimeAfter(new Date());
+                        } catch (final ParseException e) {
+                            // as we check the expression in check() everything should be fine here
+                        }
+        }
+        return null;
+    }
+
+    /**
+     * If the job is scheduled daily or weekly, return the cron expression
+     */
+    public String getCronExpression() {
+        if ( this.scheduleType == ScheduleType.DAILY ) {
+            final StringBuilder sb = new StringBuilder("0 ");
+            sb.append(String.valueOf(this.minuteOfHour));
+            sb.append(' ');
+            sb.append(String.valueOf(this.hourOfDay));
+            sb.append(" * * ?");
+            return sb.toString();
+        } else if ( this.scheduleType == ScheduleType.WEEKLY ) {
+            final StringBuilder sb = new StringBuilder("0 ");
+            sb.append(String.valueOf(this.minuteOfHour));
+            sb.append(' ');
+            sb.append(String.valueOf(this.hourOfDay));
+            sb.append(" ? * ");
+            sb.append(String.valueOf(this.dayOfWeek));
+            return sb.toString();
+        } else if ( this.scheduleType == ScheduleType.HOURLY ) {
+            final StringBuilder sb = new StringBuilder("0 ");
+            sb.append(String.valueOf(this.minuteOfHour));
+            sb.append(" * * * ?");
+            return sb.toString();
+        } else if ( this.scheduleType == ScheduleType.MONTHLY ) {
+            final StringBuilder sb = new StringBuilder("0 ");
+            sb.append(String.valueOf(this.minuteOfHour));
+            sb.append(' ');
+            sb.append(String.valueOf(this.hourOfDay));
+            sb.append(' ');
+            sb.append(String.valueOf(this.dayOfWeek));
+            sb.append(" * ?");
+            return sb.toString();
+        } else if ( this.scheduleType == ScheduleType.YEARLY ) {
+            final StringBuilder sb = new StringBuilder("0 ");
+            sb.append(String.valueOf(this.minuteOfHour));
+            sb.append(' ');
+            sb.append(String.valueOf(this.hourOfDay));
+            sb.append(' ');
+            sb.append(String.valueOf(this.dayOfWeek));
+            sb.append(' ');
+            sb.append(String.valueOf(this.monthOfYear - 1));
+            sb.append(" ?");
+            return sb.toString();
+        } else if ( this.scheduleType == ScheduleType.CRON ) {
+            return this.expression;
+        }
+        return null;
+    }
+
+    @Override
+    public String toString() {
+        return "ScheduleInfo [scheduleType=" + scheduleType
+                + ", dayOfWeek=" + dayOfWeek + ", hourOfDay=" + hourOfDay
+                + ", minuteOfHour=" + minuteOfHour + ", at=" + at
+                + ", monthOfYear=" + monthOfYear + ", expression=" + expression
+                + "]";
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/impl/support/SubPackagesTopicMatcher.java b/src/main/java/org/apache/sling/event/impl/support/SubPackagesTopicMatcher.java
new file mode 100644
index 0000000..a53ecf8
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/support/SubPackagesTopicMatcher.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.support;
+
+
+/**
+ * Sub package matcher - the topic must be in the same package or a sub package.
+ */
+public class SubPackagesTopicMatcher implements TopicMatcher {
+
+    private final String packageName;
+
+    public SubPackagesTopicMatcher(final String name) {
+        // remove last char and maybe a trailing slash
+        int lastPos = name.length() - 1;
+        if ( lastPos > 0 && name.charAt(lastPos - 1) == '/' ) {
+            this.packageName = name.substring(0, lastPos);
+        } else {
+            this.packageName = name.substring(0, lastPos) + '/';
+        }
+    }
+
+    /**
+     * @see org.apache.sling.event.impl.support.TopicMatcher#match(java.lang.String)
+     */
+    @Override
+    public String match(final String topic) {
+        final int pos = topic.lastIndexOf('/');
+        return pos > -1 && topic.substring(0, pos + 1).startsWith(this.packageName) ? topic.substring(this.packageName.length()) : null;
+    }
+
+    @Override
+    public String toString() {
+        return "SubPackageMatcher [packageName=" + packageName + "]";
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/event/impl/support/TopicMatcher.java b/src/main/java/org/apache/sling/event/impl/support/TopicMatcher.java
new file mode 100644
index 0000000..1378f4a
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/support/TopicMatcher.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.support;
+
+/**
+ * Interface for topic matchers
+ */
+public interface TopicMatcher {
+
+    /** Check if the topic matches and return the variable part - null if not matching. */
+    String match(String topic);
+}
diff --git a/src/main/java/org/apache/sling/event/impl/support/TopicMatcherHelper.java b/src/main/java/org/apache/sling/event/impl/support/TopicMatcherHelper.java
new file mode 100644
index 0000000..b53a4c7
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/impl/support/TopicMatcherHelper.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.support;
+
+public abstract class TopicMatcherHelper {
+
+    public static final TopicMatcher[] MATCH_ALL = new TopicMatcher[] {
+        new TopicMatcher() {
+
+           @Override
+           public String match(String topic) {
+               return topic;
+           }
+       }
+    };
+
+    /**
+     * Create matchers based on the topic parameters.
+     * If the topic parameters do not contain any definition
+     * <code>null</code> is returned.
+     */
+    public static TopicMatcher[] buildMatchers(final String[] topicsParam) {
+        final TopicMatcher[] matchers;
+        if ( topicsParam == null
+                || topicsParam.length == 0
+                || (topicsParam.length == 1 && (topicsParam[0] == null || topicsParam[0].length() == 0))) {
+               matchers = null;
+       } else {
+           final TopicMatcher[] newMatchers = new TopicMatcher[topicsParam.length];
+           for(int i=0; i < topicsParam.length; i++) {
+               String value = topicsParam[i];
+               if ( value != null ) {
+                   value = value.trim();
+               }
+               if ( value != null && value.length() > 0 ) {
+                   if ( value.equals("*") ) {
+                       return MATCH_ALL;
+                   }
+                   if ( value.endsWith(".") ) {
+                       newMatchers[i] = new PackageTopicMatcher(value);
+                   } else if ( value.endsWith("*") ) {
+                       newMatchers[i] = new SubPackagesTopicMatcher(value);
+                   } else {
+                       newMatchers[i] = new ExactTopicMatcher(value);
+                   }
+               }
+           }
+           matchers = newMatchers;
+        }
+        return matchers;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/event/jobs/Job.java b/src/main/java/org/apache/sling/event/jobs/Job.java
new file mode 100644
index 0000000..710e730
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/jobs/Job.java
@@ -0,0 +1,351 @@
+/*
+ 1   * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.jobs;
+
+import java.util.Calendar;
+import java.util.Set;
+
+import org.osgi.annotation.versioning.ProviderType;
+
+
+/**
+ * A job
+ *
+ *
+ * Property Types
+ *
+ * In general all scalar types and all serializable classes are supported as
+ * property types. However, in order for deseralizing classes these must be
+ * exported. Serializable classes are not searchable in the query either.
+ * Due to the above mentioned potential problems, it is advisable to not use
+ * custom classes as job properties, but rather use out of the box supported
+ * types in combination with collections.
+ *
+ * A resource provider might convert numbers to a different type, JCR is well-known
+ * for this behavior as it only supports long but neither integer nor short.
+ * Therefore if you are dealing with numbers, use the {@link #getProperty(String, Class)}
+ * method to get the correct type instead of directly casting it.
+ *
+ * @since 1.2
+ */
+@ProviderType
+public interface Job {
+
+    /**
+     * The name of the job queue processing this job.
+     * This property is set by the job handling when the job is processed.
+     * If this property is set by the client creating the job it's value is ignored
+     */
+    String PROPERTY_JOB_QUEUE_NAME = "event.job.queuename";
+
+    /**
+     * The property to track the retry count for jobs. Value is of type Integer.
+     * On first execution the value of this property is zero.
+     * This property is managed by the job handling.
+     * If this property is set by the client creating the job it's value is ignored
+     */
+    String PROPERTY_JOB_RETRY_COUNT = "event.job.retrycount";
+
+    /**
+     * The property to track the retry maximum retry count for jobs. Value is of type Integer.
+     * This property is managed by the job handling.
+     * If this property is set by the client creating the job it's value is ignored
+     */
+    String PROPERTY_JOB_RETRIES = "event.job.retries";
+
+    /**
+     * This property is set by the job handling and contains a calendar object
+     * specifying the date and time when this job has been created.
+     * If this property is set by the client creating the job it's value is ignored
+     */
+    String PROPERTY_JOB_CREATED = "slingevent:created";
+
+    /**
+     * This property is set by the job handling and contains the Sling instance ID
+     * of the instance where this job has been created.
+     */
+    String PROPERTY_JOB_CREATED_INSTANCE = "slingevent:application";
+
+    /**
+     * This property is set by the job handling and contains the Sling instance ID
+     * of the instance where this job should be processed.
+     */
+    String PROPERTY_JOB_TARGET_INSTANCE = "event.job.application";
+
+    /**
+     * This property is set by the job handling and contains a calendar object
+     * specifying the date and time when this job has been started.
+     * This property is only set if the job is currently in processing
+     * If this property is set by the client creating the job it's value is ignored
+     */
+    String PROPERTY_JOB_STARTED_TIME = "event.job.started.time";
+
+    /**
+     * The property to set a retry delay. Value is of type Long and specifies milliseconds.
+     * This property can be used to override the retry delay from the queue configuration.
+     * But it should only be used very rarely as the queue configuration should be the one
+     * in charge.
+     */
+    String PROPERTY_JOB_RETRY_DELAY = "event.job.retrydelay";
+
+    /**
+     * This property contains the optional output log of a job consumer.
+     * The value of this property is a string array.
+     * This property is read-only and can't be specified when the job is created.
+     * @since 1.3
+     */
+    String PROPERTY_JOB_PROGRESS_LOG = "slingevent:progressLog";
+
+    /**
+     * This property contains the optional ETA for a job.
+     * The value of this property is a {@link Calendar} object.
+     * This property is read-only and can't be specified when the job is created.
+     * @since 1.3
+     */
+    String PROPERTY_JOB_PROGRESS_ETA = "slingevent:progressETA";
+
+    /**
+     * This property contains optional progress information about a job,
+     * the number of steps the job consumer will perform. Each step is
+     * assumed to consume roughly the same amount if time.
+     * The value of this property is an integer.
+     * This property is read-only and can't be specified when the job is created.
+     * @since 1.3
+     */
+    String PROPERTY_JOB_PROGRESS_STEPS = "slingevent:progressSteps";
+
+    /**
+     * This property contains optional progress information about a job,
+     * the number of completed steps.
+     * The value of this property is an integer.
+     * This property is read-only and can't be specified when the job is created.
+     * @since 1.3
+     */
+    String PROPERTY_JOB_PROGRESS_STEP = "slingevent:progressStep";
+
+    /**
+     * This property contains the optional result message of a job consumer.
+     * The value of this property is a string.
+     * This property is read-only and can't be specified when the job is created.
+     * @since 1.3
+     */
+    String PROPERTY_RESULT_MESSAGE = "slingevent:resultMessage";
+
+    /**
+     * This property contains the finished date once a job is marked as finished.
+     * The value of this property is a {@link Calendar} object.
+     * This property is read-only and can't be specified when the job is created.
+     * @since 1.3
+     */
+    String PROPERTY_FINISHED_DATE = "slingevent:finishedDate";
+
+    /**
+     * This is an optional property containing a human readable title for
+     * the job
+     * @since 1.3
+     */
+    String PROPERTY_JOB_TITLE = "slingevent:jobTitle";
+
+    /**
+     * This is an optional property containing a human readable description for
+     * the job
+     * @since 1.3
+     */
+    String PROPERTY_JOB_DESCRIPTION = "slingevent:jobDescription";
+
+    /**
+     * The current job state.
+     * @since 1.3
+     */
+    enum JobState {
+        QUEUED,     // waiting in queue after adding or for restart after failing
+        ACTIVE,     // job is currently in processing
+        SUCCEEDED,  // processing finished successfully
+        STOPPED,    // processing was stopped by a user
+        GIVEN_UP,   // number of retries reached
+        ERROR,      // processing signaled CANCELLED or throw an exception
+        DROPPED     // dropped jobs
+    };
+
+    /**
+     * The job topic.
+     * @return The job topic
+     */
+    String getTopic();
+
+    /**
+     * Unique job ID.
+     * @return The unique job ID.
+     */
+    String getId();
+
+    /**
+     * Get the value of a property.
+     * @param name The property name
+     * @return The value of the property or <code>null</code>
+     */
+    Object getProperty(String name);
+
+    /**
+     * Get all property names.
+     * @return A set of property names.
+     */
+    Set<String> getPropertyNames();
+
+    /**
+     * Get a named property and convert it into the given type.
+     * This method does not support conversion into a primitive type or an
+     * array of a primitive type. It should return <code>null</code> in this
+     * case.
+     *
+     * @param name The name of the property
+     * @param type The class of the type
+     * @param <T> The class of the type
+     * @return Return named value converted to type T or <code>null</code> if
+     *         non existing or can't be converted.
+     */
+    <T> T getProperty(String name, Class<T> type);
+
+    /**
+     * Get a named property and convert it into the given type.
+     * This method does not support conversion into a primitive type or an
+     * array of a primitive type. It should return the default value in this
+     * case.
+     *
+     * @param name The name of the property
+     * @param defaultValue The default value to use if the named property does
+     *            not exist or cannot be converted to the requested type. The
+     *            default value is also used to define the type to convert the
+     *            value to. If this is <code>null</code> any existing property is
+     *            not converted.
+     * @param <T> The class of the type
+     * @return Return named value converted to type T or the default value if
+     *         non existing or can't be converted.
+     */
+    <T> T getProperty(String name, T defaultValue);
+
+    /**
+     * On first execution the value of this property is zero.
+     * This property is managed by the job handling.
+     * @return The retry count.
+     */
+    int getRetryCount();
+
+    /**
+     * The property to track the retry maximum retry count for jobs.
+     * This property is managed by the job handling.
+     * @return The number of retries.
+     */
+    int getNumberOfRetries();
+
+    /**
+     * The name of the job queue processing this job.
+     * This property is set by the job handling when the job is processed.
+     * @return The queue name or <code>null</code>
+     */
+    String getQueueName();
+
+    /**
+     * This property is set by the job handling and contains the Sling instance ID
+     * of the instance where this job should be processed.
+     * @return The sling ID or <code>null</code>
+     */
+    String getTargetInstance();
+
+    /**
+     * This property is set by the job handling and contains a calendar object
+     * specifying the date and time when this job has been started.
+     * This property is only set if the job is currently in processing
+     * @return The time the processing started or {@code null}.
+     */
+    Calendar getProcessingStarted();
+
+    /**
+     * This property is set by the job handling and contains a calendar object
+     * specifying the date and time when this job has been created.
+     * @return The time the job was created.
+     */
+    Calendar getCreated();
+
+    /**
+     * This property is set by the job handling and contains the Sling instance ID
+     * of the instance where this job has been created.
+     * @return The instance id the job was created on
+     */
+    String getCreatedInstance();
+
+    /**
+     * Get the job state
+     * @return The job state.
+     * @since 1.3
+     */
+    JobState getJobState();
+
+    /**
+     * If the job is cancelled or succeeded, this method will return the finish date.
+     * @return The finish date or <code>null</code>
+     * @since 1.3
+     */
+    Calendar getFinishedDate();
+
+    /**
+     * This method returns the message from the last job processing, regardless
+     * whether the processing failed, succeeded or was cancelled. The message
+     * is optional and can be set by a job consumer.
+     * @return The result message or <code>null</code>
+     * @since 1.3
+     */
+    String getResultMessage();
+
+    /**
+     * This method returns the optional progress log from the last job
+     * processing. The log is optional and can be set by a job consumer.
+     * @return The log or <code>null</code>
+     * @since 1.3
+     */
+    String[] getProgressLog();
+
+    /**
+     * If the job is in processing, return the optional progress step
+     * count if available. The progress information is optional and
+     * can be set by a job consumer.
+     * @return The progress step count or <code>-1</code>.
+     * @since 1.3
+     */
+    int getProgressStepCount();
+
+    /**
+     * If the job is in processing, return the optional information
+     * about the finished steps. This progress information is optional
+     * and can be set by a job consumer.
+     * In combination with {@link #getProgressStepCount()} this can
+     * be used to calculate a progress bar.
+     * @return The number of the finished progress step or <code>0</code>
+     * @since 1.3
+     */
+    int getFinishedProgressStep();
+
+    /**
+     * If the job is in processing, return the optional ETA for this job.
+     * The progress information is optional and can be set by a job consumer.
+     * @since 1.3
+     * @return The estimated ETA or <code>null</code>
+     */
+    Calendar getProgressETA();
+}
diff --git a/src/main/java/org/apache/sling/event/jobs/JobBuilder.java b/src/main/java/org/apache/sling/event/jobs/JobBuilder.java
new file mode 100644
index 0000000..1455a61
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/jobs/JobBuilder.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.jobs;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * This is a builder interface to build jobs and scheduled jobs.
+ * Instances of this class can be retrieved using {@link JobManager#createJob(String)}
+ *
+ * @since 1.3
+ */
+@ProviderType
+public interface JobBuilder {
+
+    /**
+     * Set the optional configuration properties for the job.
+     * @param props The properties of the job. All values must be {@code java.io.Serializable}.
+     * @return The job builder to continue building.
+     */
+    JobBuilder properties(final Map<String, Object> props);
+
+    /**
+     * Add the job.
+     * @return The job or <code>null</code>
+     * @see JobManager#addJob(String, Map)
+     */
+    Job add();
+
+    /**
+     * Add the job.
+     * @param errors Optional list which will be filled with error messages.
+     * @return The job or <code>null</code>
+     * @see JobManager#addJob(String, Map)
+     */
+    Job add(final List<String> errors);
+
+    /**
+     * Schedule the job
+     * @return A schedule builder to schedule the jobs
+     */
+    ScheduleBuilder schedule();
+
+    /**
+     * This is a builder interface for creating schedule information
+     */
+    public interface ScheduleBuilder {
+
+        /**
+         * Suspend this scheduling by default.
+         * Invoking this method several times has the same effect as calling it just once.
+         * @return The schedule builder to continue building.
+         */
+        ScheduleBuilder suspend();
+
+        /**
+         * Schedule the job hourly at the given minute.
+         * If the minutes argument is less than 0 or higher than 59, the job can't be scheduled.
+         * @param minute Between 0 and 59.
+         * @return The schedule builder to continue building.
+         */
+        ScheduleBuilder hourly(final int minute);
+
+        /**
+         * Schedule the job daily at the given time.
+         * If a value less than zero for hour or minute is specified or a value higher than 23 for hour or
+         * a value higher than 59 for minute than the job can't be scheduled.
+         * @param hour  Hour of the day ranging from 0 to 23.
+         * @param minute Minute of the hour ranging from 0 to 59.
+         * @return The schedule builder to continue building.
+         */
+        ScheduleBuilder daily(final int hour, final int minute);
+
+        /**
+         * Schedule the job weekly, the time needs to be specified in addition.
+         * If a value lower than 1 or higher than 7 is used for the day, the job can't be scheduled.
+         * If a value less than zero for hour or minute is specified or a value higher than 23 for hour or
+         * a value higher than 59 for minute than the job can't be scheduled.
+         * @param day Day of the week, 1:Sunday, 2:Monday, ... 7:Saturday.
+         * @param hour  Hour of the day ranging from 0 to 23.
+         * @param minute Minute of the hour ranging from 0 to 59.
+         * @return The schedule builder to continue building.
+         */
+        ScheduleBuilder weekly(final int day, final int hour, final int minute);
+
+        /**
+         * Schedule the job monthly, the time needs to be specified in addition.
+         * If a value lower than 1 or higher than 28 is used for the day, the job can't be scheduled.
+         * If a value less than zero for hour or minute is specified or a value higher than 23 for hour or
+         * a value higher than 59 for minute than the job can't be scheduled.
+         * @param day Day of the month from 1 to 28.
+         * @param hour  Hour of the day ranging from 0 to 23.
+         * @param minute Minute of the hour ranging from 0 to 59.
+         * @return The schedule builder to continue building.
+         */
+        ScheduleBuilder monthly(final int day, final int hour, final int minute);
+
+        /**
+         * Schedule the job yearly, the time needs to be specified in addition.
+         * If a value lower than 1 or higher than 12 is used for the month, the job can't be scheduled.
+         * If a value lower than 1 or higher than 28 is used for the day, the job can't be scheduled.
+         * If a value less than zero for hour or minute is specified or a value higher than 23 for hour or
+         * a value higher than 59 for minute than the job can't be scheduled.
+         * @param month Month of the year from 1 to 12.
+         * @param day Day of the month from 1 to 28.
+         * @param hour  Hour of the day ranging from 0 to 23.
+         * @param minute Minute of the hour ranging from 0 to 59.
+         * @return The schedule builder to continue building.
+         */
+        ScheduleBuilder yearly(final int month, final int day, final int hour, final int minute);
+
+        /**
+         * Schedule the job for a specific date.
+         * If no date or a a date in the past is provided, the job can't be scheduled.
+         * @param date The date
+         * @return The schedule builder to continue building.
+         */
+        ScheduleBuilder at(final Date date);
+
+        /**
+         * Schedule the job for according to the cron expression.
+         * If no expression is specified, the job can't be scheduled.
+         * @param expression The cron expression
+         * @return The schedule builder to continue building.
+         */
+        ScheduleBuilder cron(final String expression);
+
+        /**
+         * Finally add the job to the schedule
+         * @return Returns the info object if the job could be scheduled, <code>null</code>otherwise.
+         */
+        ScheduledJobInfo add();
+
+        /**
+         * Finally add the job to the schedule
+         * @param errors Optional list which will be filled with error messages.
+         * @return Returns the info object if the job could be scheduled, <code>null</code>otherwise.
+         */
+        ScheduledJobInfo add(final List<String> errors);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/event/jobs/JobManager.java b/src/main/java/org/apache/sling/event/jobs/JobManager.java
new file mode 100644
index 0000000..105b611
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/jobs/JobManager.java
@@ -0,0 +1,223 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.jobs;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.osgi.annotation.versioning.ProviderType;
+
+
+/**
+ * The job manager is the heart of the job processing.
+ * <p>
+ * The job manager allows to create new jobs, search for
+ * jobs and get statistics about the current state.
+ * <p>
+ * The terminology used in the job manager is slightly
+ * different from common terminology:
+ * Each job has a topic and a topic is associated with
+ * a queue. Queues can be created through configuration
+ * and each queue can process one or more topics.
+ *
+ * @since 3.0
+ */
+@ProviderType
+public interface JobManager {
+
+    /**
+     * Return statistics information about all queues.
+     * @return The statistics.
+     */
+    Statistics getStatistics();
+
+    /**
+     * Return statistics information about job topics.
+     * @return The statistics for all topics.
+     */
+    Iterable<TopicStatistics> getTopicStatistics();
+
+    /**
+     * Return a queue with a specific name (if running)
+     * @param name The queue name
+     * @return The queue or <code>null</code>
+     */
+    Queue getQueue(String name);
+
+    /**
+     * Return an iterator for all available queues.
+     * @return An iterator for all queues.
+     */
+    Iterable<Queue> getQueues();
+
+    /**
+     * The requested job types for the query.
+     * This can either be all (unfinished) jobs, all activated (started) or all queued jobs.
+     */
+    enum QueryType {
+        ALL,      // all means all active and all queued
+        ACTIVE,
+        QUEUED,
+        HISTORY,    // returns the complete history of cancelled and succeeded jobs (if available)
+        CANCELLED,  // history of cancelled jobs (STOPPED, GIVEN_UP, ERROR, DROPPED)
+        SUCCEEDED,  // history of succeeded jobs
+        STOPPED,    // history of stopped jobs
+        GIVEN_UP,   // history of given up jobs
+        ERROR,      // history of jobs signaled CANCELLED or throw an exception
+        DROPPED     // history of dropped jobs
+    }
+
+    /**
+     * Add a new job
+     *
+     * If the topic is <code>null</code> or illegal, no job is created and <code>null</code> is returned.
+     * If properties are provided, all of them must be serializable. If there are non serializable
+     * objects in the properties, no job is created and <code>null</code> is returned.
+     * A job topic is a hierarchical name separated by dashes, each part has to start with a letter,
+     * allowed characters are letters, numbers and the underscore.
+     *
+     * The returned job object is a snapshot of the job state taken at the time of creation. Updates
+     * to the job state are not reflected and the client needs to get a new job object using the job id.
+     *
+     * If the queue for processing this job is configured to drop the job, <code>null</code> is returned
+     * as well.
+     *
+     * @param topic The required job topic.
+     * @param properties Optional job properties. The properties must be serializable.
+     * @return The new job - or <code>null</code> if the job could not be created.
+     * @since 1.2
+     */
+    Job addJob(String topic, Map<String, Object> properties);
+
+    /**
+     * Return a job based on the unique id.
+     *
+     * The returned job object is a snapshot of the job state taken at the time of the call. Updates
+     * to the job state are not reflected and the client needs to get a new job object using the job id.
+     *
+     * @param jobId The unique identifier from {@link Job#getId()}
+     * @return A job or <code>null</code>
+     * @since 1.2
+     */
+    Job getJobById(String jobId);
+
+    /**
+     * Removes the job even if it is currently in processing.
+     *
+     * If the job exists and is not in processing, it gets removed from the processing queue.
+     * If the job exists and is in processing, it is removed from the persistence layer,
+     * however processing is not stopped.
+     *
+     * @param jobId The unique identifier from {@link Job#getId()}
+     * @return <code>true</code> if the job could be removed or does not exist anymore.
+     *         <code>false</code> otherwise.
+     * @since 1.2
+     */
+    boolean removeJobById(String jobId);
+
+    /**
+     * Find a job - either queued or active.
+     *
+     * This method searches for a job with the given topic and filter properties. If more than one
+     * job matches, the first one found is returned which could be any of the matching jobs.
+     *
+     * The returned job object is a snapshot of the job state taken at the time of the call. Updates
+     * to the job state are not reflected and the client needs to get a new job object using the job id.
+     *
+     * @param topic Topic is required.
+     * @param template The map acts like a template. The searched job
+     *                    must match the template (AND query).
+     * @return A job or <code>null</code>
+     * @since 1.2
+     */
+    Job getJob(String topic, Map<String, Object> template);
+
+    /**
+     * Return all jobs of a given type.
+     *
+     * Based on the type parameter, either the history of jobs can be returned or unfinished jobs. The type
+     * parameter can further specify which category of jobs should be returned: for the history either
+     * succeeded jobs, cancelled jobs or both in combination can be returned. For unfinished jobs, either
+     * queued jobs, started jobs or the combination can be returned.
+     * If the history is returned, the result set is sorted in descending order, listening the newest entry
+     * first. For unfinished jobs, the result set is sorted in ascending order.
+     *
+     * The returned job objects are a snapshot of the jobs state taken at the time of the call. Updates
+     * to the job states are not reflected and the client needs to get new job objects.
+     *
+     * @param type Required parameter for the type. See above.
+     * @param topic Topic can be used as a filter, if it is non-null, only jobs with this topic will be returned.
+     * @param limit A positive number indicating the maximum number of jobs returned by the iterator. A value
+     *              of zero or less indicates that all jobs should be returned.
+     * @param templates A list of filter property maps. Each map acts like a template. The searched job
+     *                    must match the template (AND query). By providing several maps, different filters
+     *                    are possible (OR query).
+     * @return A collection of jobs - the collection might be empty.
+     * @since 1.2
+     */
+    Collection<Job> findJobs(QueryType type, String topic, long limit, Map<String, Object>... templates);
+
+    /**
+     * Stop a job.
+     * When a job is stopped and the job consumer supports stopping the job processing, it is up
+     * to the job consumer how the stopping is handled. The job can be marked as finished successful,
+     * permanently failed or being retried.
+     * @param jobId The job id
+     * @since 1.3
+     */
+    void stopJobById(String jobId);
+
+    /**
+     * Retry a cancelled job.
+     * If a job has failed permanently it can be requeued with this method. The job will be
+     * removed from the history and put into the queue again. The new job will get a new job id.
+     * For all other jobs calling this method has no effect and it simply returns <code>null</code>.
+     * @param jobId The job id.
+     * @return If the job is requeued, the new job object otherwise <code>null</code>
+     */
+    Job retryJobById(String jobId);
+
+    /**
+     * Fluent API to create, start and schedule new jobs
+     * @param topic Required topic
+     * @return A job builder
+     * @since 1.3
+     */
+    JobBuilder createJob(final String topic);
+
+    /**
+     * Return all available job schedules.
+     * @return A collection of scheduled job infos
+     * @since 1.3
+     */
+    Collection<ScheduledJobInfo> getScheduledJobs();
+
+    /**
+     * Return all matching available job schedules.
+     * @param topic Topic can be used as a filter, if it is non-null, only jobs with this topic will be returned.
+     * @param limit A positive number indicating the maximum number of jobs returned by the iterator. A value
+     *              of zero or less indicates that all jobs should be returned.
+     * @param templates A list of filter property maps. Each map acts like a template. The searched job
+     *                    must match the template (AND query). By providing several maps, different filters
+     *                    are possible (OR query).
+     * @return All matching scheduled job infos.
+     * @since 1.4
+     */
+    Collection<ScheduledJobInfo> getScheduledJobs(String topic, long limit, Map<String, Object>... templates);
+}
diff --git a/src/main/java/org/apache/sling/event/jobs/NotificationConstants.java b/src/main/java/org/apache/sling/event/jobs/NotificationConstants.java
new file mode 100644
index 0000000..58c7c55
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/jobs/NotificationConstants.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.jobs;
+
+
+/**
+ * This class contains constants for event notifications.
+ *
+ * Notifications for jobs can only be received on the instance where the job
+ * action is taking place. They are not send to other instances using
+ * remove events.
+ *
+ * @since 1.3
+ */
+public abstract class NotificationConstants {
+
+    /**
+     * Asynchronous notification event when a job is started.
+     * The property {@link #NOTIFICATION_PROPERTY_JOB_TOPIC} contains the job topic,
+     * the property {@link #NOTIFICATION_PROPERTY_JOB_ID} contains the unique job id.
+     * The time stamp of the event (as a Long) is available from the property
+     * {@link org.osgi.service.event.EventConstants#TIMESTAMP}.
+     * The payload of the job is available as additional job specific properties.
+     */
+    public static final String TOPIC_JOB_STARTED = "org/apache/sling/event/notification/job/START";
+
+    /**
+     * Asynchronous notification event when a job is finished.
+     * The property {@link #NOTIFICATION_PROPERTY_JOB_TOPIC} contains the job topic,
+     * the property {@link #NOTIFICATION_PROPERTY_JOB_ID} contains the unique job id.
+     * The time stamp of the event (as a Long) is available from the property
+     * {@link org.osgi.service.event.EventConstants#TIMESTAMP}.
+     * The payload of the job is available as additional job specific properties.
+     */
+    public static final String TOPIC_JOB_FINISHED = "org/apache/sling/event/notification/job/FINISHED";
+
+    /**
+     * Asynchronous notification event when a job failed.
+     * If a job execution fails, it is rescheduled for another try.
+     * The property {@link #NOTIFICATION_PROPERTY_JOB_TOPIC} contains the job topic,
+     * the property {@link #NOTIFICATION_PROPERTY_JOB_ID} contains the unique job id.
+     * The time stamp of the event (as a Long) is available from the property
+     * {@link org.osgi.service.event.EventConstants#TIMESTAMP}.
+     * The payload of the job is available as additional job specific properties.
+     */
+    public static final String TOPIC_JOB_FAILED = "org/apache/sling/event/notification/job/FAILED";
+
+    /**
+     * Asynchronous notification event when a job is cancelled.
+     * If a job execution is cancelled it is not rescheduled.
+     * The property {@link #NOTIFICATION_PROPERTY_JOB_TOPIC} contains the job topic,
+     * the property {@link #NOTIFICATION_PROPERTY_JOB_ID} contains the unique job id.
+     * The time stamp of the event (as a Long) is available from the property
+     * {@link org.osgi.service.event.EventConstants#TIMESTAMP}.
+     * The payload of the job is available as additional job specific properties.
+     */
+    public static final String TOPIC_JOB_CANCELLED = "org/apache/sling/event/notification/job/CANCELLED";
+
+    /**
+     * Asynchronous notification event when a job is permanently removed.
+     * The property {@link #NOTIFICATION_PROPERTY_JOB_TOPIC} contains the job topic,
+     * the property {@link #NOTIFICATION_PROPERTY_JOB_ID} contains the unique job id.
+     * The payload of the job is available as additional job specific properties.
+     */
+    public static final String TOPIC_JOB_REMOVED = "org/apache/sling/event/notification/job/REMOVED";
+
+    /**
+     * Asynchronous notification event when a job is added.
+     * The property {@link #NOTIFICATION_PROPERTY_JOB_TOPIC} contains the job topic,
+     * the property {@link #NOTIFICATION_PROPERTY_JOB_ID} contains the unique job id.
+     * @since 1.6
+     */
+    public static final String TOPIC_JOB_ADDED = "org/apache/sling/event/notification/job/ADDED";
+
+    /**
+     * Property containing the job topic. Value is of type String.
+     * @see Job#getTopic()
+     */
+    public static final String NOTIFICATION_PROPERTY_JOB_TOPIC = "event.job.topic";
+
+    /**
+     * Property containing the unique job ID. Value is of type String.
+     * @see Job#getId()
+     */
+    public static final String NOTIFICATION_PROPERTY_JOB_ID = "slingevent:eventId";
+
+   private NotificationConstants() {
+        // avoid instantiation
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/event/jobs/Queue.java b/src/main/java/org/apache/sling/event/jobs/Queue.java
new file mode 100644
index 0000000..2134aba
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/jobs/Queue.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.jobs;
+
+import org.osgi.annotation.versioning.ProviderType;
+
+
+/**
+ * This is a job queue processing job events.
+ * @since 3.0
+ */
+@ProviderType
+public interface Queue {
+
+    /**
+     * Get the queue name.
+     * @return The queue name
+     */
+    String getName();
+
+    /**
+     * Return statistics information about this queue.
+     * @return The queue statistics
+     */
+    Statistics getStatistics();
+
+    /**
+     * Get the corresponding configuration.
+     * @return The queue configuration
+     */
+    QueueConfiguration getConfiguration();
+
+    /**
+     * Suspend the queue - when a queue is suspended it stops processing
+     * jobs - however already started jobs are finished (but not rescheduled).
+     * Depending on the queue implementation, the queue is only suspended
+     * for a specific time.
+     * A queue can be resumed with {@link #resume()}.
+     */
+    void suspend();
+
+    /**
+     * Resume a suspended queue. {@link #suspend()}. If the queue is not
+     * suspended, calling this method has no effect.
+     * Depending on the queue implementation, if a job failed a job queue might
+     * sleep for a configured time, before a new job is processed. By calling this
+     * method, the job queue can be woken up and force an immediate reprocessing.
+     * This feature is only supported by ordered queues at the moment. If a queue
+     * does not support this feature, calling this method has only an effect if
+     * the queue is really suspended.
+     */
+    void resume();
+
+    /**
+     * Is the queue currently suspended?
+     * @return {code true} if the queue is supsended
+     */
+    boolean isSuspended();
+
+    /**
+     * Remove all outstanding jobs and delete them. This actually cancels
+     * all outstanding jobs.
+     */
+    void removeAll();
+
+    /**
+     * Return some information about the current state of the queue. This
+     * method is meant to see the internal state of the queue for debugging
+     * or monitoring purposes.
+     * @return Additional state info
+     */
+    String getStateInfo();
+
+    /**
+     * For monitoring purposes and possible extensions from the different
+     * queue types. This method allows to query state information.
+     * @param key The key for the state
+     * @return The state or {@code null}.
+     */
+    Object getState(final String key);
+}
diff --git a/src/main/java/org/apache/sling/event/jobs/QueueConfiguration.java b/src/main/java/org/apache/sling/event/jobs/QueueConfiguration.java
new file mode 100644
index 0000000..8990289
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/jobs/QueueConfiguration.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.jobs;
+
+import org.osgi.annotation.versioning.ProviderType;
+
+
+/**
+ * The configuration of a queue.
+ * @since 3.0
+ */
+@ProviderType
+public interface QueueConfiguration {
+
+    /** The queue type. */
+    static enum Type {
+        UNORDERED,          // unordered, parallel processing (push)
+        ORDERED,            // ordered, FIFO (push)
+        TOPIC_ROUND_ROBIN   // unordered, parallel processing, executed based on topic (push)
+    }
+
+    /**
+     * The thread priority.
+     * @since 1.3
+     */
+    static enum ThreadPriority {
+        NORM,
+        MIN,
+        MAX
+    }
+
+    /**
+     * Return the retry delay in ms
+     * @return The retry delay
+     */
+    long getRetryDelayInMs();
+
+    /**
+     * Return the max number of retries, -1 for endless retry!
+     * @return Max number of retries
+     */
+    int getMaxRetries();
+
+    /**
+     * Return the queue type.
+     * @return The queue type
+     */
+    Type getType();
+
+    /**
+     * Return the thread priority for the job thread
+     * @return Thread priority
+     */
+    ThreadPriority getThreadPriority();
+
+    /**
+     * Return the max number of parallel processes.
+     * @return Max parallel processes
+     */
+    int getMaxParallel();
+
+    /**
+     * The list of topics this queue is bound to.
+     * @return All topics for this queue.
+     */
+    String[] getTopics();
+
+    /**
+     * Whether successful jobs are kept for a complete history
+     * @return <code>true</code> if successful jobs are kept.
+     * @since 1.3
+     */
+    boolean isKeepJobs();
+
+    /**
+     * Return the size for the optional thread pool for this queue.
+     * @return A positive number or <code>0</code> if the default thread pool
+     *         should be used.
+     * @since 1.3
+     */
+    int getOwnThreadPoolSize();
+
+    /**
+     * Get the ranking of this configuration.
+     * @return The ranking
+     */
+    int getRanking();
+
+    /**
+     * Prefer to run the job on the same instance it was created on.
+     * @return {@code true} if running on the creation instance is preferred.
+     * @since 1.4
+     */
+    boolean isPreferRunOnCreationInstance();
+}
diff --git a/src/main/java/org/apache/sling/event/jobs/ScheduleInfo.java b/src/main/java/org/apache/sling/event/jobs/ScheduleInfo.java
new file mode 100644
index 0000000..bb390cb
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/jobs/ScheduleInfo.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.jobs;
+
+import java.util.Date;
+
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Scheduling information.
+ * @since 1.3
+ */
+@ProviderType
+public interface ScheduleInfo {
+
+    enum ScheduleType {
+        DATE,         // scheduled for a date
+        HOURLY,       // scheduled hourly
+        DAILY,        // scheduled once a day
+        WEEKLY,       // scheduled once a week
+        MONTHLY,      // scheduled once a month
+        YEARLY,       // scheduled once a year,
+        CRON          // scheduled according to the cron expression
+    }
+
+    /**
+     * Return the scheduling type
+     * @return The scheduling type
+     */
+    ScheduleType getType();
+
+    /**
+     * Return the scheduled execution date for a schedule of type date.
+     * @return the scheduled execution date
+     */
+    Date getAt();
+
+    /**
+     * If the schedule is a cron expression, return the expression.
+     * @return The cron expression or <code>null</code>
+     */
+    String getExpression();
+
+    /**
+     * If the job is scheduled yearly, returns the month of the year
+     * @return The day of the year (from 1 to 12) or -1
+     */
+    int getMonthOfYear();
+
+    /**
+     * If the job is scheduled monthly, returns the day of the month
+     * @return The day of the month (from 1 to 28) or -1
+     */
+    int getDayOfMonth();
+
+    /**
+     * If the job is scheduled weekly, returns the day of the week
+     * @return The day of the week (from 1 to 7) or -1
+     */
+    int getDayOfWeek();
+
+    /**
+     * Return the hour of the day for daily and weekly scheduled jobs
+     * @return The hour of the day (from 0 to 23) or -1
+     */
+    int getHourOfDay();
+
+    /**
+     * Return the minute of the hour for daily, weekly and hourly scheduled jobs.
+     * @return The minute of the hour (from 0 to 59) or -1
+     */
+    int getMinuteOfHour();
+}
diff --git a/src/main/java/org/apache/sling/event/jobs/ScheduledJobInfo.java b/src/main/java/org/apache/sling/event/jobs/ScheduledJobInfo.java
new file mode 100644
index 0000000..4d37c8b
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/jobs/ScheduledJobInfo.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.jobs;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.Map;
+
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Information about a scheduled job
+ * @since 1.3
+ */
+@ProviderType
+public interface ScheduledJobInfo {
+
+    /**
+     * Get all schedules for this job
+     * @return A non null and non empty list of schedules.
+     */
+    Collection<ScheduleInfo> getSchedules();
+
+    /**
+     * Return the next scheduled execution date.
+     * @return the next scheduled execution date.
+     */
+    Date getNextScheduledExecution();
+
+    /**
+     * Return the job topic.
+     * @return The job topic
+     */
+    String getJobTopic();
+
+    /**
+     * Return the optional job topics.
+     * @return The job topics or <code>null</code>
+     */
+    Map<String, Object> getJobProperties();
+
+    /**
+     * Unschedule this scheduled job.
+     */
+    void unschedule();
+
+    /**
+     * Reschedule this job with a new rescheduling information.
+     * If rescheduling fails (due to wrong arguments), the job
+     * schedule is left as is.
+     * @return The schedule builder
+     */
+    JobBuilder.ScheduleBuilder reschedule();
+
+    /**
+     * Suspend this job scheduling.
+     * Job scheduling can be resumed with {@link #resume()}.
+     * This information is persisted and survives a restart.
+     */
+    void suspend();
+
+    /**
+     * Resume job processing. {@link #suspend()}. If the queue is not
+     * suspended, calling this method has no effect.
+     */
+    void resume();
+
+    /**
+     * Is the processing currently suspended?
+     * @return {@code true} if processing is suspended.
+     */
+    boolean isSuspended();
+}
diff --git a/src/main/java/org/apache/sling/event/jobs/Statistics.java b/src/main/java/org/apache/sling/event/jobs/Statistics.java
new file mode 100644
index 0000000..66b8d55
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/jobs/Statistics.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.jobs;
+
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Statistic information.
+ * This information is not preserved between restarts of the service.
+ * Once a service is restarted, the counters start at zero!
+ * @since 3.0
+ */
+@ProviderType
+public interface Statistics {
+
+    /**
+     * The time this service has been started
+     * @return The time this service has been started
+     */
+    long getStartTime();
+
+    /**
+     * Number of successfully finished jobs.
+     * @return Number of successfully finished jobs.
+     */
+    long getNumberOfFinishedJobs();
+
+    /**
+     * Number of permanently failing or cancelled jobs.
+     * @return  Number of permanently failing or cancelled jobs
+     */
+    long getNumberOfCancelledJobs();
+
+    /**
+     * Number of failing jobs.
+     * @return Number of failing jobs.
+     */
+    long getNumberOfFailedJobs();
+
+    /**
+     * Number of already processed jobs. This adds
+     * {@link #getNumberOfFinishedJobs()}, {@link #getNumberOfCancelledJobs()}
+     * and {@link #getNumberOfFailedJobs()}
+     * @return Number of already processed jobs
+     */
+    long getNumberOfProcessedJobs();
+
+    /**
+     * Number of jobs currently in processing.
+     * @return Number of jobs currently in processing.
+     */
+    long getNumberOfActiveJobs();
+
+    /**
+     * Number of jobs currently waiting in a queue.
+     * @return Number of jobs currently waiting in a queue.
+     */
+    long getNumberOfQueuedJobs();
+
+    /**
+     * This just adds {@link #getNumberOfActiveJobs()} and {@link #getNumberOfQueuedJobs()}
+     * @return The number of jobs
+     */
+    long getNumberOfJobs();
+
+    /**
+     * The time a job has been started last.
+     * @return The time a job has been started last.
+     */
+    long getLastActivatedJobTime();
+
+    /**
+     * The time a job has been finished/failed/cancelled last.
+     * @return  The time a job has been finished/failed/cancelled last.
+     */
+    long getLastFinishedJobTime();
+
+    /**
+     * The average waiting time of a job in the queue.
+     * @return The average waiting time of a job in the queue.
+     */
+    long getAverageWaitingTime();
+
+    /**
+     * The average processing time of a job - this only counts finished jobs.
+     * @return The average processing time of a job
+     */
+    long getAverageProcessingTime();
+
+    /**
+     * Clear all collected statistics and set the starting time to the current time.
+     * Note that not all fields are cleared, last waiting time or number of active and queued
+     * jobs is not cleared as these are currently used.
+     */
+    void reset();
+}
diff --git a/src/main/java/org/apache/sling/event/jobs/TopicStatistics.java b/src/main/java/org/apache/sling/event/jobs/TopicStatistics.java
new file mode 100644
index 0000000..0fed27a
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/jobs/TopicStatistics.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.jobs;
+
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Statistic information about a topic.
+ * This information is not preserved between restarts of the service.
+ * Once a service is restarted, the counters start at zero!
+ * @since 3.0
+ */
+@ProviderType
+public interface TopicStatistics {
+
+    /**
+     * The topic this statistics is about.
+     * @return The topic name
+     */
+    String getTopic();
+
+    /**
+     * Number of successfully finished jobs.
+     * @return Number of successfully finished jobs.
+     */
+    long getNumberOfFinishedJobs();
+
+    /**
+     * Number of permanently failing or cancelled jobs.
+     * @return Number of permanently failing or cancelled jobs.
+     */
+    long getNumberOfCancelledJobs();
+
+    /**
+     * Number of failing jobs.
+     * @return Number of failing jobs.
+     */
+    long getNumberOfFailedJobs();
+
+    /**
+     * Number of already processed jobs. This adds
+     * {@link #getNumberOfFinishedJobs()}, {@link #getNumberOfCancelledJobs()}
+     * and {@link #getNumberOfFailedJobs()}
+     * @return Number of already processed jobs.
+     */
+    long getNumberOfProcessedJobs();
+
+    /**
+     * The time a job has been started last.
+     * @return The time a job has been started last.
+     */
+    long getLastActivatedJobTime();
+
+    /**
+     * The time a job has been finished/failed/cancelled last.
+     * @return The time a job has been finished/failed/cancelled last.
+     */
+    long getLastFinishedJobTime();
+
+    /**
+     * The average waiting time of a job in the queue.
+     * @return The average waiting time of a job in the queue.
+     */
+    long getAverageWaitingTime();
+
+    /**
+     * The average processing time of a job - this only counts finished jobs.
+     * @return The average processing time of a job
+     */
+    long getAverageProcessingTime();
+}
diff --git a/src/main/java/org/apache/sling/event/jobs/consumer/JobConsumer.java b/src/main/java/org/apache/sling/event/jobs/consumer/JobConsumer.java
new file mode 100644
index 0000000..8e783d1
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/jobs/consumer/JobConsumer.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.jobs.consumer;
+
+import org.apache.sling.event.jobs.Job;
+
+import org.osgi.annotation.versioning.ConsumerType;
+
+
+
+/**
+ * A job consumer consumes a job.
+ * <p>
+ * If the job consumer needs more features like providing progress information or adding
+ * more information of the processing, {@link JobExecutor} should be implemented instead.
+ * <p>
+ * A job consumer registers itself with the {@link #PROPERTY_TOPICS} service registration
+ * property. The value of this property defines which topics a consumer is able to process.
+ * Each string value of this property is either
+ * <ul>
+ * <li>a job topic, or
+ * <li>a topic category ending with "/*" which means all topics in this category, or
+ * <li>a topic category ending with "/**" which means all topics in this category and all
+ *     sub categories. This matching is new since version 1.2.
+ * </ul>
+ * A consumer registering for just "*" or "**" is not considered.
+ * <p>
+ * For example, the value {@code org/apache/sling/jobs/*} matches the topics
+ * {@code org/apache/sling/jobs/a} and {@code org/apache/sling/jobs/b} but neither
+ * {@code org/apache/sling/jobs} nor {@code org/apache/sling/jobs/subcategory/a}. A value of
+ * {@code org/apache/sling/jobs/**} matches the same topics but also all sub topics
+ * like {@code org/apache/sling/jobs/subcategory/a} or {@code org/apache/sling/jobs/subcategory/a/c/d}.
+ * <p>
+ * If there is more than one job consumer or executor registered for a job topic, the selection is as
+ * follows:
+ * <ul>
+ * <li>If there is a single consumer registering for the exact topic, this one is used.
+ * <li>If there is more than a single consumer registering for the exact topic, the one
+ *     with the highest service ranking is used. If the ranking is equal, the one with
+ *     the lowest service ID is used.
+ * <li>If there is a single consumer registered for the category, it is used.
+ * <li>If there is more than a single consumer registered for the category, the service
+ *     with the highest service ranking is used. If the ranking is equal, the one with
+ *     the lowest service ID is used.
+ * <li>The search continues with consumer registered for deep categories. The nearest one
+ *     is tried next. If there are several, the one with the highest service ranking is
+ *     used. If the ranking is equal, the one with the lowest service ID is used.
+ * </ul>
+ * <p>
+ * If the consumer decides to process the job asynchronously, the processing must finish
+ * within the current lifetime of the job consumer. If the consumer (or the instance
+ * of the consumer) dies, the job processing will mark this processing as failed and
+ * reschedule.
+ *
+ * @since 1.0
+ */
+@ConsumerType
+public interface JobConsumer {
+
+    /**
+     * The result of the job processing.
+     */
+    enum JobResult {
+        /** Processing finished successfully. */
+        OK,
+        /** Processing failed but might be retried. */
+        FAILED,
+        /** Processing failed permanently and must not be retried. */
+        CANCEL,
+        /** Processing will be done asynchronously. */
+        ASYNC
+    }
+
+    /** Job property containing an asynchronous handler. */
+    String PROPERTY_JOB_ASYNC_HANDLER = ":sling:jobs:asynchandler";
+
+    /**
+     * If the consumer decides to process the job asynchronously, this handler
+     * interface can be used to notify finished processing. The asynchronous
+     * handler can be retried using the property name {@link #PROPERTY_JOB_ASYNC_HANDLER}.
+     */
+    interface AsyncHandler {
+
+        void failed();
+
+        void ok();
+
+        void cancel();
+    }
+
+    /**
+     * Service registration property defining the jobs this consumer is able to process.
+     * The value is either a string or an array of strings.
+     */
+    String PROPERTY_TOPICS = "job.topics";
+
+
+    /**
+     * Execute the job.
+     * <p>
+     * If the job has been processed successfully, {@link JobResult#OK} should be returned.
+     * If the job has not been processed completely, but might be rescheduled {@link JobResult#FAILED}
+     * should be returned.
+     * If the job processing failed and should not be rescheduled, {@link JobResult#CANCEL} should
+     * be returned.
+     * <p>
+     * If the consumer decides to process the job asynchronously it should return {@link JobResult#ASYNC}
+     * and notify the job manager by using the {@link AsyncHandler} interface.
+     * <p>
+     * If the processing fails with throwing an exception/throwable, the process will not be rescheduled
+     * and treated like the method would have returned {@link JobResult#CANCEL}.
+     *
+     * @param job The job
+     * @return The job result
+     */
+    JobResult process(Job job);
+}
diff --git a/src/main/java/org/apache/sling/event/jobs/consumer/JobExecutionContext.java b/src/main/java/org/apache/sling/event/jobs/consumer/JobExecutionContext.java
new file mode 100644
index 0000000..9370922
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/jobs/consumer/JobExecutionContext.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.jobs.consumer;
+
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ *
+ * @since 1.1
+ */
+@ProviderType
+public interface JobExecutionContext {
+
+    /**
+     * Report an async result.
+     * @param result Tje job execution result
+     * @throws IllegalStateException If the job is not processed asynchronously
+     *                               or if this method has already been called.
+     */
+    void asyncProcessingFinished(JobExecutionResult result);
+
+    /**
+     * If a job is stoppable, it should periodically check this method
+     * and stop processing if the method return <code>true</code>.
+     * If a job is stopped and the job executor detects this, its up
+     * to the implementation to decide the result of such a state.
+     * There might be use cases where the job returns {@link JobExecutionResult#succeeded()}
+     * although it didn't process everything, or {@link JobExecutionResult#failed()}
+     * to retry later on or {@link JobExecutionResult#cancelled()}.
+     *
+     * @return Whether this job has been stopped from the outside.
+     */
+    boolean isStopped();
+
+    /**
+     * Indicate that the job executor is able to report the progress.
+     * The progress can either be reported by a step count,
+     * assuming that all steps take roughly the same amount of time.
+     * Or the progress can be reported by an ETA containing the number
+     * of seconds the job needs to finish.
+     * This method should only be called once, consecutive calls
+     * have no effect.
+     * By using a step count of 100, the progress can be displayed
+     * in percentage.
+     * @param steps Number of total steps or -1 if the number of
+     *              steps is unknown.
+     * @param eta Number of seconds the process should take or
+     *        -1 of it's not known now.
+     */
+    void initProgress(int steps, long eta);
+
+    /**
+     * Update the progress by additionally marking the provided
+     * number of steps as finished. If the total number of finished
+     * steps is equal or higher to the initial number of steps
+     * reported in {@link #initProgress(int, long)}, then the
+     * job progress is assumed to be 100%.
+     * This method has only effect if {@link #initProgress(int, long)}
+     * has been called first with a positive number for steps
+     * @param steps The number of finished steps since the last call.
+     */
+    void incrementProgressCount(int steps);
+
+    /**
+     * Update the progress to the new ETA.
+     * This method has only effect if {@link #initProgress(int, long)}
+     * has been called first.
+     * @param eta The new ETA
+     */
+    void updateProgress(long eta);
+
+    /**
+     * Log a message.
+     * A job consumer can use this method during job processing to add additional information
+     * about the current state of job processing.
+     * As calling this method adds a significant overhead it should only
+     * be used to log a few statements per job processing. If a consumer wants
+     * to output detailed information about the processing it should persists it
+     * by itself and not use this method for it.
+     * The message and the arguments are passed to the {@link java.text.MessageFormat}
+     * class.
+     * @param message A message
+     * @param args Additional arguments
+     */
+    void log(String message, Object...args);
+
+    /**
+     * Build a result for the processing.
+     * @return The build for the result
+     */
+    ResultBuilder result();
+
+    public interface ResultBuilder {
+
+        /**
+         * Add an optional processing message.
+         * This message can be viewed using {@link org.apache.sling.event.jobs.Job#getResultMessage()}.
+         * @param message The message
+         * @return The builder to continue building the result.
+         */
+        ResultBuilder message(String message);
+
+        /**
+         * The job processing finished successfully.
+         * @return The job execution result.
+         */
+        JobExecutionResult succeeded();
+
+        /**
+         * The job processing failed and might be retried.
+         * @return The job execution result.
+         */
+        JobExecutionResult failed();
+
+        /**
+         * The job processing failed and might be retried.
+         * @param retryDelayInMs The new retry delay in ms.
+         * @return The job execution result
+         */
+        JobExecutionResult failed(long retryDelayInMs);
+
+        /**
+         * The job processing failed permanently.
+         * @return The job execution result
+         */
+        JobExecutionResult cancelled();
+    }
+}
diff --git a/src/main/java/org/apache/sling/event/jobs/consumer/JobExecutionResult.java b/src/main/java/org/apache/sling/event/jobs/consumer/JobExecutionResult.java
new file mode 100644
index 0000000..c3da0f0
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/jobs/consumer/JobExecutionResult.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.jobs.consumer;
+
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * The status of a job after it has been processed by a {@link JobExecutor}.
+ * The job executor uses the {@link JobExecutionContext} to create a result object.
+ *
+ * The result can have three states, succeeded, cancelled or failed whereas
+ * failed means that the execution is potentially retried.
+ *
+ * @since 1.1
+ */
+@ProviderType
+public interface JobExecutionResult {
+
+    /**
+     * If this returns true the job processing finished successfully.
+     * In this case {@link #cancelled()} and {@link #failed()} return
+     * <code>false</code>
+     * @return <code>true</code> for a successful processing
+     */
+    boolean succeeded();
+
+    /**
+     * If this returns true the job processing failed permanently.
+     * In this case {@link #succeeded()} and {@link #failed()} return
+     * <code>false</code>
+     * @return <code>true</code> for a permanently failed processing
+     */
+    boolean cancelled();
+
+    /**
+     * If this returns true the job processing failed but might be
+     * retried..
+     * In this case {@link #cancelled()} and {@link #succeeded()} return
+     * <code>false</code>
+     * @return <code>true</code> for a failedl processing
+     */
+    boolean failed();
+
+    /**
+     * Return the optional message.
+     * @return The message or <code>null</code>
+     */
+    String getMessage();
+
+    /**
+     * Return the retry delay in ms
+     * @return The new retry delay (&gt;= 0) or <code>null</code>
+     */
+    Long getRetryDelayInMs();
+}
diff --git a/src/main/java/org/apache/sling/event/jobs/consumer/JobExecutor.java b/src/main/java/org/apache/sling/event/jobs/consumer/JobExecutor.java
new file mode 100644
index 0000000..b720546
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/jobs/consumer/JobExecutor.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.jobs.consumer;
+
+import org.apache.sling.event.jobs.Job;
+
+import org.osgi.annotation.versioning.ConsumerType;
+
+/**
+ * A job executor consumes a job.
+ * <p>
+ * A job executor registers itself with the {@link #PROPERTY_TOPICS} service registration
+ * property. The value of this property defines which topics an executor is able to process.
+ * Each string value of this property is either
+ * <ul>
+ * <li>a job topic, or
+ * <li>a topic category ending with "/*" which means all topics in this category, or
+ * <li>a topic category ending with "/**" which means all topics in this category and all
+ *     sub categories. This matching is new since version 1.2.
+ * </ul>
+ * A consumer registering for just "*" or "**" is not considered.
+ * <p>
+ * For example, the value {@code org/apache/sling/jobs/*} matches the topics
+ * {@code org/apache/sling/jobs/a} and {@code org/apache/sling/jobs/b} but neither
+ * {@code org/apache/sling/jobs} nor {@code org/apache/sling/jobs/subcategory/a}. A value of
+ * {@code org/apache/sling/jobs/**} matches the same topics but also all sub topics
+ * like {@code org/apache/sling/jobs/subcategory/a} or {@code org/apache/sling/jobs/subcategory/a/c/d}.
+ * <p>
+ * If there is more than one job consumer or executor registered for a job topic, the selection is as
+ * follows:
+ * <ul>
+ * <li>If there is a single consumer registering for the exact topic, this one is used.
+ * <li>If there is more than a single consumer registering for the exact topic, the one
+ *     with the highest service ranking is used. If the ranking is equal, the one with
+ *     the lowest service ID is used.
+ * <li>If there is a single consumer registered for the category, it is used.
+ * <li>If there is more than a single consumer registered for the category, the service
+ *     with the highest service ranking is used. If the ranking is equal, the one with
+ *     the lowest service ID is used.
+ * <li>The search continues with consumer registered for deep categories. The nearest one
+ *     is tried next. If there are several, the one with the highest service ranking is
+ *     used. If the ranking is equal, the one with the lowest service ID is used.
+ * </ul>
+ * <p>
+ * If the executor decides to process the job asynchronously, the processing must finish
+ * within the current lifetime of the job executor. If the executor (or the instance
+ * of the executor) dies, the job processing will mark this processing as failed and
+ * reschedule.
+ *
+ * @since 1.1
+ */
+@ConsumerType
+public interface JobExecutor {
+
+    /**
+     * Service registration property defining the jobs this executor is able to process.
+     * The value is either a string or an array of strings.
+     */
+    String PROPERTY_TOPICS = "job.topics";
+
+    /**
+     * Execute the job.
+     *
+     * If the job has been processed successfully, a job result of "succeeded" should be returned. This result can
+     * be generated by calling <code>JobExecutionContext.result().succeeded()</code>
+     *
+     * If the job has not been processed completely, but might be rescheduled "failed" should be returned.
+     * This result can be generated by calling <code>JobExecutionContext.result().failed()</code>.
+     *
+     * If the job processing failed and should not be rescheduled, "cancelled" should be returned.
+     * This result can be generated by calling <code>JobExecutionContext.result().cancelled()</code>.
+     *
+     * If the executor decides to process the job asynchronously it should return <code>null</code>
+     * and notify the job manager by using the {@link JobExecutionContext#asyncProcessingFinished(JobExecutionResult)}
+     * method of the processing result.
+     *
+     * If the processing fails with throwing an exception/throwable, the process will not be rescheduled
+     * and treated like the method would have returned a "cancelled" result.
+     *
+     * Additional information can be added to the result by using the builder pattern available
+     * from {@link JobExecutionContext#result()}.
+     *
+     * @param job The job
+     * @param context The execution context.
+     * @return The job execution result or <code>null</code> for asynchronous processing.
+     */
+    JobExecutionResult process(Job job, JobExecutionContext context);
+}
diff --git a/src/main/java/org/apache/sling/event/jobs/consumer/package-info.java b/src/main/java/org/apache/sling/event/jobs/consumer/package-info.java
new file mode 100644
index 0000000..5237caa
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/jobs/consumer/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+@org.osgi.annotation.versioning.Version("1.2.1")
+package org.apache.sling.event.jobs.consumer;
+
+
diff --git a/src/main/java/org/apache/sling/event/jobs/jmx/QueuesMBean.java b/src/main/java/org/apache/sling/event/jobs/jmx/QueuesMBean.java
new file mode 100644
index 0000000..9e8af7d
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/jobs/jmx/QueuesMBean.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.sling.event.jobs.jmx;
+
+/**
+ * A Marker interface to allow the implementation to register as a service with
+ * the JMX whiteboard.
+ */
+public interface QueuesMBean {
+
+    String[] getQueueNames();
+
+}
diff --git a/src/main/java/org/apache/sling/event/jobs/jmx/StatisticsMBean.java b/src/main/java/org/apache/sling/event/jobs/jmx/StatisticsMBean.java
new file mode 100644
index 0000000..321f574
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/jobs/jmx/StatisticsMBean.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.sling.event.jobs.jmx;
+
+import java.util.Date;
+
+import org.apache.sling.event.jobs.Statistics;
+
+public interface StatisticsMBean extends Statistics {
+
+    Date getLastActivatedJobDate();
+
+    Date getLastFinishedJobDate();
+    
+    Date getStartDate();
+
+    String getName();
+
+}
diff --git a/src/main/java/org/apache/sling/event/jobs/jmx/package-info.java b/src/main/java/org/apache/sling/event/jobs/jmx/package-info.java
new file mode 100644
index 0000000..515bde1
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/jobs/jmx/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+@org.osgi.annotation.versioning.Version("1.0.1")
+package org.apache.sling.event.jobs.jmx;
+
+
diff --git a/src/main/java/org/apache/sling/event/jobs/package-info.java b/src/main/java/org/apache/sling/event/jobs/package-info.java
new file mode 100644
index 0000000..6ea3e5c
--- /dev/null
+++ b/src/main/java/org/apache/sling/event/jobs/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+@org.osgi.annotation.versioning.Version("2.0.1")
+package org.apache.sling.event.jobs;
+
+
diff --git a/src/main/resources/SLING-INF/nodetypes/event.cnd b/src/main/resources/SLING-INF/nodetypes/event.cnd
new file mode 100644
index 0000000..b29d76e
--- /dev/null
+++ b/src/main/resources/SLING-INF/nodetypes/event.cnd
@@ -0,0 +1,42 @@
+//
+//  Licensed to the Apache Software Foundation (ASF) under one
+//  or more contributor license agreements.  See the NOTICE file
+//  distributed with this work for additional information
+//  regarding copyright ownership.  The ASF licenses this file
+//  to you under the Apache License, Version 2.0 (the
+//  "License"); you may not use this file except in compliance
+//  with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing,
+//  software distributed under the License is distributed on an
+//  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+//  KIND, either express or implied.  See the License for the
+//  specific language governing permissions and limitations
+//  under the License.
+//
+
+<slingevent='http://sling.apache.org/jcr/event/1.0'>
+<nt='http://www.jcp.org/jcr/nt/1.0'>
+<mix='http://www.jcp.org/jcr/mix/1.0'>
+
+[slingevent:Event] > nt:unstructured, nt:hierarchyNode
+  - slingevent:topic (string)
+  - slingevent:application (string)
+  - slingevent:created (date)
+  - slingevent:properties (binary)
+  
+[slingevent:Job] > slingevent:Event, mix:lockable
+  - slingevent:processor (string)
+  - slingevent:id (string)
+  - slingevent:finished (date)
+ 
+[slingevent:TimedEvent] > slingevent:Event, mix:lockable
+  - slingevent:processor (string)
+  - slingevent:id (string)
+  - slingevent:expression (string)
+  - slingevent:date (date)
+  - slingevent:period (long)
+
+  
diff --git a/src/test/java/org/apache/sling/event/impl/Barrier.java b/src/test/java/org/apache/sling/event/impl/Barrier.java
new file mode 100644
index 0000000..20d4ace
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/impl/Barrier.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl;
+
+import java.util.concurrent.BrokenBarrierException;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/** Simplified version of the cyclic barrier class for testing. */
+public class Barrier extends CyclicBarrier {
+
+    public Barrier(int parties) {
+        super(parties);
+    }
+
+    public void block() {
+        try {
+            this.await();
+        } catch (InterruptedException e) {
+            // ignore
+        } catch (BrokenBarrierException e) {
+            // ignore
+        }
+    }
+
+    public boolean block(int seconds) {
+        try {
+            this.await(seconds, TimeUnit.SECONDS);
+            return true;
+        } catch (InterruptedException e) {
+            // ignore
+        } catch (BrokenBarrierException e) {
+            // ignore
+        } catch (TimeoutException e) {
+            // ignore
+        }
+        this.reset();
+        return false;
+    }
+}
diff --git a/src/test/java/org/apache/sling/event/impl/TestUtil.java b/src/test/java/org/apache/sling/event/impl/TestUtil.java
new file mode 100644
index 0000000..4be099f
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/impl/TestUtil.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl;
+
+import java.lang.reflect.Field;
+
+public class TestUtil {
+
+    private static Object getSetField(final Object obj, final String fieldName, final boolean isGet, final Object value) {
+        Class<?> clazz = obj.getClass();
+        while ( clazz != null ) {
+            try {
+                final Field field = clazz.getDeclaredField(fieldName);
+                field.setAccessible(true);
+
+                if ( isGet ) {
+                    return field.get(obj);
+                } else {
+                    field.set(obj, value);
+                    return null;
+                }
+            } catch ( final Exception ignore ) {
+                // ignore
+            }
+            clazz = clazz.getSuperclass();
+        }
+        throw new RuntimeException("Field " + fieldName + " not found on object " + obj);
+    }
+
+    public static void setFieldValue(final Object obj, final String fieldName, final Object value) {
+        getSetField(obj, fieldName, false, value);
+    }
+
+    public static Object getFieldValue(final Object obj, final String fieldName) {
+        return getSetField(obj, fieldName, true, null);
+    }
+}
diff --git a/src/test/java/org/apache/sling/event/impl/jobs/InstanceDescriptionComparatorTest.java b/src/test/java/org/apache/sling/event/impl/jobs/InstanceDescriptionComparatorTest.java
new file mode 100644
index 0000000..77caac8
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/impl/jobs/InstanceDescriptionComparatorTest.java
@@ -0,0 +1,199 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.event.impl.jobs.config.TopologyCapabilities;
+
+public class InstanceDescriptionComparatorTest {
+
+
+    @org.junit.Test public void testSingleClusterThreeInstances() {
+        final Instance cl1in1 = new Instance("1", "A", false, true);
+        final Instance cl1in2 = new Instance("1", "B", true, false);
+        final Instance cl1in3 = new Instance("1", "C", false, false);
+
+        final List<InstanceDescription> desc = new ArrayList<InstanceDescription>();
+        desc.add(cl1in2);
+        desc.add(cl1in1);
+        desc.add(cl1in3);
+        Collections.sort(desc, new TopologyCapabilities.InstanceDescriptionComparator("1"));
+
+        assertEquals("Instance0: ", cl1in2.getSlingId(), desc.get(0).getSlingId());
+        assertEquals("Instance1: ", cl1in1.getSlingId(), desc.get(1).getSlingId());
+        assertEquals("Instance2: ", cl1in3.getSlingId(), desc.get(2).getSlingId());
+    }
+
+    @org.junit.Test public void testTwoClustersThreeInstances() {
+        final Instance cl1in1 = new Instance("1", "A", false, true);
+        final Instance cl1in2 = new Instance("1", "B", true, false);
+        final Instance cl1in3 = new Instance("1", "C", false, false);
+        final Instance cl2in1 = new Instance("2", "D", false, false);
+        final Instance cl2in2 = new Instance("2", "E", false, false);
+        final Instance cl2in3 = new Instance("2", "F", true, false);
+
+        final List<InstanceDescription> desc = new ArrayList<InstanceDescription>();
+        desc.add(cl2in3);
+        desc.add(cl1in2);
+        desc.add(cl2in1);
+        desc.add(cl1in3);
+        desc.add(cl2in2);
+        desc.add(cl1in1);
+        Collections.sort(desc, new TopologyCapabilities.InstanceDescriptionComparator("1"));
+
+        assertEquals("Instance0: ", cl1in2.getSlingId(), desc.get(0).getSlingId());
+        assertEquals("Instance1: ", cl1in1.getSlingId(), desc.get(1).getSlingId());
+        assertEquals("Instance2: ", cl1in3.getSlingId(), desc.get(2).getSlingId());
+        assertEquals("Instance3: ", cl2in1.getSlingId(), desc.get(3).getSlingId());
+        assertEquals("Instance4: ", cl2in2.getSlingId(), desc.get(4).getSlingId());
+        assertEquals("Instance5: ", cl2in3.getSlingId(), desc.get(5).getSlingId());
+
+        Collections.sort(desc, new TopologyCapabilities.InstanceDescriptionComparator("2"));
+        assertEquals("Instance0: ", cl2in3.getSlingId(), desc.get(0).getSlingId());
+        assertEquals("Instance1: ", cl2in1.getSlingId(), desc.get(1).getSlingId());
+        assertEquals("Instance2: ", cl2in2.getSlingId(), desc.get(2).getSlingId());
+        assertEquals("Instance3: ", cl1in1.getSlingId(), desc.get(3).getSlingId());
+        assertEquals("Instance4: ", cl1in2.getSlingId(), desc.get(4).getSlingId());
+        assertEquals("Instance5: ", cl1in3.getSlingId(), desc.get(5).getSlingId());
+    }
+
+    @org.junit.Test public void testThreeClustersThreeInstances() {
+        final Instance cl1in1 = new Instance("1", "A", false, true);
+        final Instance cl1in2 = new Instance("1", "B", true, false);
+        final Instance cl1in3 = new Instance("1", "C", false, false);
+        final Instance cl15in1 = new Instance("15", "Z", true, false);
+        final Instance cl2in1 = new Instance("2", "D", true, false);
+        final Instance cl2in2 = new Instance("2", "E", false, false);
+        final Instance cl2in3 = new Instance("2", "F", false, false);
+
+        final List<InstanceDescription> desc = new ArrayList<InstanceDescription>();
+        desc.add(cl2in3);
+        desc.add(cl1in2);
+        desc.add(cl2in1);
+        desc.add(cl15in1);
+        desc.add(cl1in3);
+        desc.add(cl2in2);
+        desc.add(cl1in1);
+        Collections.sort(desc, new TopologyCapabilities.InstanceDescriptionComparator("1"));
+
+        assertEquals("Instance0: ", cl1in2.getSlingId(), desc.get(0).getSlingId());
+        assertEquals("Instance1: ", cl1in1.getSlingId(), desc.get(1).getSlingId());
+        assertEquals("Instance2: ", cl1in3.getSlingId(), desc.get(2).getSlingId());
+        assertEquals("Instance3: ", cl2in1.getSlingId(), desc.get(3).getSlingId());
+        assertEquals("Instance4: ", cl2in2.getSlingId(), desc.get(4).getSlingId());
+        assertEquals("Instance5: ", cl2in3.getSlingId(), desc.get(5).getSlingId());
+        assertEquals("Instance6: ", cl15in1.getSlingId(), desc.get(6).getSlingId());
+
+        Collections.sort(desc, new TopologyCapabilities.InstanceDescriptionComparator("2"));
+        assertEquals("Instance0: ", cl2in1.getSlingId(), desc.get(0).getSlingId());
+        assertEquals("Instance1: ", cl2in2.getSlingId(), desc.get(1).getSlingId());
+        assertEquals("Instance2: ", cl2in3.getSlingId(), desc.get(2).getSlingId());
+        assertEquals("Instance3: ", cl1in1.getSlingId(), desc.get(3).getSlingId());
+        assertEquals("Instance4: ", cl1in2.getSlingId(), desc.get(4).getSlingId());
+        assertEquals("Instance5: ", cl1in3.getSlingId(), desc.get(5).getSlingId());
+        assertEquals("Instance6: ", cl15in1.getSlingId(), desc.get(6).getSlingId());
+
+        Collections.sort(desc, new TopologyCapabilities.InstanceDescriptionComparator("15"));
+        assertEquals("Instance0: ", cl15in1.getSlingId(), desc.get(0).getSlingId());
+        assertEquals("Instance1: ", cl1in1.getSlingId(), desc.get(1).getSlingId());
+        assertEquals("Instance2: ", cl1in2.getSlingId(), desc.get(2).getSlingId());
+        assertEquals("Instance3: ", cl1in3.getSlingId(), desc.get(3).getSlingId());
+        assertEquals("Instance4: ", cl2in1.getSlingId(), desc.get(4).getSlingId());
+        assertEquals("Instance5: ", cl2in2.getSlingId(), desc.get(5).getSlingId());
+        assertEquals("Instance6: ", cl2in3.getSlingId(), desc.get(6).getSlingId());
+
+        Collections.sort(desc, new TopologyCapabilities.InstanceDescriptionComparator("4"));
+        assertEquals("Instance0: ", cl1in1.getSlingId(), desc.get(0).getSlingId());
+        assertEquals("Instance1: ", cl1in2.getSlingId(), desc.get(1).getSlingId());
+        assertEquals("Instance2: ", cl1in3.getSlingId(), desc.get(2).getSlingId());
+        assertEquals("Instance3: ", cl2in1.getSlingId(), desc.get(3).getSlingId());
+        assertEquals("Instance4: ", cl2in2.getSlingId(), desc.get(4).getSlingId());
+        assertEquals("Instance5: ", cl2in3.getSlingId(), desc.get(5).getSlingId());
+        assertEquals("Instance6: ", cl15in1.getSlingId(), desc.get(6).getSlingId());
+    }
+
+    private static final class Instance implements InstanceDescription {
+
+        private final String clusterId;
+        private final String instanceId;
+        private final boolean isLeader;
+        private final boolean isLocal;
+
+        public Instance(final String clusterId, final String instanceId, final boolean isLeader, final boolean isLocal) {
+            this.clusterId = clusterId;
+            this.instanceId = instanceId;
+            this.isLeader = isLeader;
+            this.isLocal = isLocal;
+        }
+
+        @Override
+        public ClusterView getClusterView() {
+            return new ClusterView() {
+
+                @Override
+                public InstanceDescription getLeader() {
+                    return null;
+                }
+
+                @Override
+                public List<InstanceDescription> getInstances() {
+                    return null;
+                }
+
+                @Override
+                public String getId() {
+                    return clusterId;
+                }
+            };
+        }
+
+        @Override
+        public boolean isLeader() {
+            return this.isLeader;
+        }
+
+        @Override
+        public boolean isLocal() {
+            return this.isLocal;
+        }
+
+        @Override
+        public String getSlingId() {
+            return this.instanceId;
+        }
+
+        @Override
+        public String getProperty(String name) {
+            return null;
+        }
+
+        @Override
+        public Map<String, String> getProperties() {
+            return null;
+        }
+    }
+}
diff --git a/src/test/java/org/apache/sling/event/impl/jobs/JobConsumerManagerTest.java b/src/test/java/org/apache/sling/event/impl/jobs/JobConsumerManagerTest.java
new file mode 100644
index 0000000..f976767
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/impl/jobs/JobConsumerManagerTest.java
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.util.Collections;
+
+import org.apache.sling.event.jobs.consumer.JobConsumer;
+import org.apache.sling.event.jobs.consumer.JobExecutor;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+
+public class JobConsumerManagerTest {
+
+    @Test public void testSimpleMappingConsumer() {
+        final BundleContext bc = Mockito.mock(BundleContext.class);
+        final JobConsumerManager jcs = new JobConsumerManager();
+        jcs.activate(bc, Collections.EMPTY_MAP);
+
+        final JobConsumer jc1 = Mockito.mock(JobConsumer.class);
+        final ServiceReference ref1 = Mockito.mock(ServiceReference.class);
+        Mockito.when(ref1.getProperty(JobConsumer.PROPERTY_TOPICS)).thenReturn("a/b");
+        Mockito.when(ref1.getProperty(Constants.SERVICE_RANKING)).thenReturn(1);
+        Mockito.when(ref1.getProperty(Constants.SERVICE_ID)).thenReturn(1L);
+        Mockito.when(bc.getService(ref1)).thenReturn(jc1);
+        jcs.bindJobConsumer(ref1);
+
+        assertNotNull(jcs.getExecutor("a/b"));
+        assertNull(jcs.getExecutor("a"));
+        assertNull(jcs.getExecutor("a/c"));
+        assertNull(jcs.getExecutor("a/b/a"));
+    }
+
+    @Test public void testCategoryMappingConsumer() {
+        final BundleContext bc = Mockito.mock(BundleContext.class);
+        final JobConsumerManager jcs = new JobConsumerManager();
+        jcs.activate(bc, Collections.EMPTY_MAP);
+
+        final JobConsumer jc1 = Mockito.mock(JobConsumer.class);
+        final ServiceReference ref1 = Mockito.mock(ServiceReference.class);
+        Mockito.when(ref1.getProperty(JobConsumer.PROPERTY_TOPICS)).thenReturn("a/*");
+        Mockito.when(ref1.getProperty(Constants.SERVICE_RANKING)).thenReturn(1);
+        Mockito.when(ref1.getProperty(Constants.SERVICE_ID)).thenReturn(1L);
+        Mockito.when(bc.getService(ref1)).thenReturn(jc1);
+        jcs.bindJobConsumer(ref1);
+
+        assertNotNull(jcs.getExecutor("a/b"));
+        assertNull(jcs.getExecutor("a"));
+        assertNotNull(jcs.getExecutor("a/c"));
+        assertNull(jcs.getExecutor("a/b/a"));
+    }
+
+    @Test public void testSubCategoryMappingConsumer() {
+        final BundleContext bc = Mockito.mock(BundleContext.class);
+        final JobConsumerManager jcs = new JobConsumerManager();
+        jcs.activate(bc, Collections.EMPTY_MAP);
+
+        final JobConsumer jc1 = Mockito.mock(JobConsumer.class);
+        final ServiceReference ref1 = Mockito.mock(ServiceReference.class);
+        Mockito.when(ref1.getProperty(JobConsumer.PROPERTY_TOPICS)).thenReturn("a/**");
+        Mockito.when(ref1.getProperty(Constants.SERVICE_RANKING)).thenReturn(1);
+        Mockito.when(ref1.getProperty(Constants.SERVICE_ID)).thenReturn(1L);
+        Mockito.when(bc.getService(ref1)).thenReturn(jc1);
+        jcs.bindJobConsumer(ref1);
+
+        assertNotNull(jcs.getExecutor("a/b"));
+        assertNull(jcs.getExecutor("a"));
+        assertNotNull(jcs.getExecutor("a/c"));
+        assertNotNull(jcs.getExecutor("a/b/a"));
+    }
+
+    @Test public void testSimpleMappingExecutor() {
+        final BundleContext bc = Mockito.mock(BundleContext.class);
+        final JobConsumerManager jcs = new JobConsumerManager();
+        jcs.activate(bc, Collections.EMPTY_MAP);
+
+        final JobExecutor jc1 = Mockito.mock(JobExecutor.class);
+        final ServiceReference ref1 = Mockito.mock(ServiceReference.class);
+        Mockito.when(ref1.getProperty(JobConsumer.PROPERTY_TOPICS)).thenReturn("a/b");
+        Mockito.when(ref1.getProperty(Constants.SERVICE_RANKING)).thenReturn(1);
+        Mockito.when(ref1.getProperty(Constants.SERVICE_ID)).thenReturn(1L);
+        Mockito.when(bc.getService(ref1)).thenReturn(jc1);
+        jcs.bindJobExecutor(ref1);
+
+        assertNotNull(jcs.getExecutor("a/b"));
+        assertNull(jcs.getExecutor("a"));
+        assertNull(jcs.getExecutor("a/c"));
+        assertNull(jcs.getExecutor("a/b/a"));
+    }
+
+    @Test public void testCategoryMappingExecutor() {
+        final BundleContext bc = Mockito.mock(BundleContext.class);
+        final JobConsumerManager jcs = new JobConsumerManager();
+        jcs.activate(bc, Collections.EMPTY_MAP);
+
+        final JobExecutor jc1 = Mockito.mock(JobExecutor.class);
+        final ServiceReference ref1 = Mockito.mock(ServiceReference.class);
+        Mockito.when(ref1.getProperty(JobExecutor.PROPERTY_TOPICS)).thenReturn("a/*");
+        Mockito.when(ref1.getProperty(Constants.SERVICE_RANKING)).thenReturn(1);
+        Mockito.when(ref1.getProperty(Constants.SERVICE_ID)).thenReturn(1L);
+        Mockito.when(bc.getService(ref1)).thenReturn(jc1);
+        jcs.bindJobExecutor(ref1);
+
+        assertNotNull(jcs.getExecutor("a/b"));
+        assertNull(jcs.getExecutor("a"));
+        assertNotNull(jcs.getExecutor("a/c"));
+        assertNull(jcs.getExecutor("a/b/a"));
+    }
+
+    @Test public void testSubCategoryMappingExecutor() {
+        final BundleContext bc = Mockito.mock(BundleContext.class);
+        final JobConsumerManager jcs = new JobConsumerManager();
+        jcs.activate(bc, Collections.EMPTY_MAP);
+
+        final JobExecutor jc1 = Mockito.mock(JobExecutor.class);
+        final ServiceReference ref1 = Mockito.mock(ServiceReference.class);
+        Mockito.when(ref1.getProperty(JobExecutor.PROPERTY_TOPICS)).thenReturn("a/**");
+        Mockito.when(ref1.getProperty(Constants.SERVICE_RANKING)).thenReturn(1);
+        Mockito.when(ref1.getProperty(Constants.SERVICE_ID)).thenReturn(1L);
+        Mockito.when(bc.getService(ref1)).thenReturn(jc1);
+        jcs.bindJobExecutor(ref1);
+
+        assertNotNull(jcs.getExecutor("a/b"));
+        assertNull(jcs.getExecutor("a"));
+        assertNotNull(jcs.getExecutor("a/c"));
+        assertNotNull(jcs.getExecutor("a/b/a"));
+    }
+
+    @Test public void testRanking() {
+        final BundleContext bc = Mockito.mock(BundleContext.class);
+        final JobConsumerManager jcs = new JobConsumerManager();
+        jcs.activate(bc, Collections.EMPTY_MAP);
+
+        final JobExecutor jc1 = Mockito.mock(JobExecutor.class);
+        final JobExecutor jc2 = Mockito.mock(JobExecutor.class);
+        final JobExecutor jc3 = Mockito.mock(JobExecutor.class);
+        final JobExecutor jc4 = Mockito.mock(JobExecutor.class);
+        final ServiceReference ref1 = Mockito.mock(ServiceReference.class);
+        Mockito.when(ref1.getProperty(JobExecutor.PROPERTY_TOPICS)).thenReturn("a/b");
+        Mockito.when(ref1.getProperty(Constants.SERVICE_RANKING)).thenReturn(1);
+        Mockito.when(ref1.getProperty(Constants.SERVICE_ID)).thenReturn(1L);
+        Mockito.when(bc.getService(ref1)).thenReturn(jc1);
+        jcs.bindJobExecutor(ref1);
+        assertEquals(jc1, jcs.getExecutor("a/b"));
+
+        final ServiceReference ref2 = Mockito.mock(ServiceReference.class);
+        Mockito.when(ref2.getProperty(JobExecutor.PROPERTY_TOPICS)).thenReturn("a/b");
+        Mockito.when(ref2.getProperty(Constants.SERVICE_RANKING)).thenReturn(10);
+        Mockito.when(ref2.getProperty(Constants.SERVICE_ID)).thenReturn(2L);
+        Mockito.when(bc.getService(ref2)).thenReturn(jc2);
+        jcs.bindJobExecutor(ref2);
+        assertEquals(jc2, jcs.getExecutor("a/b"));
+
+        final ServiceReference ref3 = Mockito.mock(ServiceReference.class);
+        Mockito.when(ref3.getProperty(JobExecutor.PROPERTY_TOPICS)).thenReturn("a/b");
+        Mockito.when(ref3.getProperty(Constants.SERVICE_RANKING)).thenReturn(5);
+        Mockito.when(ref3.getProperty(Constants.SERVICE_ID)).thenReturn(3L);
+        Mockito.when(bc.getService(ref3)).thenReturn(jc3);
+        jcs.bindJobExecutor(ref3);
+        assertEquals(jc2, jcs.getExecutor("a/b"));
+
+        final ServiceReference ref4 = Mockito.mock(ServiceReference.class);
+        Mockito.when(ref4.getProperty(JobExecutor.PROPERTY_TOPICS)).thenReturn("a/b");
+        Mockito.when(ref4.getProperty(Constants.SERVICE_RANKING)).thenReturn(5);
+        Mockito.when(ref4.getProperty(Constants.SERVICE_ID)).thenReturn(4L);
+        Mockito.when(bc.getService(ref4)).thenReturn(jc4);
+        jcs.bindJobExecutor(ref4);
+        assertEquals(jc2, jcs.getExecutor("a/b"));
+
+        jcs.unbindJobExecutor(ref2);
+        assertEquals(jc3, jcs.getExecutor("a/b"));
+
+        jcs.unbindJobExecutor(ref3);
+        assertEquals(jc4, jcs.getExecutor("a/b"));
+    }
+}
diff --git a/src/test/java/org/apache/sling/event/impl/jobs/JobsImplTest.java b/src/test/java/org/apache/sling/event/impl/jobs/JobsImplTest.java
new file mode 100644
index 0000000..4a540b0
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/impl/jobs/JobsImplTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.event.jobs.Job;
+import org.junit.Test;
+
+public class JobsImplTest {
+
+    @Test public void testSorting() {
+        final Calendar now = Calendar.getInstance();
+        final Map<String, Object> properties = new HashMap<String, Object>();
+        properties.put(Job.PROPERTY_JOB_CREATED, now);
+
+        final JobImpl job1 = new JobImpl("test", "hello_1", properties);
+        final JobImpl job2 = new JobImpl("test", "hello_2", properties);
+        final JobImpl job3 = new JobImpl("test", "hello_4", properties);
+        final JobImpl job4 = new JobImpl("test", "hello_30", properties);
+        final JobImpl job5 = new JobImpl("test", "hello_50", properties);
+
+        final List<JobImpl> list = new ArrayList<JobImpl>();
+        list.add(job5);
+        list.add(job2);
+        list.add(job1);
+        list.add(job4);
+        list.add(job3);
+
+        Collections.sort(list);
+
+        assertEquals(job1, list.get(0));
+        assertEquals(job2, list.get(1));
+        assertEquals(job3, list.get(2));
+        assertEquals(job4, list.get(3));
+        assertEquals(job5, list.get(4));
+
+    }
+}
diff --git a/src/test/java/org/apache/sling/event/impl/jobs/StatisticsImplTest.java b/src/test/java/org/apache/sling/event/impl/jobs/StatisticsImplTest.java
new file mode 100644
index 0000000..cf3b12d
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/impl/jobs/StatisticsImplTest.java
@@ -0,0 +1,268 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.sling.event.impl.jobs.stats.StatisticsImpl;
+
+public class StatisticsImplTest {
+
+    protected StatisticsImpl stat;
+
+    static long START_TIME = System.currentTimeMillis();
+
+    @org.junit.Before public void setup() {
+        this.stat = new StatisticsImpl();
+    }
+
+    @org.junit.Test public void testInitial() {
+        assertTrue(this.stat.getStartTime() >= START_TIME);
+        assertEquals(0, this.stat.getAverageProcessingTime());
+        assertEquals(0, this.stat.getAverageWaitingTime());
+        assertEquals(0, this.stat.getNumberOfActiveJobs());
+        assertEquals(0, this.stat.getNumberOfCancelledJobs());
+        assertEquals(0, this.stat.getNumberOfFailedJobs());
+        assertEquals(0, this.stat.getNumberOfFinishedJobs());
+        assertEquals(0, this.stat.getNumberOfJobs());
+        assertEquals(0, this.stat.getNumberOfProcessedJobs());
+        assertEquals(0, this.stat.getNumberOfQueuedJobs());
+        assertEquals(-1, this.stat.getLastActivatedJobTime());
+        assertEquals(-1, this.stat.getLastFinishedJobTime());
+    }
+
+    @org.junit.Test public void testIncDecQueued() {
+        this.stat.incQueued();
+        assertTrue(this.stat.getStartTime() >= START_TIME);
+        assertEquals(0, this.stat.getAverageProcessingTime());
+        assertEquals(0, this.stat.getAverageWaitingTime());
+        assertEquals(0, this.stat.getNumberOfActiveJobs());
+        assertEquals(0, this.stat.getNumberOfCancelledJobs());
+        assertEquals(0, this.stat.getNumberOfFailedJobs());
+        assertEquals(0, this.stat.getNumberOfFinishedJobs());
+        assertEquals(1, this.stat.getNumberOfJobs());
+        assertEquals(0, this.stat.getNumberOfProcessedJobs());
+        assertEquals(1, this.stat.getNumberOfQueuedJobs());
+        assertEquals(-1, this.stat.getLastActivatedJobTime());
+        assertEquals(-1, this.stat.getLastFinishedJobTime());
+
+        this.stat.incQueued();
+        assertTrue(this.stat.getStartTime() >= START_TIME);
+        assertEquals(0, this.stat.getAverageProcessingTime());
+        assertEquals(0, this.stat.getAverageWaitingTime());
+        assertEquals(0, this.stat.getNumberOfActiveJobs());
+        assertEquals(0, this.stat.getNumberOfCancelledJobs());
+        assertEquals(0, this.stat.getNumberOfFailedJobs());
+        assertEquals(0, this.stat.getNumberOfFinishedJobs());
+        assertEquals(2, this.stat.getNumberOfJobs());
+        assertEquals(0, this.stat.getNumberOfProcessedJobs());
+        assertEquals(2, this.stat.getNumberOfQueuedJobs());
+        assertEquals(-1, this.stat.getLastActivatedJobTime());
+        assertEquals(-1, this.stat.getLastFinishedJobTime());
+
+        this.stat.decQueued();
+        assertTrue(this.stat.getStartTime() >= START_TIME);
+        assertEquals(0, this.stat.getAverageProcessingTime());
+        assertEquals(0, this.stat.getAverageWaitingTime());
+        assertEquals(0, this.stat.getNumberOfActiveJobs());
+        assertEquals(0, this.stat.getNumberOfCancelledJobs());
+        assertEquals(0, this.stat.getNumberOfFailedJobs());
+        assertEquals(0, this.stat.getNumberOfFinishedJobs());
+        assertEquals(1, this.stat.getNumberOfJobs());
+        assertEquals(0, this.stat.getNumberOfProcessedJobs());
+        assertEquals(1, this.stat.getNumberOfQueuedJobs());
+        assertEquals(-1, this.stat.getLastActivatedJobTime());
+        assertEquals(-1, this.stat.getLastFinishedJobTime());
+    }
+
+    @org.junit.Test public void testFinished() {
+        long now = System.currentTimeMillis();
+        this.stat.incQueued();
+        this.stat.addActive(100);
+        this.stat.finishedJob(200);
+        this.stat.incQueued();
+        this.stat.addActive(300);
+        this.stat.finishedJob(800);
+
+        assertTrue(this.stat.getStartTime() >= START_TIME);
+        assertEquals(500, this.stat.getAverageProcessingTime());
+        assertEquals(200, this.stat.getAverageWaitingTime());
+        assertEquals(0, this.stat.getNumberOfActiveJobs());
+        assertEquals(0, this.stat.getNumberOfCancelledJobs());
+        assertEquals(0, this.stat.getNumberOfFailedJobs());
+        assertEquals(2, this.stat.getNumberOfFinishedJobs());
+        assertEquals(0, this.stat.getNumberOfJobs());
+        assertEquals(2, this.stat.getNumberOfProcessedJobs());
+        assertEquals(0, this.stat.getNumberOfQueuedJobs());
+        assertTrue(this.stat.getLastActivatedJobTime() >= now);
+        assertTrue(this.stat.getLastFinishedJobTime() >= now);
+
+        now = System.currentTimeMillis();
+        this.stat.incQueued();
+        this.stat.addActive(200);
+        assertTrue(this.stat.getStartTime() >= START_TIME);
+        assertEquals(500, this.stat.getAverageProcessingTime());
+        assertEquals(200, this.stat.getAverageWaitingTime());
+        assertEquals(1, this.stat.getNumberOfActiveJobs());
+        assertEquals(0, this.stat.getNumberOfCancelledJobs());
+        assertEquals(0, this.stat.getNumberOfFailedJobs());
+        assertEquals(2, this.stat.getNumberOfFinishedJobs());
+        assertEquals(1, this.stat.getNumberOfJobs());
+        assertEquals(2, this.stat.getNumberOfProcessedJobs());
+        assertEquals(0, this.stat.getNumberOfQueuedJobs());
+        assertTrue(this.stat.getLastActivatedJobTime() >= now);
+        assertTrue(this.stat.getLastFinishedJobTime() <= now);
+
+        now = System.currentTimeMillis();
+        this.stat.finishedJob(200);
+        assertTrue(this.stat.getStartTime() >= START_TIME);
+        assertEquals(400, this.stat.getAverageProcessingTime());
+        assertEquals(200, this.stat.getAverageWaitingTime());
+        assertEquals(0, this.stat.getNumberOfActiveJobs());
+        assertEquals(0, this.stat.getNumberOfCancelledJobs());
+        assertEquals(0, this.stat.getNumberOfFailedJobs());
+        assertEquals(3, this.stat.getNumberOfFinishedJobs());
+        assertEquals(0, this.stat.getNumberOfJobs());
+        assertEquals(3, this.stat.getNumberOfProcessedJobs());
+        assertEquals(0, this.stat.getNumberOfQueuedJobs());
+        assertTrue(this.stat.getLastActivatedJobTime() <= now);
+        assertTrue(this.stat.getLastFinishedJobTime() >= now);
+    }
+
+    @org.junit.Test public void testFailAndCancel() {
+        // we start with the results from the previous test!
+        this.testFinished();
+
+        long now = System.currentTimeMillis();
+        this.stat.incQueued();
+        this.stat.addActive(200);
+        this.stat.failedJob();
+        this.stat.incQueued();
+        assertTrue(this.stat.getStartTime() >= START_TIME);
+        assertEquals(400, this.stat.getAverageProcessingTime());
+        assertEquals(200, this.stat.getAverageWaitingTime());
+        assertEquals(0, this.stat.getNumberOfActiveJobs());
+        assertEquals(0, this.stat.getNumberOfCancelledJobs());
+        assertEquals(1, this.stat.getNumberOfFailedJobs());
+        assertEquals(3, this.stat.getNumberOfFinishedJobs());
+        assertEquals(1, this.stat.getNumberOfJobs());
+        assertEquals(4, this.stat.getNumberOfProcessedJobs());
+        assertEquals(1, this.stat.getNumberOfQueuedJobs());
+        assertTrue(this.stat.getLastActivatedJobTime() >= now);
+        assertTrue(this.stat.getLastFinishedJobTime() <= now);
+
+        now = System.currentTimeMillis();
+        this.stat.addActive(200);
+        this.stat.cancelledJob();
+        assertTrue(this.stat.getStartTime() >= START_TIME);
+        assertEquals(400, this.stat.getAverageProcessingTime());
+        assertEquals(200, this.stat.getAverageWaitingTime());
+        assertEquals(0, this.stat.getNumberOfActiveJobs());
+        assertEquals(1, this.stat.getNumberOfCancelledJobs());
+        assertEquals(1, this.stat.getNumberOfFailedJobs());
+        assertEquals(3, this.stat.getNumberOfFinishedJobs());
+        assertEquals(0, this.stat.getNumberOfJobs());
+        assertEquals(5, this.stat.getNumberOfProcessedJobs());
+        assertEquals(0, this.stat.getNumberOfQueuedJobs());
+        assertTrue(this.stat.getLastActivatedJobTime() >= now);
+        assertTrue(this.stat.getLastFinishedJobTime() <= now);
+    }
+
+    @org.junit.Test public void  testMisc() {
+        final StatisticsImpl stat2 = new StatisticsImpl(200);
+        assertEquals(200, stat2.getStartTime());
+
+        // update stat
+        this.testFailAndCancel();
+
+        long now = System.currentTimeMillis();
+        final StatisticsImpl copy = new StatisticsImpl();
+        copy.copyFrom(this.stat);
+        assertTrue(copy.getStartTime() >= now);
+        assertEquals(400, copy.getAverageProcessingTime());
+        assertEquals(200, copy.getAverageWaitingTime());
+        assertEquals(0, copy.getNumberOfActiveJobs());
+        assertEquals(1, copy.getNumberOfCancelledJobs());
+        assertEquals(1, copy.getNumberOfFailedJobs());
+        assertEquals(3, copy.getNumberOfFinishedJobs());
+        assertEquals(0, copy.getNumberOfJobs());
+        assertEquals(5, copy.getNumberOfProcessedJobs());
+        assertEquals(0, copy.getNumberOfQueuedJobs());
+        assertTrue(copy.getLastActivatedJobTime() <= now);
+        assertTrue(copy.getLastFinishedJobTime() <= now);
+
+        now = System.currentTimeMillis();
+        this.stat.incQueued();
+        this.stat.addActive(200);
+        this.stat.finishedJob(400);
+        assertEquals(400, this.stat.getAverageProcessingTime());
+        assertEquals(200, this.stat.getAverageWaitingTime());
+        assertEquals(0, this.stat.getNumberOfActiveJobs());
+        assertEquals(1, this.stat.getNumberOfCancelledJobs());
+        assertEquals(1, this.stat.getNumberOfFailedJobs());
+        assertEquals(4, this.stat.getNumberOfFinishedJobs());
+        assertEquals(0, this.stat.getNumberOfJobs());
+        assertEquals(6, this.stat.getNumberOfProcessedJobs());
+        assertEquals(0, this.stat.getNumberOfQueuedJobs());
+        assertTrue(this.stat.getLastActivatedJobTime() >= now);
+        assertTrue(this.stat.getLastFinishedJobTime() >= now);
+
+        copy.add(this.stat);
+        assertTrue(copy.getStartTime() <= now);
+        assertEquals(400, copy.getAverageProcessingTime());
+        assertEquals(200, copy.getAverageWaitingTime());
+        assertEquals(0, copy.getNumberOfActiveJobs());
+        assertEquals(2, copy.getNumberOfCancelledJobs());
+        assertEquals(2, copy.getNumberOfFailedJobs());
+        assertEquals(7, copy.getNumberOfFinishedJobs());
+        assertEquals(0, copy.getNumberOfJobs());
+        assertEquals(11, copy.getNumberOfProcessedJobs());
+        assertEquals(0, copy.getNumberOfQueuedJobs());
+        assertTrue(copy.getLastActivatedJobTime() >= now);
+        assertTrue(copy.getLastFinishedJobTime() >= now);
+
+        this.stat.incQueued();
+        this.stat.incQueued();
+        assertEquals(400, this.stat.getAverageProcessingTime());
+        assertEquals(200, this.stat.getAverageWaitingTime());
+        assertEquals(0, this.stat.getNumberOfActiveJobs());
+        assertEquals(1, this.stat.getNumberOfCancelledJobs());
+        assertEquals(1, this.stat.getNumberOfFailedJobs());
+        assertEquals(4, this.stat.getNumberOfFinishedJobs());
+        assertEquals(2, this.stat.getNumberOfJobs());
+        assertEquals(6, this.stat.getNumberOfProcessedJobs());
+        assertEquals(2, this.stat.getNumberOfQueuedJobs());
+        assertTrue(this.stat.getLastActivatedJobTime() >= now);
+        assertTrue(this.stat.getLastFinishedJobTime() >= now);
+
+        this.stat.clearQueued();
+        assertEquals(400, this.stat.getAverageProcessingTime());
+        assertEquals(200, this.stat.getAverageWaitingTime());
+        assertEquals(0, this.stat.getNumberOfActiveJobs());
+        assertEquals(1, this.stat.getNumberOfCancelledJobs());
+        assertEquals(1, this.stat.getNumberOfFailedJobs());
+        assertEquals(4, this.stat.getNumberOfFinishedJobs());
+        assertEquals(0, this.stat.getNumberOfJobs());
+        assertEquals(6, this.stat.getNumberOfProcessedJobs());
+        assertEquals(0, this.stat.getNumberOfQueuedJobs());
+        assertTrue(this.stat.getLastActivatedJobTime() >= now);
+        assertTrue(this.stat.getLastFinishedJobTime() >= now);
+    }
+}
diff --git a/src/test/java/org/apache/sling/event/impl/jobs/UtilityTest.java b/src/test/java/org/apache/sling/event/impl/jobs/UtilityTest.java
new file mode 100644
index 0000000..94c60ae
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/impl/jobs/UtilityTest.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs;
+
+import junit.framework.TestCase;
+
+import org.apache.sling.event.impl.support.ResourceHelper;
+
+public class UtilityTest extends TestCase {
+
+    public void test_filter_allowed() {
+        final String allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz0123456789_,.-+#!?$%&()=";
+        assertEquals("Allowed Characters must not be filtered", allowed,
+                ResourceHelper.filterName(allowed));
+    }
+
+    public void test_filter_illegal_jcr() {
+        assertEquals("_", ResourceHelper.filterName("["));
+        assertEquals("_", ResourceHelper.filterName("]"));
+        assertEquals("_", ResourceHelper.filterName("*"));
+        assertEquals("_", ResourceHelper.filterName("/"));
+        assertEquals("_", ResourceHelper.filterName(":"));
+        assertEquals("_", ResourceHelper.filterName("'"));
+        assertEquals("_", ResourceHelper.filterName("\""));
+
+        assertEquals("a_b", ResourceHelper.filterName("a[b"));
+        assertEquals("a_b", ResourceHelper.filterName("a]b"));
+        assertEquals("a_b", ResourceHelper.filterName("a*b"));
+        assertEquals("a_b", ResourceHelper.filterName("a/b"));
+        assertEquals("a_b", ResourceHelper.filterName("a:b"));
+        assertEquals("a_b", ResourceHelper.filterName("a'b"));
+        assertEquals("a_b", ResourceHelper.filterName("a\"b"));
+
+        assertEquals("_b", ResourceHelper.filterName("[b"));
+        assertEquals("_b", ResourceHelper.filterName("]b"));
+        assertEquals("_b", ResourceHelper.filterName("*b"));
+        assertEquals("_b", ResourceHelper.filterName("/b"));
+        assertEquals("_b", ResourceHelper.filterName(":b"));
+        assertEquals("_b", ResourceHelper.filterName("'b"));
+        assertEquals("_b", ResourceHelper.filterName("\"b"));
+
+        assertEquals("a_", ResourceHelper.filterName("a["));
+        assertEquals("a_", ResourceHelper.filterName("a]"));
+        assertEquals("a_", ResourceHelper.filterName("a*"));
+        assertEquals("a_", ResourceHelper.filterName("a/"));
+        assertEquals("a_", ResourceHelper.filterName("a:"));
+        assertEquals("a_", ResourceHelper.filterName("a'"));
+        assertEquals("a_", ResourceHelper.filterName("a\""));
+    }
+
+    public void test_filter_consecutive_replace() {
+        assertEquals("a_b_", ResourceHelper.filterName("a/[b]"));
+    }
+    
+    public void test_checkJobTopic() {
+    	assertNull (Utility.checkJobTopic("simpleTopic"));
+    	final String result = Utility.checkJobTopic("simpleTopic.withDots");
+    	assertNotNull(result);
+    	assertTrue ("Discarding job - job has an illegal job topic 'simpleTopic.withDots'".equals(result));
+    	assertNotNull (Utility.checkJobTopic(new StringBuilder("simpleTopic")));
+    	assertNotNull (Utility.checkJobTopic(null));
+    }
+    
+}
diff --git a/src/test/java/org/apache/sling/event/impl/jobs/config/InternalQueueConfigurationTest.java b/src/test/java/org/apache/sling/event/impl/jobs/config/InternalQueueConfigurationTest.java
new file mode 100644
index 0000000..d485b2b
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/impl/jobs/config/InternalQueueConfigurationTest.java
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class InternalQueueConfigurationTest {
+
+    @org.junit.Test public void testMaxParallel() {
+        final Map<String, Object> p = new HashMap<String, Object>();
+        p.put(ConfigurationConstants.PROP_NAME, "QueueConfigurationTest");
+        p.put(ConfigurationConstants.PROP_MAX_PARALLEL, -1);
+
+        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(p);
+        assertEquals(Runtime.getRuntime().availableProcessors(), c.getMaxParallel());
+
+        // Edge cases 0.0 and 1.0 (treated as int numbers)
+        p.put(ConfigurationConstants.PROP_MAX_PARALLEL, 0.0);
+        c = InternalQueueConfiguration.fromConfiguration(p);
+        assertEquals(0, c.getMaxParallel());
+
+        p.put(ConfigurationConstants.PROP_MAX_PARALLEL, 1.0);
+        c = InternalQueueConfiguration.fromConfiguration(p);
+        assertEquals(1, c.getMaxParallel());
+
+        // percentage (50%)
+        p.put(ConfigurationConstants.PROP_MAX_PARALLEL, 0.5);
+        c = InternalQueueConfiguration.fromConfiguration(p);
+        assertEquals((int) Math.round(Runtime.getRuntime().availableProcessors() * 0.5), c.getMaxParallel());
+
+        // rounding
+        p.put(ConfigurationConstants.PROP_MAX_PARALLEL, 0.90);
+        c = InternalQueueConfiguration.fromConfiguration(p);
+        assertEquals((int) Math.round(Runtime.getRuntime().availableProcessors() * 0.9), c.getMaxParallel());
+
+        p.put(ConfigurationConstants.PROP_MAX_PARALLEL, 0.99);
+        c = InternalQueueConfiguration.fromConfiguration(p);
+        assertEquals((int) Math.round(Runtime.getRuntime().availableProcessors() * 0.99), c.getMaxParallel());
+
+        // Percentages can't go over 99% (0.99)
+        p.put(ConfigurationConstants.PROP_MAX_PARALLEL, 1.01);
+        c = InternalQueueConfiguration.fromConfiguration(p);
+        assertEquals(Runtime.getRuntime().availableProcessors(), c.getMaxParallel());
+
+        // Treat negative values same a -1 (all cores)
+        p.put(ConfigurationConstants.PROP_MAX_PARALLEL, -0.5);
+        c = InternalQueueConfiguration.fromConfiguration(p);
+        assertEquals(Runtime.getRuntime().availableProcessors(), c.getMaxParallel());
+
+        p.put(ConfigurationConstants.PROP_MAX_PARALLEL, -2);
+        c = InternalQueueConfiguration.fromConfiguration(p);
+        assertEquals(Runtime.getRuntime().availableProcessors(), c.getMaxParallel());
+
+        // Invalid number results in ConfigurationConstants.DEFAULT_MAX_PARALLEL
+        p.put(ConfigurationConstants.PROP_MAX_PARALLEL, "a string");
+        c = InternalQueueConfiguration.fromConfiguration(p);
+        assertEquals(ConfigurationConstants.DEFAULT_MAX_PARALLEL, c.getMaxParallel());
+    }
+
+    @org.junit.Test public void testTopicMatchersDot() {
+        final Map<String, Object> p = new HashMap<String, Object>();
+        p.put(ConfigurationConstants.PROP_TOPICS, new String[] {"a."});
+        p.put(ConfigurationConstants.PROP_NAME, "test");
+
+        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(p);
+        assertTrue(c.isValid());
+        assertNotNull(c.match("a/b"));
+        assertNotNull(c.match("a/c"));
+        assertNull(c.match("a"));
+        assertNull(c.match("a/b/c"));
+        assertNull(c.match("t"));
+        assertNull(c.match("t/x"));
+    }
+
+    @org.junit.Test public void testTopicMatchersStar() {
+        final Map<String, Object> p = new HashMap<String, Object>();
+        p.put(ConfigurationConstants.PROP_TOPICS, new String[] {"a*"});
+        p.put(ConfigurationConstants.PROP_NAME, "test");
+
+        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(p);
+        assertTrue(c.isValid());
+        assertNotNull(c.match("a/b"));
+        assertNotNull(c.match("a/c"));
+        assertNull(c.match("a"));
+        assertNotNull(c.match("a/b/c"));
+        assertNull(c.match("t"));
+        assertNull(c.match("t/x"));
+    }
+
+    @org.junit.Test public void testTopicMatchers() {
+        final Map<String, Object> p = new HashMap<String, Object>();
+        p.put(ConfigurationConstants.PROP_TOPICS, new String[] {"a"});
+        p.put(ConfigurationConstants.PROP_NAME, "test");
+
+        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(p);
+        assertTrue(c.isValid());
+        assertNull(c.match("a/b"));
+        assertNull(c.match("a/c"));
+        assertNotNull(c.match("a"));
+        assertNull(c.match("a/b/c"));
+        assertNull(c.match("t"));
+        assertNull(c.match("t/x"));
+    }
+
+    @org.junit.Test public void testTopicMatcherAndReplacement() {
+        final Map<String, Object> p = new HashMap<String, Object>();
+        p.put(ConfigurationConstants.PROP_TOPICS, new String[] {"a."});
+        p.put(ConfigurationConstants.PROP_NAME, "test-queue-{0}");
+
+        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(p);
+        assertTrue(c.isValid());
+        final String b = "a/b";
+        assertNotNull(c.match(b));
+        assertEquals("test-queue-b", c.match(b));
+        final String d = "a/d";
+        assertNotNull(c.match(d));
+        assertEquals("test-queue-d", c.match(d));
+    }
+
+    @org.junit.Test public void testTopicMatchersDotAndSlash() {
+        final Map<String, Object> p = new HashMap<String, Object>();
+        p.put(ConfigurationConstants.PROP_TOPICS, new String[] {"a/."});
+        p.put(ConfigurationConstants.PROP_NAME, "test");
+
+        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(p);
+        assertTrue(c.isValid());
+        assertNotNull(c.match("a/b"));
+        assertNotNull(c.match("a/c"));
+        assertNull(c.match("a"));
+        assertNull(c.match("a/b/c"));
+        assertNull(c.match("t"));
+        assertNull(c.match("t/x"));
+    }
+
+    @org.junit.Test public void testTopicMatchersStarAndSlash() {
+        final Map<String, Object> p = new HashMap<String, Object>();
+        p.put(ConfigurationConstants.PROP_TOPICS, new String[] {"a/*"});
+        p.put(ConfigurationConstants.PROP_NAME, "test");
+
+        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(p);
+        assertTrue(c.isValid());
+        assertNotNull(c.match("a/b"));
+        assertNotNull(c.match("a/c"));
+        assertNull(c.match("a"));
+        assertNotNull(c.match("a/b/c"));
+        assertNull(c.match("t"));
+        assertNull(c.match("t/x"));
+    }
+
+    @org.junit.Test public void testTopicMatcherAndReplacementAndSlash() {
+        final Map<String, Object> p = new HashMap<String, Object>();
+        p.put(ConfigurationConstants.PROP_TOPICS, new String[] {"a/."});
+        p.put(ConfigurationConstants.PROP_NAME, "test-queue-{0}");
+
+        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(p);
+        assertTrue(c.isValid());
+        final String b = "a/b";
+        assertNotNull(c.match(b));
+        assertEquals("test-queue-b", c.match(b));
+        final String d = "a/d";
+        assertNotNull(c.match(d));
+        assertEquals("test-queue-d", c.match(d));
+    }
+
+    @org.junit.Test public void testNoTopicMatchers() {
+        final Map<String, Object> p = new HashMap<String, Object>();
+        p.put(ConfigurationConstants.PROP_NAME, "test");
+
+        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(p);
+        assertFalse(c.isValid());
+    }
+}
diff --git a/src/test/java/org/apache/sling/event/impl/jobs/config/JobManagerConfigurationTest.java b/src/test/java/org/apache/sling/event/impl/jobs/config/JobManagerConfigurationTest.java
new file mode 100644
index 0000000..486b823
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/impl/jobs/config/JobManagerConfigurationTest.java
@@ -0,0 +1,259 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.sling.commons.scheduler.ScheduleOptions;
+import org.apache.sling.commons.scheduler.Scheduler;
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.TopologyEvent;
+import org.apache.sling.discovery.TopologyEventListener;
+import org.apache.sling.discovery.TopologyView;
+import org.apache.sling.discovery.commons.InitDelayingTopologyEventListener;
+import org.apache.sling.event.impl.TestUtil;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class JobManagerConfigurationTest {
+
+    private TopologyView createView() {
+        final TopologyView view = Mockito.mock(TopologyView.class);
+        Mockito.when(view.isCurrent()).thenReturn(true);
+        final InstanceDescription local = Mockito.mock(InstanceDescription.class);
+        Mockito.when(local.isLeader()).thenReturn(true);
+        Mockito.when(local.isLocal()).thenReturn(true);
+        Mockito.when(local.getSlingId()).thenReturn("id");
+
+        Mockito.when(view.getLocalInstance()).thenReturn(local);
+        final ClusterView localView = Mockito.mock(ClusterView.class);
+        Mockito.when(localView.getId()).thenReturn("1");
+        Mockito.when(localView.getInstances()).thenReturn(Collections.singletonList(local));
+        Mockito.when(view.getClusterViews()).thenReturn(Collections.singleton(localView));
+        Mockito.when(local.getClusterView()).thenReturn(localView);
+
+        return view;
+    }
+
+    private static class ChangeListener implements ConfigurationChangeListener {
+
+        public final List<Boolean> events = new ArrayList<Boolean>();
+        private volatile CountDownLatch latch;
+
+        public void init(final int count) {
+            events.clear();
+            latch = new CountDownLatch(count);
+        }
+
+        public void await() throws Exception {
+            if ( !latch.await(8000, TimeUnit.MILLISECONDS) ) {
+                throw new Exception("No configuration event within 8 seconds.");
+            }
+        }
+
+        @Override
+        public void configurationChanged(boolean active) {
+            events.add(active);
+            latch.countDown();
+        }
+    }
+
+    private Scheduler createScheduler() {
+        return new Scheduler() {
+
+            @Override
+            public boolean unschedule(String jobName) {
+                // TODO Auto-generated method stub
+                return false;
+            }
+
+            @Override
+            public boolean schedule(final Object job, ScheduleOptions options) {
+                if ( job instanceof Runnable ) {
+                    final Timer t = new Timer();
+                    t.schedule(new TimerTask() {
+
+                        @Override
+                        public void run() {
+                            ((Runnable)job).run();
+                        }
+                    }, 3000);
+                    return true;
+                }
+                return false;
+            }
+
+            @Override
+            public void removeJob(String name) throws NoSuchElementException {
+                // TODO Auto-generated method stub
+
+            }
+
+            @Override
+            public boolean fireJobAt(String name, Object job, Map<String, Serializable> config, Date date, int times,
+                    long period) {
+                // TODO Auto-generated method stub
+                return false;
+            }
+
+            @Override
+            public void fireJobAt(String name, Object job, Map<String, Serializable> config, Date date) throws Exception {
+                // TODO Auto-generated method stub
+
+            }
+
+            @Override
+            public boolean fireJob(Object job, Map<String, Serializable> config, int times, long period) {
+                // TODO Auto-generated method stub
+                return false;
+            }
+
+            @Override
+            public void fireJob(Object job, Map<String, Serializable> config) throws Exception {
+                // TODO Auto-generated method stub
+
+            }
+
+            @Override
+            public void addPeriodicJob(String name, Object job, Map<String, Serializable> config, long period,
+                    boolean canRunConcurrently, boolean startImmediate) throws Exception {
+                // TODO Auto-generated method stub
+
+            }
+
+            @Override
+            public void addPeriodicJob(String name, Object job, Map<String, Serializable> config, long period,
+                    boolean canRunConcurrently) throws Exception {
+                // TODO Auto-generated method stub
+
+            }
+
+            @Override
+            public void addJob(String name, Object job, Map<String, Serializable> config, String schedulingExpression,
+                    boolean canRunConcurrently) throws Exception {
+                // TODO Auto-generated method stub
+
+            }
+
+            @Override
+            public ScheduleOptions NOW(int times, long period) {
+                // TODO Auto-generated method stub
+                return null;
+            }
+
+            @Override
+            public ScheduleOptions NOW() {
+                // TODO Auto-generated method stub
+                return null;
+            }
+
+            @Override
+            public ScheduleOptions EXPR(String expression) {
+                // TODO Auto-generated method stub
+                return null;
+            }
+
+            @Override
+            public ScheduleOptions AT(Date date, int times, long period) {
+                // TODO Auto-generated method stub
+                return null;
+            }
+
+            @Override
+            public ScheduleOptions AT(Date date) {
+                // TODO Auto-generated method stub
+                return null;
+            }
+        };
+    }
+
+    @Test public void testTopologyChange() throws Exception {
+        // mock scheduler
+        final Scheduler scheduler = this.createScheduler();
+        final ChangeListener ccl = new ChangeListener();
+
+        // add change listener and verify
+        ccl.init(1);
+        final JobManagerConfiguration config = new JobManagerConfiguration();
+        TestUtil.setFieldValue(config, "scheduler", scheduler);
+        ((AtomicBoolean)TestUtil.getFieldValue(config, "active")).set(true);
+        InitDelayingTopologyEventListener startupDelayListener = new InitDelayingTopologyEventListener(1, new TopologyEventListener() {
+            
+            @Override
+            public void handleTopologyEvent(TopologyEvent event) {
+                config.doHandleTopologyEvent(event);
+            }
+        }, scheduler);;
+        TestUtil.setFieldValue(config, "startupDelayListener", startupDelayListener);
+
+        config.addListener(ccl);
+        ccl.await();
+
+        assertEquals(1, ccl.events.size());
+        assertFalse(ccl.events.get(0));
+
+        // create init view
+        ccl.init(1);
+        final TopologyView initView = createView();
+        final TopologyEvent init = new TopologyEvent(TopologyEvent.Type.TOPOLOGY_INIT, null, initView);
+        config.handleTopologyEvent(init);
+        ccl.await();
+
+        assertEquals(1, ccl.events.size());
+        assertTrue(ccl.events.get(0));
+
+        // change view, followed by change props
+        ccl.init(2);
+        final TopologyView view2 = createView();
+        Mockito.when(initView.isCurrent()).thenReturn(false);
+        final TopologyEvent change1 = new TopologyEvent(TopologyEvent.Type.TOPOLOGY_CHANGED, initView, view2);
+        final TopologyView view3 = createView();
+        final TopologyEvent change2 = new TopologyEvent(TopologyEvent.Type.PROPERTIES_CHANGED, view2, view3);
+
+        config.handleTopologyEvent(change1);
+        Mockito.when(view2.isCurrent()).thenReturn(false);
+        config.handleTopologyEvent(change2);
+
+        ccl.await();
+        assertEquals(2, ccl.events.size());
+        assertFalse(ccl.events.get(0));
+        assertTrue(ccl.events.get(1));
+
+        // we wait another 4 secs to see if there is no another event
+        Thread.sleep(4000);
+        assertEquals(2, ccl.events.size());
+
+    }
+}
diff --git a/src/test/java/org/apache/sling/event/impl/jobs/config/TopologyCapabilitiesTest.java b/src/test/java/org/apache/sling/event/impl/jobs/config/TopologyCapabilitiesTest.java
new file mode 100644
index 0000000..1e54f3d
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/impl/jobs/config/TopologyCapabilitiesTest.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.config;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Collections;
+
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.TopologyView;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class TopologyCapabilitiesTest {
+
+    private TopologyCapabilities caps;
+
+    @Before
+    public void setup() {
+        // local cluster view
+        final ClusterView cv = Mockito.mock(ClusterView.class);
+        Mockito.when(cv.getId()).thenReturn("cluster");
+
+        // local description
+        final InstanceDescription local = Mockito.mock(InstanceDescription.class);
+        Mockito.when(local.isLeader()).thenReturn(true);
+        Mockito.when(local.getSlingId()).thenReturn("local");
+        Mockito.when(local.getProperty(TopologyCapabilities.PROPERTY_TOPICS)).thenReturn("foo,bar/*,a/**,d/1/2,d/1/*,d/**");
+        Mockito.when(local.getClusterView()).thenReturn(cv);
+
+        // topology view
+        final TopologyView tv = Mockito.mock(TopologyView.class);
+        Mockito.when(tv.getInstances()).thenReturn(Collections.singleton(local));
+        Mockito.when(tv.getLocalInstance()).thenReturn(local);
+
+        final JobManagerConfiguration config = Mockito.mock(JobManagerConfiguration.class);
+
+        caps = new TopologyCapabilities(tv, config);
+    }
+
+    @Test public void testMatching() {
+        assertEquals(1, caps.getPotentialTargets("foo").size());
+        assertEquals(0, caps.getPotentialTargets("foo/a").size());
+        assertEquals(0, caps.getPotentialTargets("bar").size());
+        assertEquals(1, caps.getPotentialTargets("bar/foo").size());
+        assertEquals(0, caps.getPotentialTargets("bar/foo/a").size());
+        assertEquals(1, caps.getPotentialTargets("a/b").size());
+        assertEquals(1, caps.getPotentialTargets("a/b(c").size());
+        assertEquals(0, caps.getPotentialTargets("x").size());
+        assertEquals(0, caps.getPotentialTargets("x/y").size());
+        assertEquals(1, caps.getPotentialTargets("d/1/2").size());
+    }
+}
diff --git a/src/test/java/org/apache/sling/event/impl/jobs/jmx/AllJobStatisticsMBeanTest.java b/src/test/java/org/apache/sling/event/impl/jobs/jmx/AllJobStatisticsMBeanTest.java
new file mode 100644
index 0000000..f2e6762
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/impl/jobs/jmx/AllJobStatisticsMBeanTest.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.sling.event.impl.jobs.jmx;
+
+import java.util.Date;
+
+import org.apache.sling.event.impl.TestUtil;
+import org.apache.sling.event.jobs.JobManager;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+public class AllJobStatisticsMBeanTest {
+
+    private AllJobStatisticsMBean mbean;
+    @Mock
+    private JobManager jobManager;
+    private long seed;
+
+    public AllJobStatisticsMBeanTest() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Before
+    public void setup() throws NoSuchFieldException {
+        mbean = new AllJobStatisticsMBean();
+        TestUtil.setFieldValue(mbean, "jobManager", jobManager);
+        seed = System.currentTimeMillis();
+        Mockito.when(jobManager.getStatistics()).thenReturn(
+                new DummyStatistics(seed));
+    }
+
+    @Test
+    public void testStatistics() {
+        Assert.assertEquals(seed + 1, mbean.getStartTime());
+        Assert.assertEquals(seed + 2, mbean.getNumberOfFinishedJobs());
+        Assert.assertEquals(seed + 3, mbean.getNumberOfCancelledJobs());
+        Assert.assertEquals(seed + 4, mbean.getNumberOfFailedJobs());
+        Assert.assertEquals(seed + 5, mbean.getNumberOfProcessedJobs());
+        Assert.assertEquals(seed + 6, mbean.getNumberOfActiveJobs());
+        Assert.assertEquals(seed + 7, mbean.getNumberOfQueuedJobs());
+        Assert.assertEquals(seed + 8, mbean.getNumberOfJobs());
+        Assert.assertEquals(seed + 9, mbean.getLastActivatedJobTime());
+        Assert.assertEquals(new Date(seed + 9), mbean.getLastActivatedJobDate());
+        Assert.assertEquals(seed + 10, mbean.getLastFinishedJobTime());
+        Assert.assertEquals(new Date(seed + 10), mbean.getLastFinishedJobDate());
+        Assert.assertEquals(seed + 11, mbean.getAverageWaitingTime());
+        Assert.assertEquals(seed + 12, mbean.getAverageProcessingTime());
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/event/impl/jobs/jmx/DummyStatistics.java b/src/test/java/org/apache/sling/event/impl/jobs/jmx/DummyStatistics.java
new file mode 100644
index 0000000..f774bf3
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/impl/jobs/jmx/DummyStatistics.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.sling.event.impl.jobs.jmx;
+
+import org.apache.sling.event.jobs.Statistics;
+
+/**
+ * Dummy statistics for testing purposes.
+ */
+public class DummyStatistics implements Statistics {
+
+    private long base;
+
+    public DummyStatistics(long base) {
+        this.base = base;
+        
+    }
+    public long getStartTime() {
+        return base+1;
+    }
+
+    public long getNumberOfFinishedJobs() {
+        return base+2;
+    }
+
+    public long getNumberOfCancelledJobs() {
+        return base+3;
+    }
+
+    public long getNumberOfFailedJobs() {
+        return base+4;
+    }
+
+    public long getNumberOfProcessedJobs() {
+        return base+5;
+    }
+
+    public long getNumberOfActiveJobs() {
+        return base+6;
+    }
+
+    public long getNumberOfQueuedJobs() {
+        return base+7;
+    }
+
+    public long getNumberOfJobs() {
+        return base+8;
+    }
+
+    public long getLastActivatedJobTime() {
+        return base+9;
+    }
+
+    public long getLastFinishedJobTime() {
+        return base+10;
+    }
+
+    public long getAverageWaitingTime() {
+        return base+11;
+    }
+
+    public long getAverageProcessingTime() {
+        return base+12;
+    }
+
+    public void reset() {
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/event/impl/jobs/jmx/QueuesMBeanImplTest.java b/src/test/java/org/apache/sling/event/impl/jobs/jmx/QueuesMBeanImplTest.java
new file mode 100644
index 0000000..f8b5740
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/impl/jobs/jmx/QueuesMBeanImplTest.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.sling.event.impl.jobs.jmx;
+
+import java.util.Date;
+import java.util.Dictionary;
+
+import org.apache.sling.event.jobs.Queue;
+import org.apache.sling.event.jobs.Statistics;
+import org.apache.sling.event.jobs.jmx.StatisticsMBean;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+
+public class QueuesMBeanImplTest {
+
+    private QueuesMBeanImpl mbean;
+    @Mock
+    private BundleContext bundleContext;
+    @Captor
+    private ArgumentCaptor<String> serviceClass;
+    @Captor
+    private ArgumentCaptor<Object> serviceObject;
+    @SuppressWarnings("rawtypes")
+    @Captor
+    private ArgumentCaptor<Dictionary> serviceProperties;
+    @Mock
+    private ServiceRegistration serviceRegistration;
+
+    public QueuesMBeanImplTest() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Before
+    public void setup() throws NoSuchFieldException {
+        mbean = new QueuesMBeanImpl();
+        mbean.activate(bundleContext);
+    }
+
+
+    @Test
+    public void testAddQueue() {
+        addQueue();
+    }
+
+    public Queue addQueue() {
+        Queue queue = Mockito.mock(Queue.class, Mockito.withSettings().extraInterfaces(Statistics.class));
+        mockStatistics((Statistics) queue);
+        Mockito.when(queue.getName()).thenReturn("queue-name");
+        Mockito.when(bundleContext.registerService(Mockito.anyString(), Mockito.any(StatisticsMBean.class), Mockito.any(Dictionary.class))).thenReturn(serviceRegistration);
+        mbean.sendEvent(new QueueStatusEvent(queue,null));
+        Mockito.verify(bundleContext, Mockito.only()).registerService(serviceClass.capture(), serviceObject.capture(), serviceProperties.capture());
+        Assert.assertEquals("Expected bean to be registerd as a StatisticsMBean ", StatisticsMBean.class.getName(), serviceClass.getValue());
+        Assert.assertTrue("Expected service to be an instance of SatisticsMBean", serviceObject.getValue() instanceof StatisticsMBean);
+        Assert.assertNotNull("Expected properties to have a jmx.objectname", serviceProperties.getValue().get("jmx.objectname"));
+        testStatistics((StatisticsMBean) serviceObject.getValue());
+        return queue;
+    }
+
+
+    @Test
+    public void updateQueue() {
+        Queue firstQueue = addQueue();
+        Queue queue = Mockito.mock(Queue.class, Mockito.withSettings().extraInterfaces(Statistics.class));
+        Mockito.when(queue.getName()).thenReturn("queue-name-changed");
+        Mockito.reset(bundleContext);
+        mbean.sendEvent(new QueueStatusEvent(queue,firstQueue));
+        Mockito.verify(bundleContext, Mockito.never()).registerService(serviceClass.capture(), serviceObject.capture(), serviceProperties.capture());
+    }
+
+    @Test
+    public void removeQueue() {
+        Queue firstQueue = addQueue();
+        mbean.sendEvent(new QueueStatusEvent(null,firstQueue));
+        Mockito.verify(serviceRegistration, Mockito.only()).unregister();
+
+    }
+
+    private void mockStatistics(Statistics queue) {
+        Mockito.when(queue.getStartTime()).thenReturn(1L);
+        Mockito.when(queue.getNumberOfFinishedJobs()).thenReturn(2L);
+        Mockito.when(queue.getNumberOfCancelledJobs()).thenReturn(3L);
+        Mockito.when(queue.getNumberOfFailedJobs()).thenReturn(4L);
+        Mockito.when(queue.getNumberOfProcessedJobs()).thenReturn(5L);
+        Mockito.when(queue.getNumberOfActiveJobs()).thenReturn(6L);
+        Mockito.when(queue.getNumberOfQueuedJobs()).thenReturn(7L);
+        Mockito.when(queue.getNumberOfJobs()).thenReturn(8L);
+        Mockito.when(queue.getLastActivatedJobTime()).thenReturn(9L);
+        Mockito.when(queue.getLastFinishedJobTime()).thenReturn(10L);
+        Mockito.when(queue.getAverageWaitingTime()).thenReturn(11L);
+        Mockito.when(queue.getAverageProcessingTime()).thenReturn(12L);
+    }
+
+    public void testStatistics(StatisticsMBean statisticsMbean) {
+        Assert.assertEquals(1, statisticsMbean.getStartTime());
+        Assert.assertEquals(2, statisticsMbean.getNumberOfFinishedJobs());
+        Assert.assertEquals(3, statisticsMbean.getNumberOfCancelledJobs());
+        Assert.assertEquals(4, statisticsMbean.getNumberOfFailedJobs());
+        Assert.assertEquals(5, statisticsMbean.getNumberOfProcessedJobs());
+        Assert.assertEquals(6, statisticsMbean.getNumberOfActiveJobs());
+        Assert.assertEquals(7, statisticsMbean.getNumberOfQueuedJobs());
+        Assert.assertEquals(8, statisticsMbean.getNumberOfJobs());
+        Assert.assertEquals(9, statisticsMbean.getLastActivatedJobTime());
+        Assert.assertEquals(new Date(9), statisticsMbean.getLastActivatedJobDate());
+        Assert.assertEquals(10, statisticsMbean.getLastFinishedJobTime());
+        Assert.assertEquals(new Date(10), statisticsMbean.getLastFinishedJobDate());
+        Assert.assertEquals(11, statisticsMbean.getAverageWaitingTime());
+        Assert.assertEquals(12, statisticsMbean.getAverageProcessingTime());
+    }
+
+
+}
diff --git a/src/test/java/org/apache/sling/event/impl/jobs/tasks/HistoryCleanUpTaskTest.java b/src/test/java/org/apache/sling/event/impl/jobs/tasks/HistoryCleanUpTaskTest.java
new file mode 100644
index 0000000..6f92c1d
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/impl/jobs/tasks/HistoryCleanUpTaskTest.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.impl.jobs.tasks;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Map;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.event.impl.jobs.JobImpl;
+import org.apache.sling.event.impl.jobs.config.JobManagerConfiguration;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.consumer.JobExecutionContext;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import com.google.common.collect.Maps;
+
+@RunWith(MockitoJUnitRunner.class)
+public class HistoryCleanUpTaskTest {
+
+    private static final String JCR_PATH = JobManagerConfiguration.DEFAULT_REPOSITORY_PATH + "/finished";
+    private static final String JCR_TOPIC = "test";
+    private static final String JCR_JOB_NAME = "test-job";
+    private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyy/MM/dd/HH/mm");
+    private static final int MAX_AGE_IN_DAYS = 60;
+
+    @Rule
+    public final SlingContext ctx = new SlingContext();
+
+    @Mock
+    private JobManagerConfiguration configuration;
+    private Job job;
+    @Mock(answer = Answers.RETURNS_MOCKS)
+    private JobExecutionContext jobContext;
+
+    private HistoryCleanUpTask task;
+
+    @Before
+    public void setUp() {
+        setUpJob();
+        setupConfiguration();
+        task = ctx.registerInjectActivateService(new HistoryCleanUpTask());
+    }
+
+    private void setupConfiguration() {
+        Mockito.when(configuration.getStoredSuccessfulJobsPath()).thenReturn(JCR_PATH);
+        Mockito.when(configuration.createResourceResolver()).thenReturn(ctx.resourceResolver());
+        ctx.registerService(JobManagerConfiguration.class, configuration);
+    }
+
+    private void setUpJob() {
+        Map<String, Object> parameters = Maps.<String, Object> newHashMap();
+        parameters.put("age", MAX_AGE_IN_DAYS * 24 * 60);
+        job = new JobImpl("not-relevant", "not-relevant_123", parameters);
+        Mockito.when(jobContext.isStopped()).thenReturn(false);
+    }
+
+    @Test
+    public void shouldNotDeleteResourcesYoungerThanRemoveDate() {
+        Resource resource = createResourceWithDaysBeforeDate(MAX_AGE_IN_DAYS / 2);
+        task.process(job, jobContext);
+        assertNotNull(ctx.resourceResolver().getResource(resource.getPath()));
+    }
+
+    @Test
+    public void shouldDeleteResourcesOlderThanRemoveDate() {
+        Resource resource = createResourceWithDaysBeforeDate(MAX_AGE_IN_DAYS * 2);
+        task.process(job, jobContext);
+        assertNull(ctx.resourceResolver().getResource(resource.getPath()));
+    }
+
+    private Resource createResourceWithDaysBeforeDate(int days) {
+        Calendar cal = Calendar.getInstance();
+        cal.add(Calendar.DAY_OF_YEAR, -days);
+        String path = JCR_PATH + '/' + JCR_TOPIC + '/' + DATE_FORMATTER.format(cal.getTime()) + '/' + JCR_JOB_NAME;
+        return ctx.create().resource(path);
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/event/it/AbstractJobHandlingTest.java b/src/test/java/org/apache/sling/event/it/AbstractJobHandlingTest.java
new file mode 100644
index 0000000..3a77364
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/it/AbstractJobHandlingTest.java
@@ -0,0 +1,462 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.it;
+
+
+import static org.ops4j.pax.exam.CoreOptions.frameworkProperty;
+import static org.ops4j.pax.exam.CoreOptions.junitBundles;
+import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
+import static org.ops4j.pax.exam.CoreOptions.options;
+import static org.ops4j.pax.exam.CoreOptions.systemProperty;
+import static org.ops4j.pax.exam.CoreOptions.when;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.discovery.PropertyProvider;
+import org.apache.sling.event.impl.jobs.config.JobManagerConfiguration;
+import org.apache.sling.event.jobs.JobManager;
+import org.apache.sling.event.jobs.consumer.JobConsumer;
+import org.apache.sling.event.jobs.consumer.JobExecutor;
+import org.ops4j.pax.exam.Configuration;
+import org.ops4j.pax.exam.CoreOptions;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.cm.ConfigurationAdminOptions;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.service.event.EventConstants;
+import org.osgi.service.event.EventHandler;
+import org.slf4j.LoggerFactory;
+
+public abstract class AbstractJobHandlingTest {
+
+    private static final String BUNDLE_JAR_SYS_PROP = "project.bundle.file";
+
+    /** The property containing the build directory. */
+    private static final String SYS_PROP_BUILD_DIR = "bundle.build.dir";
+
+    private static final String DEFAULT_BUILD_DIR = "target";
+
+    private static final String PORT_CONFIG = "org.osgi.service.http.port";
+
+    protected static final int DEFAULT_TEST_TIMEOUT = 1000*60*5;
+
+    @Inject
+    protected EventAdmin eventAdmin;
+
+    @Inject
+    protected ConfigurationAdmin configAdmin;
+
+    @Inject
+    protected BundleContext bc;
+
+    protected List<ServiceRegistration<?>> registrations = new ArrayList<>();
+
+    @Configuration
+    public Option[] config() {
+        final String buildDir = System.getProperty(SYS_PROP_BUILD_DIR, DEFAULT_BUILD_DIR);
+        final String bundleFileName = System.getProperty( BUNDLE_JAR_SYS_PROP );
+        final File bundleFile = new File( bundleFileName );
+        if ( !bundleFile.canRead() ) {
+            throw new IllegalArgumentException( "Cannot read from bundle file " + bundleFileName + " specified in the "
+                + BUNDLE_JAR_SYS_PROP + " system property" );
+        }
+
+        String localRepo = System.getProperty("maven.repo.local", "");
+
+        final String jackrabbitVersion = "2.13.1";
+        final String oakVersion = "1.5.7";
+
+        final String slingHome = new File(buildDir + File.separatorChar + "sling_" + System.currentTimeMillis()).getAbsolutePath();
+
+        return options(
+                frameworkProperty("sling.home").value(slingHome),
+                frameworkProperty("repository.home").value(slingHome + File.separatorChar + "repository"),
+                when( localRepo.length() > 0 ).useOptions(
+                        systemProperty("org.ops4j.pax.url.mvn.localRepository").value(localRepo)
+                ),
+                when( System.getProperty(PORT_CONFIG) != null ).useOptions(
+                        systemProperty(PORT_CONFIG).value(System.getProperty(PORT_CONFIG))),
+                systemProperty("pax.exam.osgi.unresolved.fail").value("true"),
+
+                ConfigurationAdminOptions.newConfiguration("org.apache.felix.jaas.ConfigurationSpi")
+                    .create(true)
+                    .put("jaas.defaultRealmName", "jackrabbit.oak")
+                    .put("jaas.configProviderName", "FelixJaasProvider")
+                    .asOption(),
+                ConfigurationAdminOptions.factoryConfiguration("org.apache.felix.jaas.Configuration.factory")
+                    .create(true)
+                    .put("jaas.controlFlag", "optional")
+                    .put("jaas.classname", "org.apache.jackrabbit.oak.spi.security.authentication.GuestLoginModule")
+                    .put("jaas.ranking", 300)
+                    .asOption(),
+                ConfigurationAdminOptions.factoryConfiguration("org.apache.felix.jaas.Configuration.factory")
+                    .create(true)
+                    .put("jaas.controlFlag", "required")
+                    .put("jaas.classname", "org.apache.jackrabbit.oak.security.authentication.user.LoginModuleImpl")
+                    .asOption(),
+                ConfigurationAdminOptions.factoryConfiguration("org.apache.felix.jaas.Configuration.factory")
+                    .create(true)
+                    .put("jaas.controlFlag", "sufficient")
+                    .put("jaas.classname", "org.apache.jackrabbit.oak.security.authentication.token.TokenLoginModule")
+                    .put("jaas.ranking", 200)
+                    .asOption(),
+                ConfigurationAdminOptions.newConfiguration("org.apache.jackrabbit.oak.security.authentication.AuthenticationConfigurationImpl")
+                    .create(true)
+                    .put("org.apache.jackrabbit.oak.authentication.configSpiName", "FelixJaasProvider")
+                    .asOption(),
+                ConfigurationAdminOptions.newConfiguration("org.apache.jackrabbit.oak.security.user.UserConfigurationImpl")
+                    .create(true)
+                    .put("groupsPath", "/home/groups")
+                    .put("usersPath", "/home/users")
+                    .put("defaultPath", "1")
+                    .put("importBehavior", "besteffort")
+                    .asOption(),
+                ConfigurationAdminOptions.newConfiguration("org.apache.jackrabbit.oak.security.user.RandomAuthorizableNodeName")
+                    .create(true)
+                    .put("enabledActions", new String[] {"org.apache.jackrabbit.oak.spi.security.user.action.AccessControlAction"})
+                    .put("userPrivilegeNames", new String[] {"jcr:all"})
+                    .put("groupPrivilegeNames", new String[] {"jcr:read"})
+                    .asOption(),
+                ConfigurationAdminOptions.newConfiguration("org.apache.jackrabbit.oak.spi.security.user.action.DefaultAuthorizableActionProvider")
+                    .create(true)
+                    .put("length", 21)
+                    .asOption(),
+                ConfigurationAdminOptions.newConfiguration("org.apache.jackrabbit.oak.plugins.segment.SegmentNodeStoreService")
+                    .create(true)
+                    .put("name", "Default NodeStore")
+                    .asOption(),
+
+                ConfigurationAdminOptions.factoryConfiguration("org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended")
+                    .create(true)
+                    .put("user.mapping", "org.apache.sling.event=admin")
+                    .asOption(),
+                ConfigurationAdminOptions.newConfiguration("org.apache.sling.jcr.resource.internal.JcrSystemUserValidator")
+                    .create(true)
+                    .put("allow.only.system.user", "false")
+                    .asOption(),
+
+                    // logging
+                systemProperty("pax.exam.logging").value("none"),
+                mavenBundle("org.apache.sling", "org.apache.sling.commons.log", "4.0.6"),
+                mavenBundle("org.apache.sling", "org.apache.sling.commons.logservice", "1.0.6"),
+                mavenBundle("org.slf4j", "slf4j-api", "1.7.13"),
+                mavenBundle("org.slf4j", "jcl-over-slf4j", "1.7.13"),
+                mavenBundle("org.slf4j", "log4j-over-slf4j", "1.7.13"),
+
+                mavenBundle("commons-io", "commons-io", "2.4"),
+                mavenBundle("commons-fileupload", "commons-fileupload", "1.3.1"),
+                mavenBundle("commons-collections", "commons-collections", "3.2.2"),
+                mavenBundle("commons-codec", "commons-codec", "1.10"),
+                mavenBundle("commons-lang", "commons-lang", "2.6"),
+                mavenBundle("commons-pool", "commons-pool", "1.6"),
+
+                mavenBundle("org.apache.servicemix.bundles", "org.apache.servicemix.bundles.concurrent", "1.3.4_1"),
+
+                mavenBundle("org.apache.geronimo.bundles", "commons-httpclient", "3.1_1"),
+                mavenBundle("org.apache.tika", "tika-core", "1.9"),
+                mavenBundle("org.apache.tika", "tika-bundle", "1.9"),
+
+                // infrastructure
+                mavenBundle("org.apache.felix", "org.apache.felix.http.servlet-api", "1.1.2"),
+                mavenBundle("org.apache.felix", "org.apache.felix.http.jetty", "3.1.6"),
+                mavenBundle("org.apache.felix", "org.apache.felix.eventadmin", "1.4.8"),
+                mavenBundle("org.apache.felix", "org.apache.felix.scr", "2.0.6"),
+                mavenBundle("org.apache.felix", "org.apache.felix.configadmin", "1.8.10"),
+                mavenBundle("org.apache.felix", "org.apache.felix.inventory", "1.0.4"),
+                mavenBundle("org.apache.felix", "org.apache.felix.metatype", "1.1.2"),
+
+                // sling
+                mavenBundle("org.apache.sling", "org.apache.sling.settings", "1.3.8"),
+                mavenBundle("org.apache.sling", "org.apache.sling.commons.osgi", "2.3.0"),
+                mavenBundle("org.apache.sling", "org.apache.sling.commons.json", "2.0.16"),
+                mavenBundle("org.apache.sling", "org.apache.sling.commons.mime", "2.1.8"),
+                mavenBundle("org.apache.sling", "org.apache.sling.commons.classloader", "1.3.2"),
+                mavenBundle("org.apache.sling", "org.apache.sling.commons.scheduler", "2.4.14"),
+                mavenBundle("org.apache.sling", "org.apache.sling.commons.threads", "3.2.4"),
+
+                mavenBundle("org.apache.sling", "org.apache.sling.auth.core", "1.3.12"),
+                mavenBundle("org.apache.sling", "org.apache.sling.discovery.api", "1.0.2"),
+                mavenBundle("org.apache.sling", "org.apache.sling.discovery.commons", "1.0.12"),
+                mavenBundle("org.apache.sling", "org.apache.sling.discovery.standalone", "1.0.2"),
+
+                mavenBundle("org.apache.sling", "org.apache.sling.api", "2.14.2"),
+                mavenBundle("org.apache.sling", "org.apache.sling.resourceresolver", "1.4.18"),
+                mavenBundle("org.apache.sling", "org.apache.sling.adapter", "2.1.2"),
+                mavenBundle("org.apache.sling", "org.apache.sling.jcr.resource", "2.8.0"),
+                mavenBundle("org.apache.sling", "org.apache.sling.jcr.classloader", "3.2.2"),
+                mavenBundle("org.apache.sling", "org.apache.sling.jcr.contentloader", "2.1.8"),
+                mavenBundle("org.apache.sling", "org.apache.sling.engine", "2.6.2"),
+                mavenBundle("org.apache.sling", "org.apache.sling.serviceusermapper", "1.2.2"),
+
+                mavenBundle("org.apache.sling", "org.apache.sling.jcr.jcr-wrapper", "2.0.0"),
+                mavenBundle("org.apache.sling", "org.apache.sling.jcr.api", "2.4.0"),
+                mavenBundle("org.apache.sling", "org.apache.sling.jcr.base", "2.4.0"),
+
+                mavenBundle("com.google.guava", "guava", "15.0"),
+                mavenBundle("org.apache.jackrabbit", "jackrabbit-api", jackrabbitVersion),
+                mavenBundle("org.apache.jackrabbit", "jackrabbit-jcr-commons", jackrabbitVersion),
+                mavenBundle("org.apache.jackrabbit", "jackrabbit-spi", jackrabbitVersion),
+                mavenBundle("org.apache.jackrabbit", "jackrabbit-spi-commons", jackrabbitVersion),
+                mavenBundle("org.apache.jackrabbit", "jackrabbit-jcr-rmi", jackrabbitVersion),
+
+                mavenBundle("org.apache.felix", "org.apache.felix.jaas", "0.0.4"),
+
+                mavenBundle("org.apache.jackrabbit", "oak-core", oakVersion),
+                mavenBundle("org.apache.jackrabbit", "oak-commons", oakVersion),
+                mavenBundle("org.apache.jackrabbit", "oak-lucene", oakVersion),
+                mavenBundle("org.apache.jackrabbit", "oak-blob", oakVersion),
+                mavenBundle("org.apache.jackrabbit", "oak-jcr", oakVersion),
+
+                mavenBundle("org.apache.jackrabbit", "oak-segment", oakVersion),
+
+                mavenBundle("org.apache.sling", "org.apache.sling.jcr.oak.server", "1.1.0"),
+
+                mavenBundle("org.apache.sling", "org.apache.sling.testing.tools", "1.0.6"),
+                mavenBundle("org.apache.httpcomponents", "httpcore-osgi", "4.1.2"),
+                mavenBundle("org.apache.httpcomponents", "httpclient-osgi", "4.1.2"),
+
+
+                // SLING-5560: delaying start of the sling.event bundle to
+                // ensure the parameter 'startup.delay' is properly set to 1sec
+                // for these ITs - as otherwise, the default of 30sec applies -
+                // which will cause the tests to fail
+                // @see setup() where the bundle is finally started - after reconfig
+                CoreOptions.bundle( bundleFile.toURI().toString() ).start(false),
+
+                junitBundles()
+           );
+    }
+
+    protected JobManager getJobManager() {
+        JobManager result = null;
+        int count = 0;
+        do {
+            final ServiceReference<JobManager> sr = this.bc.getServiceReference(JobManager.class);
+            if ( sr != null ) {
+                result = this.bc.getService(sr);
+            } else {
+                count++;
+                if ( count == 10 ) {
+                    break;
+                }
+                sleep(500);
+            }
+
+        } while ( result == null );
+        return result;
+    }
+
+    protected void sleep(final long time) {
+        try {
+            Thread.sleep(time);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            // ignore
+        }
+    }
+
+    public void setup() throws IOException {
+        // set load delay to 3 sec
+        final org.osgi.service.cm.Configuration c2 = this.configAdmin.getConfiguration("org.apache.sling.event.impl.jobs.jcr.PersistenceHandler", null);
+        Dictionary<String, Object> p2 = new Hashtable<String, Object>();
+        p2.put(JobManagerConfiguration.PROPERTY_BACKGROUND_LOAD_DELAY, 3L);
+        // and startup.delay to 1sec - otherwise default of 30sec breaks tests!
+        p2.put(JobManagerConfiguration.PROPERTY_STARTUP_DELAY, 1L);
+        c2.update(p2);
+
+        // SLING-5560 : since the above (re)config is now applied, we're safe
+        // to go ahead and start the sling.event bundle.
+        // this time, the JobManagerConfiguration will be activated
+        // with the 'startup.delay' set to 1sec - so that ITs actually succeed
+        try {
+            Bundle[] bundles = bc.getBundles();
+            for (Bundle bundle : bundles) {
+                if (bundle.getSymbolicName().contains("sling.event")) {
+                    // assuming we only have 1 bundle that contains 'sling.event'
+                    LoggerFactory.getLogger(getClass()).info("starting bundle... "+bundle);
+                    bundle.start();
+                    break;
+                }
+            }
+        } catch (BundleException e) {
+            LoggerFactory.getLogger(getClass()).error("could not start sling.event bundle: "+e, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    private int deleteCount;
+
+    private void delete(final Resource rsrc )
+    throws PersistenceException {
+        final ResourceResolver resolver = rsrc.getResourceResolver();
+        for(final Resource child : rsrc.getChildren()) {
+            delete(child);
+        }
+        resolver.delete(rsrc);
+        deleteCount++;
+        if ( deleteCount >= 20 ) {
+            resolver.commit();
+            deleteCount = 0;
+        }
+    }
+
+    public void cleanup() {
+        // clean job area
+        final ServiceReference<ResourceResolverFactory> ref = this.bc.getServiceReference(ResourceResolverFactory.class);
+        final ResourceResolverFactory factory = this.bc.getService(ref);
+        ResourceResolver resolver = null;
+        try {
+            resolver = factory.getAdministrativeResourceResolver(null);
+            final Resource rsrc = resolver.getResource("/var/eventing");
+            if ( rsrc != null ) {
+                delete(rsrc);
+                resolver.commit();
+            }
+        } catch ( final LoginException le ) {
+            // ignore
+        } catch (final PersistenceException e) {
+            // ignore
+        } catch ( final Exception e ) {
+            // sometimes an NPE is thrown from the repository, as we
+            // are in the cleanup, we can ignore this
+        } finally {
+            if ( resolver != null ) {
+                resolver.close();
+            }
+        }
+        // unregister all services
+        for(final ServiceRegistration<?> reg : this.registrations) {
+            reg.unregister();
+        }
+        this.registrations.clear();
+
+        // remove all configurations
+        try {
+            final org.osgi.service.cm.Configuration[] cfgs = this.configAdmin.listConfigurations(null);
+            if ( cfgs != null ) {
+                for(final org.osgi.service.cm.Configuration c : cfgs) {
+                    try {
+                        c.delete();
+                    } catch (final IOException io) {
+                        // ignore
+                    }
+                }
+            }
+        } catch (final IOException io) {
+            // ignore
+        } catch (final InvalidSyntaxException e) {
+            // ignore
+        }
+        this.sleep(1000);
+    }
+
+    /**
+     * Helper method to register an event handler
+     */
+    protected ServiceRegistration<EventHandler> registerEventHandler(final String topic,
+            final EventHandler handler) {
+        final Dictionary<String, Object> props = new Hashtable<String, Object>();
+        props.put(EventConstants.EVENT_TOPIC, topic);
+        final ServiceRegistration<EventHandler> reg = this.bc.registerService(EventHandler.class,
+                handler, props);
+        this.registrations.add(reg);
+        return reg;
+    }
+
+    protected long getConsumerChangeCount() {
+        long result = -1;
+        try {
+            final Collection<ServiceReference<PropertyProvider>> refs = this.bc.getServiceReferences(PropertyProvider.class, "(changeCount=*)");
+            if ( !refs.isEmpty() ) {
+                result = (Long)refs.iterator().next().getProperty("changeCount");
+            }
+        } catch ( final InvalidSyntaxException ignore ) {
+            // ignore
+        }
+        return result;
+    }
+
+    protected void waitConsumerChangeCount(final long minimum) {
+        do {
+            final long cc = getConsumerChangeCount();
+            if ( cc >= minimum ) {
+                // we need to wait for the topology events (TODO)
+                sleep(200);
+                return;
+            }
+            sleep(50);
+        } while ( true );
+    }
+
+    /**
+     * Helper method to register a job consumer
+     */
+    protected ServiceRegistration<JobConsumer> registerJobConsumer(final String topic,
+            final JobConsumer handler) {
+        long cc = this.getConsumerChangeCount();
+        final Dictionary<String, Object> props = new Hashtable<String, Object>();
+        props.put(JobConsumer.PROPERTY_TOPICS, topic);
+        final ServiceRegistration<JobConsumer> reg = this.bc.registerService(JobConsumer.class,
+                handler, props);
+        this.registrations.add(reg);
+        this.waitConsumerChangeCount(cc + 1);
+        return reg;
+    }
+
+    /**
+     * Helper method to register a job executor
+     */
+    protected ServiceRegistration<JobExecutor> registerJobExecutor(final String topic,
+            final JobExecutor handler) {
+        long cc = this.getConsumerChangeCount();
+        final Dictionary<String, Object> props = new Hashtable<String, Object>();
+        props.put(JobConsumer.PROPERTY_TOPICS, topic);
+        final ServiceRegistration<JobExecutor> reg = this.bc.registerService(JobExecutor.class,
+                handler, props);
+        this.registrations.add(reg);
+        this.waitConsumerChangeCount(cc + 1);
+        return reg;
+    }
+
+    protected void unregister(final ServiceRegistration<?> reg) {
+        if ( reg != null ) {
+            this.registrations.remove(reg);
+            reg.unregister();
+        }
+    }
+}
diff --git a/src/test/java/org/apache/sling/event/it/ChaosTest.java b/src/test/java/org/apache/sling/event/it/ChaosTest.java
new file mode 100644
index 0000000..100beec
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/it/ChaosTest.java
@@ -0,0 +1,402 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.it;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.sling.discovery.TopologyEvent;
+import org.apache.sling.discovery.TopologyEvent.Type;
+import org.apache.sling.discovery.TopologyEventListener;
+import org.apache.sling.discovery.TopologyView;
+import org.apache.sling.event.impl.jobs.config.ConfigurationConstants;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.JobManager;
+import org.apache.sling.event.jobs.NotificationConstants;
+import org.apache.sling.event.jobs.QueueConfiguration;
+import org.apache.sling.event.jobs.consumer.JobConsumer;
+import org.apache.sling.testing.tools.sling.TimeoutsProvider;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventHandler;
+
+@RunWith(PaxExam.class)
+public class ChaosTest extends AbstractJobHandlingTest {
+
+    /** Duration for firing jobs in seconds. */
+    private static final long DURATION = 1 * 60;
+    
+    private static final int NUM_ORDERED_THREADS = 3;
+    private static final int NUM_PARALLEL_THREADS = 6;
+    private static final int NUM_ROUND_THREADS = 6;
+
+    private static final int NUM_ORDERED_TOPICS = 2;
+    private static final int NUM_PARALLEL_TOPICS = 8;
+    private static final int NUM_ROUND_TOPICS = 8;
+
+    private static final String ORDERED_TOPIC_PREFIX = "sling/chaos/ordered/";
+    private static final String PARALLEL_TOPIC_PREFIX = "sling/chaos/parallel/";
+    private static final String ROUND_TOPIC_PREFIX = "sling/chaos/round/";
+
+    private static final String[] ORDERED_TOPICS = new String[NUM_ORDERED_TOPICS];
+    private static final String[] PARALLEL_TOPICS = new String[NUM_PARALLEL_TOPICS];
+    private static final String[] ROUND_TOPICS = new String[NUM_ROUND_TOPICS];
+
+    static {
+        for(int i=0; i<NUM_ORDERED_TOPICS; i++) {
+            ORDERED_TOPICS[i] = ORDERED_TOPIC_PREFIX + String.valueOf(i);
+        }
+        for(int i=0; i<NUM_PARALLEL_TOPICS; i++) {
+            PARALLEL_TOPICS[i] = PARALLEL_TOPIC_PREFIX + String.valueOf(i);
+        }
+        for(int i=0; i<NUM_ROUND_TOPICS; i++) {
+            ROUND_TOPICS[i] = ROUND_TOPIC_PREFIX + String.valueOf(i);
+        }
+    }
+
+    @Override
+    @Before
+    public void setup() throws IOException {
+        super.setup();
+
+        // create ordered test queue
+        final org.osgi.service.cm.Configuration orderedConfig = this.configAdmin.createFactoryConfiguration("org.apache.sling.event.jobs.QueueConfiguration", null);
+        final Dictionary<String, Object> orderedProps = new Hashtable<String, Object>();
+        orderedProps.put(ConfigurationConstants.PROP_NAME, "chaos-ordered");
+        orderedProps.put(ConfigurationConstants.PROP_TYPE, QueueConfiguration.Type.ORDERED.name());
+        orderedProps.put(ConfigurationConstants.PROP_TOPICS, ORDERED_TOPICS);
+        orderedProps.put(ConfigurationConstants.PROP_RETRIES, 2);
+        orderedProps.put(ConfigurationConstants.PROP_RETRY_DELAY, 2000L);
+        orderedConfig.update(orderedProps);
+
+        // create round robin test queue
+        final org.osgi.service.cm.Configuration rrConfig = this.configAdmin.createFactoryConfiguration("org.apache.sling.event.jobs.QueueConfiguration", null);
+        final Dictionary<String, Object> rrProps = new Hashtable<String, Object>();
+        rrProps.put(ConfigurationConstants.PROP_NAME, "chaos-roundrobin");
+        rrProps.put(ConfigurationConstants.PROP_TYPE, QueueConfiguration.Type.TOPIC_ROUND_ROBIN.name());
+        rrProps.put(ConfigurationConstants.PROP_TOPICS, ROUND_TOPICS);
+        rrProps.put(ConfigurationConstants.PROP_RETRIES, 2);
+        rrProps.put(ConfigurationConstants.PROP_RETRY_DELAY, 2000L);
+        rrProps.put(ConfigurationConstants.PROP_MAX_PARALLEL, 5);
+        rrConfig.update(rrProps);
+
+        this.sleep(1000L);
+    }
+
+    @Override
+    @After
+    public void cleanup() {
+        super.cleanup();
+    }
+
+    /**
+     * Setup consumers
+     */
+    private void setupJobConsumers() {
+        for(int i=0; i<NUM_ORDERED_TOPICS; i++) {
+            this.registerJobConsumer(ORDERED_TOPICS[i],
+
+                new JobConsumer() {
+
+                    @Override
+                    public JobResult process(final Job job) {
+                        return JobResult.OK;
+                    }
+                });
+        }
+        for(int i=0; i<NUM_PARALLEL_TOPICS; i++) {
+            this.registerJobConsumer(PARALLEL_TOPICS[i],
+
+                new JobConsumer() {
+
+                    @Override
+                    public JobResult process(final Job job) {
+                        return JobResult.OK;
+                    }
+                });
+        }
+        for(int i=0; i<NUM_ROUND_TOPICS; i++) {
+            this.registerJobConsumer(ROUND_TOPICS[i],
+
+                new JobConsumer() {
+
+                    @Override
+                    public JobResult process(final Job job) {
+                        return JobResult.OK;
+                    }
+                });
+        }
+    }
+
+    private static final class CreateJobThread extends Thread {
+
+        private final String[] topics;
+
+        private final JobManager jobManager;
+
+        private final Random random = new Random();
+
+        final Map<String, AtomicLong> created;
+
+        final AtomicLong finishedThreads;
+
+        public CreateJobThread(final JobManager jobManager,
+                final String[] topics,
+                final Map<String, AtomicLong> created,
+                final AtomicLong finishedThreads) {
+            this.topics = topics;
+            this.jobManager = jobManager;
+            this.created = created;
+            this.finishedThreads = finishedThreads;
+        }
+
+        @Override
+        public void run() {
+            int index = 0;
+            final long startTime = System.currentTimeMillis();
+            final long endTime = startTime + DURATION * 1000;
+            while ( System.currentTimeMillis() < endTime ) {
+                final String topic = topics[index];
+                if ( jobManager.addJob(topic, null) != null ) {
+                    created.get(topic).incrementAndGet();
+
+                    index++;
+                    if ( index == topics.length ) {
+                        index = 0;
+                    }
+                }
+                final int sleepTime = random.nextInt(200);
+                try {
+                    Thread.sleep(sleepTime);
+                } catch ( final InterruptedException ie) {
+                    Thread.currentThread().interrupt();
+                }
+            }
+            finishedThreads.incrementAndGet();
+        }
+
+    }
+
+    /**
+     * Setup job creation threads
+     */
+    private void setupJobCreationThreads(final List<Thread> threads,
+            final JobManager jobManager,
+            final Map<String, AtomicLong> created,
+            final AtomicLong finishedThreads) {
+        for(int i=0;i<NUM_ORDERED_THREADS;i++) {
+            threads.add(new CreateJobThread(jobManager, ORDERED_TOPICS, created, finishedThreads));
+        }
+        for(int i=0;i<NUM_PARALLEL_THREADS;i++) {
+            threads.add(new CreateJobThread(jobManager, PARALLEL_TOPICS, created, finishedThreads));
+        }
+        for(int i=0;i<NUM_ROUND_THREADS;i++) {
+            threads.add(new CreateJobThread(jobManager, ROUND_TOPICS, created, finishedThreads));
+        }
+    }
+
+    /**
+     * Setup chaos thread(s)
+     *
+     * Chaos is right now created by sending topology changing/changed events randomly
+     */
+    private void setupChaosThreads(final List<Thread> threads,
+            final AtomicLong finishedThreads) {
+        final List<TopologyView> views = new ArrayList<TopologyView>();
+        // register topology listener
+        final ServiceRegistration<TopologyEventListener> reg = this.bc.registerService(TopologyEventListener.class, new TopologyEventListener() {
+
+            @Override
+            public void handleTopologyEvent(final TopologyEvent event) {
+                if ( event.getType() == Type.TOPOLOGY_INIT ) {
+                    views.add(event.getNewView());
+                }
+            }
+        }, null);
+        while ( views.isEmpty() ) {
+            this.sleep(10);
+        }
+        reg.unregister();
+        final TopologyView view = views.get(0);
+
+        try {
+            final Collection<ServiceReference<TopologyEventListener>> refs = this.bc.getServiceReferences(TopologyEventListener.class, null);
+            assertNotNull(refs);
+            assertFalse(refs.isEmpty());
+            TopologyEventListener found = null;
+            for(final ServiceReference<TopologyEventListener> ref : refs) {
+                final TopologyEventListener listener = this.bc.getService(ref);
+                if ( listener != null && listener.getClass().getName().equals("org.apache.sling.event.impl.jobs.config.TopologyHandler") ) {
+                    found = listener;
+                    break;
+                }
+                bc.ungetService(ref);
+            }
+            assertNotNull(found);
+            final TopologyEventListener tel = found;
+
+            threads.add(new Thread() {
+
+                private final Random random = new Random();
+
+                @Override
+                public void run() {
+                    final long startTime = System.currentTimeMillis();
+                    // this thread runs 30 seconds longer than the job creation thread
+                    final long endTime = startTime + (DURATION +30) * 1000;
+                    while ( System.currentTimeMillis() < endTime ) {
+                        final int sleepTime = random.nextInt(25) + 15;
+                        try {
+                            Thread.sleep(sleepTime * 1000);
+                        } catch ( final InterruptedException ie) {
+                            Thread.currentThread().interrupt();
+                        }
+                        tel.handleTopologyEvent(new TopologyEvent(Type.TOPOLOGY_CHANGING, view, null));
+                        final int changingTime = random.nextInt(20) + 3;
+                        try {
+                            Thread.sleep(changingTime * 1000);
+                        } catch ( final InterruptedException ie) {
+                            Thread.currentThread().interrupt();
+                        }
+                        tel.handleTopologyEvent(new TopologyEvent(Type.TOPOLOGY_CHANGED, view, view));
+                    }
+                    tel.getClass().getName();
+                    finishedThreads.incrementAndGet();
+                }
+            });
+        } catch (InvalidSyntaxException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test(timeout=DURATION * 16000L)
+    public void testDoChaos() throws Exception {
+        final JobManager jobManager = this.getJobManager();
+
+        // setup added, created and finished map
+        // added and finished are filled by notifications
+        // created is filled by the threads starting jobs
+        final Map<String, AtomicLong> added = new HashMap<String, AtomicLong>();
+        final Map<String, AtomicLong> created = new HashMap<String, AtomicLong>();
+        final Map<String, AtomicLong> finished = new HashMap<String, AtomicLong>();
+        final List<String> topics = new ArrayList<String>();
+        for(int i=0;i<NUM_ORDERED_TOPICS;i++) {
+            added.put(ORDERED_TOPICS[i], new AtomicLong());
+            created.put(ORDERED_TOPICS[i], new AtomicLong());
+            finished.put(ORDERED_TOPICS[i], new AtomicLong());
+            topics.add(ORDERED_TOPICS[i]);
+        }
+        for(int i=0;i<NUM_PARALLEL_TOPICS;i++) {
+            added.put(PARALLEL_TOPICS[i], new AtomicLong());
+            created.put(PARALLEL_TOPICS[i], new AtomicLong());
+            finished.put(PARALLEL_TOPICS[i], new AtomicLong());
+            topics.add(PARALLEL_TOPICS[i]);
+        }
+        for(int i=0;i<NUM_ROUND_TOPICS;i++) {
+            added.put(ROUND_TOPICS[i], new AtomicLong());
+            created.put(ROUND_TOPICS[i], new AtomicLong());
+            finished.put(ROUND_TOPICS[i], new AtomicLong());
+            topics.add(ROUND_TOPICS[i]);
+        }
+
+        final List<Thread> threads = new ArrayList<Thread>();
+        final AtomicLong finishedThreads = new AtomicLong();
+
+        this.registerEventHandler("org/apache/sling/event/notification/job/*",
+                new EventHandler() {
+
+                    @Override
+                    public void handleEvent(final Event event) {
+                        final String topic = (String) event.getProperty(NotificationConstants.NOTIFICATION_PROPERTY_JOB_TOPIC);
+                        if ( NotificationConstants.TOPIC_JOB_FINISHED.equals(event.getTopic())) {
+                            finished.get(topic).incrementAndGet();
+                        } else if ( NotificationConstants.TOPIC_JOB_ADDED.equals(event.getTopic())) {
+                            added.get(topic).incrementAndGet();
+                        }
+                    }
+                });
+
+        // setup job consumers
+        this.setupJobConsumers();
+
+        // setup job creation tests
+        this.setupJobCreationThreads(threads, jobManager, created, finishedThreads);
+
+        this.setupChaosThreads(threads, finishedThreads);
+
+        System.out.println("Starting threads...");
+        // start threads
+        for(final Thread t : threads) {
+            t.setDaemon(true);
+            t.start();
+        }
+
+        System.out.println("Sleeping for " + DURATION + " seconds to wait for threads to finish...");
+        // for sure we can sleep for the duration
+        this.sleep(DURATION * 1000);
+
+        System.out.println("Polling for threads to finish...");
+        // wait until threads are finished
+        while ( finishedThreads.get() < threads.size() ) {
+            this.sleep(100);
+        }
+
+        System.out.println("Waiting for job handling to finish...");
+        final Set<String> allTopics = new HashSet<String>(topics);
+        while ( !allTopics.isEmpty() ) {
+            final Iterator<String> iter = allTopics.iterator();
+            while ( iter.hasNext() ) {
+                final String topic = iter.next();
+                if ( finished.get(topic).get() == created.get(topic).get() ) {
+                    iter.remove();
+                }
+            }
+            this.sleep(100);
+        }
+/* We could try to enable this with Oak again - but right now JR observation handler is too
+ * slow.
+            System.out.println("Checking notifications...");
+            for(final String topic : topics) {
+                assertEquals("Checking topic " + topic, created.get(topic).get(), added.get(topic).get());
+            }
+ */
+
+    }
+}
diff --git a/src/test/java/org/apache/sling/event/it/ClassloadingTest.java b/src/test/java/org/apache/sling/event/it/ClassloadingTest.java
new file mode 100644
index 0000000..b182401
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/it/ClassloadingTest.java
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.it;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.sling.event.impl.jobs.config.ConfigurationConstants;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.JobManager;
+import org.apache.sling.event.jobs.NotificationConstants;
+import org.apache.sling.event.jobs.QueueConfiguration;
+import org.apache.sling.event.jobs.consumer.JobConsumer;
+import org.apache.sling.testing.tools.retry.RetryLoop;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventHandler;
+
+@RunWith(PaxExam.class)
+public class ClassloadingTest extends AbstractJobHandlingTest {
+
+    private static final int CONDITION_INTERVAL_MILLIS = 50;
+    private static final int CONDITION_TIMEOUT_SECONDS = 5;
+
+    private static final String QUEUE_NAME = "cltest";
+    private static final String TOPIC = "sling/cltest";
+
+    @Override
+    @Before
+    public void setup() throws IOException {
+        super.setup();
+
+        // create ignore test queue
+        final org.osgi.service.cm.Configuration orderedConfig = this.configAdmin.createFactoryConfiguration("org.apache.sling.event.jobs.QueueConfiguration", null);
+        final Dictionary<String, Object> orderedProps = new Hashtable<String, Object>();
+        orderedProps.put(ConfigurationConstants.PROP_NAME, QUEUE_NAME);
+        orderedProps.put(ConfigurationConstants.PROP_TYPE, QueueConfiguration.Type.UNORDERED.name());
+        orderedProps.put(ConfigurationConstants.PROP_TOPICS, TOPIC);
+        orderedConfig.update(orderedProps);
+
+        this.sleep(1000L);
+    }
+
+    @Override
+    @After
+    public void cleanup() {
+        super.cleanup();
+    }
+
+    @Test(timeout = DEFAULT_TEST_TIMEOUT)
+    public void testSimpleClassloading() throws Exception {
+        final AtomicInteger processedJobsCount = new AtomicInteger(0);
+        final List<Event> finishedEvents = Collections.synchronizedList(new ArrayList<Event>());
+        final CountDownLatch latch = new CountDownLatch(1);
+        this.registerJobConsumer(TOPIC,
+                new JobConsumer() {
+                    @Override
+                    public JobResult process(Job job) {
+                        processedJobsCount.incrementAndGet();
+                        return JobResult.OK;
+                    }
+                });
+        this.registerEventHandler(NotificationConstants.TOPIC_JOB_FINISHED,
+                new EventHandler() {
+
+                    @Override
+                    public void handleEvent(Event event) {
+                        finishedEvents.add(event);
+                        latch.countDown();
+                    }
+                });
+        final JobManager jobManager = this.getJobManager();
+
+        final List<String> list = new ArrayList<String>();
+        list.add("1");
+        list.add("2");
+
+        final Map<String, String> map = new HashMap<String, String>();
+        map.put("a", "a1");
+        map.put("b", "b2");
+
+        // we start a single job
+        final Map<String, Object> props = new HashMap<String, Object>();
+        props.put("string", "Hello");
+        props.put("int", new Integer(5));
+        props.put("long", new Long(7));
+        props.put("list", list);
+        props.put("map", map);
+
+        final String jobId = jobManager.addJob(TOPIC, props).getId();
+        try {
+            latch.await(5, TimeUnit.SECONDS);
+            assertFalse("At least one finished job", finishedEvents.isEmpty());
+            assertEquals(1, processedJobsCount.get());
+
+            final String jobTopic = (String)finishedEvents.get(0).getProperty(NotificationConstants.NOTIFICATION_PROPERTY_JOB_TOPIC);
+            assertNotNull(jobTopic);
+            assertEquals("Hello", finishedEvents.get(0).getProperty("string"));
+            assertEquals(new Integer(5), Integer.valueOf(finishedEvents.get(0).getProperty("int").toString()));
+            assertEquals(new Long(7), Long.valueOf(finishedEvents.get(0).getProperty("long").toString()));
+            assertEquals(list, finishedEvents.get(0).getProperty("list"));
+            assertEquals(map, finishedEvents.get(0).getProperty("map"));
+        } finally {
+            jobManager.removeJobById(jobId);
+        }
+    }
+
+    @Test(timeout = DEFAULT_TEST_TIMEOUT)
+    public void testFailedClassloading() throws Exception {
+        final AtomicInteger failedJobsCount = new AtomicInteger(0);
+        final List<Event> finishedEvents = Collections.synchronizedList(new ArrayList<Event>());
+        this.registerJobConsumer(TOPIC + "/failed",
+                new JobConsumer() {
+
+                    @Override
+                    public JobResult process(Job job) {
+                        failedJobsCount.incrementAndGet();
+                        return JobResult.OK;
+                    }
+                });
+        this.registerEventHandler(NotificationConstants.TOPIC_JOB_FINISHED,
+                new EventHandler() {
+
+                    @Override
+                    public void handleEvent(Event event) {
+                        finishedEvents.add(event);
+                    }
+                });
+        final JobManager jobManager = this.getJobManager();
+
+        // dao is an invisible class for the dynamic class loader as it is not public
+        // therefore scheduling this job should fail!
+        final DataObject dao = new DataObject();
+
+        // we start a single job
+        final Map<String, Object> props = new HashMap<String, Object>();
+        props.put("dao", dao);
+
+        final String id = jobManager.addJob(TOPIC + "/failed", props).getId();
+
+        try {
+            // wait until the conditions are met
+            new RetryLoop(new RetryLoop.Condition() {
+
+                @Override
+                public boolean isTrue() throws Exception {
+                    return failedJobsCount.get() == 0
+                            && finishedEvents.size() == 0
+                            && jobManager.findJobs(JobManager.QueryType.ALL, TOPIC + "/failed", -1,
+                                    (Map<String, Object>[]) null).size() == 1
+                            && jobManager.getStatistics().getNumberOfQueuedJobs() == 0
+                            && jobManager.getStatistics().getNumberOfActiveJobs() == 0;
+                }
+
+                @Override
+                public String getDescription() {
+                    return "Waiting for job failure to be recorded. Conditions " +
+                           "faildJobsCount=" + failedJobsCount.get() +
+                           ", finishedEvents=" + finishedEvents.size() +
+                           ", findJobs= " + jobManager.findJobs(JobManager.QueryType.ALL, TOPIC + "/failed", -1,
+                                   (Map<String, Object>[]) null).size()
+                           +", queuedJobs=" + jobManager.getStatistics().getNumberOfQueuedJobs()
+                           +", activeJobs=" + jobManager.getStatistics().getNumberOfActiveJobs();
+                }
+            }, CONDITION_TIMEOUT_SECONDS, CONDITION_INTERVAL_MILLIS);
+
+            jobManager.removeJobById(id); // moves the job to the history section
+            assertEquals(0, jobManager.findJobs(JobManager.QueryType.ALL, TOPIC + "/failed", -1, (Map<String, Object>[])null).size());
+        } finally {
+            jobManager.removeJobById(id); // removes the job permanently
+        }
+    }
+
+    private static final class DataObject implements Serializable {
+        private static final long serialVersionUID = 1L;
+    }
+}
diff --git a/src/test/java/org/apache/sling/event/it/HistoryTest.java b/src/test/java/org/apache/sling/event/it/HistoryTest.java
new file mode 100644
index 0000000..d797f35
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/it/HistoryTest.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.it;
+
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+
+import org.apache.sling.event.impl.jobs.config.ConfigurationConstants;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.JobManager;
+import org.apache.sling.event.jobs.QueueConfiguration;
+import org.apache.sling.event.jobs.consumer.JobExecutionContext;
+import org.apache.sling.event.jobs.consumer.JobExecutionResult;
+import org.apache.sling.event.jobs.consumer.JobExecutor;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+
+@RunWith(PaxExam.class)
+public class HistoryTest extends AbstractJobHandlingTest {
+
+    private static final String TOPIC = "sling/test/history";
+
+    private static final String PROP_COUNTER = "counter";
+
+    @Override
+    @Before
+    public void setup() throws IOException {
+        super.setup();
+
+        // create test queue - we use an ordered queue to have a stable processing order
+        // keep the jobs in the history
+        final org.osgi.service.cm.Configuration config = this.configAdmin.createFactoryConfiguration("org.apache.sling.event.jobs.QueueConfiguration", null);
+        Dictionary<String, Object> props = new Hashtable<String, Object>();
+        props.put(ConfigurationConstants.PROP_NAME, "test");
+        props.put(ConfigurationConstants.PROP_TYPE, QueueConfiguration.Type.ORDERED.name());
+        props.put(ConfigurationConstants.PROP_TOPICS, new String[] {TOPIC});
+        props.put(ConfigurationConstants.PROP_RETRIES, 2);
+        props.put(ConfigurationConstants.PROP_RETRY_DELAY, 2L);
+        props.put(ConfigurationConstants.PROP_KEEP_JOBS, true);
+        config.update(props);
+
+        this.sleep(1000L);
+    }
+
+    @Override
+    @After
+    public void cleanup() {
+        super.cleanup();
+    }
+
+    private Job addJob(final long counter) {
+        final Map<String, Object> props = new HashMap<String, Object>();
+        props.put(PROP_COUNTER, counter);
+        return this.getJobManager().addJob(TOPIC, props );
+    }
+
+    /**
+     * Test history.
+     * Start 10 jobs and cancel some of them and succeed others
+     */
+    @Test(timeout = DEFAULT_TEST_TIMEOUT)
+    public void testHistory() throws Exception {
+        this.registerJobExecutor(TOPIC,
+                new JobExecutor() {
+
+                    @Override
+                    public JobExecutionResult process(final Job job, final JobExecutionContext context) {
+                        sleep(5L);
+                        final long count = job.getProperty(PROP_COUNTER, Long.class);
+                        if ( count == 2 || count == 5 || count == 7 ) {
+                            return context.result().message(Job.JobState.ERROR.name()).cancelled();
+                        }
+                        return context.result().message(Job.JobState.SUCCEEDED.name()).succeeded();
+                    }
+
+                });
+        for(int i = 0; i< 10; i++) {
+            this.addJob(i);
+        }
+        this.sleep(200L);
+        while ( this.getJobManager().findJobs(JobManager.QueryType.HISTORY, TOPIC, -1, (Map<String, Object>[])null).size() < 10 ) {
+            this.sleep(20L);
+        }
+        Collection<Job> col = this.getJobManager().findJobs(JobManager.QueryType.HISTORY, TOPIC, -1, (Map<String, Object>[])null);
+        assertEquals(10, col.size());
+        assertEquals(0, this.getJobManager().findJobs(JobManager.QueryType.ACTIVE, TOPIC, -1, (Map<String, Object>[])null).size());
+        assertEquals(0, this.getJobManager().findJobs(JobManager.QueryType.QUEUED, TOPIC, -1, (Map<String, Object>[])null).size());
+        assertEquals(0, this.getJobManager().findJobs(JobManager.QueryType.ALL, TOPIC, -1, (Map<String, Object>[])null).size());
+        assertEquals(3, this.getJobManager().findJobs(JobManager.QueryType.CANCELLED, TOPIC, -1, (Map<String, Object>[])null).size());
+        assertEquals(0, this.getJobManager().findJobs(JobManager.QueryType.DROPPED, TOPIC, -1, (Map<String, Object>[])null).size());
+        assertEquals(3, this.getJobManager().findJobs(JobManager.QueryType.ERROR, TOPIC, -1, (Map<String, Object>[])null).size());
+        assertEquals(0, this.getJobManager().findJobs(JobManager.QueryType.GIVEN_UP, TOPIC, -1, (Map<String, Object>[])null).size());
+        assertEquals(0, this.getJobManager().findJobs(JobManager.QueryType.STOPPED, TOPIC, -1, (Map<String, Object>[])null).size());
+        assertEquals(7, this.getJobManager().findJobs(JobManager.QueryType.SUCCEEDED, TOPIC, -1, (Map<String, Object>[])null).size());
+
+        // find all topics
+        assertEquals(7, this.getJobManager().findJobs(JobManager.QueryType.SUCCEEDED, null, -1, (Map<String, Object>[])null).size());
+
+        // verify order, message and state
+        long last = 9;
+        for(final Job j : col) {
+            assertNotNull(j.getFinishedDate());
+            final long count = j.getProperty(PROP_COUNTER, Long.class);
+            assertEquals(last, count);
+            if ( count == 2 || count == 5 || count == 7 ) {
+                assertEquals(Job.JobState.ERROR, j.getJobState());
+            } else {
+                assertEquals(Job.JobState.SUCCEEDED, j.getJobState());
+            }
+            assertEquals(j.getJobState().name(), j.getResultMessage());
+            last--;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/event/it/JobHandlingTest.java b/src/test/java/org/apache/sling/event/it/JobHandlingTest.java
new file mode 100644
index 0000000..54867ce
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/it/JobHandlingTest.java
@@ -0,0 +1,436 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.it;
+
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.sling.event.impl.Barrier;
+import org.apache.sling.event.impl.jobs.config.ConfigurationConstants;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.JobManager;
+import org.apache.sling.event.jobs.NotificationConstants;
+import org.apache.sling.event.jobs.QueueConfiguration;
+import org.apache.sling.event.jobs.consumer.JobConsumer;
+import org.apache.sling.event.jobs.consumer.JobExecutionContext;
+import org.apache.sling.event.jobs.consumer.JobExecutionResult;
+import org.apache.sling.event.jobs.consumer.JobExecutor;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventHandler;
+
+@RunWith(PaxExam.class)
+public class JobHandlingTest extends AbstractJobHandlingTest {
+
+    public static final String TOPIC = "sling/test";
+
+    @Override
+    @Before
+    public void setup() throws IOException {
+        super.setup();
+
+        // create test queue
+        final org.osgi.service.cm.Configuration config = this.configAdmin.createFactoryConfiguration("org.apache.sling.event.jobs.QueueConfiguration", null);
+        Dictionary<String, Object> props = new Hashtable<String, Object>();
+        props.put(ConfigurationConstants.PROP_NAME, "test");
+        props.put(ConfigurationConstants.PROP_TYPE, QueueConfiguration.Type.UNORDERED.name());
+        props.put(ConfigurationConstants.PROP_TOPICS, new String[] {TOPIC, TOPIC + "2"});
+        props.put(ConfigurationConstants.PROP_RETRIES, 2);
+        props.put(ConfigurationConstants.PROP_RETRY_DELAY, 2000L);
+        config.update(props);
+
+        this.sleep(1000L);
+    }
+
+    @Override
+    @After
+    public void cleanup() {
+        super.cleanup();
+    }
+
+    /**
+     * Test simple job execution.
+     * The job is executed once and finished successfully.
+     */
+    @Test(timeout = DEFAULT_TEST_TIMEOUT)
+    public void testSimpleJobExecutionUsingJobConsumer() throws Exception {
+        final Barrier cb = new Barrier(2);
+
+        this.registerJobConsumer(TOPIC,
+                new JobConsumer() {
+
+            @Override
+                    public JobResult process(final Job job) {
+                        cb.block();
+                        return JobResult.OK;
+                    }
+                 });
+
+        this.getJobManager().addJob(TOPIC, null);
+        assertTrue("No event received in the given time.", cb.block(5));
+        cb.reset();
+        assertFalse("Unexpected event received in the given time.", cb.block(5));
+    }
+
+    /**
+     * Test simple job execution.
+     * The job is executed once and finished successfully.
+     */
+    @Test(timeout = DEFAULT_TEST_TIMEOUT)
+    public void testSimpleJobExecutionUsingJobExecutor() throws Exception {
+        final Barrier cb = new Barrier(2);
+
+        this.registerJobExecutor(TOPIC,
+                new JobExecutor() {
+
+                    @Override
+                    public JobExecutionResult process(final Job job, final JobExecutionContext context) {
+                        cb.block();
+                        return context.result().succeeded();
+                    }
+                });
+
+        this.getJobManager().addJob(TOPIC, null);
+        assertTrue("No event received in the given time.", cb.block(5));
+        cb.reset();
+        assertFalse("Unexpected event received in the given time.", cb.block(5));
+    }
+
+    @Test(timeout = DEFAULT_TEST_TIMEOUT)
+    public void testManyJobs() throws Exception {
+        this.registerJobConsumer(TOPIC,
+                new JobConsumer() {
+
+                    @Override
+                    public JobResult process(final Job job) {
+                        return JobResult.OK;
+                    }
+
+                 });
+        final AtomicInteger count = new AtomicInteger(0);
+        this.registerEventHandler(NotificationConstants.TOPIC_JOB_FINISHED,
+                new EventHandler() {
+                    @Override
+                    public void handleEvent(final Event event) {
+                        count.incrementAndGet();
+                    }
+                 });
+
+        // we start "some" jobs
+        final int COUNT = 300;
+        for(int i = 0; i < COUNT; i++ ) {
+            this.getJobManager().addJob(TOPIC, null);
+        }
+        while ( count.get() < COUNT ) {
+            this.sleep(50);
+        }
+        assertEquals("Finished count", COUNT, count.get());
+        assertEquals("Finished count", COUNT, this.getJobManager().getStatistics().getNumberOfFinishedJobs());
+    }
+
+    /**
+     * Test canceling a job
+     * The job execution always fails
+     */
+    @Test(timeout = DEFAULT_TEST_TIMEOUT)
+    public void testCancelJob() throws Exception {
+        final Barrier cb = new Barrier(2);
+        final Barrier cb2 = new Barrier(2);
+        this.registerJobConsumer(TOPIC,
+                new JobConsumer() {
+
+                    @Override
+                    public JobResult process(Job job) {
+                        cb.block();
+                        cb2.block();
+                        return JobResult.FAILED;
+                    }
+                });
+
+        final Map<String, Object> jobProperties = Collections.singletonMap("id", (Object)"cancelJobId");
+        @SuppressWarnings("unchecked")
+        final Map<String, Object>[] jobPropertiesAsArray = new Map[1];
+        jobPropertiesAsArray[0] = jobProperties;
+
+        // create job
+        final JobManager jobManager = this.getJobManager();
+        jobManager.addJob(TOPIC, jobProperties);
+        cb.block();
+
+        assertEquals(1, jobManager.findJobs(JobManager.QueryType.ALL, TOPIC, -1, jobPropertiesAsArray).size());
+        // job is currently waiting, therefore cancel fails
+        final Job e1 = jobManager.getJob(TOPIC, jobProperties);
+        assertNotNull(e1);
+        cb2.block(); // and continue job
+
+        sleep(200);
+
+        // the job is now in the queue again
+        final Job e2 = jobManager.getJob(TOPIC, jobProperties);
+        assertNotNull(e2);
+        assertTrue(jobManager.removeJobById(e2.getId()));
+        assertEquals(0, jobManager.findJobs(JobManager.QueryType.ALL, TOPIC, -1, jobPropertiesAsArray).size());
+        final Collection<Job> col = jobManager.findJobs(JobManager.QueryType.HISTORY, TOPIC, -1,
+                jobPropertiesAsArray);
+        try {
+            assertEquals(1, col.size());
+        } finally {
+            for(final Job j : col) {
+                jobManager.removeJobById(j.getId());
+            }
+        }
+   }
+
+    /**
+     * Test get a job
+     */
+    @Test(timeout = DEFAULT_TEST_TIMEOUT)
+    public void testGetJob() throws Exception {
+        final Barrier cb = new Barrier(2);
+        final Barrier cb2 = new Barrier(2);
+        this.registerJobConsumer(TOPIC,
+                new JobConsumer() {
+
+                    @Override
+                    public JobResult process(Job job) {
+                        cb.block();
+                        cb2.block();
+                        return JobResult.OK;
+                    }
+                });
+        final JobManager jobManager = this.getJobManager();
+        final Job j = jobManager.addJob(TOPIC, null);
+        cb.block();
+
+        assertNotNull(jobManager.getJob(TOPIC, null));
+
+        cb2.block(); // and continue job
+
+        jobManager.removeJobById(j.getId());
+    }
+
+    /**
+     * Reschedule test.
+     * The job is rescheduled two times before it fails.
+     */
+    @Test(timeout = DEFAULT_TEST_TIMEOUT)
+    public void testStartJobAndReschedule() throws Exception {
+        final List<Integer> retryCountList = new ArrayList<Integer>();
+        final Barrier cb = new Barrier(2);
+
+        this.registerJobConsumer(TOPIC,
+                new JobConsumer() {
+                    int retryCount;
+
+                    @Override
+                    public JobResult process(Job job) {
+                        int retry = 0;
+                        if ( job.getProperty(Job.PROPERTY_JOB_RETRY_COUNT) != null ) {
+                            retry = (Integer)job.getProperty(Job.PROPERTY_JOB_RETRY_COUNT);
+                        }
+                        if ( retry == retryCount ) {
+                            retryCountList.add(retry);
+                        }
+                        retryCount++;
+                        cb.block();
+                        return JobResult.FAILED;
+                    }
+                });
+
+        final JobManager jobManager = this.getJobManager();
+        final Job job = jobManager.addJob(TOPIC, null);
+
+        assertTrue("No event received in the given time.", cb.block(5));
+        cb.reset();
+        // the job is retried after two seconds, so we wait again
+        assertTrue("No event received in the given time.", cb.block(5));
+        cb.reset();
+        // the job is retried after two seconds, so we wait again
+        assertTrue("No event received in the given time.", cb.block(5));
+        // we have reached the retry so we expect to not get an event
+        cb.reset();
+        assertFalse("Unexpected event received in the given time.", cb.block(5));
+        assertEquals("Unexpected number of retries", 3, retryCountList.size());
+
+        jobManager.removeJobById(job.getId());
+    }
+
+    /**
+     * Notifications.
+     * We send several jobs which are treated different and then see
+     * how many invocations have been sent.
+     */
+    @Test(timeout = DEFAULT_TEST_TIMEOUT)
+    public void testNotifications() throws Exception {
+        final List<String> cancelled = Collections.synchronizedList(new ArrayList<String>());
+        final List<String> failed = Collections.synchronizedList(new ArrayList<String>());
+        final List<String> finished = Collections.synchronizedList(new ArrayList<String>());
+        final List<String> started = Collections.synchronizedList(new ArrayList<String>());
+        this.registerJobConsumer(TOPIC,
+                new JobConsumer() {
+
+                    @Override
+                    public JobResult process(Job job) {
+                        // events 1 and 4 finish the first time
+                        final String id = (String)job.getProperty("id");
+                        if ( "1".equals(id) || "4".equals(id) ) {
+                            return JobResult.OK;
+
+                        // 5 fails always
+                        } else if ( "5".equals(id) ) {
+                            return JobResult.FAILED;
+                        } else {
+                            int retry = 0;
+                            if ( job.getProperty(Job.PROPERTY_JOB_RETRY_COUNT) != null ) {
+                                retry = (Integer)job.getProperty(Job.PROPERTY_JOB_RETRY_COUNT);
+                            }
+                            // 2 fails the first time
+                            if ( "2".equals(id) ) {
+                                if ( retry == 0 ) {
+                                    return JobResult.FAILED;
+                                } else {
+                                    return JobResult.OK;
+                                }
+                            }
+                            // 3 fails the first and second time
+                            if ( "3".equals(id) ) {
+                                if ( retry == 0 || retry == 1 ) {
+                                    return JobResult.FAILED;
+                                } else {
+                                    return JobResult.OK;
+                                }
+                            }
+                        }
+                        return JobResult.FAILED;
+                    }
+                });
+        this.registerEventHandler(NotificationConstants.TOPIC_JOB_CANCELLED,
+                new EventHandler() {
+
+                    @Override
+                    public void handleEvent(Event event) {
+                        final String id = (String)event.getProperty("id");
+                        cancelled.add(id);
+                    }
+                });
+        this.registerEventHandler(NotificationConstants.TOPIC_JOB_FAILED,
+                new EventHandler() {
+
+                    @Override
+                    public void handleEvent(Event event) {
+                        final String id = (String)event.getProperty("id");
+                        failed.add(id);
+                    }
+                });
+        this.registerEventHandler(NotificationConstants.TOPIC_JOB_FINISHED,
+                new EventHandler() {
+
+                    @Override
+                    public void handleEvent(Event event) {
+                        final String id = (String)event.getProperty("id");
+                        finished.add(id);
+                    }
+                });
+        this.registerEventHandler(NotificationConstants.TOPIC_JOB_STARTED,
+                new EventHandler() {
+
+                    @Override
+                    public void handleEvent(Event event) {
+                        final String id = (String)event.getProperty("id");
+                        started.add(id);
+                    }
+                });
+
+        final JobManager jobManager = this.getJobManager();
+
+        jobManager.addJob(TOPIC, Collections.singletonMap("id", (Object)"1"));
+        jobManager.addJob(TOPIC, Collections.singletonMap("id", (Object)"2"));
+        jobManager.addJob(TOPIC, Collections.singletonMap("id", (Object)"3"));
+        jobManager.addJob(TOPIC, Collections.singletonMap("id", (Object)"4"));
+        jobManager.addJob(TOPIC, Collections.singletonMap("id", (Object)"5"));
+
+        int count = 0;
+        final long startTime = System.currentTimeMillis();
+        do {
+            count = finished.size() + cancelled.size();
+            // after 25 seconds we cancel the test
+            if ( System.currentTimeMillis() - startTime > 25000 ) {
+                throw new Exception("Timeout during notification test.");
+            }
+        } while ( count < 5 || started.size() < 10 );
+        assertEquals("Finished count", 4, finished.size());
+        assertEquals("Cancelled count", 1, cancelled.size());
+        assertEquals("Started count", 10, started.size());
+        assertEquals("Failed count", 5, failed.size());
+    }
+
+    /**
+     * Test sending of jobs with and without a processor
+     */
+    @Test(timeout = DEFAULT_TEST_TIMEOUT)
+    public void testNoJobProcessor() throws Exception {
+        final AtomicInteger count = new AtomicInteger(0);
+
+        this.registerJobConsumer(TOPIC,
+                new JobConsumer() {
+
+            @Override
+            public JobResult process(final Job job) {
+                count.incrementAndGet();
+
+                return JobResult.OK;
+            }
+         });
+
+        final JobManager jobManager = this.getJobManager();
+
+        // we start 20 jobs, every second job has no processor
+        final int COUNT = 20;
+        for(int i = 0; i < COUNT; i++ ) {
+            final String jobTopic = (i % 2 == 0 ? TOPIC : TOPIC + "2");
+
+            jobManager.addJob(jobTopic, null);
+        }
+        while ( jobManager.getStatistics().getNumberOfFinishedJobs() < COUNT / 2) {
+            this.sleep(50);
+        }
+
+        assertEquals("Finished count", COUNT / 2, count.get());
+        // unprocessed count should be 0 as there is no job consumer for this job
+        assertEquals("Unprocessed count", 0, jobManager.getStatistics().getNumberOfJobs());
+        assertEquals("Finished count", COUNT / 2, jobManager.getStatistics().getNumberOfFinishedJobs());
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/event/it/OrderedQueueTest.java b/src/test/java/org/apache/sling/event/it/OrderedQueueTest.java
new file mode 100644
index 0000000..be23f69
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/it/OrderedQueueTest.java
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.it;
+
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.sling.event.impl.Barrier;
+import org.apache.sling.event.impl.jobs.config.ConfigurationConstants;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.JobManager;
+import org.apache.sling.event.jobs.NotificationConstants;
+import org.apache.sling.event.jobs.Queue;
+import org.apache.sling.event.jobs.QueueConfiguration;
+import org.apache.sling.event.jobs.consumer.JobConsumer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventHandler;
+
+@RunWith(PaxExam.class)
+public class OrderedQueueTest extends AbstractJobHandlingTest {
+
+    @Override
+    @Before
+    public void setup() throws IOException {
+        super.setup();
+
+        // create ordered test queue
+        final org.osgi.service.cm.Configuration orderedConfig = this.configAdmin.createFactoryConfiguration("org.apache.sling.event.jobs.QueueConfiguration", null);
+        final Dictionary<String, Object> orderedProps = new Hashtable<String, Object>();
+        orderedProps.put(ConfigurationConstants.PROP_NAME, "orderedtest");
+        orderedProps.put(ConfigurationConstants.PROP_TYPE, QueueConfiguration.Type.ORDERED.name());
+        orderedProps.put(ConfigurationConstants.PROP_TOPICS, "sling/orderedtest/*");
+        orderedProps.put(ConfigurationConstants.PROP_RETRIES, 2);
+        orderedProps.put(ConfigurationConstants.PROP_RETRY_DELAY, 2000L);
+        orderedConfig.update(orderedProps);
+
+        this.sleep(1000L);
+    }
+
+    @Override
+    @After
+    public void cleanup() {
+        super.cleanup();
+    }
+
+    /**
+     * Ordered Queue Test
+     */
+    @Test(timeout = DEFAULT_TEST_TIMEOUT)
+    public void testOrderedQueue() throws Exception {
+        final JobManager jobManager = this.getJobManager();
+
+        // register consumer and event handler
+        final Barrier cb = new Barrier(2);
+        final AtomicInteger count = new AtomicInteger(0);
+        final AtomicInteger parallelCount = new AtomicInteger(0);
+        this.registerJobConsumer("sling/orderedtest/*",
+                new JobConsumer() {
+
+                    private volatile int lastCounter = -1;
+
+                    @Override
+                    public JobResult process(final Job job) {
+                        final int counter = job.getProperty("counter", -10);
+                        assertNotEquals("Counter property is missing", -10, counter);
+                        assertTrue("Counter should only increment by max of 1 " + counter + " - " + lastCounter,
+                                counter == lastCounter || counter == lastCounter +1);
+                        lastCounter = counter;
+                        if ("sling/orderedtest/start".equals(job.getTopic()) ) {
+                            cb.block();
+                            return JobResult.OK;
+                        }
+                        if ( parallelCount.incrementAndGet() > 1 ) {
+                            parallelCount.decrementAndGet();
+                            return JobResult.FAILED;
+                        }
+                        final String topic = job.getTopic();
+                        if ( topic.endsWith("sub1") ) {
+                            final int i = (Integer)job.getProperty(Job.PROPERTY_JOB_RETRY_COUNT);
+                            if ( i == 0 ) {
+                                parallelCount.decrementAndGet();
+                                return JobResult.FAILED;
+                            }
+                        }
+                        try {
+                            Thread.sleep(30);
+                        } catch (InterruptedException ie) {
+                            // ignore
+                        }
+                        parallelCount.decrementAndGet();
+                        return JobResult.OK;
+                    }
+                });
+        this.registerEventHandler(NotificationConstants.TOPIC_JOB_FINISHED,
+                new EventHandler() {
+
+                    @Override
+                    public void handleEvent(final Event event) {
+                        count.incrementAndGet();
+                    }
+                });
+
+        // we first sent one event to get the queue started
+        final Map<String, Object> properties = new HashMap<String, Object>();
+        properties.put("counter", -1);
+        jobManager.addJob("sling/orderedtest/start", properties);
+        assertTrue("No event received in the given time.", cb.block(5));
+        cb.reset();
+
+        // get the queue
+        final Queue q = jobManager.getQueue("orderedtest");
+        assertNotNull("Queue 'orderedtest' should exist!", q);
+
+        // suspend it
+        q.suspend();
+
+        final int NUM_JOBS = 30;
+
+        // we start "some" jobs:
+        for(int i = 0; i < NUM_JOBS; i++ ) {
+            final String subTopic = "sling/orderedtest/sub" + (i % 10);
+            properties.clear();
+            properties.put("counter", i);
+            jobManager.addJob(subTopic, properties);
+        }
+        // start the queue
+        q.resume();
+        while ( count.get() < NUM_JOBS +1 ) {
+            try {
+                Thread.sleep(500);
+            } catch (InterruptedException ie) {
+                // ignore
+            }
+        }
+        // we started one event before the test, so add one
+        assertEquals("Finished count", NUM_JOBS + 1, count.get());
+        assertEquals("Finished count", NUM_JOBS + 1, jobManager.getStatistics().getNumberOfFinishedJobs());
+        assertEquals("Finished count", NUM_JOBS + 1, q.getStatistics().getNumberOfFinishedJobs());
+        assertEquals("Failed count", NUM_JOBS / 10, q.getStatistics().getNumberOfFailedJobs());
+        assertEquals("Cancelled count", 0, q.getStatistics().getNumberOfCancelledJobs());
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/event/it/RoundRobinQueueTest.java b/src/test/java/org/apache/sling/event/it/RoundRobinQueueTest.java
new file mode 100644
index 0000000..00dfe2d
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/it/RoundRobinQueueTest.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.it;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.sling.event.impl.Barrier;
+import org.apache.sling.event.impl.jobs.config.ConfigurationConstants;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.JobManager;
+import org.apache.sling.event.jobs.NotificationConstants;
+import org.apache.sling.event.jobs.Queue;
+import org.apache.sling.event.jobs.QueueConfiguration;
+import org.apache.sling.event.jobs.consumer.JobConsumer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventHandler;
+
+@RunWith(PaxExam.class)
+public class RoundRobinQueueTest extends AbstractJobHandlingTest {
+
+    private static final String QUEUE_NAME = "roundrobintest";
+    private static final String TOPIC = "sling/roundrobintest";
+    private static int MAX_PAR = 5;
+    private static int NUM_JOBS = 300;
+
+    @Override
+    @Before
+    public void setup() throws IOException {
+        super.setup();
+
+        // create round robin test queue
+        final org.osgi.service.cm.Configuration rrConfig = this.configAdmin.createFactoryConfiguration("org.apache.sling.event.jobs.QueueConfiguration", null);
+        final Dictionary<String, Object> rrProps = new Hashtable<String, Object>();
+        rrProps.put(ConfigurationConstants.PROP_NAME, QUEUE_NAME);
+        rrProps.put(ConfigurationConstants.PROP_TYPE, QueueConfiguration.Type.TOPIC_ROUND_ROBIN.name());
+        rrProps.put(ConfigurationConstants.PROP_TOPICS, TOPIC + "/*");
+        rrProps.put(ConfigurationConstants.PROP_RETRIES, 2);
+        rrProps.put(ConfigurationConstants.PROP_RETRY_DELAY, 2000L);
+        rrProps.put(ConfigurationConstants.PROP_MAX_PARALLEL, MAX_PAR);
+        rrConfig.update(rrProps);
+
+        this.sleep(1000L);
+    }
+
+    @Override
+    @After
+    public void cleanup() {
+        super.cleanup();
+    }
+
+    @Test(timeout = DEFAULT_TEST_TIMEOUT)
+    public void testRoundRobinQueue() throws Exception {
+        final JobManager jobManager = this.getJobManager();
+
+        final Barrier cb = new Barrier(2);
+
+        this.registerJobConsumer(TOPIC + "/start",
+                new JobConsumer() {
+
+                    @Override
+                    public JobResult process(final Job job) {
+                        cb.block();
+                        return JobResult.OK;
+                    }
+                });
+
+        // register new consumer and event handle
+        final AtomicInteger count = new AtomicInteger(0);
+        final AtomicInteger parallelCount = new AtomicInteger(0);
+        final Set<Integer> maxParticipants = new HashSet<Integer>();
+
+        this.registerJobConsumer(TOPIC + "/*",
+                new JobConsumer() {
+
+                    @Override
+                    public JobResult process(final Job job) {
+                        final int max = parallelCount.incrementAndGet();
+                        if ( max > MAX_PAR ) {
+                            parallelCount.decrementAndGet();
+                            return JobResult.FAILED;
+                        }
+                        synchronized ( maxParticipants ) {
+                            maxParticipants.add(max);
+                        }
+                        sleep(job.getProperty("sleep", 30));
+                        parallelCount.decrementAndGet();
+                        return JobResult.OK;
+                    }
+                });
+        this.registerEventHandler(NotificationConstants.TOPIC_JOB_FINISHED,
+                new EventHandler() {
+
+                    @Override
+                    public void handleEvent(final Event event) {
+                        count.incrementAndGet();
+                    }
+                });
+
+        // we first sent one event to get the queue started
+        jobManager.addJob(TOPIC + "/start", null);
+        assertTrue("No event received in the given time.", cb.block(5));
+        cb.reset();
+
+        // get the queue
+        final Queue q = jobManager.getQueue(QUEUE_NAME);
+        assertNotNull("Queue '" + QUEUE_NAME + "' should exist!", q);
+
+        // suspend it
+        q.suspend();
+
+        // we start "some" jobs:
+        for(int i = 0; i < NUM_JOBS; i++ ) {
+            final String subTopic = TOPIC + "/sub" + (i % 10);
+            final Map<String, Object> props = new HashMap<String, Object>();
+            if ( i < 10 ) {
+                props.put("sleep", 300);
+            } else {
+                props.put("sleep", 30);
+            }
+            jobManager.addJob(subTopic, props);
+        }
+        // start the queue
+        q.resume();
+        while ( count.get() < NUM_JOBS  + 1 ) {
+            assertEquals("Failed count", 0, q.getStatistics().getNumberOfFailedJobs());
+            assertEquals("Cancelled count", 0, q.getStatistics().getNumberOfCancelledJobs());
+            sleep(300);
+        }
+        // we started one event before the test, so add one
+        assertEquals("Finished count", NUM_JOBS + 1, count.get());
+        assertEquals("Finished count", NUM_JOBS + 1, jobManager.getStatistics().getNumberOfFinishedJobs());
+        assertEquals("Finished count", NUM_JOBS + 1, q.getStatistics().getNumberOfFinishedJobs());
+        assertEquals("Failed count", 0, q.getStatistics().getNumberOfFailedJobs());
+        assertEquals("Cancelled count", 0, q.getStatistics().getNumberOfCancelledJobs());
+        for(int i=1; i <= MAX_PAR; i++) {
+            assertTrue("# Participants " + String.valueOf(i) + " not in " + maxParticipants,
+                    maxParticipants.contains(i));
+        }
+    }
+}
diff --git a/src/test/java/org/apache/sling/event/it/SchedulingTest.java b/src/test/java/org/apache/sling/event/it/SchedulingTest.java
new file mode 100644
index 0000000..567ee70
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/it/SchedulingTest.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.it;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.ScheduledJobInfo;
+import org.apache.sling.event.jobs.consumer.JobConsumer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+
+@RunWith(PaxExam.class)
+public class SchedulingTest extends AbstractJobHandlingTest {
+
+    private static final String TOPIC = "job/scheduled/topic";
+
+    @Override
+    @Before
+    public void setup() throws IOException {
+        super.setup();
+
+        this.sleep(1000L);
+    }
+
+    @Override
+    @After
+    public void cleanup() {
+        super.cleanup();
+    }
+
+    @Test(timeout = DEFAULT_TEST_TIMEOUT)
+    public void testScheduling() throws Exception {
+        final AtomicInteger counter = new AtomicInteger();
+
+        this.registerJobConsumer(TOPIC, new JobConsumer() {
+
+            @Override
+            public JobResult process(final Job job) {
+                if ( job.getTopic().equals(TOPIC) ) {
+                    counter.incrementAndGet();
+                }
+                return JobResult.OK;
+            }
+
+        });
+
+        // we schedule three jobs
+        final ScheduledJobInfo info1 = this.getJobManager().createJob(TOPIC).schedule().hourly(5).add();
+        assertNotNull(info1);
+        final ScheduledJobInfo info2 = this.getJobManager().createJob(TOPIC).schedule().daily(10, 5).add();
+        assertNotNull(info2);
+        final ScheduledJobInfo info3 = this.getJobManager().createJob(TOPIC).schedule().weekly(3, 19, 12).add();
+        assertNotNull(info3);
+
+        assertEquals(3, this.getJobManager().getScheduledJobs().size()); // scheduled jobs
+        info3.unschedule();
+        assertEquals(2, this.getJobManager().getScheduledJobs().size()); // scheduled jobs
+        info1.unschedule();
+        assertEquals(1, this.getJobManager().getScheduledJobs().size()); // scheduled jobs
+        info2.unschedule();
+        assertEquals(0, this.getJobManager().getScheduledJobs().size()); // scheduled jobs
+    }
+}
diff --git a/src/test/java/org/apache/sling/event/it/TimedJobsTest.java b/src/test/java/org/apache/sling/event/it/TimedJobsTest.java
new file mode 100644
index 0000000..0b2d556
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/it/TimedJobsTest.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.it;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.ScheduledJobInfo;
+import org.apache.sling.event.jobs.consumer.JobConsumer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+
+@RunWith(PaxExam.class)
+public class TimedJobsTest extends AbstractJobHandlingTest {
+
+    private static final String TOPIC = "timed/test/topic";
+
+    @Override
+    @Before
+    public void setup() throws IOException {
+        super.setup();
+
+        this.sleep(1000L);
+    }
+
+    @Override
+    @After
+    public void cleanup() {
+        super.cleanup();
+    }
+
+    @Test(timeout = DEFAULT_TEST_TIMEOUT)
+    public void testTimedJob() throws Exception {
+        final AtomicInteger counter = new AtomicInteger();
+
+        this.registerJobConsumer(TOPIC, new JobConsumer() {
+
+            @Override
+            public JobResult process(final Job job) {
+                if ( job.getTopic().equals(TOPIC) ) {
+                    counter.incrementAndGet();
+                }
+                return JobResult.OK;
+            }
+
+        });
+
+        final Date d = new Date();
+        d.setTime(System.currentTimeMillis() + 3000); // run in 3 seconds
+
+        // create scheduled job
+        final ScheduledJobInfo info = this.getJobManager().createJob(TOPIC).schedule().at(d).add();
+        assertNotNull(info);
+
+        while ( counter.get() == 0 ) {
+            this.sleep(1000);
+        }
+        assertEquals(0, this.getJobManager().getScheduledJobs().size()); // job is not scheduled anymore
+        info.unschedule();
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/event/it/TopicMatchingTest.java b/src/test/java/org/apache/sling/event/it/TopicMatchingTest.java
new file mode 100644
index 0000000..40f7439
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/it/TopicMatchingTest.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.it;
+
+import java.io.IOException;
+
+import org.apache.sling.event.impl.Barrier;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.NotificationConstants;
+import org.apache.sling.event.jobs.consumer.JobExecutionContext;
+import org.apache.sling.event.jobs.consumer.JobExecutionResult;
+import org.apache.sling.event.jobs.consumer.JobExecutor;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventHandler;
+
+@RunWith(PaxExam.class)
+public class TopicMatchingTest extends AbstractJobHandlingTest {
+
+    public static final String TOPIC = "sling/test/a";
+
+    @Override
+    @Before
+    public void setup() throws IOException {
+        super.setup();
+
+        this.sleep(1000L);
+    }
+
+    @Override
+    @After
+    public void cleanup() {
+        super.cleanup();
+    }
+
+    /**
+     * Test simple pattern matching /*
+     */
+    @Test(timeout = DEFAULT_TEST_TIMEOUT)
+    public void testSimpleMatching() throws Exception {
+        final Barrier barrier = new Barrier(2);
+
+        this.registerJobExecutor("sling/test/*",
+                new JobExecutor() {
+
+                    @Override
+                    public JobExecutionResult process(final Job job, final JobExecutionContext context) {
+                        return context.result().succeeded();
+                    }
+                });
+        this.registerEventHandler(NotificationConstants.TOPIC_JOB_FINISHED,
+                new EventHandler() {
+
+                    @Override
+                    public void handleEvent(final Event event) {
+                        barrier.block();
+                    }
+                });
+
+        this.getJobManager().addJob(TOPIC, null);
+        barrier.block();
+    }
+
+    /**
+     * Test deep pattern matching /**
+     */
+    @Test(timeout = DEFAULT_TEST_TIMEOUT)
+    public void testDeepMatching() throws Exception {
+        final Barrier barrier = new Barrier(2);
+
+        this.registerJobExecutor("sling/**",
+                new JobExecutor() {
+
+                    @Override
+                    public JobExecutionResult process(final Job job, final JobExecutionContext context) {
+                        return context.result().succeeded();
+                    }
+                });
+        this.registerEventHandler(NotificationConstants.TOPIC_JOB_FINISHED,
+                new EventHandler() {
+
+                    @Override
+                    public void handleEvent(final Event event) {
+                        barrier.block();
+                    }
+                });
+
+        this.getJobManager().addJob(TOPIC, null);
+        barrier.block();
+    }
+
+    /**
+     * Test ordering of matchers
+     */
+    @Test(timeout = DEFAULT_TEST_TIMEOUT)
+    public void testOrdering() throws Exception {
+        final Barrier barrier1 = new Barrier(2);
+        final Barrier barrier2 = new Barrier(2);
+        final Barrier barrier3 = new Barrier(2);
+
+        this.registerJobExecutor("sling/**",
+                new JobExecutor() {
+
+                    @Override
+                    public JobExecutionResult process(final Job job, final JobExecutionContext context) {
+                        barrier1.block();
+                        return context.result().succeeded();
+                    }
+                });
+        final ServiceRegistration<JobExecutor> reg2 = this.registerJobExecutor("sling/test/*",
+                new JobExecutor() {
+
+                    @Override
+                    public JobExecutionResult process(final Job job, final JobExecutionContext context) {
+                        barrier2.block();
+                        return context.result().succeeded();
+                    }
+                });
+        final ServiceRegistration<JobExecutor> reg3 = this.registerJobExecutor(TOPIC,
+                new JobExecutor() {
+
+                    @Override
+                    public JobExecutionResult process(final Job job, final JobExecutionContext context) {
+                        barrier3.block();
+                        return context.result().succeeded();
+                    }
+                });
+
+        // first test, all three registered, reg3 should get the precedence
+        this.getJobManager().addJob(TOPIC, null);
+        barrier3.block();
+
+        // second test, unregister reg3, now it should be reg2
+        long cc = this.getConsumerChangeCount();
+        this.unregister(reg3);
+        this.waitConsumerChangeCount(cc + 1);
+        this.getJobManager().addJob(TOPIC, null);
+        barrier2.block();
+
+        // third test, unregister reg2, reg1 is now the only one
+        cc = this.getConsumerChangeCount();
+        this.unregister(reg2);
+        this.waitConsumerChangeCount(cc + 1);
+        this.getJobManager().addJob(TOPIC, null);
+        barrier1.block();
+    }
+}
diff --git a/src/test/java/org/apache/sling/event/it/UnorderedQueueTest.java b/src/test/java/org/apache/sling/event/it/UnorderedQueueTest.java
new file mode 100644
index 0000000..bd0eea3
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/it/UnorderedQueueTest.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.event.it;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.sling.event.impl.Barrier;
+import org.apache.sling.event.impl.jobs.config.ConfigurationConstants;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.JobManager;
+import org.apache.sling.event.jobs.NotificationConstants;
+import org.apache.sling.event.jobs.Queue;
+import org.apache.sling.event.jobs.QueueConfiguration;
+import org.apache.sling.event.jobs.consumer.JobConsumer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventHandler;
+
+@RunWith(PaxExam.class)
+public class UnorderedQueueTest extends AbstractJobHandlingTest {
+
+    private static final String QUEUE_NAME = "unorderedtestqueue";
+    private static final String TOPIC = "sling/unorderedtest";
+    private static int MAX_PAR = 5;
+    private static int NUM_JOBS = 300;
+
+    @Override
+    @Before
+    public void setup() throws IOException {
+        super.setup();
+
+        // create round robin test queue
+        final org.osgi.service.cm.Configuration rrConfig = this.configAdmin.createFactoryConfiguration("org.apache.sling.event.jobs.QueueConfiguration", null);
+        final Dictionary<String, Object> rrProps = new Hashtable<String, Object>();
+        rrProps.put(ConfigurationConstants.PROP_NAME, QUEUE_NAME);
+        rrProps.put(ConfigurationConstants.PROP_TYPE, QueueConfiguration.Type.UNORDERED.name());
+        rrProps.put(ConfigurationConstants.PROP_TOPICS, TOPIC + "/*");
+        rrProps.put(ConfigurationConstants.PROP_RETRIES, 2);
+        rrProps.put(ConfigurationConstants.PROP_RETRY_DELAY, 2000L);
+        rrProps.put(ConfigurationConstants.PROP_MAX_PARALLEL, MAX_PAR);
+        rrConfig.update(rrProps);
+
+        this.sleep(1000L);
+    }
+
+    @Override
+    @After
+    public void cleanup() {
+        super.cleanup();
+    }
+
+    @Test(timeout = DEFAULT_TEST_TIMEOUT)
+    public void testUnorderedQueue() throws Exception {
+        final JobManager jobManager = this.getJobManager();
+
+        final Barrier cb = new Barrier(2);
+
+        this.registerJobConsumer(TOPIC + "/start",
+                new JobConsumer() {
+
+                    @Override
+                    public JobResult process(final Job job) {
+                        cb.block();
+                        return JobResult.OK;
+                    }
+                });
+
+        // register new consumer and event handle
+        final AtomicInteger count = new AtomicInteger(0);
+        final AtomicInteger parallelCount = new AtomicInteger(0);
+        final Set<Integer> maxParticipants = new HashSet<Integer>();
+
+        this.registerJobConsumer(TOPIC + "/*",
+                new JobConsumer() {
+
+                    @Override
+                    public JobResult process(final Job job) {
+                        final int max = parallelCount.incrementAndGet();
+                        if ( max > MAX_PAR ) {
+                            parallelCount.decrementAndGet();
+                            return JobResult.FAILED;
+                        }
+                        synchronized ( maxParticipants ) {
+                            maxParticipants.add(max);
+                        }
+                        sleep(job.getProperty("sleep", 30));
+                        parallelCount.decrementAndGet();
+                        return JobResult.OK;
+                    }
+                });
+        this.registerEventHandler(NotificationConstants.TOPIC_JOB_FINISHED,
+                new EventHandler() {
+
+                    @Override
+                    public void handleEvent(final Event event) {
+                        count.incrementAndGet();
+                    }
+                });
+
+        // we first sent one event to get the queue started
+        jobManager.addJob(TOPIC + "/start", null);
+        assertTrue("No event received in the given time.", cb.block(5));
+        cb.reset();
+
+        // get the queue
+        final Queue q = jobManager.getQueue(QUEUE_NAME);
+        assertNotNull("Queue '" + QUEUE_NAME + "' should exist!", q);
+
+        // suspend it
+        q.suspend();
+
+        // we start "some" jobs:
+        for(int i = 0; i < NUM_JOBS; i++ ) {
+            final String subTopic = TOPIC + "/sub" + (i % 10);
+            final Map<String, Object> props = new HashMap<String, Object>();
+            if ( i < 10 ) {
+                props.put("sleep", 300);
+            } else {
+                props.put("sleep", 30);
+            }
+            jobManager.addJob(subTopic, props);
+        }
+        // start the queue
+        q.resume();
+        while ( count.get() < NUM_JOBS  + 1 ) {
+            assertEquals("Failed count", 0, q.getStatistics().getNumberOfFailedJobs());
+            assertEquals("Cancelled count", 0, q.getStatistics().getNumberOfCancelledJobs());
+            sleep(300);
+        }
+        // we started one event before the test, so add one
+        assertEquals("Finished count", NUM_JOBS + 1, count.get());
+        assertEquals("Finished count", NUM_JOBS + 1, jobManager.getStatistics().getNumberOfFinishedJobs());
+        assertEquals("Finished count", NUM_JOBS + 1, q.getStatistics().getNumberOfFinishedJobs());
+        assertEquals("Failed count", 0, q.getStatistics().getNumberOfFailedJobs());
+        assertEquals("Cancelled count", 0, q.getStatistics().getNumberOfCancelledJobs());
+        for(int i=1; i <= MAX_PAR; i++) {
+            assertTrue("# Participants " + String.valueOf(i) + " not in " + maxParticipants,
+                    maxParticipants.contains(i));
+        }
+    }
+}

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.