Repository: kagkarlsson/db-scheduler (java)

Issue:
SchedulerListener Implementation Not Receiving SchedulerEventType.UNRESOLVED_TASK Event
### Description
I am trying to listen for the SchedulerEventType.UNRESOLVED_TASK event using my custom SchedulerListener implementation. However, this specific event is never passed to the onSchedulerEvent(SchedulerEventType type) method. Other events are successfully passed, but UNRESOLVED_TASK is not.
The current setup works when using a StatsRegistry implementation, where the SchedulerStatsEvent.UNRESOLVED_TASK is passed to the register(SchedulerStatsEvent e) method. However, StatsBuilder has been deprecated in favor of SchedulerListener (reference: [db-scheduler SchedulerBuilder.java#L138](https://github.com/kagkarlsson/db-scheduler/blob/master/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/SchedulerBuilder.java#L138)).

### Expected Behavior
The SchedulerListener should receive the SchedulerEventType.UNRESOLVED_TASK event in the onSchedulerEvent method, similar to how StatsRegistry receives the corresponding event.

### Current Workaround
The StatsRegistry is currently being used as a workaround, but the goal is to have this functionality working with SchedulerListener.

### Steps to Reproduce
1. Implement a custom SchedulerListener.
2. Listen for the SchedulerEventType.UNRESOLVED_TASK event.
3. Observe that the event is not passed to the listener's method.

### Environment
Library Version: 15.3.0
Java Version: 21


Discussion:

Patch:
diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/SchedulerBuilder.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/SchedulerBuilder.java
--- a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/SchedulerBuilder.java
+++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/SchedulerBuilder.java
@@ -19,6 +19,7 @@
 
 import com.github.kagkarlsson.scheduler.event.ExecutionInterceptor;
 import com.github.kagkarlsson.scheduler.event.SchedulerListener;
+import com.github.kagkarlsson.scheduler.event.SchedulerListeners;
 import com.github.kagkarlsson.scheduler.jdbc.AutodetectJdbcCustomization;
 import com.github.kagkarlsson.scheduler.jdbc.JdbcCustomization;
 import com.github.kagkarlsson.scheduler.jdbc.JdbcTaskRepository;
@@ -241,7 +242,8 @@ public Scheduler build() {
       schedulerName = new SchedulerName.Hostname();
     }
 
-    final TaskResolver taskResolver = new TaskResolver(statsRegistry, clock, knownTasks);
+    final TaskResolver taskResolver =
+        new TaskResolver(new SchedulerListeners(schedulerListeners), clock, knownTasks);
     final JdbcCustomization jdbcCustomization =
         ofNullable(this.jdbcCustomization)
             .orElseGet(
diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/SchedulerClient.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/SchedulerClient.java
--- a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/SchedulerClient.java
+++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/SchedulerClient.java
@@ -24,7 +24,6 @@
 import com.github.kagkarlsson.scheduler.jdbc.JdbcCustomization;
 import com.github.kagkarlsson.scheduler.jdbc.JdbcTaskRepository;
 import com.github.kagkarlsson.scheduler.serializer.Serializer;
-import com.github.kagkarlsson.scheduler.stats.StatsRegistry;
 import com.github.kagkarlsson.scheduler.task.Execution;
 import com.github.kagkarlsson.scheduler.task.SchedulableInstance;
 import com.github.kagkarlsson.scheduler.task.ScheduledTaskInstance;
@@ -375,8 +374,8 @@ public Builder jdbcCustomization(JdbcCustomization jdbcCustomization) {
     }
 
     public SchedulerClient build() {
-      TaskResolver taskResolver = new TaskResolver(StatsRegistry.NOOP, knownTasks);
       final SystemClock clock = new SystemClock();
+      TaskResolver taskResolver = new TaskResolver(SchedulerListeners.NOOP, clock, knownTasks);
 
       final JdbcCustomization jdbcCustomization =
           ofNullable(this.jdbcCustomization)
diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/TaskResolver.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/TaskResolver.java
--- a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/TaskResolver.java
+++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/TaskResolver.java
@@ -15,12 +15,12 @@
 
 import static java.util.function.Function.identity;
 
-import com.github.kagkarlsson.scheduler.stats.StatsRegistry;
+import com.github.kagkarlsson.scheduler.event.SchedulerListener.SchedulerEventType;
+import com.github.kagkarlsson.scheduler.event.SchedulerListeners;
 import com.github.kagkarlsson.scheduler.task.Task;
 import java.time.Duration;
 import java.time.Instant;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -32,21 +32,14 @@
 @SuppressWarnings("rawtypes")
 public class TaskResolver {
   private static final Logger LOG = LoggerFactory.getLogger(TaskResolver.class);
-  private final StatsRegistry statsRegistry;
+  private final SchedulerListeners schedulerListeners;
   private final Clock clock;
   private final Map<String, Task> taskMap;
   private final Map<String, UnresolvedTask> unresolvedTasks = new ConcurrentHashMap<>();
 
-  public TaskResolver(StatsRegistry statsRegistry, Task<?>... knownTasks) {
-    this(statsRegistry, Arrays.asList(knownTasks));
-  }
-
-  public TaskResolver(StatsRegistry statsRegistry, List<Task<?>> knownTasks) {
-    this(statsRegistry, new SystemClock(), knownTasks);
-  }
-
-  public TaskResolver(StatsRegistry statsRegistry, Clock clock, List<Task<?>> knownTasks) {
-    this.statsRegistry = statsRegistry;
+  public TaskResolver(
+      SchedulerListeners schedulerListeners, Clock clock, List<Task<?>> knownTasks) {
+    this.schedulerListeners = schedulerListeners;
     this.clock = clock;
     this.taskMap = knownTasks.stream().collect(Collectors.toMap(Task::getName, identity()));
   }
@@ -59,7 +52,7 @@ public Optional<Task> resolve(String taskName, boolean addUnresolvedToExclusionF
     Task task = taskMap.get(taskName);
     if (task == null && addUnresolvedToExclusionFilter) {
       addUnresolved(taskName);
-      statsRegistry.register(StatsRegistry.SchedulerStatsEvent.UNRESOLVED_TASK);
+      schedulerListeners.onSchedulerEvent(SchedulerEventType.UNRESOLVED_TASK);
       LOG.info(
           "Found execution with unknown task-name '{}'. Adding it to the list of known unresolved task-names.",
           taskName);
diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/event/SchedulerListeners.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/event/SchedulerListeners.java
--- a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/event/SchedulerListeners.java
+++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/event/SchedulerListeners.java
@@ -13,6 +13,8 @@
  */
 package com.github.kagkarlsson.scheduler.event;
 
+import static java.util.Arrays.asList;
+
 import com.github.kagkarlsson.scheduler.CurrentlyExecuting;
 import com.github.kagkarlsson.scheduler.event.SchedulerListener.CandidateEventType;
 import com.github.kagkarlsson.scheduler.event.SchedulerListener.SchedulerEventType;
@@ -30,6 +32,10 @@ public class SchedulerListeners implements SchedulerListener {
 
   private final List<SchedulerListener> schedulerListeners;
 
+  public SchedulerListeners(SchedulerListener... schedulerListeners) {
+    this(asList(schedulerListeners));
+  }
+
   public SchedulerListeners(List<SchedulerListener> schedulerListeners) {
     this.schedulerListeners = schedulerListeners;
   }


Test patch:
diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/testhelper/TestHelper.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/testhelper/TestHelper.java
--- a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/testhelper/TestHelper.java
+++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/testhelper/TestHelper.java
@@ -17,6 +17,8 @@
 import com.github.kagkarlsson.scheduler.SchedulerBuilder;
 import com.github.kagkarlsson.scheduler.SchedulerName;
 import com.github.kagkarlsson.scheduler.TaskResolver;
+import com.github.kagkarlsson.scheduler.event.SchedulerListener;
+import com.github.kagkarlsson.scheduler.event.SchedulerListeners;
 import com.github.kagkarlsson.scheduler.jdbc.DefaultJdbcCustomization;
 import com.github.kagkarlsson.scheduler.jdbc.JdbcTaskRepository;
 import com.github.kagkarlsson.scheduler.logging.LogLevel;
@@ -44,6 +46,7 @@ public static ManualSchedulerBuilder createManualScheduler(
   }
 
   public static class ManualSchedulerBuilder extends SchedulerBuilder {
+
     private SettableClock clock;
 
     public ManualSchedulerBuilder(DataSource dataSource, List<Task<?>> knownTasks) {
@@ -65,13 +68,19 @@ public ManualSchedulerBuilder statsRegistry(StatsRegistry statsRegistry) {
       return this;
     }
 
+    public ManualSchedulerBuilder addSchedulerListener(SchedulerListener listener) {
+      this.schedulerListeners.add(listener);
+      return this;
+    }
+
     public ManualSchedulerBuilder pollingStrategy(PollingStrategyConfig pollingStrategyConfig) {
       super.pollingStrategyConfig = pollingStrategyConfig;
       return this;
     }
 
     public ManualScheduler build() {
-      final TaskResolver taskResolver = new TaskResolver(statsRegistry, clock, knownTasks);
+      final TaskResolver taskResolver =
+          new TaskResolver(new SchedulerListeners(schedulerListeners), clock, knownTasks);
       final JdbcTaskRepository schedulerTaskRepository =
           new JdbcTaskRepository(
               dataSource,
diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/CustomTableNameTest.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/CustomTableNameTest.java
--- a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/CustomTableNameTest.java
+++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/CustomTableNameTest.java
@@ -4,8 +4,8 @@
 
 import com.github.kagkarlsson.jdbc.JdbcRunner;
 import com.github.kagkarlsson.jdbc.RowMapper;
+import com.github.kagkarlsson.scheduler.event.SchedulerListeners;
 import com.github.kagkarlsson.scheduler.jdbc.JdbcTaskRepository;
-import com.github.kagkarlsson.scheduler.stats.StatsRegistry;
 import com.github.kagkarlsson.scheduler.task.SchedulableTaskInstance;
 import com.github.kagkarlsson.scheduler.task.Task;
 import com.github.kagkarlsson.scheduler.task.TaskInstance;
@@ -39,7 +39,7 @@ public void setUp() {
             DB.getDataSource(),
             false,
             CUSTOM_TABLENAME,
-            new TaskResolver(StatsRegistry.NOOP, knownTasks),
+            new TaskResolver(SchedulerListeners.NOOP, new SystemClock(), knownTasks),
             new SchedulerName.Fixed(SCHEDULER_NAME),
             false,
             new SystemClock());
diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/DeadExecutionsTest.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/DeadExecutionsTest.java
--- a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/DeadExecutionsTest.java
+++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/DeadExecutionsTest.java
@@ -7,8 +7,8 @@
 import static org.hamcrest.Matchers.nullValue;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import com.github.kagkarlsson.scheduler.event.SchedulerListeners;
 import com.github.kagkarlsson.scheduler.jdbc.JdbcTaskRepository;
-import com.github.kagkarlsson.scheduler.stats.StatsRegistry;
 import com.github.kagkarlsson.scheduler.task.*;
 import com.github.kagkarlsson.scheduler.task.helper.OneTimeTask;
 import com.github.kagkarlsson.scheduler.testhelper.ManualScheduler;
@@ -47,7 +47,9 @@ public void setUp() {
         new NonCompletingTask<>(
             "NonCompleting", Void.class, nonCompletingExecutionHandler, deadExecutionHandler);
 
-    TaskResolver taskResolver = new TaskResolver(StatsRegistry.NOOP, oneTimeTask, nonCompleting);
+    TaskResolver taskResolver =
+        new TaskResolver(
+            SchedulerListeners.NOOP, settableClock, List.of(oneTimeTask, nonCompleting));
 
     jdbcTaskRepository =
         new JdbcTaskRepository(
diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/JdbcTaskRepositoryTest.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/JdbcTaskRepositoryTest.java
--- a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/JdbcTaskRepositoryTest.java
+++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/JdbcTaskRepositoryTest.java
@@ -16,11 +16,12 @@
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import co.unruly.matchers.OptionalMatchers;
+import com.github.kagkarlsson.scheduler.event.SchedulerListener.SchedulerEventType;
+import com.github.kagkarlsson.scheduler.event.SchedulerListeners;
 import com.github.kagkarlsson.scheduler.exceptions.FailedToScheduleBatchException;
-import com.github.kagkarlsson.scheduler.helper.TestableRegistry;
+import com.github.kagkarlsson.scheduler.helper.TestableListener;
 import com.github.kagkarlsson.scheduler.helper.TimeHelper;
 import com.github.kagkarlsson.scheduler.jdbc.JdbcTaskRepository;
-import com.github.kagkarlsson.scheduler.stats.StatsRegistry.SchedulerStatsEvent;
 import com.github.kagkarlsson.scheduler.task.*;
 import com.github.kagkarlsson.scheduler.task.helper.OneTimeTask;
 import java.time.Duration;
@@ -50,7 +51,7 @@ public class JdbcTaskRepositoryTest {
   private OneTimeTask<Void> alternativeOneTimeTask;
   private OneTimeTask<Integer> oneTimeTaskWithData;
   private TaskResolver taskResolver;
-  private TestableRegistry testableRegistry;
+  private TestableListener testableListener;
 
   @BeforeEach
   public void setUp() {
@@ -63,8 +64,9 @@ public void setUp() {
     knownTasks.add(oneTimeTask);
     knownTasks.add(oneTimeTaskWithData);
     knownTasks.add(alternativeOneTimeTask);
-    testableRegistry = new TestableRegistry(true, Collections.emptyList());
-    taskResolver = new TaskResolver(testableRegistry, knownTasks);
+    testableListener = new TestableListener(true, Collections.emptyList());
+    taskResolver =
+        new TaskResolver(new SchedulerListeners(testableListener), new SystemClock(), knownTasks);
     taskRepository = createRepository(taskResolver, false);
   }
 
@@ -214,13 +216,13 @@ public void get_due_should_not_include_previously_unresolved() {
         new SchedulableTaskInstance<>(unresolved1.instance("id"), now));
     assertThat(taskRepository.getDue(now, POLLING_LIMIT), hasSize(0));
     assertThat(taskResolver.getUnresolved(), hasSize(1));
-    assertEquals(1, testableRegistry.getCount(SchedulerStatsEvent.UNRESOLVED_TASK));
+    assertEquals(1, testableListener.getCount(SchedulerEventType.UNRESOLVED_TASK));
 
     assertThat(taskRepository.getDue(now, POLLING_LIMIT), hasSize(0));
     assertThat(taskResolver.getUnresolved(), hasSize(1));
     assertEquals(
         1,
-        testableRegistry.getCount(SchedulerStatsEvent.UNRESOLVED_TASK),
+        testableListener.getCount(SchedulerEventType.UNRESOLVED_TASK),
         "Execution should not have have been in the ResultSet");
 
     // 1, 2
@@ -228,14 +230,14 @@ public void get_due_should_not_include_previously_unresolved() {
         new SchedulableTaskInstance<>(unresolved2.instance("id"), now));
     assertThat(taskRepository.getDue(now, POLLING_LIMIT), hasSize(0));
     assertThat(taskResolver.getUnresolved(), hasSize(2));
-    assertEquals(2, testableRegistry.getCount(SchedulerStatsEvent.UNRESOLVED_TASK));
+    assertEquals(2, testableListener.getCount(SchedulerEventType.UNRESOLVED_TASK));
 
     // 1, 2, 3
     taskRepository.createIfNotExists(
         new SchedulableTaskInstance<>(unresolved3.instance("id"), now));
     assertThat(taskRepository.getDue(now, POLLING_LIMIT), hasSize(0));
     assertThat(taskResolver.getUnresolved(), hasSize(3));
-    assertEquals(3, testableRegistry.getCount(SchedulerStatsEvent.UNRESOLVED_TASK));
+    assertEquals(3, testableListener.getCount(SchedulerEventType.UNRESOLVED_TASK));
   }
 
   @Test
@@ -427,19 +429,20 @@ public void get_dead_executions_should_not_include_previously_unresolved() {
     final Instant timeDied = now.minus(Duration.ofDays(5));
     createDeadExecution(oneTimeTask.instance("id1"), timeDied);
 
-    TaskResolver taskResolverMissingTask = new TaskResolver(testableRegistry);
+    TaskResolver taskResolverMissingTask =
+        new TaskResolver(new SchedulerListeners(testableListener), new SystemClock(), List.of());
     JdbcTaskRepository repoMissingTask = createRepository(taskResolverMissingTask, false);
 
     assertThat(taskResolverMissingTask.getUnresolved(), hasSize(0));
-    assertEquals(0, testableRegistry.getCount(SchedulerStatsEvent.UNRESOLVED_TASK));
+    assertEquals(0, testableListener.getCount(SchedulerEventType.UNRESOLVED_TASK));
 
     assertThat(repoMissingTask.getDeadExecutions(timeDied), hasSize(0));
     assertThat(taskResolverMissingTask.getUnresolved(), hasSize(1));
-    assertEquals(1, testableRegistry.getCount(SchedulerStatsEvent.UNRESOLVED_TASK));
+    assertEquals(1, testableListener.getCount(SchedulerEventType.UNRESOLVED_TASK));
 
     assertThat(repoMissingTask.getDeadExecutions(timeDied), hasSize(0));
     assertThat(taskResolverMissingTask.getUnresolved(), hasSize(1));
-    assertEquals(1, testableRegistry.getCount(SchedulerStatsEvent.UNRESOLVED_TASK));
+    assertEquals(1, testableListener.getCount(SchedulerEventType.UNRESOLVED_TASK));
   }
 
   @Test
@@ -488,7 +491,7 @@ public void lockAndGetDue_should_not_include_previously_unresolved() {
     assertThat(taskRepository.lockAndGetDue(now, POLLING_LIMIT), hasSize(0));
     assertThat(taskRepository.lockAndGetDue(now, POLLING_LIMIT), hasSize(0));
     assertThat(taskResolver.getUnresolved(), hasSize(1));
-    assertEquals(1, testableRegistry.getCount(SchedulerStatsEvent.UNRESOLVED_TASK));
+    assertEquals(1, testableListener.getCount(SchedulerEventType.UNRESOLVED_TASK));
   }
 
   @Test
diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/TaskResolverTest.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/TaskResolverTest.java
new file mode 100644
--- /dev/null
+++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/TaskResolverTest.java
@@ -0,0 +1,30 @@
+package com.github.kagkarlsson.scheduler;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import com.github.kagkarlsson.scheduler.event.SchedulerListener;
+import com.github.kagkarlsson.scheduler.event.SchedulerListener.SchedulerEventType;
+import com.github.kagkarlsson.scheduler.event.SchedulerListeners;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class TaskResolverTest {
+
+  @Mock SchedulerListener mockScheduleListener;
+
+  @Test
+  void shouldProduceUnresolvedTaskEventWhenTheTaskResolverIsUnableToResolveTheTask() {
+    TaskResolver taskResolver =
+        new TaskResolver(
+            new SchedulerListeners(mockScheduleListener), new SystemClock(), List.of());
+
+    taskResolver.resolve("unknown-task");
+
+    verify(mockScheduleListener, times(1)).onSchedulerEvent(SchedulerEventType.UNRESOLVED_TASK);
+  }
+}
diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/compatibility/CompatibilityTest.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/compatibility/CompatibilityTest.java
--- a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/compatibility/CompatibilityTest.java
+++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/compatibility/CompatibilityTest.java
@@ -24,12 +24,12 @@
 import com.github.kagkarlsson.scheduler.TaskResolver;
 import com.github.kagkarlsson.scheduler.TestTasks;
 import com.github.kagkarlsson.scheduler.TestTasks.DoNothingHandler;
+import com.github.kagkarlsson.scheduler.event.SchedulerListeners;
 import com.github.kagkarlsson.scheduler.helper.ExecutionCompletedCondition;
-import com.github.kagkarlsson.scheduler.helper.TestableRegistry;
+import com.github.kagkarlsson.scheduler.helper.TestableListener;
 import com.github.kagkarlsson.scheduler.jdbc.AutodetectJdbcCustomization;
 import com.github.kagkarlsson.scheduler.jdbc.JdbcCustomization;
 import com.github.kagkarlsson.scheduler.jdbc.JdbcTaskRepository;
-import com.github.kagkarlsson.scheduler.stats.StatsRegistry;
 import com.github.kagkarlsson.scheduler.task.Execution;
 import com.github.kagkarlsson.scheduler.task.SchedulableInstance;
 import com.github.kagkarlsson.scheduler.task.SchedulableTaskInstance;
@@ -76,7 +76,7 @@ public abstract class CompatibilityTest {
   private RecurringTask<Void> recurring;
   private RecurringTask<Integer> recurringWithData;
 
-  private TestableRegistry testableRegistry;
+  private TestableListener testableListener;
   private ExecutionCompletedCondition completed12Condition;
 
   public CompatibilityTest(boolean supportsSelectForUpdate, boolean shouldHavePersistentTimezone) {
@@ -114,7 +114,7 @@ public void setUp() {
             new DoNothingHandler<>());
 
     completed12Condition = new ExecutionCompletedCondition(12);
-    testableRegistry = new TestableRegistry(false, singletonList(completed12Condition));
+    testableListener = new TestableListener(false, singletonList(completed12Condition));
   }
 
   @AfterEach
@@ -130,7 +130,7 @@ public void test_compatibility_fetch_and_lock_on_execute() {
             .pollUsingFetchAndLockOnExecute(0, UPPER_LIMIT_FRACTION_OF_THREADS_FOR_FETCH)
             .heartbeatInterval(Duration.ofMillis(100))
             .schedulerName(new SchedulerName.Fixed("test"))
-            .statsRegistry(testableRegistry)
+            .addSchedulerListener(testableListener)
             .commitWhenAutocommitDisabled(commitWhenAutocommitDisabled())
             .build();
     stopScheduler.register(scheduler);
@@ -150,7 +150,7 @@ public void test_compatibility_lock_and_fetch() {
             .pollUsingLockAndFetch(0.5, 1.0)
             .heartbeatInterval(Duration.ofMillis(500))
             .schedulerName(new SchedulerName.Fixed("test"))
-            .statsRegistry(testableRegistry)
+            .addSchedulerListener(testableListener)
             .commitWhenAutocommitDisabled(commitWhenAutocommitDisabled());
 
     getJdbcCustomization().ifPresent(builder::jdbcCustomization);
@@ -175,7 +175,7 @@ private void testCompatibilityForSchedulerConfiguration(Scheduler scheduler) {
           scheduler.start();
           completed12Condition.waitFor();
           scheduler.stop();
-          testableRegistry.assertNoFailures();
+          testableListener.assertNoFailures();
 
           assertThat(delayingHandlerRecurring.timesExecuted.get(), greaterThan(10));
           assertThat(delayingHandlerOneTime.timesExecuted.get(), is(1));
@@ -270,7 +270,8 @@ private void assertCorrectOrder(
   }
 
   private JdbcTaskRepository createJdbcTaskRepository(boolean orderByPriority) {
-    TaskResolver taskResolver = new TaskResolver(StatsRegistry.NOOP, new ArrayList<>());
+    SystemClock clock = new SystemClock();
+    TaskResolver taskResolver = new TaskResolver(SchedulerListeners.NOOP, clock, new ArrayList<>());
     taskResolver.addTask(oneTime);
 
     DataSource dataSource = getDataSource();
@@ -285,7 +286,7 @@ private JdbcTaskRepository createJdbcTaskRepository(boolean orderByPriority) {
             taskResolver,
             new SchedulerName.Fixed("scheduler1"),
             orderByPriority,
-            new SystemClock());
+            clock);
     return jdbcTaskRepository;
   }
 
@@ -295,7 +296,8 @@ public void test_has_peristent_time_zone() {
       return;
     }
 
-    TaskResolver defaultTaskResolver = new TaskResolver(StatsRegistry.NOOP, new ArrayList<>());
+    TaskResolver defaultTaskResolver =
+        new TaskResolver(SchedulerListeners.NOOP, new SystemClock(), List.of());
     defaultTaskResolver.addTask(oneTime);
 
     JdbcTaskRepository winterTaskRepo = createRepositoryForForZone(defaultTaskResolver, "CET");
@@ -312,7 +314,8 @@ public void test_has_peristent_time_zone() {
   }
 
   private void doJDBCRepositoryCompatibilityTestUsingData(String data) {
-    TaskResolver taskResolver = new TaskResolver(StatsRegistry.NOOP, new ArrayList<>());
+    SystemClock clock = new SystemClock();
+    TaskResolver taskResolver = new TaskResolver(SchedulerListeners.NOOP, clock, List.of());
     taskResolver.addTask(oneTime);
 
     DataSource dataSource = getDataSource();
@@ -324,7 +327,7 @@ private void doJDBCRepositoryCompatibilityTestUsingData(String data) {
             taskResolver,
             new SchedulerName.Fixed("scheduler1"),
             false,
-            new SystemClock());
+            clock);
 
     final JdbcTaskRepository jdbcTaskRepositoryWithPriority =
         new JdbcTaskRepository(
@@ -334,7 +337,7 @@ private void doJDBCRepositoryCompatibilityTestUsingData(String data) {
             taskResolver,
             new SchedulerName.Fixed("scheduler1"),
             true,
-            new SystemClock());
+            clock);
 
     final Instant now = truncatedInstantNow();
 
@@ -376,7 +379,8 @@ private void doJDBCRepositoryCompatibilityTestUsingData(String data) {
 
   @Test
   public void test_jdbc_repository_compatibility_set_data() {
-    TaskResolver taskResolver = new TaskResolver(StatsRegistry.NOOP, new ArrayList<>());
+    SystemClock clock = new SystemClock();
+    TaskResolver taskResolver = new TaskResolver(SchedulerListeners.NOOP, clock, List.of());
     taskResolver.addTask(recurringWithData);
 
     DataSource dataSource = getDataSource();
@@ -388,7 +392,7 @@ public void test_jdbc_repository_compatibility_set_data() {
             taskResolver,
             new SchedulerName.Fixed("scheduler1"),
             false,
-            new SystemClock());
+            clock);
 
     final Instant now = truncatedInstantNow();
 
@@ -429,7 +433,8 @@ public void test_jdbc_customization_supports_timestamps_in_utc() {
         "Running  test_jdbc_customization_supports_timestamps_in_utc for: "
             + jdbcCustomization.get().getName());
 
-    TaskResolver defaultTaskResolver = new TaskResolver(StatsRegistry.NOOP, new ArrayList<>());
+    TaskResolver defaultTaskResolver =
+        new TaskResolver(SchedulerListeners.NOOP, new SystemClock(), List.of());
     defaultTaskResolver.addTask(oneTime);
 
     JdbcTaskRepository taskRepo =
diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/DeadExecutionTest.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/DeadExecutionTest.java
--- a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/DeadExecutionTest.java
+++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/DeadExecutionTest.java
@@ -1,13 +1,13 @@
 package com.github.kagkarlsson.scheduler.functional;
 
-import static com.github.kagkarlsson.scheduler.stats.StatsRegistry.SchedulerStatsEvent;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import com.github.kagkarlsson.scheduler.EmbeddedPostgresqlExtension;
 import com.github.kagkarlsson.scheduler.Scheduler;
 import com.github.kagkarlsson.scheduler.SchedulerName;
 import com.github.kagkarlsson.scheduler.StopSchedulerExtension;
-import com.github.kagkarlsson.scheduler.helper.TestableRegistry;
+import com.github.kagkarlsson.scheduler.event.SchedulerListener.SchedulerEventType;
+import com.github.kagkarlsson.scheduler.helper.TestableListener;
 import com.github.kagkarlsson.scheduler.task.CompletionHandler;
 import com.github.kagkarlsson.scheduler.task.ExecutionComplete;
 import com.github.kagkarlsson.scheduler.task.ExecutionOperations;
@@ -44,17 +44,17 @@ public void complete(
                             }
                           });
 
-          TestableRegistry.Condition completedCondition = TestableRegistry.Conditions.completed(2);
+          TestableListener.Condition completedCondition = TestableListener.Conditions.completed(2);
 
-          TestableRegistry registry =
-              TestableRegistry.create().waitConditions(completedCondition).build();
+          TestableListener listener =
+              TestableListener.create().waitConditions(completedCondition).build();
 
           Scheduler scheduler =
               Scheduler.create(postgres.getDataSource(), customTask)
                   .pollingInterval(Duration.ofMillis(100))
                   .heartbeatInterval(Duration.ofMillis(100))
                   .schedulerName(new SchedulerName.Fixed("test"))
-                  .statsRegistry(registry)
+                  .addSchedulerListener(listener)
                   .build();
           stopScheduler.register(scheduler);
 
@@ -62,7 +62,7 @@ public void complete(
           scheduler.start();
           completedCondition.waitFor();
 
-          assertEquals(registry.getCount(SchedulerStatsEvent.DEAD_EXECUTION), 1);
+          assertEquals(listener.getCount(SchedulerEventType.DEAD_EXECUTION), 1);
         });
   }
 }
diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/DeleteUnresolvedTest.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/DeleteUnresolvedTest.java
--- a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/DeleteUnresolvedTest.java
+++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/DeleteUnresolvedTest.java
@@ -5,8 +5,8 @@
 import com.github.kagkarlsson.scheduler.DbUtils;
 import com.github.kagkarlsson.scheduler.EmbeddedPostgresqlExtension;
 import com.github.kagkarlsson.scheduler.TestTasks;
-import com.github.kagkarlsson.scheduler.helper.TestableRegistry;
-import com.github.kagkarlsson.scheduler.stats.StatsRegistry;
+import com.github.kagkarlsson.scheduler.event.SchedulerListener.SchedulerEventType;
+import com.github.kagkarlsson.scheduler.helper.TestableListener;
 import com.github.kagkarlsson.scheduler.task.helper.OneTimeTask;
 import com.github.kagkarlsson.scheduler.task.helper.Tasks;
 import com.github.kagkarlsson.scheduler.testhelper.ManualScheduler;
@@ -43,19 +43,19 @@ public void should_delete_executions_with_old_unresolved_tasknames() {
 
     OneTimeTask<Void> onetime = Tasks.oneTime("onetime").execute(TestTasks.DO_NOTHING);
 
-    TestableRegistry testableRegistry = new TestableRegistry(false, Collections.emptyList());
+    TestableListener testableListener = new TestableListener(false, Collections.emptyList());
     // Missing task with name 'onetime'
     ManualScheduler scheduler =
         TestHelper.createManualScheduler(postgres.getDataSource())
             .clock(clock)
-            .statsRegistry(testableRegistry)
+            .addSchedulerListener(testableListener)
             .build();
 
     scheduler.schedule(onetime.instance("id1"), clock.now());
-    assertEquals(0, testableRegistry.getCount(StatsRegistry.SchedulerStatsEvent.UNRESOLVED_TASK));
+    assertEquals(0, testableListener.getCount(SchedulerEventType.UNRESOLVED_TASK));
 
     scheduler.runAnyDueExecutions();
-    assertEquals(1, testableRegistry.getCount(StatsRegistry.SchedulerStatsEvent.UNRESOLVED_TASK));
+    assertEquals(1, testableListener.getCount(SchedulerEventType.UNRESOLVED_TASK));
 
     assertEquals(1, DbUtils.countExecutions(postgres.getDataSource()));
 
diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/DetectStaleHeartbeatTest.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/DetectStaleHeartbeatTest.java
--- a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/DetectStaleHeartbeatTest.java
+++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/DetectStaleHeartbeatTest.java
@@ -13,7 +13,7 @@
 import com.github.kagkarlsson.scheduler.SchedulerName;
 import com.github.kagkarlsson.scheduler.StopSchedulerExtension;
 import com.github.kagkarlsson.scheduler.TestTasks.PausingHandler;
-import com.github.kagkarlsson.scheduler.helper.TestableRegistry;
+import com.github.kagkarlsson.scheduler.helper.TestableListener;
 import com.github.kagkarlsson.scheduler.task.DeadExecutionHandler.CancelDeadExecution;
 import com.github.kagkarlsson.scheduler.task.helper.OneTimeTask;
 import com.github.kagkarlsson.scheduler.task.helper.Tasks;
@@ -43,12 +43,12 @@ public void test_dead_execution() throws InterruptedException {
             .onDeadExecution(new CancelDeadExecution<>())
             .execute(handler);
 
-    TestableRegistry.Condition ranUpdateHeartbeats =
-        TestableRegistry.Conditions.ranUpdateHeartbeats(3);
-    TestableRegistry.Condition ranExecuteDue = TestableRegistry.Conditions.ranExecuteDue(1);
+    TestableListener.Condition ranUpdateHeartbeats =
+        TestableListener.Conditions.ranUpdateHeartbeats(3);
+    TestableListener.Condition ranExecuteDue = TestableListener.Conditions.ranExecuteDue(1);
 
-    TestableRegistry registry =
-        TestableRegistry.create().waitConditions(ranUpdateHeartbeats, ranExecuteDue).build();
+    TestableListener listener =
+        TestableListener.create().waitConditions(ranUpdateHeartbeats, ranExecuteDue).build();
 
     Scheduler scheduler =
         Scheduler.create(postgres.getDataSource(), customTask)
@@ -56,7 +56,7 @@ public void test_dead_execution() throws InterruptedException {
             .heartbeatInterval(Duration.ofMillis(30))
             .missedHeartbeatsLimit(10)
             .schedulerName(new SchedulerName.Fixed("test"))
-            .statsRegistry(registry)
+            .addSchedulerListener(listener)
             .build();
     stopScheduler.register(scheduler);
 
diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/ExecutorPoolTest.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/ExecutorPoolTest.java
--- a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/ExecutorPoolTest.java
+++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/ExecutorPoolTest.java
@@ -10,7 +10,7 @@
 import com.github.kagkarlsson.scheduler.SchedulerName;
 import com.github.kagkarlsson.scheduler.StopSchedulerExtension;
 import com.github.kagkarlsson.scheduler.TestTasks;
-import com.github.kagkarlsson.scheduler.helper.TestableRegistry;
+import com.github.kagkarlsson.scheduler.helper.TestableListener;
 import com.github.kagkarlsson.scheduler.task.ExecutionComplete;
 import com.github.kagkarlsson.scheduler.task.helper.OneTimeTask;
 import com.github.kagkarlsson.scheduler.testhelper.SettableClock;
@@ -74,15 +74,15 @@ public void test_execute_until_none_left_high_volume() {
   private void testExecuteUntilNoneLeft(int pollingLimit, int threads, int executionsToRun) {
     Instant now = Instant.now();
     OneTimeTask<Void> task = TestTasks.oneTime("onetime-a", Void.class, TestTasks.DO_NOTHING);
-    TestableRegistry.Condition condition = TestableRegistry.Conditions.completed(executionsToRun);
-    TestableRegistry registry = TestableRegistry.create().waitConditions(condition).build();
+    TestableListener.Condition condition = TestableListener.Conditions.completed(executionsToRun);
+    TestableListener listener = TestableListener.create().waitConditions(condition).build();
 
     Scheduler scheduler =
         Scheduler.create(postgres.getDataSource(), task)
             .threads(threads)
             .pollingInterval(Duration.ofMinutes(1))
             .schedulerName(new SchedulerName.Fixed("test"))
-            .statsRegistry(registry)
+            .addSchedulerListener(listener)
             .build();
     stopScheduler.register(scheduler);
 
@@ -95,7 +95,7 @@ private void testExecuteUntilNoneLeft(int pollingLimit, int threads, int executi
           scheduler.start();
           condition.waitFor();
 
-          List<ExecutionComplete> completed = registry.getCompleted();
+          List<ExecutionComplete> completed = listener.getCompleted();
           assertThat(completed, hasSize(executionsToRun));
           completed.forEach(
               e -> {
@@ -104,7 +104,7 @@ private void testExecuteUntilNoneLeft(int pollingLimit, int threads, int executi
                 assertThat(
                     durationUntilExecuted, TimeMatchers.shorterThan(Duration.ofMillis(1100)));
               });
-          registry.assertNoFailures();
+          listener.assertNoFailures();
         },
         waitingForConditionTimedOut(scheduler));
   }
diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/ImmediateExecutionTest.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/ImmediateExecutionTest.java
--- a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/ImmediateExecutionTest.java
+++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/ImmediateExecutionTest.java
@@ -7,7 +7,7 @@
 
 import co.unruly.matchers.TimeMatchers;
 import com.github.kagkarlsson.scheduler.*;
-import com.github.kagkarlsson.scheduler.helper.TestableRegistry;
+import com.github.kagkarlsson.scheduler.helper.TestableListener;
 import com.github.kagkarlsson.scheduler.task.*;
 import com.github.kagkarlsson.scheduler.task.helper.CustomTask;
 import com.github.kagkarlsson.scheduler.task.helper.OneTimeTask;
@@ -42,22 +42,22 @@ public void test_immediate_execution() {
         () -> {
           Instant now = Instant.now();
           OneTimeTask<Void> task = TestTasks.oneTime("onetime-a", Void.class, TestTasks.DO_NOTHING);
-          TestableRegistry.Condition completedCondition = TestableRegistry.Conditions.completed(1);
-          TestableRegistry.Condition executeDueCondition =
-              TestableRegistry.Conditions.ranExecuteDue(1);
+          TestableListener.Condition completedCondition = TestableListener.Conditions.completed(1);
+          TestableListener.Condition executeDueCondition =
+              TestableListener.Conditions.ranExecuteDue(1);
 
-          TestableRegistry registry =
-              TestableRegistry.create()
+          TestableListener listener =
+              TestableListener.create()
                   .waitConditions(executeDueCondition, completedCondition)
                   .build();
 
-          Scheduler scheduler = createAndStartScheduler(task, registry);
+          Scheduler scheduler = createAndStartScheduler(task, listener);
           executeDueCondition.waitFor();
 
           scheduler.schedule(task.instance("1"), clock.now());
           completedCondition.waitFor();
 
-          List<ExecutionComplete> completed = registry.getCompleted();
+          List<ExecutionComplete> completed = listener.getCompleted();
           assertThat(completed, hasSize(1));
           completed.forEach(
               e -> {
@@ -65,7 +65,7 @@ public void test_immediate_execution() {
                 Duration durationUntilExecuted = Duration.between(now, e.getTimeDone());
                 assertThat(durationUntilExecuted, TimeMatchers.shorterThan(Duration.ofSeconds(1)));
               });
-          registry.assertNoFailures();
+          listener.assertNoFailures();
         });
   }
 
@@ -87,22 +87,22 @@ public void test_immediate_execution_by_completion_handler() {
                         }
                       });
 
-          TestableRegistry.Condition completedCondition = TestableRegistry.Conditions.completed(2);
-          TestableRegistry.Condition executeDueCondition =
-              TestableRegistry.Conditions.ranExecuteDue(1);
+          TestableListener.Condition completedCondition = TestableListener.Conditions.completed(2);
+          TestableListener.Condition executeDueCondition =
+              TestableListener.Conditions.ranExecuteDue(1);
 
-          TestableRegistry registry =
-              TestableRegistry.create()
+          TestableListener listener =
+              TestableListener.create()
                   .waitConditions(executeDueCondition, completedCondition)
                   .build();
 
-          Scheduler scheduler = createAndStartScheduler(task, registry);
+          Scheduler scheduler = createAndStartScheduler(task, listener);
           executeDueCondition.waitFor();
 
           scheduler.schedule(task.instance("id1", 1), clock.now());
           completedCondition.waitFor();
 
-          List<ExecutionComplete> completed = registry.getCompleted();
+          List<ExecutionComplete> completed = listener.getCompleted();
           assertThat(completed, hasSize(2));
           completed.forEach(
               e -> {
@@ -111,17 +111,17 @@ public void test_immediate_execution_by_completion_handler() {
                 assertThat(durationUntilExecuted, TimeMatchers.shorterThan(Duration.ofSeconds(1)));
               });
           assertEquals(scheduler.getScheduledExecutions().size(), 0);
-          registry.assertNoFailures();
+          listener.assertNoFailures();
         });
   }
 
-  private Scheduler createAndStartScheduler(Task task, TestableRegistry registry) {
+  private Scheduler createAndStartScheduler(Task task, TestableListener listener) {
     Scheduler scheduler =
         Scheduler.create(postgres.getDataSource(), task)
             .pollingInterval(Duration.ofMinutes(1))
             .enableImmediateExecution()
             .schedulerName(new SchedulerName.Fixed("test"))
-            .statsRegistry(registry)
+            .addSchedulerListener(listener)
             .build();
     stopScheduler.register(scheduler);
 
diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/PriorityExecutionTest.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/PriorityExecutionTest.java
--- a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/PriorityExecutionTest.java
+++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/functional/PriorityExecutionTest.java
@@ -12,7 +12,7 @@
 import com.github.kagkarlsson.scheduler.SchedulerName.Fixed;
 import com.github.kagkarlsson.scheduler.StopSchedulerExtension;
 import com.github.kagkarlsson.scheduler.TestTasks;
-import com.github.kagkarlsson.scheduler.helper.TestableRegistry;
+import com.github.kagkarlsson.scheduler.helper.TestableListener;
 import com.github.kagkarlsson.scheduler.task.ExecutionComplete;
 import com.github.kagkarlsson.scheduler.task.helper.OneTimeTask;
 import java.time.Duration;
@@ -33,15 +33,15 @@ public class PriorityExecutionTest {
 
   @Test
   public void test_when_priority_is_enabled() {
-    TestableRegistry.Condition condition = TestableRegistry.Conditions.completed(4);
-    TestableRegistry registry = TestableRegistry.create().waitConditions(condition).build();
+    TestableListener.Condition condition = TestableListener.Conditions.completed(4);
+    TestableListener listener = TestableListener.create().waitConditions(condition).build();
 
     Scheduler scheduler =
         Scheduler.create(postgres.getDataSource(), oneTimeTask)
             .threads(1) // 1 thread to force being sequential
             .pollingInterval(ofMinutes(1))
             .schedulerName(new Fixed("test"))
-            .statsRegistry(registry)
+            .addSchedulerListener(listener)
             .enablePriority()
             .build();
 
@@ -70,7 +70,7 @@ public void test_when_priority_is_enabled() {
           scheduler.start();
           condition.waitFor();
 
-          List<ExecutionComplete> completed = registry.getCompleted();
+          List<ExecutionComplete> completed = listener.getCompleted();
           assertThat(completed, hasSize(4));
 
           // when priority is enabled
@@ -97,21 +97,21 @@ public void test_when_priority_is_enabled() {
                   Instant.parse("2020-01-01T18:00:00Z"),
                   Instant.parse("2020-01-01T10:00:00Z")));
 
-          registry.assertNoFailures();
+          listener.assertNoFailures();
         });
   }
 
   @Test
   public void test_when_priority_is_disabled() {
-    TestableRegistry.Condition condition = TestableRegistry.Conditions.completed(4);
-    TestableRegistry registry = TestableRegistry.create().waitConditions(condition).build();
+    TestableListener.Condition condition = TestableListener.Conditions.completed(4);
+    TestableListener listener = TestableListener.create().waitConditions(condition).build();
 
     Scheduler scheduler =
         Scheduler.create(postgres.getDataSource(), oneTimeTask)
             .threads(1) // 1 thread to force being sequential
             .pollingInterval(ofMinutes(1))
             .schedulerName(new Fixed("test"))
-            .statsRegistry(registry)
+            .addSchedulerListener(listener)
             .build();
 
     stopScheduler.register(scheduler);
@@ -141,7 +141,7 @@ public void test_when_priority_is_disabled() {
           scheduler.start();
           condition.waitFor();
 
-          List<ExecutionComplete> completed = registry.getCompleted();
+          List<ExecutionComplete> completed = listener.getCompleted();
           assertThat(completed, hasSize(4));
 
           // when priority is disabled
@@ -166,7 +166,7 @@ public void test_when_priority_is_disabled() {
                   .map(e -> e.taskInstance.getPriority())
                   .collect(Collectors.toList());
 
-          registry.assertNoFailures();
+          listener.assertNoFailures();
         });
   }
 }
diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/helper/ExecutionCompletedCondition.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/helper/ExecutionCompletedCondition.java
--- a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/helper/ExecutionCompletedCondition.java
+++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/helper/ExecutionCompletedCondition.java
@@ -1,12 +1,13 @@
 package com.github.kagkarlsson.scheduler.helper;
 
-import com.github.kagkarlsson.scheduler.stats.StatsRegistry;
+import com.github.kagkarlsson.scheduler.event.SchedulerListener.CandidateEventType;
+import com.github.kagkarlsson.scheduler.event.SchedulerListener.SchedulerEventType;
 import com.github.kagkarlsson.scheduler.task.ExecutionComplete;
 import com.github.kagkarlsson.scheduler.task.ExecutionComplete.Result;
 import java.util.concurrent.CountDownLatch;
 import org.slf4j.LoggerFactory;
 
-public class ExecutionCompletedCondition implements TestableRegistry.Condition {
+public class ExecutionCompletedCondition implements TestableListener.Condition {
 
   private final CountDownLatch completed;
   private final int numberCompleted;
@@ -28,13 +29,10 @@ public void waitFor() {
   }
 
   @Override
-  public void apply(StatsRegistry.SchedulerStatsEvent e) {}
+  public void apply(SchedulerEventType e) {}
 
   @Override
-  public void apply(StatsRegistry.CandidateStatsEvent e) {}
-
-  @Override
-  public void apply(StatsRegistry.ExecutionStatsEvent e) {}
+  public void apply(CandidateEventType e) {}
 
   @Override
   public void applyExecutionComplete(ExecutionComplete complete) {
diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/helper/RanExecuteDueCondition.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/helper/RanExecuteDueCondition.java
--- a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/helper/RanExecuteDueCondition.java
+++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/helper/RanExecuteDueCondition.java
@@ -1,11 +1,13 @@
 package com.github.kagkarlsson.scheduler.helper;
 
-import com.github.kagkarlsson.scheduler.stats.StatsRegistry;
+import com.github.kagkarlsson.scheduler.event.SchedulerListener;
+import com.github.kagkarlsson.scheduler.event.SchedulerListener.CandidateEventType;
+import com.github.kagkarlsson.scheduler.event.SchedulerListener.SchedulerEventType;
 import com.github.kagkarlsson.scheduler.task.ExecutionComplete;
 import java.util.concurrent.CountDownLatch;
 import org.slf4j.LoggerFactory;
 
-public class RanExecuteDueCondition implements TestableRegistry.Condition {
+public class RanExecuteDueCondition implements TestableListener.Condition {
 
   private final CountDownLatch count;
   private final int waitForCount;
@@ -27,8 +29,8 @@ public void waitFor() {
   }
 
   @Override
-  public void apply(StatsRegistry.SchedulerStatsEvent e) {
-    if (e == StatsRegistry.SchedulerStatsEvent.RAN_EXECUTE_DUE) {
+  public void apply(SchedulerListener.SchedulerEventType e) {
+    if (e == SchedulerEventType.RAN_EXECUTE_DUE) {
       LoggerFactory.getLogger(RanExecuteDueCondition.class)
           .info("Received event executed-due, counting down");
       count.countDown();
@@ -36,10 +38,7 @@ public void apply(StatsRegistry.SchedulerStatsEvent e) {
   }
 
   @Override
-  public void apply(StatsRegistry.CandidateStatsEvent e) {}
-
-  @Override
-  public void apply(StatsRegistry.ExecutionStatsEvent e) {}
+  public void apply(CandidateEventType e) {}
 
   @Override
   public void applyExecutionComplete(ExecutionComplete complete) {}
diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/helper/RanUpdateHeartbeatsCondition.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/helper/RanUpdateHeartbeatsCondition.java
--- a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/helper/RanUpdateHeartbeatsCondition.java
+++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/helper/RanUpdateHeartbeatsCondition.java
@@ -1,13 +1,13 @@
 package com.github.kagkarlsson.scheduler.helper;
 
-import com.github.kagkarlsson.scheduler.stats.StatsRegistry;
-import com.github.kagkarlsson.scheduler.stats.StatsRegistry.SchedulerStatsEvent;
+import com.github.kagkarlsson.scheduler.event.SchedulerListener;
+import com.github.kagkarlsson.scheduler.event.SchedulerListener.SchedulerEventType;
 import com.github.kagkarlsson.scheduler.task.ExecutionComplete;
 import java.util.concurrent.CountDownLatch;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class RanUpdateHeartbeatsCondition implements TestableRegistry.Condition {
+public class RanUpdateHeartbeatsCondition implements TestableListener.Condition {
   private static final Logger LOG = LoggerFactory.getLogger(RanUpdateHeartbeatsCondition.class);
 
   private final CountDownLatch count;
@@ -30,18 +30,15 @@ public void waitFor() {
   }
 
   @Override
-  public void apply(StatsRegistry.SchedulerStatsEvent e) {
-    if (e == SchedulerStatsEvent.RAN_UPDATE_HEARTBEATS) {
+  public void apply(SchedulerListener.SchedulerEventType e) {
+    if (e == SchedulerEventType.RAN_UPDATE_HEARTBEATS) {
       LOG.info("Received event update-heartbeats, counting down");
       count.countDown();
     }
   }
 
   @Override
-  public void apply(StatsRegistry.CandidateStatsEvent e) {}
-
-  @Override
-  public void apply(StatsRegistry.ExecutionStatsEvent e) {}
+  public void apply(SchedulerListener.CandidateEventType e) {}
 
   @Override
   public void applyExecutionComplete(ExecutionComplete complete) {}
diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/helper/TestableRegistry.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/helper/TestableListener.java
similarity index 51%
rename from db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/helper/TestableRegistry.java
rename to db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/helper/TestableListener.java
--- a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/helper/TestableRegistry.java
+++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/helper/TestableListener.java
@@ -1,7 +1,11 @@
 package com.github.kagkarlsson.scheduler.helper;
 
-import com.github.kagkarlsson.scheduler.stats.StatsRegistry;
+import com.github.kagkarlsson.scheduler.CurrentlyExecuting;
+import com.github.kagkarlsson.scheduler.event.SchedulerListener;
+import com.github.kagkarlsson.scheduler.task.Execution;
 import com.github.kagkarlsson.scheduler.task.ExecutionComplete;
+import com.github.kagkarlsson.scheduler.task.TaskInstanceId;
+import java.time.Instant;
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicLong;
@@ -9,61 +13,74 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class TestableRegistry implements StatsRegistry {
+public class TestableListener implements SchedulerListener {
 
-  public static final EnumSet<SchedulerStatsEvent> FAILURE_EVENTS =
+  public static final EnumSet<SchedulerEventType> FAILURE_EVENTS =
       EnumSet.of(
-          SchedulerStatsEvent.UNEXPECTED_ERROR,
-          SchedulerStatsEvent.COMPLETIONHANDLER_ERROR,
-          SchedulerStatsEvent.FAILUREHANDLER_ERROR,
-          SchedulerStatsEvent.DEAD_EXECUTION);
+          SchedulerEventType.UNEXPECTED_ERROR,
+          SchedulerEventType.COMPLETIONHANDLER_ERROR,
+          SchedulerEventType.FAILUREHANDLER_ERROR,
+          SchedulerEventType.DEAD_EXECUTION);
 
-  private static final Logger REGISTRY_LOGGER = LoggerFactory.getLogger(TestableRegistry.class);
+  private static final Logger LISTENER_LOGGER = LoggerFactory.getLogger(TestableListener.class);
 
   private final List<ExecutionComplete> completed;
-  private final List<SchedulerStatsEvent> stats;
+  private final List<SchedulerEventType> stats;
   private List<Condition> waitConditions;
   private Map<String, AtomicLong> counters = new ConcurrentHashMap();
   private final boolean logEvents;
 
-  public TestableRegistry(boolean logEvents, List<Condition> waitConditions) {
+  public TestableListener(boolean logEvents, List<Condition> waitConditions) {
     this.logEvents = logEvents;
     this.waitConditions = Collections.synchronizedList(waitConditions);
     this.completed = Collections.synchronizedList(new ArrayList<>());
     this.stats = Collections.synchronizedList(new ArrayList<>());
   }
 
-  public static TestableRegistry.Builder create() {
-    return new TestableRegistry.Builder();
+  public static TestableListener.Builder create() {
+    return new TestableListener.Builder();
   }
 
   @Override
-  public void register(SchedulerStatsEvent e) {
-    this.stats.add(e);
-    applyToConditions(e);
-    log(e);
-    countEvent(e);
+  public void onExecutionScheduled(TaskInstanceId taskInstanceId, Instant executionTime) {
+    log(taskInstanceId, executionTime);
   }
 
   @Override
-  public void register(CandidateStatsEvent e) {
-    applyToConditions(e);
-    log(e);
-    countEvent(e);
+  public void onExecutionStart(CurrentlyExecuting currentlyExecuting) {
+    logOnExecutionStart(currentlyExecuting);
   }
 
   @Override
-  public void register(ExecutionStatsEvent e) {
-    applyToConditions(e);
-    log(e);
-    countEvent(e);
+  public void onExecutionComplete(ExecutionComplete executionComplete) {
+    completed.add(executionComplete);
+    applyToConditions(executionComplete);
+    log(executionComplete);
   }
 
   @Override
-  public void registerSingleCompletedExecution(ExecutionComplete completeEvent) {
-    completed.add(completeEvent);
-    applyToConditions(completeEvent);
-    log(completeEvent);
+  public void onExecutionDead(Execution execution) {
+    logOnExecutionDead(execution);
+  }
+
+  @Override
+  public void onExecutionFailedHeartbeat(CurrentlyExecuting currentlyExecuting) {
+    logOnExecutionFailedHeartbeat(currentlyExecuting);
+  }
+
+  @Override
+  public void onSchedulerEvent(SchedulerEventType type) {
+    this.stats.add(type);
+    applyToConditions(type);
+    log(type);
+    countEvent(type);
+  }
+
+  @Override
+  public void onCandidateEvent(CandidateEventType type) {
+    applyToConditions(type);
+    log(type);
+    countEvent(type);
   }
 
   public List<ExecutionComplete> getCompleted() {
@@ -84,15 +101,11 @@ public void assertNoFailures() {
         });
   }
 
-  private void applyToConditions(SchedulerStatsEvent e) {
-    waitConditions.forEach(c -> c.apply(e));
-  }
-
-  private void applyToConditions(CandidateStatsEvent e) {
+  private void applyToConditions(SchedulerEventType e) {
     waitConditions.forEach(c -> c.apply(e));
   }
 
-  private void applyToConditions(ExecutionStatsEvent e) {
+  private void applyToConditions(CandidateEventType e) {
     waitConditions.forEach(c -> c.apply(e));
   }
 
@@ -121,15 +134,27 @@ private String counterKey(Class enumClass, String enumName) {
     return enumClass.getName() + "." + enumName;
   }
 
-  private void log(SchedulerStatsEvent e) {
-    log("Event: " + e.name());
+  public void log(TaskInstanceId taskInstanceId, Instant executionTime) {
+    log("Execution scheduled: " + taskInstanceId + " at " + executionTime);
+  }
+
+  public void logOnExecutionStart(CurrentlyExecuting currentlyExecuting) {
+    log("Execution started: " + currentlyExecuting);
   }
 
-  private void log(CandidateStatsEvent e) {
+  public void logOnExecutionDead(Execution execution) {
+    log("Execution died: " + execution);
+  }
+
+  public void logOnExecutionFailedHeartbeat(CurrentlyExecuting currentlyExecuting) {
+    log("Execution failed heartbeat: " + currentlyExecuting);
+  }
+
+  private void log(SchedulerEventType e) {
     log("Event: " + e.name());
   }
 
-  private void log(ExecutionStatsEvent e) {
+  private void log(CandidateEventType e) {
     log("Event: " + e.name());
   }
 
@@ -139,39 +164,37 @@ private void log(ExecutionComplete completeEvent) {
 
   private void log(String s) {
     if (logEvents) {
-      REGISTRY_LOGGER.info(s);
+      LISTENER_LOGGER.info(s);
     }
   }
 
   public interface Condition {
     void waitFor();
 
-    void apply(SchedulerStatsEvent e);
-
-    void apply(CandidateStatsEvent e);
+    void apply(SchedulerEventType e);
 
-    void apply(ExecutionStatsEvent e);
+    void apply(CandidateEventType e);
 
     void applyExecutionComplete(ExecutionComplete complete);
   }
 
   public static class Builder {
 
-    private List<Condition> waitConditions = new ArrayList<>();
+    private final List<Condition> waitConditions = new ArrayList<>();
     private boolean logEvents = false;
 
-    public Builder waitConditions(Condition... waitConditions) {
+    public TestableListener.Builder waitConditions(Condition... waitConditions) {
       this.waitConditions.addAll(Arrays.asList(waitConditions));
       return this;
     }
 
-    public Builder logEvents() {
+    public TestableListener.Builder logEvents() {
       this.logEvents = true;
       return this;
     }
 
-    public TestableRegistry build() {
-      return new TestableRegistry(logEvents, waitConditions);
+    public TestableListener build() {
+      return new TestableListener(logEvents, waitConditions);
     }
   }