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/20 14:37:47 UTC

[sling-org-apache-sling-event] branch master created (now 3d52d44)

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

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


      at 3d52d44  SLING-7167 Adjust READMEs

This branch includes the following new commits:

     new 139dab1  SLING-6739 : copied bundles/extensions/event into event/api and event/resources - done via svn cp to preserve history of both new parts
     new e2d9cf0  SLING-6739 : split of sling.event to sling.event.resource (and api) - initial pom.xml adjustments
     new 47276aa  SLING-6739 : split of sling.event to sling.event.resource (and api). Use old project name / bundle symbolic name and version for compatibility
     new 35f9a0d  SLING-6824 : Migrate to R6 annotations, clean up dependencies
     new e09069c  SLING-6823 : Use Timer instead of scheduler for delayed execution
     new 1534e85  SLING-6782 : Sling Job implementation should avoid unnecessary writes to the repository. Apply patch from Jörg Hoh. Modified the patch to be able to remove duplicate code from ResourceUtil
     new 27fd813  SLING-6863 Event: webconsole.configurationFactory.nameHint Property has to be part of OCD
     new 39b5cce  SLING-6863 Event: webconsole.configurationFactory.nameHint Property has to be part of OCD
     new 2bfcd3e  SLING-6823 : Use Timer instead of scheduler for delayed execution
     new cc2ea9e  Remove unused import
     new ba8b875  Remove the commons.json bundle from the tests of event.resource
     new 696d904  Update to pax exam 4.11.0
     new 2b00fbf  SLING-6975: Use the Statistics from the getStatistics() method of queues for jmx if the queue isn't an instanceof Statistics itself.
     new bfff1ed  [maven-release-plugin] prepare release org.apache.sling.event-4.2.4
     new 28bf5d8  [maven-release-plugin] prepare for next development iteration
     new e1b60ef  SLING-6739 : embedding sling.event.api 1.0.0 and explicitly exporting event.jobs, event.jobs.consumer, event.jobs.jmx
     new a98f6bd  SLING-7027: Fix eta calculation for jobs and make sure we use the new eta on an updated.
     new 01a8b91  SLING-7039: Clean up jobs in state dropped and errors.
     new fab5cab  [maven-release-plugin] prepare release org.apache.sling.event-4.2.6
     new 1d974f4  [maven-release-plugin] prepare for next development iteration
     new 8857146  Downgrade to o.a.s.testing.tools 1.0.16 to not have a SNAPSHOT without need
     new ffbce1f  SLING-7091 : Label and value are interchanged for queue type
     new 05767a9  SLING-7138 - Some web console plugins in the 'Sling' category don't appear
     new 733d161  [maven-release-plugin] prepare release org.apache.sling.event-4.2.8
     new e8073bd  [maven-release-plugin] prepare for next development iteration
     new 3d52d44  SLING-7167 Adjust READMEs

The 26 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


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

[sling-org-apache-sling-event] 17/26: SLING-7027: Fix eta calculation for jobs and make sure we use the new eta on an updated.

Posted by ro...@apache.org.
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.git

commit a98f6bd4c5291795d27780880bffc2210039d3bf
Author: Karl Pauls <pa...@apache.org>
AuthorDate: Fri Aug 4 12:54:08 2017 +0000

    SLING-7027: Fix eta calculation for jobs and make sure we use the new eta on an updated.
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1804116 13f79535-47bb-0310-9956-ffa450edef68
---
 src/main/java/org/apache/sling/event/impl/jobs/JobImpl.java | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

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
index 5514ebf..c95aaef 100644
--- a/src/main/java/org/apache/sling/event/impl/jobs/JobImpl.java
+++ b/src/main/java/org/apache/sling/event/impl/jobs/JobImpl.java
@@ -278,7 +278,7 @@ public class JobImpl implements Job, Comparable<JobImpl> {
             final Calendar now = Calendar.getInstance();
             final long elapsed = now.getTimeInMillis() - this.getProcessingStarted().getTimeInMillis();
 
-            final long eta = elapsed * steps / step;
+            final long eta = System.currentTimeMillis() + (elapsed / current) * (steps - current);
             now.setTimeInMillis(eta);
             this.setProperty(Job.PROPERTY_JOB_PROGRESS_ETA, now);
             return new String[] {Job.PROPERTY_JOB_PROGRESS_STEP, Job.PROPERTY_JOB_PROGRESS_ETA};
@@ -291,7 +291,7 @@ public class JobImpl implements Job, Comparable<JobImpl> {
             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);
+            this.setProperty(Job.PROPERTY_JOB_PROGRESS_ETA, finishCal);
         } else {
             this.properties.remove(Job.PROPERTY_JOB_PROGRESS_ETA);
         }

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

[sling-org-apache-sling-event] 24/26: [maven-release-plugin] prepare release org.apache.sling.event-4.2.8

Posted by ro...@apache.org.
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.git

commit 733d16196ec0eff4d6619c9e67dd6f7707ed38f3
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Wed Sep 20 11:53:37 2017 +0000

    [maven-release-plugin] prepare release org.apache.sling.event-4.2.8
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1809005 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/pom.xml b/pom.xml
index a9a1465..7665ba8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -29,7 +29,7 @@
 
     <artifactId>org.apache.sling.event</artifactId>
     <packaging>bundle</packaging>
-    <version>4.2.7-SNAPSHOT</version>
+    <version>4.2.8</version>
 
     <name>Apache Sling Event Support</name>
     <description>
@@ -37,9 +37,9 @@
     </description>
 
     <scm>
-        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/event/resource</connection>
-        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/event/resource</developerConnection>
-        <url>http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/event/resource</url>
+        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/tags/org.apache.sling.event-4.2.8</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/tags/org.apache.sling.event-4.2.8</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/tags/org.apache.sling.event-4.2.8</url>
     </scm>
 
     <properties>

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

[sling-org-apache-sling-event] 21/26: Downgrade to o.a.s.testing.tools 1.0.16 to not have a SNAPSHOT without need

Posted by ro...@apache.org.
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.git

commit 8857146ea6cbb7d61828d29647350bce73e49def
Author: Karl Pauls <pa...@apache.org>
AuthorDate: Thu Aug 10 12:32:36 2017 +0000

    Downgrade to o.a.s.testing.tools 1.0.16 to not have a SNAPSHOT without need
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1804666 13f79535-47bb-0310-9956-ffa450edef68
---
 src/test/java/org/apache/sling/event/it/AbstractJobHandlingTest.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/test/java/org/apache/sling/event/it/AbstractJobHandlingTest.java b/src/test/java/org/apache/sling/event/it/AbstractJobHandlingTest.java
index ce69345..c51ecd9 100644
--- a/src/test/java/org/apache/sling/event/it/AbstractJobHandlingTest.java
+++ b/src/test/java/org/apache/sling/event/it/AbstractJobHandlingTest.java
@@ -247,7 +247,7 @@ public abstract class AbstractJobHandlingTest {
 
                 mavenBundle("org.apache.sling", "org.apache.sling.jcr.oak.server", "1.1.0"),
 
-                mavenBundle("org.apache.sling", "org.apache.sling.testing.tools", "1.0.17-SNAPSHOT"),
+                mavenBundle("org.apache.sling", "org.apache.sling.testing.tools", "1.0.16"),
                 mavenBundle("org.apache.httpcomponents", "httpcore-osgi", "4.1.2"),
                 mavenBundle("org.apache.httpcomponents", "httpclient-osgi", "4.1.2"),
 

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

[sling-org-apache-sling-event] 20/26: [maven-release-plugin] prepare for next development iteration

Posted by ro...@apache.org.
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.git

commit 1d974f4bff6dba6f3f9914d07b3f33d210ab077b
Author: Karl Pauls <pa...@apache.org>
AuthorDate: Thu Aug 10 09:46:09 2017 +0000

    [maven-release-plugin] prepare for next development iteration
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1804652 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/pom.xml b/pom.xml
index 558ea54..a9a1465 100644
--- a/pom.xml
+++ b/pom.xml
@@ -29,7 +29,7 @@
 
     <artifactId>org.apache.sling.event</artifactId>
     <packaging>bundle</packaging>
-    <version>4.2.6</version>
+    <version>4.2.7-SNAPSHOT</version>
 
     <name>Apache Sling Event Support</name>
     <description>
@@ -37,9 +37,9 @@
     </description>
 
     <scm>
-        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/tags/org.apache.sling.event-4.2.6</connection>
-        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/tags/org.apache.sling.event-4.2.6</developerConnection>
-        <url>http://svn.apache.org/viewvc/sling/tags/org.apache.sling.event-4.2.6</url>
+        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/event/resource</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/event/resource</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/event/resource</url>
     </scm>
 
     <properties>

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

[sling-org-apache-sling-event] 18/26: SLING-7039: Clean up jobs in state dropped and errors.

Posted by ro...@apache.org.
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.git

commit 01a8b91b0849441ea1bd14432f4d334dbe65eeb5
Author: Karl Pauls <pa...@apache.org>
AuthorDate: Thu Aug 10 08:29:12 2017 +0000

    SLING-7039: Clean up jobs in state dropped and errors.
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1804640 13f79535-47bb-0310-9956-ffa450edef68
---
 .../impl/jobs/config/JobManagerConfiguration.java  |  33 +++--
 .../sling/event/impl/jobs/tasks/CleanUpTask.java   |  74 ++++++++++-
 .../event/impl/jobs/tasks/HistoryCleanUpTask.java  |  14 +--
 .../jobs/tasks/HistoryCleanUpRemovedJobsTest.java  | 137 +++++++++++++++++++++
 4 files changed, 237 insertions(+), 21 deletions(-)

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
index f3647c6..45ecc3b 100644
--- 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
@@ -18,16 +18,6 @@
  */
 package org.apache.sling.event.impl.jobs.config;
 
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Timer;
-import java.util.TimerTask;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicLong;
-
 import org.apache.sling.api.resource.LoginException;
 import org.apache.sling.api.resource.PersistenceException;
 import org.apache.sling.api.resource.ResourceResolver;
@@ -58,6 +48,16 @@ import org.osgi.service.metatype.annotations.ObjectClassDefinition;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
 /**
  * Configuration of the job handling
  *
@@ -87,6 +87,11 @@ public class JobManagerConfiguration {
               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.")
         long startup_delay() default 30;
+
+        @AttributeDefinition(name = "Clean-up removed jobs period",
+            description = "Specify the periodic interval in minutes (default is 48h - use 0 to disable) after which " +
+                    "removed jobs (ERROR or DROPPED) should be cleaned from the repository.")
+        int cleanup_period() default 2880;
     }
     /** Logger. */
     private final Logger logger = LoggerFactory.getLogger("org.apache.sling.event.impl.jobs");
@@ -149,6 +154,8 @@ public class JobManagerConfiguration {
     /** The resource path where scheduled jobs are stored - ending with a slash. */
     private String scheduledJobsPathWithSlash;
 
+    private volatile int historyCleanUpRemovedJobs;
+
     /** List of topology awares. */
     private final List<ConfigurationChangeListener> listeners = new ArrayList<>();
 
@@ -200,6 +207,8 @@ public class JobManagerConfiguration {
             DEFAULT_SCHEDULED_JOBS_PATH);
         this.scheduledJobsPathWithSlash = this.scheduledJobsPath + "/";
 
+        this.historyCleanUpRemovedJobs = config.cleanup_period();
+
         // create initial resources
         final ResourceResolver resolver = this.createResourceResolver();
         try {
@@ -254,6 +263,9 @@ public class JobManagerConfiguration {
         this.stopProcessing();
     }
 
+    public int getHistoryCleanUpRemovedJobs() {
+        return this.historyCleanUpRemovedJobs;
+    }
     /**
      * Is this component still active?
      * @return Active?
@@ -450,7 +462,6 @@ public class JobManagerConfiguration {
 
     /**
      * Stop processing
-     * @param deactivate Whether to deactivate the capabilities
      */
     private void stopProcessing() {
         logger.debug("Stopping job processing...");
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
index 7fdcb88..2b9cf4f 100644
--- 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
@@ -18,18 +18,23 @@
  */
 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.queues.ResultBuilderImpl;
 import org.apache.sling.event.impl.jobs.scheduling.JobSchedulerImpl;
+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.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Iterator;
+
 /**
  * Maintenance task...
  *
@@ -91,9 +96,72 @@ public class CleanUpTask {
             }
         }
 
+
+        if (this.configuration.getHistoryCleanUpRemovedJobs() > 0 &&
+                schedulerRuns % 60 == 1) {
+            Calendar removeDate = Calendar.getInstance();
+            removeDate.add(Calendar.MINUTE, - this.configuration.getHistoryCleanUpRemovedJobs());
+            this.historyCleanUpRemovedJobs(removeDate);
+        }
+
         logger.debug("Job manager maintenance: Finished #{}", this.schedulerRuns);
     }
 
+    private void historyCleanUpRemovedJobs(Calendar since) {
+        ResourceResolver resolver = this.configuration.createResourceResolver();
+        try {
+            HistoryCleanUpTask.cleanup(
+                    since,
+                    resolver,
+                    new JobExecutionContext() {
+                        @Override
+                        public void asyncProcessingFinished(JobExecutionResult result) {
+
+                        }
+
+                        @Override
+                        public boolean isStopped() {
+                            return false;
+                        }
+
+                        @Override
+                        public void initProgress(int steps, long eta) {
+
+                        }
+
+                        @Override
+                        public void incrementProgressCount(int steps) {
+
+                        }
+
+                        @Override
+                        public void updateProgress(long eta) {
+
+                        }
+
+                        @Override
+                        public void log(String message, Object... args) {
+
+                        }
+
+                        @Override
+                        public ResultBuilder result() {
+                            return new ResultBuilderImpl();
+                        }
+                    },
+                    this.configuration.getStoredCancelledJobsPath(),
+                    null,
+                    Arrays.asList(
+                            Job.JobState.DROPPED.name(),
+                            Job.JobState.ERROR.name()
+                    ));
+        } catch (PersistenceException e) {
+            this.logger.warn("Exception during job resource tree cleanup.", e);
+        } finally {
+            resolver.close();
+        }
+    }
+
     /**
      * Simple empty folder removes empty folders for the last ten minutes
      * starting five minutes ago.
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
index 29feafa..6cda613 100644
--- 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
@@ -18,12 +18,6 @@
  */
 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.sling.api.resource.PersistenceException;
 import org.apache.sling.api.resource.Resource;
 import org.apache.sling.api.resource.ResourceResolver;
@@ -41,6 +35,12 @@ import org.osgi.service.component.annotations.Reference;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Iterator;
+import java.util.List;
+
 /**
  * Task to clean up the history,
  * A clean up task can be configured with three properties:
@@ -117,7 +117,7 @@ public class HistoryCleanUpTask implements JobExecutor {
         return context.result().succeeded();
     }
 
-    private void cleanup(final Calendar removeDate,
+    static void cleanup(final Calendar removeDate,
             final ResourceResolver resolver,
             final JobExecutionContext context,
             final String basePath,
diff --git a/src/test/java/org/apache/sling/event/impl/jobs/tasks/HistoryCleanUpRemovedJobsTest.java b/src/test/java/org/apache/sling/event/impl/jobs/tasks/HistoryCleanUpRemovedJobsTest.java
new file mode 100644
index 0000000..6860ae2
--- /dev/null
+++ b/src/test/java/org/apache/sling/event/impl/jobs/tasks/HistoryCleanUpRemovedJobsTest.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 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.impl.jobs.scheduling.JobSchedulerImpl;
+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 HistoryCleanUpRemovedJobsTest {
+
+    private static final String JCR_PATH = JobManagerConfiguration.DEFAULT_REPOSITORY_PATH + "/cancelled";
+    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;
+    @Mock
+    private JobSchedulerImpl jobScheduler;
+
+    private CleanUpTask task;
+
+    @Before
+    public void setUp() {
+        setupConfiguration();
+        setUpTask();
+    }
+
+    private void setupConfiguration() {
+        Mockito.when(configuration.getStoredCancelledJobsPath()).thenReturn(JCR_PATH);
+        Mockito.when(configuration.createResourceResolver()).thenReturn(ctx.resourceResolver());
+        Mockito.when(configuration.getHistoryCleanUpRemovedJobs()).thenReturn(1);
+    }
+
+    private void setUpTask() {
+        task = new CleanUpTask(configuration, jobScheduler);
+    }
+
+    @Test
+    public void shouldNotDeleteDroppedResourcesYoungerThanRemoveDate() {
+        Calendar calendar = Calendar.getInstance();
+        calendar.add(Calendar.SECOND, -1);
+        Resource resource = createResourceForDate(calendar, Job.JobState.DROPPED.name());
+        task.run();
+        assertNotNull(ctx.resourceResolver().getResource(resource.getPath()));
+    }
+
+    @Test
+    public void shouldNotDeleteErrorResourcesYoungerThanRemoveDate() {
+        Calendar calendar = Calendar.getInstance();
+        calendar.add(Calendar.SECOND, -1);
+        Resource resource = createResourceForDate(calendar, Job.JobState.ERROR.name());
+        task.run();
+        assertNotNull(ctx.resourceResolver().getResource(resource.getPath()));
+    }
+
+    @Test
+    public void shouldNotDeleteSuccessfulResourcesOlderThanRemoveDate() {
+        Calendar calendar = Calendar.getInstance();
+
+        calendar.add(Calendar.MINUTE,-1);
+        Resource resource = createResourceForDate(calendar, Job.JobState.SUCCEEDED.name());
+
+        task.run();
+        assertNotNull(ctx.resourceResolver().getResource(resource.getPath()));
+    }
+
+    @Test
+    public void shouldDeleteDroppedResourcesOlderThanRemoveDate() {
+        Calendar calendar = Calendar.getInstance();
+
+        calendar.add(Calendar.MINUTE,-1);
+        Resource resource = createResourceForDate(calendar, Job.JobState.DROPPED.name());
+
+        task.run();
+        assertNull(ctx.resourceResolver().getResource(resource.getPath()));
+    }
+
+    @Test
+    public void shouldDeleteErrorResourcesOlderThanRemoveDate() {
+        Calendar calendar = Calendar.getInstance();
+
+        calendar.add(Calendar.MINUTE,-1);
+        Resource resource = createResourceForDate(calendar, Job.JobState.DROPPED.name());
+
+        task.run();
+        assertNull(ctx.resourceResolver().getResource(resource.getPath()));
+    }
+
+    private Resource createResourceForDate(Calendar cal, String status) {
+        String path = JCR_PATH + '/' + JCR_TOPIC + '/' + DATE_FORMATTER.format(cal.getTime()) + '/' + JCR_JOB_NAME;
+        return ctx.create().resource(path, JobImpl.PROPERTY_FINISHED_STATE, status);
+    }
+
+}

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

[sling-org-apache-sling-event] 05/26: SLING-6823 : Use Timer instead of scheduler for delayed execution

Posted by ro...@apache.org.
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.git

commit e09069c079193d9a4e786953c5c64a9563214305
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Thu May 4 09:07:53 2017 +0000

    SLING-6823 : Use Timer instead of scheduler for delayed execution
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1793757 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml                                            |  8 -----
 .../impl/jobs/config/JobManagerConfiguration.java  | 34 ++++++++++------------
 .../sling/event/impl/jobs/queues/JobQueueImpl.java | 17 +++++++----
 .../sling/event/impl/jobs/queues/QueueManager.java |  4 ---
 .../event/impl/jobs/queues/QueueServices.java      |  3 --
 .../impl/jobs/scheduling/JobSchedulerImpl.java     | 10 +++----
 6 files changed, 31 insertions(+), 45 deletions(-)

diff --git a/pom.xml b/pom.xml
index 3f81be8..eab232e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -82,16 +82,8 @@
                             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>
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
index 4f75a4c..b9262fb 100644
--- 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
@@ -18,12 +18,13 @@
  */
 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.Timer;
+import java.util.TimerTask;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicLong;
 
@@ -494,27 +495,22 @@ public class JobManagerConfiguration {
         } 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();
-                            }
+            final Timer timer = new Timer();
+            timer.schedule(new TimerTask()
+            {
+
+                @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();
                 }
-            }
+            }, this.backgroundLoadDelay * 1000);
         }
         logger.debug("Job processing started");
     }
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
index 0ab1a6b..34ff143 100644
--- 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
@@ -23,6 +23,8 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -80,7 +82,7 @@ public class JobQueueImpl
     private final QueueServices services;
 
     /** The map of events we're processing. */
-    private final Map<String, JobHandler> processingJobsLists = new HashMap<String, JobHandler>();
+    private final Map<String, JobHandler> processingJobsLists = new HashMap<>();
 
     private final ThreadPool threadPool;
 
@@ -676,7 +678,6 @@ public class JobQueueImpl
                 this.isSleepingUntil = fireDate.getTime();
             }
 
-            final String jobName = "Waiting:" + queueName + ":" + handler.hashCode();
             final Runnable t = new Runnable() {
                 @Override
                 public void run() {
@@ -695,10 +696,14 @@ public class JobQueueImpl
                 }
             };
             this.waitCounter.incrementAndGet();
-            if ( !services.scheduler.schedule(t, services.scheduler.AT(fireDate).name(jobName)) ) {
-                // if scheduling fails run the thread directly
-                t.run();
-            }
+            final Timer timer = new Timer();
+            timer.schedule(new TimerTask() {
+
+                @Override
+                public void run() {
+                    t.run();
+                }
+            }, delay);
         } else {
             // put directly into queue
             this.requeue(handler);
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
index 7141984..2a98f8a 100644
--- 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
@@ -85,9 +85,6 @@ public class QueueManager
     private EventAdmin eventAdmin;
 
     @Reference
-    private Scheduler scheduler;
-
-    @Reference
     private JobConsumerManager jobConsumerManager;
 
     @Reference
@@ -135,7 +132,6 @@ public class QueueManager
         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;
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
index e39235f..31beab2 100644
--- 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
@@ -18,7 +18,6 @@
  */
 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;
@@ -41,8 +40,6 @@ public class QueueServices {
 
     public ThreadPoolManager threadPoolManager;
 
-    public Scheduler scheduler;
-
     public StatisticsManager statisticsManager;
 
     public ThreadPool eventingThreadPool;
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
index b4b7612..180dabe 100644
--- 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
@@ -88,7 +88,7 @@ public class JobSchedulerImpl
     private final ScheduledJobHandler scheduledJobHandler;
 
     /** All scheduled jobs, by scheduler name */
-    private final Map<String, ScheduledJobInfoImpl> scheduledJobs = new HashMap<String, ScheduledJobInfoImpl>();
+    private final Map<String, ScheduledJobInfoImpl> scheduledJobs = new HashMap<>();
 
     /**
      * Create the scheduler
@@ -241,7 +241,7 @@ public class JobSchedulerImpl
                             break;
                     }
                     // Create configuration for scheduled job
-                    final Map<String, Serializable> config = new HashMap<String, Serializable>();
+                    final Map<String, Serializable> config = new HashMap<>();
                     config.put(PROPERTY_READ_JOB, info);
                     config.put(PROPERTY_SCHEDULE_INDEX, index);
                     this.scheduler.schedule(this, options.name(name).config(config).canRunConcurrently(false));
@@ -305,7 +305,7 @@ public class JobSchedulerImpl
                 this.scheduledJobHandler.remove(info);
             } else {
                 // update schedule list
-                final List<ScheduleInfo> infos = new ArrayList<ScheduleInfo>();
+                final List<ScheduleInfo> infos = new ArrayList<>();
                 for(final ScheduleInfo i : info.getSchedules() ) {
                     if ( i != si ) { // no need to use equals
                         infos.add(i);
@@ -424,7 +424,7 @@ public class JobSchedulerImpl
     public Collection<ScheduledJobInfo> getScheduledJobs(final String topic,
             final long limit,
             final Map<String, Object>... templates) {
-        final List<ScheduledJobInfo> jobs = new ArrayList<ScheduledJobInfo>();
+        final List<ScheduledJobInfo> jobs = new ArrayList<>();
         long count = 0;
         synchronized ( this.scheduledJobs ) {
             for(final ScheduledJobInfoImpl job : this.scheduledJobs.values() ) {
@@ -504,7 +504,7 @@ public class JobSchedulerImpl
             final boolean isSuspended,
             final List<ScheduleInfoImpl> scheduleInfos,
             final List<String> errors) {
-        final List<String> msgs = new ArrayList<String>();
+        final List<String> msgs = new ArrayList<>();
         if ( scheduleName == null || scheduleName.length() == 0 ) {
             msgs.add("Schedule name not specified");
         }

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

[sling-org-apache-sling-event] 15/26: [maven-release-plugin] prepare for next development iteration

Posted by ro...@apache.org.
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.git

commit 28bf5d8942c9cc937548a934cf3ead0eb57f21ae
Author: Karl Pauls <pa...@apache.org>
AuthorDate: Wed Jun 21 14:36:28 2017 +0000

    [maven-release-plugin] prepare for next development iteration
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1799459 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/pom.xml b/pom.xml
index c3398ad..c059694 100644
--- a/pom.xml
+++ b/pom.xml
@@ -29,7 +29,7 @@
 
     <artifactId>org.apache.sling.event</artifactId>
     <packaging>bundle</packaging>
-    <version>4.2.4</version>
+    <version>4.2.5-SNAPSHOT</version>
 
     <name>Apache Sling Event Support</name>
     <description>
@@ -37,9 +37,9 @@
     </description>
 
     <scm>
-        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/tags/org.apache.sling.event-4.2.4</connection>
-        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/tags/org.apache.sling.event-4.2.4</developerConnection>
-        <url>http://svn.apache.org/viewvc/sling/tags/org.apache.sling.event-4.2.4</url>
+        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/event/resource</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/event/resource</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/event/resource</url>
     </scm>
 
     <properties>

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

[sling-org-apache-sling-event] 07/26: SLING-6863 Event: webconsole.configurationFactory.nameHint Property has to be part of OCD

Posted by ro...@apache.org.
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.git

commit 27fd813d17bf5e28ae8c6fdbef65c1b32a31c82f
Author: Stefan Seifert <ss...@apache.org>
AuthorDate: Thu May 18 12:27:37 2017 +0000

    SLING-6863 Event: webconsole.configurationFactory.nameHint Property has to be part of OCD
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1795508 13f79535-47bb-0310-9956-ffa450edef68
---
 .../sling/event/impl/jobs/config/InternalQueueConfiguration.java     | 5 ++++-
 .../sling/event/impl/jobs/config/InternalQueueConfigurationTest.java | 5 +++++
 2 files changed, 9 insertions(+), 1 deletion(-)

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
index 4922a72..d01f77f 100644
--- 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
@@ -39,7 +39,6 @@ import org.slf4j.LoggerFactory;
            name="org.apache.sling.event.jobs.QueueConfiguration",
            configurationPolicy=ConfigurationPolicy.REQUIRE,
            property={
-                   "webconsole.configurationFactory.nameHint=Queue: {" + ConfigurationConstants.PROP_NAME + "}",
                    Constants.SERVICE_VENDOR + "=The Apache Software Foundation"
            })
 @Designate(ocd = InternalQueueConfiguration.Config.class, factory = true)
@@ -122,6 +121,10 @@ public class InternalQueueConfiguration
               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.")
          int service_ranking() default 0;
+     
+         // Internal Name hint for web console.
+         String webconsole_configurationFactory_nameHint() default "Queue: {" + ConfigurationConstants.PROP_NAME + "}";
+
      }
 
     /** Logger. */
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
index 817407f..948d953 100644
--- 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
@@ -107,6 +107,11 @@ public class InternalQueueConfigurationTest {
             public int service_ranking() {
                 return 0;
             }
+
+            @Override
+            public String webconsole_configurationFactory_nameHint() {
+                return null;
+            }
         };
     }
 

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

[sling-org-apache-sling-event] 26/26: SLING-7167 Adjust READMEs

Posted by ro...@apache.org.
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.git

commit 3d52d446e73bfae5043fe18be383ccc540660214
Author: Oliver Lietz <ol...@apache.org>
AuthorDate: Tue Oct 3 09:54:14 2017 +0000

    SLING-7167 Adjust READMEs
    
    add uniform header linking to Sling project
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1810839 13f79535-47bb-0310-9956-ffa450edef68
---
 README.md | 18 ++++++++++--------
 1 file changed, 10 insertions(+), 8 deletions(-)

diff --git a/README.md b/README.md
index a017cb5..5658584 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,21 @@
-# Sling Event (Jobs) bundle.
+# Apache Sling Event Support
+
+This module is part of the [Apache Sling](https://sling.apache.org) project.
 
 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
+## 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
+## Design and implementation
 
 
-## Processing model
+### 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. 
@@ -35,7 +37,7 @@ periodic maintenance classes or triggered by calls to the QueueManager or trigge
 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
+### 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 
@@ -57,7 +59,7 @@ the write operation and the target sling instance wont know about the job until
 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
+### 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
@@ -65,7 +67,7 @@ if there are any instances that don't exist any more. If there are, the topology
 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
+### 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.
 
@@ -74,7 +76,7 @@ especially when the queues are large, as jobs complexity and resource requiremen
 2. When the topology changes, with many jobs the cost of reallocating jobs may be prohibitive.
 
 
-# Scheduled Jobs.
+## 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

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

[sling-org-apache-sling-event] 06/26: SLING-6782 : Sling Job implementation should avoid unnecessary writes to the repository. Apply patch from Jörg Hoh. Modified the patch to be able to remove duplicate code from ResourceUtil

Posted by ro...@apache.org.
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.git

commit 1534e851abe6e44d306b2546aaaad7dfb54889a1
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Fri May 5 09:22:56 2017 +0000

    SLING-6782 : Sling Job implementation should avoid unnecessary writes to the repository. Apply patch from Jörg Hoh. Modified the patch to be able to remove duplicate code from ResourceUtil
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1793991 13f79535-47bb-0310-9956-ffa450edef68
---
 .../sling/event/impl/EnvironmentComponent.java     |   5 +-
 .../sling/event/impl/EventingThreadPool.java       |   3 +-
 .../apache/sling/event/impl/jobs/JobHandler.java   |   4 +-
 .../sling/event/impl/jobs/JobManagerImpl.java      |   9 +-
 .../impl/jobs/config/JobManagerConfiguration.java  |   7 +-
 .../impl/jobs/notifications/NewJobSender.java      |   3 +-
 .../sling/event/impl/jobs/queues/QueueManager.java |   5 +-
 .../impl/jobs/scheduling/ScheduledJobHandler.java  |   2 +-
 .../event/impl/jobs/tasks/CheckTopologyTask.java   |   6 +-
 .../sling/event/impl/jobs/tasks/UpgradeTask.java   |   4 +-
 .../sling/event/impl/support/ResourceHelper.java   | 187 +++++----------------
 11 files changed, 65 insertions(+), 170 deletions(-)

diff --git a/src/main/java/org/apache/sling/event/impl/EnvironmentComponent.java b/src/main/java/org/apache/sling/event/impl/EnvironmentComponent.java
index 7c3caad..56b5bd3 100644
--- a/src/main/java/org/apache/sling/event/impl/EnvironmentComponent.java
+++ b/src/main/java/org/apache/sling/event/impl/EnvironmentComponent.java
@@ -26,6 +26,7 @@ import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Deactivate;
 import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
 
 /**
  * Environment component. This component provides "global settings"
@@ -45,11 +46,11 @@ public class EnvironmentComponent {
     /**
      * Our thread pool.
      */
-    @Reference(service=EventingThreadPool.class)
+    @Reference(service=EventingThreadPool.class, policyOption=ReferencePolicyOption.GREEDY)
     private ThreadPool threadPool;
 
     /** Sling settings service. */
-    @Reference
+    @Reference(policyOption=ReferencePolicyOption.GREEDY)
     private SlingSettingsService settingsService;
 
     /**
diff --git a/src/main/java/org/apache/sling/event/impl/EventingThreadPool.java b/src/main/java/org/apache/sling/event/impl/EventingThreadPool.java
index 652f09f..0cf1531 100644
--- a/src/main/java/org/apache/sling/event/impl/EventingThreadPool.java
+++ b/src/main/java/org/apache/sling/event/impl/EventingThreadPool.java
@@ -29,6 +29,7 @@ import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Deactivate;
 import org.osgi.service.component.annotations.Modified;
 import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
 import org.osgi.service.metatype.annotations.AttributeDefinition;
 import org.osgi.service.metatype.annotations.Designate;
 import org.osgi.service.metatype.annotations.ObjectClassDefinition;
@@ -57,7 +58,7 @@ public class EventingThreadPool implements ThreadPool {
         int minPoolSize() default 35;
     }
 
-    @Reference
+    @Reference(policyOption=ReferencePolicyOption.GREEDY)
     private ThreadPoolManager threadPoolManager;
 
     /** The real thread pool used. */
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
index 6337b6c..a3203fc 100644
--- a/src/main/java/org/apache/sling/event/impl/jobs/JobHandler.java
+++ b/src/main/java/org/apache/sling/event/impl/jobs/JobHandler.java
@@ -124,7 +124,7 @@ public class JobHandler {
                     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);
+                        final Map<String, Object> props = new HashMap<>(vm);
                         props.put(JobImpl.PROPERTY_FINISHED_STATE, state.name());
                         if ( isSuccess ) {
                             // we set the finish date to start date + duration
@@ -181,7 +181,7 @@ public class JobHandler {
                     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);
+                    final Map<String, Object> props = new HashMap<>(vm);
                     props.remove(Job.PROPERTY_JOB_QUEUE_NAME);
                     if ( targetId == null ) {
                         props.remove(Job.PROPERTY_JOB_TARGET_INSTANCE);
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
index b5c6372..279d5ca 100644
--- a/src/main/java/org/apache/sling/event/impl/jobs/JobManagerImpl.java
+++ b/src/main/java/org/apache/sling/event/impl/jobs/JobManagerImpl.java
@@ -66,6 +66,7 @@ import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Deactivate;
 import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
 import org.osgi.service.event.Event;
 import org.osgi.service.event.EventAdmin;
 import org.osgi.service.event.EventConstants;
@@ -92,10 +93,10 @@ public class JobManagerImpl
     /** Default logger. */
     private final Logger logger = LoggerFactory.getLogger(this.getClass());
 
-    @Reference
+    @Reference(policyOption=ReferencePolicyOption.GREEDY)
     private EventAdmin eventAdmin;
 
-    @Reference
+    @Reference(policyOption=ReferencePolicyOption.GREEDY)
     private Scheduler scheduler;
 
     @Reference
@@ -104,7 +105,7 @@ public class JobManagerImpl
     @Reference
     private QueuesMBean queuesMBean;
 
-    @Reference
+    @Reference(policyOption=ReferencePolicyOption.GREEDY)
     private ThreadPoolManager threadPoolManager;
 
     /** The job manager configuration. */
@@ -637,7 +638,7 @@ public class JobManagerImpl
         if ( logger.isDebugEnabled() ) {
             logger.debug("Storing new job {} at {}", Utility.toString(jobTopic, properties), path);
         }
-        ResourceHelper.getOrCreateResource(resolver,
+        ResourceHelper.createAndCommitResource(resolver,
                 path,
                 properties);
 
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
index b9262fb..418b064 100644
--- 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
@@ -52,6 +52,7 @@ import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Deactivate;
 import org.osgi.service.component.annotations.Modified;
 import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
 import org.osgi.service.metatype.annotations.AttributeDefinition;
 import org.osgi.service.metatype.annotations.Designate;
 import org.osgi.service.metatype.annotations.ObjectClassDefinition;
@@ -156,16 +157,16 @@ public class JobManagerConfiguration {
     @Reference
     private EnvironmentComponent environment;
 
-    @Reference
+    @Reference(policyOption=ReferencePolicyOption.GREEDY)
     private ResourceResolverFactory resourceResolverFactory;
 
     @Reference
     private QueueConfigurationManager queueConfigManager;
 
-    @Reference
+    @Reference(policyOption=ReferencePolicyOption.GREEDY)
     private Scheduler scheduler;
 
-    @Reference
+    @Reference(policyOption=ReferencePolicyOption.GREEDY)
     private ServiceUserMapped serviceUserMapped;
 
     /** Is this still active? */
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
index 03c5769..dc16be2 100644
--- 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
@@ -36,6 +36,7 @@ import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Deactivate;
 import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
 import org.osgi.service.event.Event;
 import org.osgi.service.event.EventAdmin;
 import org.slf4j.Logger;
@@ -56,7 +57,7 @@ public class NewJobSender implements ResourceChangeListener, ExternalResourceCha
     private JobManagerConfiguration configuration;
 
     /** The event admin. */
-    @Reference
+    @Reference(policyOption=ReferencePolicyOption.GREEDY)
     private EventAdmin eventAdmin;
 
     /** Service registration for the event handler. */
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
index 2a98f8a..11f24a9 100644
--- 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
@@ -56,6 +56,7 @@ import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Deactivate;
 import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
 import org.osgi.service.event.Event;
 import org.osgi.service.event.EventAdmin;
 import org.osgi.service.event.EventConstants;
@@ -81,7 +82,7 @@ public class QueueManager
     /** Default logger. */
     private final Logger logger = LoggerFactory.getLogger(this.getClass());
 
-    @Reference
+    @Reference(policyOption=ReferencePolicyOption.GREEDY)
     private EventAdmin eventAdmin;
 
     @Reference
@@ -90,7 +91,7 @@ public class QueueManager
     @Reference
     private QueuesMBean queuesMBean;
 
-    @Reference
+    @Reference(policyOption=ReferencePolicyOption.GREEDY)
     private ThreadPoolManager threadPoolManager;
 
     /**
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
index deb3955..01ca010 100644
--- 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
@@ -277,7 +277,7 @@ public class ScheduledJobHandler implements Runnable {
             } else {
                 logger.debug("Storing new scheduled job {} at {}", properties, path);
             }
-            ResourceHelper.getOrCreateResource(resolver,
+            ResourceHelper.createAndCommitResource(resolver,
                     path,
                     properties);
             // put back real schedule infos
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
index 879ee2a..c5750be 100644
--- 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
@@ -140,7 +140,7 @@ public class CheckTopologyTask {
                                             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 Map<String, Object> props = new HashMap<>(vm);
                                             props.remove(Job.PROPERTY_JOB_STARTED_TIME);
 
                                             final String newPath;
@@ -251,7 +251,7 @@ public class CheckTopologyTask {
 
                             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);
+                                final Map<String, Object> props = new HashMap<>(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);
@@ -287,7 +287,7 @@ public class CheckTopologyTask {
                         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);
+                            final Map<String, Object> props = new HashMap<>(vm);
                             props.remove(Job.PROPERTY_JOB_QUEUE_NAME);
                             props.remove(Job.PROPERTY_JOB_TARGET_INSTANCE);
                             props.remove(Job.PROPERTY_JOB_STARTED_TIME);
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
index 99c7b0e..842addf 100644
--- 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
@@ -120,7 +120,7 @@ public class UpgradeTask {
                     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 Map<String, Object> props = new HashMap<>(vm);
                     final String newPath;
                     if ( targetId != null ) {
                         newPath = configuration.getAssginedJobsPath() + '/' + targetId + '/' + topicResource.getName() + rsrc.getPath().substring(topicResource.getPath().length());
@@ -198,7 +198,7 @@ public class UpgradeTask {
         try {
             final ValueMap vm = ResourceHelper.getValueMap(jobResource);
             // check for binary properties
-            Map<String, Object> binaryProperties = new HashMap<String, Object>();
+            Map<String, Object> binaryProperties = new HashMap<>();
             final ObjectInputStream ois = vm.get("slingevent:properties", ObjectInputStream.class);
             if ( ois != null ) {
                 try {
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
index 4383480..83c583b 100644
--- a/src/main/java/org/apache/sling/event/impl/support/ResourceHelper.java
+++ b/src/main/java/org/apache/sling/event/impl/support/ResourceHelper.java
@@ -25,7 +25,6 @@ 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;
@@ -173,17 +172,17 @@ public abstract class ResourceHelper {
     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);
+            final Map<String, Object> result = new HashMap<>(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 = new ArrayList<>();
                         }
                         hasReadError.add(new Exception("Unable to deserialize property '" + entry.getKey() + "' : " + entry.getValue()));
                     } else {
-                        final List<ScheduleInfo> infos = new ArrayList<ScheduleInfo>();
+                        final List<ScheduleInfo> infos = new ArrayList<>();
                         for(final String i : infoArray) {
                             final ScheduleInfoImpl info = ScheduleInfoImpl.deserialize(i);
                             if ( info != null ) {
@@ -192,7 +191,7 @@ public abstract class ResourceHelper {
                         }
                         if ( infos.size() < infoArray.length ) {
                             if ( hasReadError == null ) {
-                                hasReadError = new ArrayList<Exception>();
+                                hasReadError = new ArrayList<>();
                             }
                             hasReadError.add(new Exception("Unable to deserialize property '" + entry.getKey() + "' : " + Arrays.toString(infoArray)));
                         } else {
@@ -206,7 +205,7 @@ public abstract class ResourceHelper {
                         entry.setValue(value);
                     } else {
                         if ( hasReadError == null ) {
-                            hasReadError = new ArrayList<Exception>();
+                            hasReadError = new ArrayList<>();
                         }
                         // let's find out which class might be missing
                         ObjectInputStream ois = null;
@@ -256,171 +255,61 @@ public abstract class ResourceHelper {
     public static void getOrCreateBasePath(final ResourceResolver resolver,
             final String path)
     throws PersistenceException {
-       getOrCreateResource(resolver,
+        ResourceUtil.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)
+    /**
+     * Create the resource and commit it
+     * @param resolver The resource resolver
+     * @param path The path of the resource
+     * @param props The properties
+     * @return The created resource
+     * @throws PersistenceException If something goes wrong
+     */
+    public static Resource createAndCommitResource(final ResourceResolver resolver,
+            final String path,
+            final Map<String, Object> props)
     throws PersistenceException {
-       return getOrCreateResource(resolver,
+       return ResourceUtil.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
+     * If any resource along the parent path needs to be created,
+     * this is committed immediately. The resource itself is not committed.
+     * This is the task of the caller.
      *
      * @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.
+     * @param props    The properties of the new resource.
+     * @return The resource for the path.
+     * @throws PersistenceException If something goes wrong
      */
-    private static Resource getOrCreateResource(
-            final ResourceResolver resolver,
+    public static Resource getOrCreateResource(final ResourceResolver resolver,
             final String path,
-            final Map<String, Object> resourceProperties,
-            final String intermediateResourceType,
-            final boolean autoCommit)
+            final Map<String, Object> props)
     throws PersistenceException {
-        PersistenceException mostRecentPE = null;
-        for(int i=0;i<5;i++) {
-            try {
-                return getOrCreateResourceInternal(resolver,
+        // create parent path with auto commit set to true
+        final String parentPath = ResourceUtil.getParent(path);
+        ResourceUtil.getOrCreateResource(resolver,
+                parentPath,
+                ResourceHelper.RESOURCE_TYPE_FOLDER,
+                ResourceHelper.RESOURCE_TYPE_FOLDER,
+                true);
+        // now create resource itself
+        return ResourceUtil.getOrCreateResource(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;
+                        props,
+                        ResourceHelper.RESOURCE_TYPE_FOLDER,
+                        false);
     }
 }
\ No newline at end of file

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

[sling-org-apache-sling-event] 11/26: Remove the commons.json bundle from the tests of event.resource

Posted by ro...@apache.org.
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.git

commit ba8b87551be023b738fd3cb5f5b9d2d05b235d59
Author: Karl Pauls <pa...@apache.org>
AuthorDate: Fri Jun 2 11:08:50 2017 +0000

    Remove the commons.json bundle from the tests of event.resource
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1797370 13f79535-47bb-0310-9956-ffa450edef68
---
 .../org/apache/sling/event/it/AbstractJobHandlingTest.java     | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/test/java/org/apache/sling/event/it/AbstractJobHandlingTest.java b/src/test/java/org/apache/sling/event/it/AbstractJobHandlingTest.java
index 0146878..ce69345 100644
--- a/src/test/java/org/apache/sling/event/it/AbstractJobHandlingTest.java
+++ b/src/test/java/org/apache/sling/event/it/AbstractJobHandlingTest.java
@@ -183,6 +183,7 @@ public abstract class AbstractJobHandlingTest {
                 mavenBundle("commons-collections", "commons-collections", "3.2.2"),
                 mavenBundle("commons-codec", "commons-codec", "1.10"),
                 mavenBundle("commons-lang", "commons-lang", "2.6"),
+                mavenBundle("org.apache.commons", "commons-lang3", "3.5"),
                 mavenBundle("commons-pool", "commons-pool", "1.6"),
 
                 mavenBundle("org.apache.servicemix.bundles", "org.apache.servicemix.bundles.concurrent", "1.3.4_1"),
@@ -203,7 +204,6 @@ public abstract class AbstractJobHandlingTest {
                 // 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.johnzon", "1.0.0"),
@@ -217,12 +217,12 @@ public abstract class AbstractJobHandlingTest {
 
                 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.adapter", "2.1.10"),
                 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.jcr.contentloader", "2.2.4"),
                 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.serviceusermapper", "1.3.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"),
@@ -247,7 +247,7 @@ public abstract class AbstractJobHandlingTest {
 
                 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.sling", "org.apache.sling.testing.tools", "1.0.17-SNAPSHOT"),
                 mavenBundle("org.apache.httpcomponents", "httpcore-osgi", "4.1.2"),
                 mavenBundle("org.apache.httpcomponents", "httpclient-osgi", "4.1.2"),
 

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

[sling-org-apache-sling-event] 10/26: Remove unused import

Posted by ro...@apache.org.
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.git

commit cc2ea9e6bbda2050060ca3205dcdb64a6af1ce66
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Mon May 22 05:48:22 2017 +0000

    Remove unused import
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1795774 13f79535-47bb-0310-9956-ffa450edef68
---
 .../java/org/apache/sling/event/it/ChaosTest.java   | 21 ++++++++++-----------
 1 file changed, 10 insertions(+), 11 deletions(-)

diff --git a/src/test/java/org/apache/sling/event/it/ChaosTest.java b/src/test/java/org/apache/sling/event/it/ChaosTest.java
index 100beec..1538593 100644
--- a/src/test/java/org/apache/sling/event/it/ChaosTest.java
+++ b/src/test/java/org/apache/sling/event/it/ChaosTest.java
@@ -45,7 +45,6 @@ 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;
@@ -62,7 +61,7 @@ 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;
@@ -98,7 +97,7 @@ public class ChaosTest extends AbstractJobHandlingTest {
 
         // 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>();
+        final Dictionary<String, Object> orderedProps = new Hashtable<>();
         orderedProps.put(ConfigurationConstants.PROP_NAME, "chaos-ordered");
         orderedProps.put(ConfigurationConstants.PROP_TYPE, QueueConfiguration.Type.ORDERED.name());
         orderedProps.put(ConfigurationConstants.PROP_TOPICS, ORDERED_TOPICS);
@@ -108,7 +107,7 @@ public class ChaosTest extends AbstractJobHandlingTest {
 
         // 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>();
+        final Dictionary<String, Object> rrProps = new Hashtable<>();
         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);
@@ -239,7 +238,7 @@ public class ChaosTest extends AbstractJobHandlingTest {
      */
     private void setupChaosThreads(final List<Thread> threads,
             final AtomicLong finishedThreads) {
-        final List<TopologyView> views = new ArrayList<TopologyView>();
+        final List<TopologyView> views = new ArrayList<>();
         // register topology listener
         final ServiceRegistration<TopologyEventListener> reg = this.bc.registerService(TopologyEventListener.class, new TopologyEventListener() {
 
@@ -313,10 +312,10 @@ public class ChaosTest extends AbstractJobHandlingTest {
         // 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>();
+        final Map<String, AtomicLong> added = new HashMap<>();
+        final Map<String, AtomicLong> created = new HashMap<>();
+        final Map<String, AtomicLong> finished = new HashMap<>();
+        final List<String> topics = new ArrayList<>();
         for(int i=0;i<NUM_ORDERED_TOPICS;i++) {
             added.put(ORDERED_TOPICS[i], new AtomicLong());
             created.put(ORDERED_TOPICS[i], new AtomicLong());
@@ -336,7 +335,7 @@ public class ChaosTest extends AbstractJobHandlingTest {
             topics.add(ROUND_TOPICS[i]);
         }
 
-        final List<Thread> threads = new ArrayList<Thread>();
+        final List<Thread> threads = new ArrayList<>();
         final AtomicLong finishedThreads = new AtomicLong();
 
         this.registerEventHandler("org/apache/sling/event/notification/job/*",
@@ -379,7 +378,7 @@ public class ChaosTest extends AbstractJobHandlingTest {
         }
 
         System.out.println("Waiting for job handling to finish...");
-        final Set<String> allTopics = new HashSet<String>(topics);
+        final Set<String> allTopics = new HashSet<>(topics);
         while ( !allTopics.isEmpty() ) {
             final Iterator<String> iter = allTopics.iterator();
             while ( iter.hasNext() ) {

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

[sling-org-apache-sling-event] 16/26: SLING-6739 : embedding sling.event.api 1.0.0 and explicitly exporting event.jobs, event.jobs.consumer, event.jobs.jmx

Posted by ro...@apache.org.
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.git

commit e1b60efc22388d41906f47f6ef37ff2457574e1a
Author: Stefan Egli <st...@apache.org>
AuthorDate: Wed Jul 5 15:03:52 2017 +0000

    SLING-6739 : embedding sling.event.api 1.0.0 and explicitly exporting event.jobs, event.jobs.consumer, event.jobs.jmx
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1800888 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml                                            |  22 +-
 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 --
 20 files changed, 13 insertions(+), 2018 deletions(-)

diff --git a/pom.xml b/pom.xml
index c059694..9ff4cc7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -66,6 +66,11 @@
                             org.apache.felix.inventory;resolution:=optional,
                             *
                         </Import-Package>
+                        <Export-Package>
+                        	org.apache.sling.event.jobs,
+                        	org.apache.sling.event.jobs.consumer,
+                        	org.apache.sling.event.jobs.jmx
+                        </Export-Package>
                        <DynamicImport-Package>
                             javax.servlet,
                             javax.servlet.http,
@@ -80,7 +85,8 @@
                         <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.*"
+                            quartz;inline="org/quartz/CronExpression.*|org/quartz/ValueSet.*",
+                            org.apache.sling.event.api
                         </Embed-Dependency>
                     </instructions>
                 </configuration>
@@ -362,12 +368,10 @@
             <version>1</version>
             <scope>test</scope>
         </dependency>
-<!-- SLING-6739 : update once sling.event.api is released (and then remove identical packages in this bundle)
-         <dependency>
-        	<groupId>org.apache.sling</groupId>
-        	<artifactId>org.apache.sling.event.api</artifactId>
-        	<version>1.0.0</version>
-        	<type>bundle</type>
-        </dependency>
- -->    </dependencies>
+    	<dependency>
+    		<groupId>org.apache.sling</groupId>
+    		<artifactId>org.apache.sling.event.api</artifactId>
+    		<version>1.0.0</version>
+    	</dependency>
+    </dependencies>
 </project>
diff --git a/src/main/java/org/apache/sling/event/jobs/Job.java b/src/main/java/org/apache/sling/event/jobs/Job.java
deleted file mode 100644
index 710e730..0000000
--- a/src/main/java/org/apache/sling/event/jobs/Job.java
+++ /dev/null
@@ -1,351 +0,0 @@
-/*
- 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
deleted file mode 100644
index 1455a61..0000000
--- a/src/main/java/org/apache/sling/event/jobs/JobBuilder.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 105b611..0000000
--- a/src/main/java/org/apache/sling/event/jobs/JobManager.java
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 58c7c55..0000000
--- a/src/main/java/org/apache/sling/event/jobs/NotificationConstants.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 2134aba..0000000
--- a/src/main/java/org/apache/sling/event/jobs/Queue.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 8990289..0000000
--- a/src/main/java/org/apache/sling/event/jobs/QueueConfiguration.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * 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
deleted file mode 100644
index bb390cb..0000000
--- a/src/main/java/org/apache/sling/event/jobs/ScheduleInfo.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 4d37c8b..0000000
--- a/src/main/java/org/apache/sling/event/jobs/ScheduledJobInfo.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 66b8d55..0000000
--- a/src/main/java/org/apache/sling/event/jobs/Statistics.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 0fed27a..0000000
--- a/src/main/java/org/apache/sling/event/jobs/TopicStatistics.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 8e783d1..0000000
--- a/src/main/java/org/apache/sling/event/jobs/consumer/JobConsumer.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 9370922..0000000
--- a/src/main/java/org/apache/sling/event/jobs/consumer/JobExecutionContext.java
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * 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
deleted file mode 100644
index c3da0f0..0000000
--- a/src/main/java/org/apache/sling/event/jobs/consumer/JobExecutionResult.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * 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
deleted file mode 100644
index b720546..0000000
--- a/src/main/java/org/apache/sling/event/jobs/consumer/JobExecutor.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 5237caa..0000000
--- a/src/main/java/org/apache/sling/event/jobs/consumer/package-info.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 9e8af7d..0000000
--- a/src/main/java/org/apache/sling/event/jobs/jmx/QueuesMBean.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 321f574..0000000
--- a/src/main/java/org/apache/sling/event/jobs/jmx/StatisticsMBean.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 515bde1..0000000
--- a/src/main/java/org/apache/sling/event/jobs/jmx/package-info.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 6ea3e5c..0000000
--- a/src/main/java/org/apache/sling/event/jobs/package-info.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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;
-
-

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

[sling-org-apache-sling-event] 02/26: SLING-6739 : split of sling.event to sling.event.resource (and api) - initial pom.xml adjustments

Posted by ro...@apache.org.
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.git

commit e2d9cf0f9bc6f1b87477303a820ae663a00def6d
Author: Stefan Egli <st...@apache.org>
AuthorDate: Wed Mar 29 09:30:12 2017 +0000

    SLING-6739 : split of sling.event to sling.event.resource (and api) - initial pom.xml adjustments
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1789306 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml | 19 +++++++++++++------
 1 file changed, 13 insertions(+), 6 deletions(-)

diff --git a/pom.xml b/pom.xml
index 4c12105..91bb496 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,9 +27,9 @@
         <relativePath />
     </parent>
 
-    <artifactId>org.apache.sling.event</artifactId>
+    <artifactId>org.apache.sling.event.resource</artifactId>
     <packaging>bundle</packaging>
-    <version>4.2.3-SNAPSHOT</version>
+    <version>0.0.1-SNAPSHOT</version>
 
     <name>Apache Sling Event Support</name>
     <description>
@@ -37,9 +37,9 @@
     </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>
+        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/event/resource</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/event/resourc33</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/event/resource</url>
     </scm>
 
     <properties>
@@ -373,5 +373,12 @@
             <version>1</version>
             <scope>test</scope>
         </dependency>
-    </dependencies>
+<!-- SLING-6739 : update once sling.event.api is released (and then remove identical packages in this bundle)
+         <dependency>
+        	<groupId>org.apache.sling</groupId>
+        	<artifactId>org.apache.sling.event.api</artifactId>
+        	<version>1.0.0</version>
+        	<type>bundle</type>
+        </dependency>
+ -->    </dependencies>
 </project>

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

[sling-org-apache-sling-event] 08/26: SLING-6863 Event: webconsole.configurationFactory.nameHint Property has to be part of OCD

Posted by ro...@apache.org.
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.git

commit 39b5cce26cd6651d3bc91f36b4850f3c4fa4282d
Author: Stefan Seifert <ss...@apache.org>
AuthorDate: Thu May 18 12:51:07 2017 +0000

    SLING-6863 Event: webconsole.configurationFactory.nameHint Property has to be part of OCD
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1795515 13f79535-47bb-0310-9956-ffa450edef68
---
 .../apache/sling/event/impl/jobs/config/MainQueueConfiguration.java  | 5 +++++
 1 file changed, 5 insertions(+)

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
index 9f45b83..cbaf50b 100644
--- 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
@@ -166,6 +166,11 @@ public class MainQueueConfiguration {
             public boolean queue_keepJobs() {
                 return false;
             }
+
+            @Override
+            public String webconsole_configurationFactory_nameHint() {
+                return "Queue: {" + ConfigurationConstants.PROP_NAME + "}";
+            }
         });
     }
 

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

[sling-org-apache-sling-event] 13/26: SLING-6975: Use the Statistics from the getStatistics() method of queues for jmx if the queue isn't an instanceof Statistics itself.

Posted by ro...@apache.org.
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.git

commit 2b00fbf72e87200a04479385f692aa6b74668745
Author: Karl Pauls <pa...@apache.org>
AuthorDate: Wed Jun 21 11:19:32 2017 +0000

    SLING-6975: Use the Statistics from the getStatistics() method of queues for jmx if the queue isn't an instanceof Statistics itself.
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1799427 13f79535-47bb-0310-9956-ffa450edef68
---
 .../apache/sling/event/impl/jobs/jmx/QueueMBeanImpl.java |  7 ++-----
 .../sling/event/impl/jobs/jmx/QueuesMBeanImplTest.java   | 16 ++++++++++++++++
 2 files changed, 18 insertions(+), 5 deletions(-)

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
index 615ac90..d623515 100644
--- 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
@@ -31,11 +31,8 @@ public class QueueMBeanImpl extends AbstractJobStatistics {
 
     public QueueMBeanImpl(Queue queue) {
         this.name = queue.getName();
-        if (queue instanceof Statistics) {
-            this.statistics = (Statistics) queue;
-        } else {
-            this.statistics = new EmptyStatistics();
-        }
+        Statistics stats = queue instanceof Statistics ? (Statistics) queue : queue.getStatistics();
+        this.statistics = stats != null ? stats : new EmptyStatistics();
     }
 
     @Override
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
index f8b5740..89cc0f0 100644
--- 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
@@ -78,6 +78,22 @@ public class QueuesMBeanImplTest {
         testStatistics((StatisticsMBean) serviceObject.getValue());
         return queue;
     }
+    
+    @Test
+    public void testAddQueueWithStatistics() {
+        Queue queue = Mockito.mock(Queue.class);
+        Statistics stats = Mockito.mock(Statistics.class);
+        mockStatistics(stats);
+        Mockito.when(queue.getStatistics()).thenReturn(stats);
+        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());
+    }
 
 
     @Test

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

[sling-org-apache-sling-event] 19/26: [maven-release-plugin] prepare release org.apache.sling.event-4.2.6

Posted by ro...@apache.org.
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.git

commit fab5cab1f3d3893b6b28c3692ccf31ed3fc50e94
Author: Karl Pauls <pa...@apache.org>
AuthorDate: Thu Aug 10 09:45:53 2017 +0000

    [maven-release-plugin] prepare release org.apache.sling.event-4.2.6
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1804650 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/pom.xml b/pom.xml
index 9ff4cc7..558ea54 100644
--- a/pom.xml
+++ b/pom.xml
@@ -29,7 +29,7 @@
 
     <artifactId>org.apache.sling.event</artifactId>
     <packaging>bundle</packaging>
-    <version>4.2.5-SNAPSHOT</version>
+    <version>4.2.6</version>
 
     <name>Apache Sling Event Support</name>
     <description>
@@ -37,9 +37,9 @@
     </description>
 
     <scm>
-        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/event/resource</connection>
-        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/event/resource</developerConnection>
-        <url>http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/event/resource</url>
+        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/tags/org.apache.sling.event-4.2.6</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/tags/org.apache.sling.event-4.2.6</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/tags/org.apache.sling.event-4.2.6</url>
     </scm>
 
     <properties>

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

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

Posted by ro...@apache.org.
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.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>.

[sling-org-apache-sling-event] 09/26: SLING-6823 : Use Timer instead of scheduler for delayed execution

Posted by ro...@apache.org.
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.git

commit 2bfcd3ebb2fc129a6df379c7f5a9c82e0b6946c0
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Mon May 22 05:12:09 2017 +0000

    SLING-6823 : Use Timer instead of scheduler for delayed execution
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1795773 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml                                            |   2 +-
 .../impl/jobs/config/JobManagerConfiguration.java  |   6 +-
 .../jobs/config/JobManagerConfigurationTest.java   | 125 +--------------------
 .../sling/event/it/AbstractJobHandlingTest.java    |   3 +-
 4 files changed, 7 insertions(+), 129 deletions(-)

diff --git a/pom.xml b/pom.xml
index eab232e..e420635 100644
--- a/pom.xml
+++ b/pom.xml
@@ -259,7 +259,7 @@
         <dependency>
         	<groupId>org.apache.sling</groupId>
         	<artifactId>org.apache.sling.discovery.commons</artifactId>
-        	<version>1.0.12</version>
+        	<version>1.0.20</version>
         	<scope>provided</scope>
         </dependency>
         <dependency>
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
index 418b064..f3647c6 100644
--- 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
@@ -33,7 +33,6 @@ 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;
@@ -164,9 +163,6 @@ public class JobManagerConfiguration {
     private QueueConfigurationManager queueConfigManager;
 
     @Reference(policyOption=ReferencePolicyOption.GREEDY)
-    private Scheduler scheduler;
-
-    @Reference(policyOption=ReferencePolicyOption.GREEDY)
     private ServiceUserMapped serviceUserMapped;
 
     /** Is this still active? */
@@ -226,7 +222,7 @@ public class JobManagerConfiguration {
                 public void handleTopologyEvent(TopologyEvent event) {
                     doHandleTopologyEvent(event);
                 }
-            }, this.scheduler, logger);
+            }, logger);
         } else {
             logger.debug("activate: job manager will start without delay. ({}:{})", config.startup_delay(), this.startupDelay);
         }
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
index 486b823..fa5ccf6 100644
--- 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
@@ -22,21 +22,13 @@ 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;
@@ -69,7 +61,7 @@ public class JobManagerConfigurationTest {
 
     private static class ChangeListener implements ConfigurationChangeListener {
 
-        public final List<Boolean> events = new ArrayList<Boolean>();
+        public final List<Boolean> events = new ArrayList<>();
         private volatile CountDownLatch latch;
 
         public void init(final int count) {
@@ -90,132 +82,21 @@ public class JobManagerConfigurationTest {
         }
     }
 
-    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);
diff --git a/src/test/java/org/apache/sling/event/it/AbstractJobHandlingTest.java b/src/test/java/org/apache/sling/event/it/AbstractJobHandlingTest.java
index eb3de77..0146878 100644
--- a/src/test/java/org/apache/sling/event/it/AbstractJobHandlingTest.java
+++ b/src/test/java/org/apache/sling/event/it/AbstractJobHandlingTest.java
@@ -206,12 +206,13 @@ public abstract class AbstractJobHandlingTest {
                 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.johnzon", "1.0.0"),
                 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.commons", "1.0.20"),
                 mavenBundle("org.apache.sling", "org.apache.sling.discovery.standalone", "1.0.2"),
 
                 mavenBundle("org.apache.sling", "org.apache.sling.api", "2.14.2"),

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

[sling-org-apache-sling-event] 14/26: [maven-release-plugin] prepare release org.apache.sling.event-4.2.4

Posted by ro...@apache.org.
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.git

commit bfff1ed24a075aa8987b1e47e19a75f60a1ec3cf
Author: Karl Pauls <pa...@apache.org>
AuthorDate: Wed Jun 21 14:36:11 2017 +0000

    [maven-release-plugin] prepare release org.apache.sling.event-4.2.4
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1799457 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/pom.xml b/pom.xml
index 0b9dfd6..c3398ad 100644
--- a/pom.xml
+++ b/pom.xml
@@ -29,7 +29,7 @@
 
     <artifactId>org.apache.sling.event</artifactId>
     <packaging>bundle</packaging>
-    <version>4.2.3-SNAPSHOT</version>
+    <version>4.2.4</version>
 
     <name>Apache Sling Event Support</name>
     <description>
@@ -37,9 +37,9 @@
     </description>
 
     <scm>
-        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/event/resource</connection>
-        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/event/resource</developerConnection>
-        <url>http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/event/resource</url>
+        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/tags/org.apache.sling.event-4.2.4</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/tags/org.apache.sling.event-4.2.4</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/tags/org.apache.sling.event-4.2.4</url>
     </scm>
 
     <properties>

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

[sling-org-apache-sling-event] 23/26: SLING-7138 - Some web console plugins in the 'Sling' category don't appear

Posted by ro...@apache.org.
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.git

commit 05767a96cd6db462220c8eaf89105d67496421f7
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Tue Sep 19 08:15:37 2017 +0000

    SLING-7138 - Some web console plugins in the 'Sling' category don't appear
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1808841 13f79535-47bb-0310-9956-ffa450edef68
---
 .../java/org/apache/sling/event/impl/jobs/console/WebConsolePlugin.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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
index 00b08ad..a2956e4 100644
--- 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
@@ -64,7 +64,7 @@ property = {
     Constants.SERVICE_VENDOR + "=The Apache Software Foundation",
     "felix.webconsole.label=slingevent",
     "felix.webconsole.title=Jobs",
-    "felix.webconsole.category=SLING",
+    "felix.webconsole.category=Sling",
     JobConsumer.PROPERTY_TOPICS + "=sling/webconsole/test"
 })
 public class WebConsolePlugin extends HttpServlet implements JobConsumer {

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

[sling-org-apache-sling-event] 04/26: SLING-6824 : Migrate to R6 annotations, clean up dependencies

Posted by ro...@apache.org.
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.git

commit 35f9a0da03c5ff1d178eca587874b76406910239
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Thu May 4 08:03:38 2017 +0000

    SLING-6824 : Migrate to R6 annotations, clean up dependencies
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1793745 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml                                            |  17 +-
 .../sling/event/impl/EnvironmentComponent.java     |  19 +-
 .../sling/event/impl/EventingThreadPool.java       |  53 +++---
 .../sling/event/impl/jobs/JobConsumerManager.java  | 110 +++++------
 .../sling/event/impl/jobs/JobManagerImpl.java      |  35 ++--
 .../org/apache/sling/event/impl/jobs/Utility.java  |  19 +-
 .../impl/jobs/config/ConfigurationConstants.java   |   4 -
 .../jobs/config/InternalQueueConfiguration.java    | 204 +++++++++++----------
 .../impl/jobs/config/JobManagerConfiguration.java  | 110 +++++------
 .../impl/jobs/config/MainQueueConfiguration.java   | 178 ++++++++++++------
 .../jobs/config/QueueConfigurationManager.java     |  24 +--
 .../event/impl/jobs/config/TopologyHandler.java    |  18 +-
 .../event/impl/jobs/console/InventoryPlugin.java   |  25 ++-
 .../event/impl/jobs/console/WebConsolePlugin.java  |  22 +--
 .../event/impl/jobs/jmx/AllJobStatisticsMBean.java |  17 +-
 .../sling/event/impl/jobs/jmx/QueuesMBeanImpl.java |  26 +--
 .../impl/jobs/notifications/NewJobSender.java      |  14 +-
 .../sling/event/impl/jobs/queues/QueueManager.java |  40 ++--
 .../event/impl/jobs/stats/StatisticsManager.java   |  16 +-
 .../event/impl/jobs/tasks/HistoryCleanUpTask.java  |  17 +-
 .../event/impl/jobs/JobConsumerManagerTest.java    |  40 +++-
 .../config/InternalQueueConfigurationTest.java     | 160 ++++++++++------
 .../sling/event/it/AbstractJobHandlingTest.java    |  10 +-
 23 files changed, 636 insertions(+), 542 deletions(-)

diff --git a/pom.xml b/pom.xml
index 641a79b..3f81be8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -205,6 +205,10 @@
     </profiles>
     <dependencies>
         <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.component.annotations</artifactId>
+        </dependency>
+        <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.discovery.api</artifactId>
             <version>1.0.0</version>
@@ -223,8 +227,6 @@
         <dependency>
             <groupId>org.osgi</groupId>
             <artifactId>osgi.core</artifactId>
-            <version>6.0.0</version>
-            <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>org.osgi</groupId>
@@ -233,11 +235,6 @@
             <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>
@@ -316,13 +313,13 @@
         <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.testing.tools</artifactId>
-            <version>1.0.6</version>
+            <version>1.0.14</version>
             <scope>test</scope>
         </dependency>
         <dependency>
           <groupId>org.apache.sling</groupId>
           <artifactId>org.apache.sling.testing.sling-mock</artifactId>
-          <version>1.6.0</version>
+          <version>2.2.6</version>
           <scope>test</scope>
         </dependency>
 
@@ -364,7 +361,7 @@
         <dependency>
             <groupId>org.apache.felix</groupId>
             <artifactId>org.apache.felix.framework</artifactId>
-            <version>5.4.0</version>
+            <version>5.6.2</version>
             <scope>test</scope>
         </dependency>
         <dependency>
diff --git a/src/main/java/org/apache/sling/event/impl/EnvironmentComponent.java b/src/main/java/org/apache/sling/event/impl/EnvironmentComponent.java
index b3f7929..7c3caad 100644
--- a/src/main/java/org/apache/sling/event/impl/EnvironmentComponent.java
+++ b/src/main/java/org/apache/sling/event/impl/EnvironmentComponent.java
@@ -18,14 +18,14 @@
  */
 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;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
 
 /**
  * Environment component. This component provides "global settings"
@@ -35,14 +35,17 @@ import org.apache.sling.settings.SlingSettingsService;
  * This component needs to be immediate to set the global variables
  * (application id and thread pool).
  */
-@Component(immediate=true)
-@Service(value=EnvironmentComponent.class)
+@Component(immediate=true,
+    property = {
+            Constants.SERVICE_VENDOR + "=The Apache Software Foundation"
+    },
+    service={EnvironmentComponent.class})
 public class EnvironmentComponent {
 
     /**
      * Our thread pool.
      */
-    @Reference(referenceInterface=EventingThreadPool.class)
+    @Reference(service=EventingThreadPool.class)
     private ThreadPool threadPool;
 
     /** Sling settings service. */
diff --git a/src/main/java/org/apache/sling/event/impl/EventingThreadPool.java b/src/main/java/org/apache/sling/event/impl/EventingThreadPool.java
index 0821866..652f09f 100644
--- a/src/main/java/org/apache/sling/event/impl/EventingThreadPool.java
+++ b/src/main/java/org/apache/sling/event/impl/EventingThreadPool.java
@@ -18,34 +18,44 @@
  */
 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;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
 
 
 /**
  * The configurable eventing thread pool.
  */
-@Component(label="Apache Sling Job Thread Pool",
+@Component(service = EventingThreadPool.class,
+           property = {
+              Constants.SERVICE_VENDOR + "=The Apache Software Foundation"
+})
+@Designate(ocd = EventingThreadPool.Config.class)
+public class EventingThreadPool implements ThreadPool {
+
+    @ObjectClassDefinition(name = "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 {
+                  + "configuration.")
+    public @interface Config {
+
+        @AttributeDefinition(name = "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.")
+        int minPoolSize() default 35;
+    }
 
     @Reference
     private ThreadPoolManager threadPoolManager;
@@ -53,14 +63,6 @@ public class EventingThreadPool implements ThreadPool {
     /** 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
     }
@@ -79,9 +81,8 @@ public class EventingThreadPool implements ThreadPool {
      */
     @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);
+    protected void activate(final Config config) {
+        this.configure(config.minPoolSize());
     }
 
     private void configure(final int maxPoolSize) {
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
index 08660e8..5aa4eb3 100644
--- a/src/main/java/org/apache/sling/event/impl/jobs/JobConsumerManager.java
+++ b/src/main/java/org/apache/sling/event/impl/jobs/JobConsumerManager.java
@@ -27,17 +27,6 @@ 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;
@@ -53,55 +42,62 @@ import org.osgi.framework.BundleContext;
 import org.osgi.framework.Constants;
 import org.osgi.framework.ServiceReference;
 import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
 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",
+@Component(service = JobConsumerManager.class,
+    property = {
+          Constants.SERVICE_VENDOR + "=The Apache Software Foundation"
+    })
+@Designate(ocd = JobConsumerManager.Config.class)
+public class JobConsumerManager {
+
+    @ObjectClassDefinition(name = "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",
+                     + "in a cluster are not affected.")
+    public @interface Config {
+
+        @AttributeDefinition(name = "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());
+        boolean org_apache_sling_installer_configuration_persist() default false;
 
-    @Property(unbounded=PropertyUnbounded.ARRAY, value = "*",
-              label="Topic Whitelist",
+        @AttributeDefinition(name = "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";
+        String[] job_consumermanager_whitelist() default "*";
 
-    @Property(unbounded=PropertyUnbounded.ARRAY,
-              label="Topic Blacklist",
+        @AttributeDefinition(name = "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";
+        String[] job_consumermanager_blacklist();
+    }
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
 
     /** The map with the consumers, keyed by topic, sorted by service ranking. */
-    private final Map<String, List<ConsumerInfo>> topicToConsumerMap = new HashMap<String, List<ConsumerInfo>>();
+    private final Map<String, List<ConsumerInfo>> topicToConsumerMap = new HashMap<>();
 
     /** ServiceRegistration for propagation. */
-    private ServiceRegistration propagationService;
+    private ServiceRegistration<PropertyProvider> propagationService;
 
     private String topics;
 
@@ -113,10 +109,10 @@ public class JobConsumerManager {
 
     private BundleContext bundleContext;
 
-    private final Map<String, Object[]> listenerMap = new HashMap<String, Object[]>();
+    private final Map<String, Object[]> listenerMap = new HashMap<>();
 
     private Dictionary<String, Object> getRegistrationProperties() {
-        final Dictionary<String, Object> serviceProps = new Hashtable<String, Object>();
+        final Dictionary<String, Object> serviceProps = new Hashtable<>();
         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
@@ -127,16 +123,16 @@ public class JobConsumerManager {
     }
 
     @Activate
-    protected void activate(final BundleContext bc, final Map<String, Object> props) {
+    protected void activate(final BundleContext bc, final Config config) {
         this.bundleContext = bc;
-        this.modified(bc, props);
+        this.modified(bc, config);
     }
 
     @Modified
-    protected void modified(final BundleContext bc, final Map<String, Object> props) {
+    protected void modified(final BundleContext bc, final Config config) {
         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)));
+        this.whitelistMatchers = TopicMatcherHelper.buildMatchers(config.job_consumermanager_whitelist());
+        this.blacklistMatchers = TopicMatcherHelper.buildMatchers(config.job_consumermanager_blacklist());
 
         final boolean enable = this.whitelistMatchers != null && this.blacklistMatchers != TopicMatcherHelper.MATCH_ALL;
         if ( wasEnabled != enable ) {
@@ -145,7 +141,7 @@ public class JobConsumerManager {
             }
             if ( enable ) {
                 logger.debug("Registering property provider with: {}", this.topics);
-                this.propagationService = bc.registerService(PropertyProvider.class.getName(),
+                this.propagationService = bc.registerService(PropertyProvider.class,
                         new PropertyProvider() {
 
                             @Override
@@ -240,7 +236,10 @@ public class JobConsumerManager {
      * Bind a new consumer
      * @param serviceReference The service reference to the consumer.
      */
-    protected void bindJobConsumer(final ServiceReference serviceReference) {
+    @Reference(service=JobConsumer.class,
+            cardinality=ReferenceCardinality.MULTIPLE,
+            policy=ReferencePolicy.DYNAMIC)
+    protected void bindJobConsumer(final ServiceReference<JobConsumer> serviceReference) {
         this.bindService(serviceReference, true);
     }
 
@@ -248,7 +247,7 @@ public class JobConsumerManager {
      * Unbind a consumer
      * @param serviceReference The service reference to the consumer.
      */
-    protected void unbindJobConsumer(final ServiceReference serviceReference) {
+    protected void unbindJobConsumer(final ServiceReference<JobConsumer> serviceReference) {
         this.unbindService(serviceReference, true);
     }
 
@@ -256,7 +255,10 @@ public class JobConsumerManager {
      * Bind a new executor
      * @param serviceReference The service reference to the executor.
      */
-    protected void bindJobExecutor(final ServiceReference serviceReference) {
+    @Reference(service=JobExecutor.class,
+            cardinality=ReferenceCardinality.MULTIPLE,
+            policy=ReferencePolicy.DYNAMIC)
+    protected void bindJobExecutor(final ServiceReference<JobExecutor> serviceReference) {
         this.bindService(serviceReference, false);
     }
 
@@ -264,7 +266,7 @@ public class JobConsumerManager {
      * Unbind a executor
      * @param serviceReference The service reference to the executor.
      */
-    protected void unbindJobExecutor(final ServiceReference serviceReference) {
+    protected void unbindJobExecutor(final ServiceReference<JobExecutor> serviceReference) {
         this.unbindService(serviceReference, false);
     }
 
@@ -273,7 +275,7 @@ public class JobConsumerManager {
      * @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) {
+    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);
@@ -285,7 +287,7 @@ public class JobConsumerManager {
                         if ( topic.length() > 0 ) {
                             List<ConsumerInfo> consumers = this.topicToConsumerMap.get(topic);
                             if ( consumers == null ) {
-                                consumers = new ArrayList<JobConsumerManager.ConsumerInfo>();
+                                consumers = new ArrayList<>();
                                 this.topicToConsumerMap.put(topic, consumers);
                                 changed = true;
                             }
@@ -310,7 +312,7 @@ public class JobConsumerManager {
      * @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) {
+    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);
@@ -367,7 +369,7 @@ public class JobConsumerManager {
         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>();
+            final List<String> topicList = new ArrayList<>();
             for(final String topic : this.topicToConsumerMap.keySet() ) {
                 // check whitelist
                 if ( this.match(topic, this.whitelistMatchers) ) {
@@ -400,13 +402,13 @@ public class JobConsumerManager {
      */
     private final static class ConsumerInfo implements Comparable<ConsumerInfo> {
 
-        public final ServiceReference serviceReference;
+        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) {
+        public ConsumerInfo(final ServiceReference<?> serviceReference, final boolean isConsumer) {
             this.serviceReference = serviceReference;
             this.isConsumer = isConsumer;
             final Object sr = serviceReference.getProperty(Constants.SERVICE_RANKING);
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
index b76fda4..b5c6372 100644
--- a/src/main/java/org/apache/sling/event/impl/jobs/JobManagerImpl.java
+++ b/src/main/java/org/apache/sling/event/impl/jobs/JobManagerImpl.java
@@ -28,13 +28,6 @@ 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;
@@ -69,6 +62,10 @@ 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.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
 import org.osgi.service.event.Event;
 import org.osgi.service.event.EventAdmin;
 import org.osgi.service.event.EventConstants;
@@ -80,15 +77,15 @@ 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})
-})
+@Component(immediate=true,
+    service={JobManager.class, EventHandler.class, Runnable.class},
+    property = {
+            Constants.SERVICE_VENDOR + "=The Apache Software Foundation",
+            "scheduler.period:Long=60",
+            "scheduler.concurrent:Boolean=false",
+            EventConstants.EVENT_TOPIC + "=" + ResourceHelper.BUNDLE_EVENT_STARTED,
+            EventConstants.EVENT_TOPIC + "=" + ResourceHelper.BUNDLE_EVENT_UPDATED
+    })
 public class JobManagerImpl
     implements JobManager, EventHandler, Runnable {
 
@@ -383,7 +380,7 @@ public class JobManagerImpl
                                        || type == QueryType.ERROR
                                        || type == QueryType.GIVEN_UP
                                        || type == QueryType.STOPPED;
-        final List<Job> result = new ArrayList<Job>();
+        final List<Job> result = new ArrayList<>();
         final ResourceResolver resolver = this.configuration.createResourceResolver();
         final StringBuilder buf = new StringBuilder(64);
         try {
@@ -609,7 +606,7 @@ public class JobManagerImpl
         final String path = this.configuration.getUniquePath(info.targetId, jobTopic, jobId, jobProperties);
 
         // create properties
-        final Map<String, Object> properties = new HashMap<String, Object>();
+        final Map<String, Object> properties = new HashMap<>();
 
         if ( jobProperties != null ) {
             for(final Map.Entry<String, Object> entry : jobProperties.entrySet() ) {
@@ -720,7 +717,7 @@ public class JobManagerImpl
                                   errorMessage});
             return null;
         }
-        final List<String> errorList = new ArrayList<String>();
+        final List<String> errorList = new ArrayList<>();
         Job result = this.addJobInternal(topic, properties, errorList);
         if ( errors != null ) {
             errors.addAll(errorList);
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
index c93fe51..0449acb 100644
--- a/src/main/java/org/apache/sling/event/impl/jobs/Utility.java
+++ b/src/main/java/org/apache/sling/event/impl/jobs/Utility.java
@@ -40,8 +40,6 @@ 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
@@ -55,7 +53,7 @@ public abstract class Utility {
                 } 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";
             }
@@ -90,7 +88,7 @@ public abstract class Utility {
      * @return New event object.
      */
     public static Event toEvent(final Job job) {
-        final Map<String, Object> eventProps = new HashMap<String, Object>();
+        final Map<String, Object> eventProps = new HashMap<>();
         eventProps.putAll(((JobImpl)job).getProperties());
         eventProps.put(ResourceHelper.PROPERTY_JOB_ID, job.getId());
         eventProps.remove(JobConsumer.PROPERTY_JOB_ASYNC_HANDLER);
@@ -257,7 +255,7 @@ public abstract class Utility {
      * @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 List<Resource> children = new ArrayList<>();
         final Iterator<Resource> monthIter = rsrc.listChildren();
         while ( monthIter.hasNext() ) {
             final Resource monthResource = monthIter.next();
@@ -267,15 +265,4 @@ public abstract class Utility {
         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/ConfigurationConstants.java b/src/main/java/org/apache/sling/event/impl/jobs/config/ConfigurationConstants.java
index 18f4886..d152782 100644
--- 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
@@ -25,14 +25,10 @@ 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";
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
index c8457a2..4922a72 100644
--- 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
@@ -21,103 +21,108 @@ 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.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.osgi.service.metatype.annotations.Option;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-@Component(metatype=true,
+@Component(service=InternalQueueConfiguration.class,
            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",
+           configurationPolicy=ConfigurationPolicy.REQUIRE,
+           property={
+                   "webconsole.configurationFactory.nameHint=Queue: {" + ConfigurationConstants.PROP_NAME + "}",
+                   Constants.SERVICE_VENDOR + "=The Apache Software Foundation"
+           })
+@Designate(ocd = InternalQueueConfiguration.Config.class, factory = true)
+public class InternalQueueConfiguration
+    implements QueueConfiguration, Comparable<InternalQueueConfiguration> {
+
+    @ObjectClassDefinition(name = "Apache Sling Job Queue Configuration",
+            description="The configuration of a job processing queue.")
+    public @interface Config {
+
+        @AttributeDefinition(name="Name",
+              description="The name of the queue. If matching is used the token {0} can be used to substitute the real value.")
+        String queue_name();
+
+        @AttributeDefinition(name="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",
+                        + "ends with a dot nor with a star, this is assumed to define an exact topic.")
+        String[] queue_topics();
+
+        @AttributeDefinition(name = "Type",
+              description="The queue type.",
+              options = {@Option(label="UNORDERED",value="Parallel"),
+                      @Option(label="ORDERED",value="Ordered"),
+                      @Option(label="TOPIC_ROUND_ROBIN",value="Topic Round Robin")})
+        String queue_type() default "UNORDERED";
+
+        @AttributeDefinition(
+                 name="Priority",
+                 description="The priority for the threads used by this queue. Default is norm.",
+                 options = {
+                         @Option(label="NORM",value="Norm"),
+                         @Option(label="MIN",value="Min"),
+                         @Option(label="MAX",value="Max")
+                 })
+         String queue_priority() default ConfigurationConstants.DEFAULT_PRIORITY;
+
+         @AttributeDefinition(name="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.")
+         int queue_retries() default ConfigurationConstants.DEFAULT_RETRIES;
+
+         @AttributeDefinition(name="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.")
+         long queue_retrydelay() default ConfigurationConstants.DEFAULT_RETRY_DELAY;
+
+         @AttributeDefinition(name="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).")
+         double queue_maxparallel() default ConfigurationConstants.DEFAULT_MAX_PARALLEL;
+
+         @AttributeDefinition(name="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",
+                        + "to provide a complete history.")
+         boolean queue_keepJobs() default false;
+
+         @AttributeDefinition(name="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",
+                        + "be run on the instance where the job was created.")
+         boolean queue_preferRunOnCreationInstance() default false;
+
+         @AttributeDefinition(name="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.")
+         int queue_threadPoolSize() default 0;
+
+         @AttributeDefinition(name="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> {
+                        + "If more than one queue matches a job topic, the one with the highest ranking is used.")
+         int service_ranking() default 0;
+     }
 
     /** Logger. */
     private final Logger logger = LoggerFactory.getLogger(this.getClass());
@@ -166,9 +171,9 @@ public class InternalQueueConfiguration
     /**
      * Create a new configuration from a config
      */
-    public static InternalQueueConfiguration fromConfiguration(final Map<String, Object> params) {
+    public static InternalQueueConfiguration fromConfiguration(final Map<String, Object> props, final Config config) {
         final InternalQueueConfiguration c = new InternalQueueConfiguration();
-        c.activate(params);
+        c.activate(props, config);
         return c;
     }
 
@@ -180,28 +185,27 @@ public class InternalQueueConfiguration
      * Create a new queue configuration
      */
     @Activate
-    protected void activate(final Map<String, Object> params) {
-        this.name = PropertiesUtil.toString(params.get(ConfigurationConstants.PROP_NAME), null);
+    protected void activate(final Map<String, Object> props, final Config config) {
+        this.name = config.queue_name();
         try {
-            this.priority = ThreadPriority.valueOf(PropertiesUtil.toString(params.get(ConfigurationConstants.PROP_PRIORITY), ConfigurationConstants.DEFAULT_PRIORITY));
+            this.priority = ThreadPriority.valueOf(config.queue_priority());
         } catch ( final IllegalArgumentException iae) {
-            logger.warn("Invalid value for queue priority. Using default instead of : {}", params.get(ConfigurationConstants.PROP_PRIORITY));
+            logger.warn("Invalid value for queue priority. Using default instead of : {}", config.queue_priority());
             this.priority = ThreadPriority.valueOf(ConfigurationConstants.DEFAULT_PRIORITY);
         }
         try {
-            this.type = Type.valueOf(PropertiesUtil.toString(params.get(ConfigurationConstants.PROP_TYPE), ConfigurationConstants.DEFAULT_TYPE));
+            this.type = Type.valueOf(config.queue_type());
         } catch ( final IllegalArgumentException iae) {
-            logger.error("Invalid value for queue type configuration: {}", params.get(ConfigurationConstants.PROP_TYPE));
+            logger.error("Invalid value for queue type configuration: {}", config.queue_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);
+        this.retries = config.queue_retries();
+        this.retryDelay = config.queue_retrydelay();
 
         // 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);
+        final double inMaxParallel = config.queue_maxparallel();
         logger.debug("Max parallel for queue {} is {}", this.name, inMaxParallel);
         if ((inMaxParallel == Math.floor(inMaxParallel)) && !Double.isInfinite(inMaxParallel)) {
             // integral type
@@ -224,18 +228,18 @@ public class InternalQueueConfiguration
         if ( this.type == Type.ORDERED ) {
             this.maxParallelProcesses = 1;
         }
-        final String[] topicsParam = PropertiesUtil.toStringArray(params.get(ConfigurationConstants.PROP_TOPICS));
+        final String[] topicsParam = config.queue_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.keepJobs = config.queue_keepJobs();
+        this.serviceRanking = config.service_ranking();
+        this.ownThreadPoolSize = config.queue_threadPoolSize();
+        this.preferCreationInstance = config.queue_preferRunOnCreationInstance();
+        this.pid = (String)props.get(Constants.SERVICE_PID);
         this.valid = this.checkIsValid();
     }
 
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
index 3cc8b72..4f75a4c 100644
--- 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
@@ -27,14 +27,6 @@ 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;
@@ -46,7 +38,6 @@ 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;
@@ -54,6 +45,15 @@ 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.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -61,35 +61,32 @@ 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),
+@Component(immediate=true,
+           service=JobManagerConfiguration.class,
+           name="org.apache.sling.event.impl.jobs.jcr.PersistenceHandler",
+           property = {
+                   Constants.SERVICE_VENDOR + "=The Apache Software Foundation",
+                   JobManagerConfiguration.PROPERTY_REPOSITORY_PATH + "=" + JobManagerConfiguration.DEFAULT_REPOSITORY_PATH,
+                   JobManagerConfiguration.PROPERTY_SCHEDULED_JOBS_PATH + "=" + JobManagerConfiguration.DEFAULT_SCHEDULED_JOBS_PATH,
+                   JobManagerConfiguration.PROPERTY_BACKGROUND_LOAD_DELAY + ":Long=" + JobManagerConfiguration.DEFAULT_BACKGROUND_LOAD_DELAY
 })
+@Designate(ocd = JobManagerConfiguration.Config.class)
 public class JobManagerConfiguration {
 
+    @ObjectClassDefinition(name = "Apache Sling Job Manager",
+            description="This is the central service of the job handling.")
+    public @interface Config {
+
+        @AttributeDefinition(name = "Disable Distribution",
+        description="If the distribution is disabled, all jobs will be processed on the leader only! "
+                + "Please use this switch with care.")
+        boolean job_consumermanager_disableDistribution() default false;
+
+        @AttributeDefinition(name = "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.")
+        long startup_delay() default 30;
+    }
     /** Logger. */
     private final Logger logger = LoggerFactory.getLogger("org.apache.sling.event.impl.jobs");
 
@@ -102,12 +99,6 @@ public class JobManagerConfiguration {
     /** 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";
 
@@ -117,24 +108,9 @@ public class JobManagerConfiguration {
     /** 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;
 
@@ -173,7 +149,7 @@ public class JobManagerConfiguration {
     private String scheduledJobsPathWithSlash;
 
     /** List of topology awares. */
-    private final List<ConfigurationChangeListener> listeners = new ArrayList<ConfigurationChangeListener>();
+    private final List<ConfigurationChangeListener> listeners = new ArrayList<>();
 
     /** The environment component. */
     @Reference
@@ -200,11 +176,12 @@ public class JobManagerConfiguration {
     /**
      * Activate this component.
      * @param props Configuration properties
+     * @param config Configuration properties
      * @throws RuntimeException If the default paths can't be created
      */
     @Activate
-    protected void activate(final Map<String, Object> props) {
-        this.update(props);
+    protected void activate(final Map<String, Object> props, final Config config) {
+        this.update(props, config);
         this.jobsBasePathWithSlash = PropertiesUtil.toString(props.get(PROPERTY_REPOSITORY_PATH),
                 DEFAULT_REPOSITORY_PATH) + '/';
 
@@ -240,7 +217,7 @@ public class JobManagerConfiguration {
 
         // SLING-5560 : use an InitDelayingTopologyEventListener
         if (this.startupDelay > 0) {
-            logger.debug("activate: job manager will start in {} sec. ({})", this.startupDelay, PROPERTY_STARTUP_DELAY);
+            logger.debug("activate: job manager will start in {} sec. ({})", this.startupDelay, config.startup_delay());
             this.startupDelayListener = new InitDelayingTopologyEventListener(startupDelay, new TopologyEventListener() {
 
                 @Override
@@ -249,7 +226,7 @@ public class JobManagerConfiguration {
                 }
             }, this.scheduler, logger);
         } else {
-            logger.debug("activate: job manager will start without delay. ({}:{})", PROPERTY_STARTUP_DELAY, this.startupDelay);
+            logger.debug("activate: job manager will start without delay. ({}:{})", config.startup_delay(), this.startupDelay);
         }
     }
 
@@ -257,14 +234,13 @@ public class JobManagerConfiguration {
      * 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);
+    protected void update(final Map<String, Object> props, final Config config) {
+        this.disabledDistribution = config.job_consumermanager_disableDistribution();
         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);
+        this.startupDelay = config.startup_delay();
     }
 
     /**
@@ -622,7 +598,7 @@ public class JobManagerConfiguration {
         }
     }
 
-    private final Map<String, Job> retryList = new HashMap<String, Job>();
+    private final Map<String, Job> retryList = new HashMap<>();
 
     public void addJobToRetryList(final Job job) {
         synchronized ( retryList ) {
@@ -631,7 +607,7 @@ public class JobManagerConfiguration {
     }
 
     public List<Job> clearJobRetryList() {
-        final List<Job> result = new ArrayList<Job>();
+        final List<Job> result = new ArrayList<>();
         synchronized ( this.retryList ) {
             result.addAll(retryList.values());
             retryList.clear();
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
index 9287e6b..9f45b83 100644
--- 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
@@ -18,17 +18,17 @@
  */
 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 java.lang.annotation.Annotation;
+import java.util.Collections;
+
 import org.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.osgi.service.metatype.annotations.Option;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -37,42 +37,49 @@ 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."),
-})
+@Component(name="org.apache.sling.event.impl.jobs.DefaultJobManager",
+           service=MainQueueConfiguration.class,
+           property={
+                   Constants.SERVICE_VENDOR + "=The Apache Software Foundation"
+           })
+@Designate(ocd=MainQueueConfiguration.Config.class)
 public class MainQueueConfiguration {
 
+    @ObjectClassDefinition(name = "Apache Sling Job Default Queue",
+           description="The configuration of the default job queue.")
+    public @interface Config {
+
+        @AttributeDefinition(
+                name="Priority",
+                description="The priority for the threads used by this queue. Default is norm.",
+                options = {
+                        @Option(label="NORM",value="Norm"),
+                        @Option(label="MIN",value="Min"),
+                        @Option(label="MAX",value="Max")
+                })
+        String queue_priority() default ConfigurationConstants.DEFAULT_PRIORITY;
+
+        @AttributeDefinition(name="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.")
+        int queue_retries() default ConfigurationConstants.DEFAULT_RETRIES;
+
+        @AttributeDefinition(name="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.")
+        long queue_retrydelay() default ConfigurationConstants.DEFAULT_RETRY_DELAY;
+
+        @AttributeDefinition(name="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.")
+        int queue_maxparallel() default ConfigurationConstants.DEFAULT_MAX_PARALLEL;
+    }
+
     public static final String MAIN_QUEUE_NAME = "<main queue>";
 
     /** Default logger. */
@@ -82,27 +89,84 @@ public class MainQueueConfiguration {
 
     /**
      * Activate this component.
-     * @param props Configuration properties
+     * @param config Configuration properties
      */
     @Activate
-    protected void activate(final Map<String, Object> props) {
-        this.update(props);
+    protected void activate(final Config config) {
+        this.update(config);
     }
 
     /**
      * Configure this component.
-     * @param props Configuration properties
+     * @param config 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);
+    protected void update(final Config config) {
+        logger.debug("properties for queue {}: {}", MAIN_QUEUE_NAME, config);
+        this.mainConfiguration = InternalQueueConfiguration.fromConfiguration(
+                Collections.singletonMap(Constants.SERVICE_PID, (Object)"org.apache.sling.event.impl.jobs.DefaultJobManager"),
+                new InternalQueueConfiguration.Config() {
+
+            @Override
+            public Class<? extends Annotation> annotationType() {
+                return InternalQueueConfiguration.Config.class;
+            }
+
+            @Override
+            public int service_ranking() {
+                return 0;
+            }
+
+            @Override
+            public String queue_type() {
+                return InternalQueueConfiguration.Type.UNORDERED.name();
+            }
+
+            @Override
+            public String[] queue_topics() {
+                return new String[] {"*"};
+            }
+
+            @Override
+            public int queue_threadPoolSize() {
+                return 0;
+            }
+
+            @Override
+            public long queue_retrydelay() {
+                return config.queue_retrydelay();
+            }
+
+            @Override
+            public int queue_retries() {
+                return config.queue_retries();
+            }
+
+            @Override
+            public String queue_priority() {
+                return config.queue_priority();
+            }
+
+            @Override
+            public boolean queue_preferRunOnCreationInstance() {
+                return false;
+            }
+
+            @Override
+            public String queue_name() {
+                return MAIN_QUEUE_NAME;
+            }
+
+            @Override
+            public double queue_maxparallel() {
+                return config.queue_maxparallel();
+            }
+
+            @Override
+            public boolean queue_keepJobs() {
+                return false;
+            }
+        });
     }
 
     /**
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
index ce2b79a..753568b 100644
--- 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
@@ -22,22 +22,21 @@ 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;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
 
 
 /**
  * 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")
+@Component(service = QueueConfigurationManager.class,
+property = {
+        Constants.SERVICE_VENDOR + "=The Apache Software Foundation"
+})
 public class QueueConfigurationManager {
 
     /** Empty configuration array. */
@@ -47,7 +46,7 @@ public class QueueConfigurationManager {
     private volatile InternalQueueConfiguration[] orderedConfigs = EMPTY_CONFIGS;
 
     /** All configurations. */
-    private final List<InternalQueueConfiguration> configurations = new ArrayList<InternalQueueConfiguration>();
+    private final List<InternalQueueConfiguration> configurations = new ArrayList<>();
 
     /** The main queue configuration. */
     @Reference
@@ -57,6 +56,9 @@ public class QueueConfigurationManager {
      * Add a new queue configuration.
      * @param config A new queue configuration.
      */
+    @Reference(service=InternalQueueConfiguration.class, policy=ReferencePolicy.DYNAMIC,
+            cardinality=ReferenceCardinality.MULTIPLE,
+            updated="updateConfig")
     protected void bindConfig(final InternalQueueConfiguration config) {
         synchronized ( configurations ) {
             configurations.add(config);
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
index 2c49ca2..58da224 100644
--- 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
@@ -22,13 +22,13 @@ 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.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -36,8 +36,10 @@ 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)
+@Component(service = TopologyEventListener.class,
+    property = {
+            Constants.SERVICE_VENDOR + "=The Apache Software Foundation"
+    })
 public class TopologyHandler implements TopologyEventListener, Runnable {
 
     /** The logger. */
@@ -47,7 +49,7 @@ public class TopologyHandler implements TopologyEventListener, Runnable {
     private JobManagerConfiguration configuration;
 
     /** A local queue for async handling of the events */
-    private final BlockingQueue<QueueItem> queue = new LinkedBlockingQueue<QueueItem>();
+    private final BlockingQueue<QueueItem> queue = new LinkedBlockingQueue<>();
 
     /** Active flag. */
     private final AtomicBoolean isActive = new AtomicBoolean(false);
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
index 619136c..f3f3a18 100644
--- 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
@@ -31,11 +31,6 @@ 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;
@@ -48,19 +43,23 @@ 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.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
 
 /**
  * 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)
+@Component(service={InventoryPrinter.class},
+    property = {
+        Constants.SERVICE_VENDOR + "=The Apache Software Foundation",
+        InventoryPrinter.NAME + "=slingjobs",
+        InventoryPrinter.TITLE + "=Sling Jobs",
+        InventoryPrinter.FORMAT + "=TEXT",
+        InventoryPrinter.FORMAT + "=JSON",
+        InventoryPrinter.WEBCONSOLE + ":Boolean=false"
 })
 public class InventoryPlugin implements InventoryPrinter {
 
@@ -325,7 +324,7 @@ public class InventoryPlugin implements InventoryPrinter {
             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>();
+                final List<String> instances = new ArrayList<>();
                 for(final InstanceDescription id : entry.getValue()) {
                     if ( id.isLocal() ) {
                         instances.add("local");
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
index ffe94b5..00b08ad 100644
--- 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
@@ -33,11 +33,6 @@ 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;
@@ -53,6 +48,9 @@ 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.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -61,13 +59,13 @@ import org.slf4j.LoggerFactory;
  * 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"})
+@Component(service={javax.servlet.Servlet.class, JobConsumer.class},
+property = {
+    Constants.SERVICE_VENDOR + "=The Apache Software Foundation",
+    "felix.webconsole.label=slingevent",
+    "felix.webconsole.title=Jobs",
+    "felix.webconsole.category=SLING",
+    JobConsumer.PROPERTY_TOPICS + "=sling/webconsole/test"
 })
 public class WebConsolePlugin extends HttpServlet implements JobConsumer {
 
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
index 0e696d6..154a926 100644
--- 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
@@ -17,23 +17,24 @@
  */
 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;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
 
-@Component
-@Service(value = StatisticsMBean.class)
-@Properties(@Property(name = "jmx.objectname", value = "org.apache.sling:type=queues,name=AllQueues"))
+@Component(service =  StatisticsMBean.class,
+    property = {
+            "jmx.objectname=org.apache.sling:type=queues,name=AllQueues",
+            Constants.SERVICE_VENDOR + "=The Apache Software Foundation"
+})
 public class AllJobStatisticsMBean extends AbstractJobStatistics {
 
     private static final long TTL = 1000L;
     private long agregateStatisticsTTL = 0L;
     private Statistics aggregateStatistics;
+
     @Reference
     private JobManager jobManager;
 
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
index 83a56e9..3227266 100644
--- 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
@@ -32,26 +32,26 @@ 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")
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+
+@Component(service =  QueuesMBean.class,
+property = {
+        "jmx.objectname=org.apache.sling:type=queues,name=QueueNames",
+        Constants.SERVICE_VENDOR + "=The Apache Software Foundation"
+})
 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 Map<String, QueueMBeanHolder> queues = new ConcurrentHashMap<>();
     private String[] names;
     private AtomicLong sequence = new AtomicLong(System.currentTimeMillis());
     private BundleContext bundleContext;
@@ -147,7 +147,7 @@ public class QueuesMBeanImpl extends StandardEmitterMBean implements QueuesMBean
 
     private QueueMBeanHolder add(Queue queue) {
         QueueMBeanImpl queueMBean = new QueueMBeanImpl(queue);
-        ServiceRegistration serviceRegistration = bundleContext
+        ServiceRegistration<?> serviceRegistration = bundleContext
                 .registerService(StatisticsMBean.class.getName(), queueMBean,
                         createProperties(
                                 "jmx.objectname","org.apache.sling:type=queues,name="+queue.getName(),
@@ -160,7 +160,7 @@ public class QueuesMBeanImpl extends StandardEmitterMBean implements QueuesMBean
     }
 
     private Dictionary<String, Object> createProperties(Object ... values) {
-        Dictionary<String, Object> props = new Hashtable<String, Object>();
+        Dictionary<String, Object> props = new Hashtable<>();
         for ( int i = 0; i < values.length; i+=2) {
             props.put((String) values[i], values[i+1]);
         }
@@ -175,7 +175,7 @@ public class QueuesMBeanImpl extends StandardEmitterMBean implements QueuesMBean
     @Override
     public String[] getQueueNames() {
         if (names == null) {
-            List<String> lnames = new ArrayList<String>(queues.keySet());
+            List<String> lnames = new ArrayList<>(queues.keySet());
             Collections.sort(lnames);
             names = lnames.toArray(new String[lnames.size()]);
         }
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
index 590026f..03c5769 100644
--- 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
@@ -22,10 +22,6 @@ 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;
@@ -36,6 +32,10 @@ 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.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
 import org.osgi.service.event.Event;
 import org.osgi.service.event.EventAdmin;
 import org.slf4j.Logger;
@@ -45,7 +45,7 @@ import org.slf4j.LoggerFactory;
  * This component receives resource added events and sends a job
  * created event.
  */
-@Component
+@Component(service = {})
 public class NewJobSender implements ResourceChangeListener, ExternalResourceChangeListener {
 
     /** Logger. */
@@ -68,7 +68,7 @@ public class NewJobSender implements ResourceChangeListener, ExternalResourceCha
      */
     @Activate
     protected void activate(final BundleContext bundleContext) {
-        final Dictionary<String, Object> properties = new Hashtable<String, Object>();
+        final Dictionary<String, Object> properties = new Hashtable<>();
         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());
@@ -104,7 +104,7 @@ public class NewJobSender implements ResourceChangeListener, ExternalResourceCha
 
                 if ( path.indexOf("_", topicEnd + 1) != -1 ) {
                 	// only job id and topic are guaranteed
-                	final Dictionary<String, Object> properties = new Hashtable<String, Object>();
+                	final Dictionary<String, Object> properties = new Hashtable<>();
                 	properties.put(NotificationConstants.NOTIFICATION_PROPERTY_JOB_ID, jobId);
                     properties.put(NotificationConstants.NOTIFICATION_PROPERTY_JOB_TOPIC, topic);
 
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
index fd7fcf8..7141984 100644
--- 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
@@ -29,13 +29,6 @@ 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;
@@ -48,7 +41,6 @@ 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;
@@ -59,6 +51,11 @@ 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.framework.Constants;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
 import org.osgi.service.event.Event;
 import org.osgi.service.event.EventAdmin;
 import org.osgi.service.event.EventConstants;
@@ -70,13 +67,14 @@ 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)
-})
+@Component(immediate=true,
+           service={Runnable.class, QueueManager.class, EventHandler.class},
+           property={
+                   Scheduler.PROPERTY_SCHEDULER_PERIOD + ":Long=60",
+                   Scheduler.PROPERTY_SCHEDULER_CONCURRENT + ":Boolean=false",
+                   EventConstants.EVENT_TOPIC + "=" + NotificationConstants.TOPIC_JOB_ADDED,
+                   Constants.SERVICE_VENDOR + "=The Apache Software Foundation"
+           })
 public class QueueManager
     implements Runnable, EventHandler, ConfigurationChangeListener {
 
@@ -101,7 +99,7 @@ public class QueueManager
     /**
      * Our thread pool.
      */
-    @Reference(referenceInterface=EventingThreadPool.class)
+    @Reference(service=EventingThreadPool.class)
     private ThreadPool threadPool;
 
     /** The job manager configuration. */
@@ -115,7 +113,7 @@ public class QueueManager
     private final Object queuesLock = new Object();
 
     /** All active queues. */
-    private final Map<String, JobQueueImpl> queues = new ConcurrentHashMap<String, JobQueueImpl>();
+    private final Map<String, JobQueueImpl> queues = new ConcurrentHashMap<>();
 
     /** We count the scheduler runs. */
     private volatile long schedulerRuns;
@@ -294,7 +292,7 @@ public class QueueManager
     private void restart() {
         // let's rename/close all queues and clear them
         synchronized ( queuesLock ) {
-            final List<JobQueueImpl> queues = new ArrayList<JobQueueImpl>(this.queues.values());
+            final List<JobQueueImpl> queues = new ArrayList<>(this.queues.values());
             for(final JobQueueImpl queue : queues ) {
                 this.outdateQueue(queue);
             }
@@ -382,7 +380,7 @@ public class QueueManager
      * Scan the resource tree for topics.
      */
     private Set<String> scanTopics() {
-        final Set<String> topics = new HashSet<String>();
+        final Set<String> topics = new HashSet<>();
 
         final ResourceResolver resolver = this.configuration.createResourceResolver();
         try {
@@ -420,12 +418,12 @@ public class QueueManager
      * 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>>();
+        final Map<QueueInfo, Set<String>> mapping = new HashMap<>();
         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>();
+                queueTopics = new HashSet<>();
                 mapping.put(queueInfo, queueTopics);
             }
             queueTopics.add(topic);
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
index c2d3d15..455cb1e 100644
--- 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
@@ -22,19 +22,21 @@ 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;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
 
 /**
  * The statistics manager keeps track of all statistics related tasks.
  */
-@Component
-@Service(value=StatisticsManager.class)
+@Component(service=StatisticsManager.class,
+    property = {
+        Constants.SERVICE_VENDOR + "=The Apache Software Foundation"
+})
 public class StatisticsManager {
 
     /** The job manager configuration. */
@@ -56,10 +58,10 @@ public class StatisticsManager {
     };
 
     /** Statistics per topic. */
-    private final ConcurrentMap<String, TopicStatistics> topicStatistics = new ConcurrentHashMap<String, TopicStatistics>();
+    private final ConcurrentMap<String, TopicStatistics> topicStatistics = new ConcurrentHashMap<>();
 
     /** Statistics per queue. */
-    private final ConcurrentMap<String, Statistics> queueStatistics = new ConcurrentHashMap<String, Statistics>();
+    private final ConcurrentMap<String, Statistics> queueStatistics = new ConcurrentHashMap<>();
 
     /**
      * Get the global statistics.
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
index 6cb136b..29feafa 100644
--- 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
@@ -24,10 +24,6 @@ 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;
@@ -39,6 +35,9 @@ 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.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -52,9 +51,11 @@ import org.slf4j.LoggerFactory;
  *           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")
+@Component(service = JobExecutor.class,
+    property = {
+        JobExecutor.PROPERTY_TOPICS + "=org/apache/sling/event/impl/jobs/tasks/HistoryCleanUpTask",
+        Constants.SERVICE_VENDOR + "=The Apache Software Foundation"
+})
 public class HistoryCleanUpTask implements JobExecutor {
 
     private static final String PROPERTY_AGE = "age";
@@ -88,7 +89,7 @@ public class HistoryCleanUpTask implements JobExecutor {
 
         final List<String> stateList;
         if ( states != null ) {
-            stateList = new ArrayList<String>();
+            stateList = new ArrayList<>();
             for(final String s : states) {
                 stateList.add(s);
             }
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
index f976767..a9d1b83 100644
--- a/src/test/java/org/apache/sling/event/impl/jobs/JobConsumerManagerTest.java
+++ b/src/test/java/org/apache/sling/event/impl/jobs/JobConsumerManagerTest.java
@@ -22,7 +22,7 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 
-import java.util.Collections;
+import java.lang.annotation.Annotation;
 
 import org.apache.sling.event.jobs.consumer.JobConsumer;
 import org.apache.sling.event.jobs.consumer.JobExecutor;
@@ -34,10 +34,34 @@ import org.osgi.framework.ServiceReference;
 
 public class JobConsumerManagerTest {
 
+    private JobConsumerManager.Config getDefaultConfig() {
+        return new JobConsumerManager.Config() {
+
+            @Override
+            public Class<? extends Annotation> annotationType() {
+                return JobConsumerManager.Config.class;
+            }
+
+            @Override
+            public boolean org_apache_sling_installer_configuration_persist() {
+                return false;
+            }
+
+            @Override
+            public String[] job_consumermanager_whitelist() {
+                return new String[] {"*"};
+            }
+
+            @Override
+            public String[] job_consumermanager_blacklist() {
+                return null;
+            }
+        };
+    }
     @Test public void testSimpleMappingConsumer() {
         final BundleContext bc = Mockito.mock(BundleContext.class);
         final JobConsumerManager jcs = new JobConsumerManager();
-        jcs.activate(bc, Collections.EMPTY_MAP);
+        jcs.activate(bc, getDefaultConfig());
 
         final JobConsumer jc1 = Mockito.mock(JobConsumer.class);
         final ServiceReference ref1 = Mockito.mock(ServiceReference.class);
@@ -56,7 +80,7 @@ public class JobConsumerManagerTest {
     @Test public void testCategoryMappingConsumer() {
         final BundleContext bc = Mockito.mock(BundleContext.class);
         final JobConsumerManager jcs = new JobConsumerManager();
-        jcs.activate(bc, Collections.EMPTY_MAP);
+        jcs.activate(bc, getDefaultConfig());
 
         final JobConsumer jc1 = Mockito.mock(JobConsumer.class);
         final ServiceReference ref1 = Mockito.mock(ServiceReference.class);
@@ -75,7 +99,7 @@ public class JobConsumerManagerTest {
     @Test public void testSubCategoryMappingConsumer() {
         final BundleContext bc = Mockito.mock(BundleContext.class);
         final JobConsumerManager jcs = new JobConsumerManager();
-        jcs.activate(bc, Collections.EMPTY_MAP);
+        jcs.activate(bc, getDefaultConfig());
 
         final JobConsumer jc1 = Mockito.mock(JobConsumer.class);
         final ServiceReference ref1 = Mockito.mock(ServiceReference.class);
@@ -94,7 +118,7 @@ public class JobConsumerManagerTest {
     @Test public void testSimpleMappingExecutor() {
         final BundleContext bc = Mockito.mock(BundleContext.class);
         final JobConsumerManager jcs = new JobConsumerManager();
-        jcs.activate(bc, Collections.EMPTY_MAP);
+        jcs.activate(bc, getDefaultConfig());
 
         final JobExecutor jc1 = Mockito.mock(JobExecutor.class);
         final ServiceReference ref1 = Mockito.mock(ServiceReference.class);
@@ -113,7 +137,7 @@ public class JobConsumerManagerTest {
     @Test public void testCategoryMappingExecutor() {
         final BundleContext bc = Mockito.mock(BundleContext.class);
         final JobConsumerManager jcs = new JobConsumerManager();
-        jcs.activate(bc, Collections.EMPTY_MAP);
+        jcs.activate(bc, getDefaultConfig());
 
         final JobExecutor jc1 = Mockito.mock(JobExecutor.class);
         final ServiceReference ref1 = Mockito.mock(ServiceReference.class);
@@ -132,7 +156,7 @@ public class JobConsumerManagerTest {
     @Test public void testSubCategoryMappingExecutor() {
         final BundleContext bc = Mockito.mock(BundleContext.class);
         final JobConsumerManager jcs = new JobConsumerManager();
-        jcs.activate(bc, Collections.EMPTY_MAP);
+        jcs.activate(bc, getDefaultConfig());
 
         final JobExecutor jc1 = Mockito.mock(JobExecutor.class);
         final ServiceReference ref1 = Mockito.mock(ServiceReference.class);
@@ -151,7 +175,7 @@ public class JobConsumerManagerTest {
     @Test public void testRanking() {
         final BundleContext bc = Mockito.mock(BundleContext.class);
         final JobConsumerManager jcs = new JobConsumerManager();
-        jcs.activate(bc, Collections.EMPTY_MAP);
+        jcs.activate(bc, getDefaultConfig());
 
         final JobExecutor jc1 = Mockito.mock(JobExecutor.class);
         final JobExecutor jc2 = Mockito.mock(JobExecutor.class);
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
index d485b2b..817407f 100644
--- 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
@@ -24,68 +24,128 @@ import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
+import java.lang.annotation.Annotation;
+import java.util.Collections;
 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);
+    private InternalQueueConfiguration.Config createConfig(final double maxParallel) {
+        return createConfig(null, "QueueConfigurationTest", maxParallel);
+    }
+
+    private InternalQueueConfiguration.Config createConfig(final String[] topics) {
+        return createConfig(topics, "QueueConfigurationTest", ConfigurationConstants.DEFAULT_MAX_PARALLEL);
+    }
+
+    private InternalQueueConfiguration.Config createConfig(final String[] topics, final String name) {
+        return createConfig(topics, name, ConfigurationConstants.DEFAULT_MAX_PARALLEL);
+    }
+
+    private InternalQueueConfiguration.Config createConfig(final String[] topics,
+            final String name,
+            final double maxParallel) {
+        return new InternalQueueConfiguration.Config() {
+
+            @Override
+            public Class<? extends Annotation> annotationType() {
+                return InternalQueueConfiguration.Config.class;
+            }
+
+            @Override
+            public String queue_name() {
+                return name;
+            }
+
+            @Override
+            public String[] queue_topics() {
+                return topics;
+            }
+
+            @Override
+            public String queue_type() {
+                return "UNORDERED";
+            }
+
+            @Override
+            public String queue_priority() {
+                return ConfigurationConstants.DEFAULT_PRIORITY;
+            }
+
+            @Override
+            public int queue_retries() {
+                return ConfigurationConstants.DEFAULT_RETRIES;
+            }
+
+            @Override
+            public long queue_retrydelay() {
+                return ConfigurationConstants.DEFAULT_RETRY_DELAY;
+            }
+
+            @Override
+            public double queue_maxparallel() {
+                return maxParallel;
+            }
+
+            @Override
+            public boolean queue_keepJobs() {
+                return false;
+            }
+
+            @Override
+            public boolean queue_preferRunOnCreationInstance() {
+                return false;
+            }
+
+            @Override
+            public int queue_threadPoolSize() {
+                return 0;
+            }
+
+            @Override
+            public int service_ranking() {
+                return 0;
+            }
+        };
+    }
 
-        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(p);
+    @org.junit.Test public void testMaxParallel() {
+        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(Collections.<String, Object>emptyMap(), createConfig(-1));
         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);
+        c = InternalQueueConfiguration.fromConfiguration(Collections.<String, Object>emptyMap(), createConfig(0.0));
         assertEquals(0, c.getMaxParallel());
 
-        p.put(ConfigurationConstants.PROP_MAX_PARALLEL, 1.0);
-        c = InternalQueueConfiguration.fromConfiguration(p);
+        c = InternalQueueConfiguration.fromConfiguration(Collections.<String, Object>emptyMap(), createConfig(1.0));
         assertEquals(1, c.getMaxParallel());
 
         // percentage (50%)
-        p.put(ConfigurationConstants.PROP_MAX_PARALLEL, 0.5);
-        c = InternalQueueConfiguration.fromConfiguration(p);
+        c = InternalQueueConfiguration.fromConfiguration(Collections.<String, Object>emptyMap(), createConfig(0.5));
         assertEquals((int) Math.round(Runtime.getRuntime().availableProcessors() * 0.5), c.getMaxParallel());
 
         // rounding
-        p.put(ConfigurationConstants.PROP_MAX_PARALLEL, 0.90);
-        c = InternalQueueConfiguration.fromConfiguration(p);
+        c = InternalQueueConfiguration.fromConfiguration(Collections.<String, Object>emptyMap(), createConfig(0.90));
         assertEquals((int) Math.round(Runtime.getRuntime().availableProcessors() * 0.9), c.getMaxParallel());
 
-        p.put(ConfigurationConstants.PROP_MAX_PARALLEL, 0.99);
-        c = InternalQueueConfiguration.fromConfiguration(p);
+        c = InternalQueueConfiguration.fromConfiguration(Collections.<String, Object>emptyMap(), createConfig(0.99));
         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);
+        c = InternalQueueConfiguration.fromConfiguration(Collections.<String, Object>emptyMap(), createConfig(1.01));
         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);
+        c = InternalQueueConfiguration.fromConfiguration(Collections.<String, Object>emptyMap(), createConfig(-0.5));
         assertEquals(Runtime.getRuntime().availableProcessors(), c.getMaxParallel());
 
-        p.put(ConfigurationConstants.PROP_MAX_PARALLEL, -2);
-        c = InternalQueueConfiguration.fromConfiguration(p);
+        c = InternalQueueConfiguration.fromConfiguration(Collections.<String, Object>emptyMap(), createConfig(-2));
         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);
+        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(Collections.<String, Object>emptyMap(), createConfig(new String[] {"a."}));
         assertTrue(c.isValid());
         assertNotNull(c.match("a/b"));
         assertNotNull(c.match("a/c"));
@@ -96,11 +156,7 @@ public class InternalQueueConfigurationTest {
     }
 
     @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);
+        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(Collections.<String, Object>emptyMap(), createConfig(new String[] {"a*"}));
         assertTrue(c.isValid());
         assertNotNull(c.match("a/b"));
         assertNotNull(c.match("a/c"));
@@ -111,11 +167,7 @@ public class InternalQueueConfigurationTest {
     }
 
     @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);
+        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(Collections.<String, Object>emptyMap(), createConfig(new String[] {"a"}));
         assertTrue(c.isValid());
         assertNull(c.match("a/b"));
         assertNull(c.match("a/c"));
@@ -126,11 +178,7 @@ public class InternalQueueConfigurationTest {
     }
 
     @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);
+        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(Collections.<String, Object>emptyMap(), createConfig(new String[] {"a."}, "test-queue-{0}"));
         assertTrue(c.isValid());
         final String b = "a/b";
         assertNotNull(c.match(b));
@@ -141,11 +189,7 @@ public class InternalQueueConfigurationTest {
     }
 
     @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);
+        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(Collections.<String, Object>emptyMap(), createConfig(new String[] {"a/."}));
         assertTrue(c.isValid());
         assertNotNull(c.match("a/b"));
         assertNotNull(c.match("a/c"));
@@ -156,11 +200,11 @@ public class InternalQueueConfigurationTest {
     }
 
     @org.junit.Test public void testTopicMatchersStarAndSlash() {
-        final Map<String, Object> p = new HashMap<String, Object>();
+        final Map<String, Object> p = new HashMap<>();
         p.put(ConfigurationConstants.PROP_TOPICS, new String[] {"a/*"});
         p.put(ConfigurationConstants.PROP_NAME, "test");
 
-        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(p);
+        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(Collections.<String, Object>emptyMap(), createConfig(new String[] {"a/*"}));
         assertTrue(c.isValid());
         assertNotNull(c.match("a/b"));
         assertNotNull(c.match("a/c"));
@@ -171,11 +215,7 @@ public class InternalQueueConfigurationTest {
     }
 
     @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);
+        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(Collections.<String, Object>emptyMap(), createConfig(new String[] {"a/."}, "test-queue-{0}"));
         assertTrue(c.isValid());
         final String b = "a/b";
         assertNotNull(c.match(b));
@@ -186,10 +226,10 @@ public class InternalQueueConfigurationTest {
     }
 
     @org.junit.Test public void testNoTopicMatchers() {
-        final Map<String, Object> p = new HashMap<String, Object>();
+        final Map<String, Object> p = new HashMap<>();
         p.put(ConfigurationConstants.PROP_NAME, "test");
 
-        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(p);
+        InternalQueueConfiguration c = InternalQueueConfiguration.fromConfiguration(Collections.<String, Object>emptyMap(), createConfig(null));
         assertFalse(c.isValid());
     }
 }
diff --git a/src/test/java/org/apache/sling/event/it/AbstractJobHandlingTest.java b/src/test/java/org/apache/sling/event/it/AbstractJobHandlingTest.java
index 3a77364..eb3de77 100644
--- a/src/test/java/org/apache/sling/event/it/AbstractJobHandlingTest.java
+++ b/src/test/java/org/apache/sling/event/it/AbstractJobHandlingTest.java
@@ -293,10 +293,10 @@ public abstract class AbstractJobHandlingTest {
     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>();
+        Dictionary<String, Object> p2 = new Hashtable<>();
         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);
+        p2.put("startup.delay", 1L);
         c2.update(p2);
 
         // SLING-5560 : since the above (re)config is now applied, we're safe
@@ -390,7 +390,7 @@ public abstract class AbstractJobHandlingTest {
      */
     protected ServiceRegistration<EventHandler> registerEventHandler(final String topic,
             final EventHandler handler) {
-        final Dictionary<String, Object> props = new Hashtable<String, Object>();
+        final Dictionary<String, Object> props = new Hashtable<>();
         props.put(EventConstants.EVENT_TOPIC, topic);
         final ServiceRegistration<EventHandler> reg = this.bc.registerService(EventHandler.class,
                 handler, props);
@@ -429,7 +429,7 @@ public abstract class AbstractJobHandlingTest {
     protected ServiceRegistration<JobConsumer> registerJobConsumer(final String topic,
             final JobConsumer handler) {
         long cc = this.getConsumerChangeCount();
-        final Dictionary<String, Object> props = new Hashtable<String, Object>();
+        final Dictionary<String, Object> props = new Hashtable<>();
         props.put(JobConsumer.PROPERTY_TOPICS, topic);
         final ServiceRegistration<JobConsumer> reg = this.bc.registerService(JobConsumer.class,
                 handler, props);
@@ -444,7 +444,7 @@ public abstract class AbstractJobHandlingTest {
     protected ServiceRegistration<JobExecutor> registerJobExecutor(final String topic,
             final JobExecutor handler) {
         long cc = this.getConsumerChangeCount();
-        final Dictionary<String, Object> props = new Hashtable<String, Object>();
+        final Dictionary<String, Object> props = new Hashtable<>();
         props.put(JobConsumer.PROPERTY_TOPICS, topic);
         final ServiceRegistration<JobExecutor> reg = this.bc.registerService(JobExecutor.class,
                 handler, props);

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

[sling-org-apache-sling-event] 22/26: SLING-7091 : Label and value are interchanged for queue type

Posted by ro...@apache.org.
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.git

commit ffbce1f1788276e6cc99c1712d390b641a89efe7
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Wed Aug 30 11:57:00 2017 +0000

    SLING-7091 : Label and value are interchanged for queue type
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1806689 13f79535-47bb-0310-9956-ffa450edef68
---
 .../event/impl/jobs/config/InternalQueueConfiguration.java | 14 +++++++-------
 .../event/impl/jobs/config/MainQueueConfiguration.java     |  6 +++---
 2 files changed, 10 insertions(+), 10 deletions(-)

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
index d01f77f..537dd05 100644
--- 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
@@ -63,18 +63,18 @@ public class InternalQueueConfiguration
 
         @AttributeDefinition(name = "Type",
               description="The queue type.",
-              options = {@Option(label="UNORDERED",value="Parallel"),
-                      @Option(label="ORDERED",value="Ordered"),
-                      @Option(label="TOPIC_ROUND_ROBIN",value="Topic Round Robin")})
+              options = {@Option(label="Parallel",value="UNORDERED"),
+                      @Option(label="Ordered",value="ORDERED"),
+                      @Option(label="Topic Round Robin",value="TOPIC_ROUND_ROBIN")})
         String queue_type() default "UNORDERED";
 
         @AttributeDefinition(
                  name="Priority",
                  description="The priority for the threads used by this queue. Default is norm.",
                  options = {
-                         @Option(label="NORM",value="Norm"),
-                         @Option(label="MIN",value="Min"),
-                         @Option(label="MAX",value="Max")
+                         @Option(label="Norm",value="NORM"),
+                         @Option(label="Min",value="MIN"),
+                         @Option(label="Max",value="MAX")
                  })
          String queue_priority() default ConfigurationConstants.DEFAULT_PRIORITY;
 
@@ -121,7 +121,7 @@ public class InternalQueueConfiguration
               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.")
          int service_ranking() default 0;
-     
+
          // Internal Name hint for web console.
          String webconsole_configurationFactory_nameHint() default "Queue: {" + ConfigurationConstants.PROP_NAME + "}";
 
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
index cbaf50b..3bcc120 100644
--- 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
@@ -53,9 +53,9 @@ public class MainQueueConfiguration {
                 name="Priority",
                 description="The priority for the threads used by this queue. Default is norm.",
                 options = {
-                        @Option(label="NORM",value="Norm"),
-                        @Option(label="MIN",value="Min"),
-                        @Option(label="MAX",value="Max")
+                        @Option(label="Norm",value="NORM"),
+                        @Option(label="Min",value="MIN"),
+                        @Option(label="Max",value="MAX")
                 })
         String queue_priority() default ConfigurationConstants.DEFAULT_PRIORITY;
 

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

[sling-org-apache-sling-event] 03/26: SLING-6739 : split of sling.event to sling.event.resource (and api). Use old project name / bundle symbolic name and version for compatibility

Posted by ro...@apache.org.
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.git

commit 47276aa93ad0f6d6c9ed494e0ed19c92037e7093
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Mon Apr 10 05:52:57 2017 +0000

    SLING-6739 : split of sling.event to sling.event.resource (and api). Use old project name / bundle symbolic name and version for compatibility
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1790780 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/pom.xml b/pom.xml
index 91bb496..641a79b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,9 +27,9 @@
         <relativePath />
     </parent>
 
-    <artifactId>org.apache.sling.event.resource</artifactId>
+    <artifactId>org.apache.sling.event</artifactId>
     <packaging>bundle</packaging>
-    <version>0.0.1-SNAPSHOT</version>
+    <version>4.2.3-SNAPSHOT</version>
 
     <name>Apache Sling Event Support</name>
     <description>
@@ -38,7 +38,7 @@
 
     <scm>
         <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/event/resource</connection>
-        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/event/resourc33</developerConnection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/event/resource</developerConnection>
         <url>http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/event/resource</url>
     </scm>
 

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

[sling-org-apache-sling-event] 25/26: [maven-release-plugin] prepare for next development iteration

Posted by ro...@apache.org.
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.git

commit e8073bdc8a209da1ae9bc807ce022f8d5fd7a84e
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Wed Sep 20 11:53:52 2017 +0000

    [maven-release-plugin] prepare for next development iteration
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1809007 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/pom.xml b/pom.xml
index 7665ba8..57880f8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -29,7 +29,7 @@
 
     <artifactId>org.apache.sling.event</artifactId>
     <packaging>bundle</packaging>
-    <version>4.2.8</version>
+    <version>4.2.9-SNAPSHOT</version>
 
     <name>Apache Sling Event Support</name>
     <description>
@@ -37,9 +37,9 @@
     </description>
 
     <scm>
-        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/tags/org.apache.sling.event-4.2.8</connection>
-        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/tags/org.apache.sling.event-4.2.8</developerConnection>
-        <url>http://svn.apache.org/viewvc/sling/tags/org.apache.sling.event-4.2.8</url>
+        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/event/resource</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/event/resource</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/event/resource</url>
     </scm>
 
     <properties>

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

[sling-org-apache-sling-event] 12/26: Update to pax exam 4.11.0

Posted by ro...@apache.org.
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.git

commit 696d90426ae801bcd5301b8269fd4867e01de1fe
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Mon Jun 5 14:12:04 2017 +0000

    Update to pax exam 4.11.0
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1797666 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/pom.xml b/pom.xml
index e420635..0b9dfd6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -44,8 +44,8 @@
 
     <properties>
         <site.jira.version.id>12315369</site.jira.version.id>
-        <exam.version>4.4.0</exam.version>
-        <url.version>2.4.5</url.version>
+        <exam.version>4.11.0</exam.version>
+        <url.version>2.5.2</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>

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