Repository: kagkarlsson/db-scheduler (java)

Issue:
Subclass RuntimeException in StandardSchedulerClient for better error handling
Due to `StandardSchedulerClient` throwing plain `RuntimeException` instances there is no way to distinguish (and possibly ignore) exceptions other than by doing string matching of the supplied message:
```
        public void cancel(TaskInstanceId taskInstanceId) {
...
            if (execution.isPresent()) {
                if (execution.get().isPicked()) {
                    throw new RuntimeException(String.format("Could not cancel schedule, the execution with name '%s' and id '%s' is currently executing", taskName, instanceId));
                }
...
            } else {
                throw new RuntimeException(String.format("Could not cancel schedule - no task with name '%s' and id '%s' was found.", taskName, instanceId));
            }
        }
```
Almost the same is done in `reschedule()`, except for slightly different message wording.

Did you consider having `TaskInstanceAlreadyRunningException` and `TaskInstanceNotFoundException` runtime exception classes? The messages can stay the same in case someone is already doing string matching for that purpose.


Discussion:
Right now I'm able to work around this issue by upgrading to the latest version (was running 9.1 where there was no `ScheduledExecution#isPicked()` method, so I had no other way that wasn't ugly and/or tedious), but a slightly richer exception model would be nice.
Good thing v9.2 was helpful.

I agree that replacing the generic `RunTimeException` would be helpful here, I will put it on the backlog 👍 

Patch:
diff --git a/db-scheduler-boot-starter/src/main/java/com/github/kagkarlsson/scheduler/boot/autoconfigure/DbSchedulerAutoConfiguration.java b/db-scheduler-boot-starter/src/main/java/com/github/kagkarlsson/scheduler/boot/autoconfigure/DbSchedulerAutoConfiguration.java
--- a/db-scheduler-boot-starter/src/main/java/com/github/kagkarlsson/scheduler/boot/autoconfigure/DbSchedulerAutoConfiguration.java
+++ b/db-scheduler-boot-starter/src/main/java/com/github/kagkarlsson/scheduler/boot/autoconfigure/DbSchedulerAutoConfiguration.java
@@ -26,6 +26,7 @@
 import com.github.kagkarlsson.scheduler.boot.config.DbSchedulerStarter;
 import com.github.kagkarlsson.scheduler.boot.config.startup.ContextReadyStart;
 import com.github.kagkarlsson.scheduler.boot.config.startup.ImmediateStart;
+import com.github.kagkarlsson.scheduler.exceptions.SerializationException;
 import com.github.kagkarlsson.scheduler.stats.MicrometerStatsRegistry;
 import com.github.kagkarlsson.scheduler.stats.StatsRegistry;
 import com.github.kagkarlsson.scheduler.task.OnStartup;
@@ -41,6 +42,7 @@
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import javax.sql.DataSource;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator;
@@ -241,7 +243,7 @@ public byte[] serialize(Object data) {
                 out.writeObject(data);
                 return bos.toByteArray();
             } catch (Exception e) {
-                throw new RuntimeException("Failed to serialize object", e);
+                throw new SerializationException("Failed to serialize object", e);
             }
         }
 
@@ -252,7 +254,7 @@ public <T> T deserialize(Class<T> clazz, byte[] serializedData) {
                  ObjectInput in = new ConfigurableObjectInputStream(bis, Thread.currentThread().getContextClassLoader())) {
                 return clazz.cast(in.readObject());
             } catch (Exception e) {
-                throw new RuntimeException("Failed to deserialize object", e);
+                throw new SerializationException("Failed to deserialize object", e);
             }
         }
     };
diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/ScheduledExecution.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/ScheduledExecution.java
--- a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/ScheduledExecution.java
+++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/ScheduledExecution.java
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 package com.github.kagkarlsson.scheduler;
+import com.github.kagkarlsson.scheduler.exceptions.DataClassMismatchException;
 import com.github.kagkarlsson.scheduler.task.Execution;
-import com.github.kagkarlsson.scheduler.task.TaskInstance;
 import com.github.kagkarlsson.scheduler.task.TaskInstanceId;
 import java.time.Instant;
 import java.util.Objects;
@@ -40,7 +40,7 @@ public DATA_TYPE getData() {
         if (dataClass.isInstance(this.execution.taskInstance.getData())) {
             return (DATA_TYPE) this.execution.taskInstance.getData();
         }
-        throw new DataClassMismatchException();
+        throw new DataClassMismatchException(dataClass, this.execution.taskInstance.getData().getClass());
     }
 
     public Instant getLastSuccess() {
@@ -74,6 +74,4 @@ public boolean equals(Object o) {
     public int hashCode() {
         return Objects.hash(execution);
     }
-    public static class DataClassMismatchException extends RuntimeException {
-    }
 }
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
@@ -15,6 +15,8 @@
  */
 package com.github.kagkarlsson.scheduler;
 
+import com.github.kagkarlsson.scheduler.exceptions.TaskInstanceNotFoundException;
+import com.github.kagkarlsson.scheduler.exceptions.TaskInstanceCurrentlyExecutingException;
 import com.github.kagkarlsson.scheduler.jdbc.DefaultJdbcCustomization;
 import com.github.kagkarlsson.scheduler.jdbc.JdbcCustomization;
 import com.github.kagkarlsson.scheduler.jdbc.JdbcTaskRepository;
@@ -238,7 +240,7 @@ public <T> void reschedule(TaskInstanceId taskInstanceId, Instant newExecutionTi
             Optional<Execution> execution = taskRepository.getExecution(taskName, instanceId);
             if (execution.isPresent()) {
                 if (execution.get().isPicked()) {
-                    throw new RuntimeException(String.format("Could not reschedule, the execution with name '%s' and id '%s' is currently executing", taskName, instanceId));
+                    throw new TaskInstanceCurrentlyExecutingException(taskName, instanceId);
                 }
 
                 boolean success;
@@ -252,7 +254,7 @@ public <T> void reschedule(TaskInstanceId taskInstanceId, Instant newExecutionTi
                     notifyListeners(ClientEvent.EventType.RESCHEDULE, taskInstanceId, newExecutionTime);
                 }
             } else {
-                throw new RuntimeException(String.format("Could not reschedule - no task with name '%s' and id '%s' was found.", taskName, instanceId));
+                throw new TaskInstanceNotFoundException(taskName, instanceId);
             }
         }
 
@@ -263,13 +265,13 @@ public void cancel(TaskInstanceId taskInstanceId) {
             Optional<Execution> execution = taskRepository.getExecution(taskName, instanceId);
             if (execution.isPresent()) {
                 if (execution.get().isPicked()) {
-                    throw new RuntimeException(String.format("Could not cancel schedule, the execution with name '%s' and id '%s' is currently executing", taskName, instanceId));
+                    throw new TaskInstanceCurrentlyExecutingException(taskName, instanceId);
                 }
 
                 taskRepository.remove(execution.get());
                 notifyListeners(ClientEvent.EventType.CANCEL, taskInstanceId, execution.get().executionTime);
             } else {
-                throw new RuntimeException(String.format("Could not cancel schedule - no task with name '%s' and id '%s' was found.", taskName, instanceId));
+                throw new TaskInstanceNotFoundException(taskName, instanceId);
             }
         }
 
diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/Serializer.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/Serializer.java
--- a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/Serializer.java
+++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/Serializer.java
@@ -17,6 +17,8 @@
 
 import java.io.*;
 
+import com.github.kagkarlsson.scheduler.exceptions.SerializationException;
+
 public interface Serializer {
 
     byte[] serialize(Object data);
@@ -33,7 +35,7 @@ public byte[] serialize(Object data) {
                 out.writeObject(data);
                 return bos.toByteArray();
             } catch (Exception e) {
-                throw new RuntimeException("Failed to serialize object", e);
+                throw new SerializationException("Failed to serialize object", e);
             }
         }
 
@@ -44,7 +46,7 @@ public <T> T deserialize(Class<T> clazz, byte[] serializedData) {
                  ObjectInput in = new ObjectInputStream(bis)) {
                 return clazz.cast(in.readObject());
             } catch (Exception e) {
-                throw new RuntimeException("Failed to deserialize object", e);
+                throw new SerializationException("Failed to deserialize object", e);
             }
         }
     };
diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/DataClassMismatchException.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/DataClassMismatchException.java
new file mode 100644
--- /dev/null
+++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/DataClassMismatchException.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright (C) Gustav Karlsson
+ *
+ * Licensed 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 com.github.kagkarlsson.scheduler.exceptions;
+
+public class DataClassMismatchException extends DbSchedulerException {
+    private static final long serialVersionUID = 6333316294241471977L;
+
+    public DataClassMismatchException(Class expectedClass, Class actualClass) {
+        super(String.format("Task data mismatch. Expected class : %s, actual : %s", expectedClass, actualClass));
+    }
+}
diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/DbSchedulerException.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/DbSchedulerException.java
new file mode 100644
--- /dev/null
+++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/DbSchedulerException.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (C) Gustav Karlsson
+ *
+ * Licensed 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 com.github.kagkarlsson.scheduler.exceptions;
+
+public abstract class DbSchedulerException extends RuntimeException {
+    static final long serialVersionUID = -2132850112553296790L;
+
+    public DbSchedulerException(String message){
+        super(message);
+    }
+
+    public DbSchedulerException(String message, Throwable e){
+        super(message, e);
+    }
+}
diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/ExecutionException.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/ExecutionException.java
new file mode 100644
--- /dev/null
+++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/ExecutionException.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright (C) Gustav Karlsson
+ *
+ * Licensed 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 com.github.kagkarlsson.scheduler.exceptions;
+
+import com.github.kagkarlsson.scheduler.task.Execution;
+
+public class ExecutionException extends TaskInstanceException {
+    private static final long serialVersionUID = -4732028463501966553L;
+    private final long version;
+
+    public ExecutionException(String message, Execution execution){
+        super(message, execution.taskInstance.getTaskName(), execution.taskInstance.getId());
+        this.version = execution.version;
+    }
+
+    public ExecutionException(String message, Execution execution, Throwable ex){
+        super(message, execution.taskInstance.getTaskName(), execution.taskInstance.getId(), ex);
+        this.version = execution.version;
+    }
+
+    public long getVersion() {
+        return version;
+    }
+}
diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/SerializationException.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/SerializationException.java
new file mode 100644
--- /dev/null
+++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/SerializationException.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright (C) Gustav Karlsson
+ *
+ * Licensed 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 com.github.kagkarlsson.scheduler.exceptions;
+
+public class SerializationException extends DbSchedulerException {
+    private static final long serialVersionUID = -5209721067354085350L;
+
+    public SerializationException(String msg, Throwable cause) {
+        super(msg, cause);
+    }
+}
diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/TaskInstanceCurrentlyExecutingException.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/TaskInstanceCurrentlyExecutingException.java
new file mode 100644
--- /dev/null
+++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/TaskInstanceCurrentlyExecutingException.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright (C) Gustav Karlsson
+ *
+ * Licensed 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 com.github.kagkarlsson.scheduler.exceptions;
+
+public class TaskInstanceCurrentlyExecutingException extends TaskInstanceException {
+    private static final long serialVersionUID = 6016933189040296444L;
+
+    public TaskInstanceCurrentlyExecutingException(String taskName, String instanceId) {
+        super("Cannot use SchedulerClient to modify a task-instance that is currently executing. If this occurs often, consider only modifying executions not picked that has the next execution-time a certain time into the future.", taskName, instanceId);
+    }
+}
diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/TaskInstanceException.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/TaskInstanceException.java
new file mode 100644
--- /dev/null
+++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/TaskInstanceException.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright (C) Gustav Karlsson
+ *
+ * Licensed 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 com.github.kagkarlsson.scheduler.exceptions;
+
+public class TaskInstanceException extends DbSchedulerException {
+    static final long serialVersionUID = -2132850112553296790L;
+    private static final String TASK_NAME_INSTANCE_MESSAGE_PART = " (task name: %s, instance id: %s)";
+
+    private final String taskName;
+    private final String instanceId;
+
+    public TaskInstanceException(String message, String taskName, String instanceId, Throwable ex){
+        super(message + String.format(TASK_NAME_INSTANCE_MESSAGE_PART, taskName, instanceId), ex);
+        this.taskName = taskName;
+        this.instanceId = instanceId;
+    }
+
+    public TaskInstanceException(String message, String taskName, String instanceId){
+        super(message + String.format(TASK_NAME_INSTANCE_MESSAGE_PART, taskName, instanceId));
+        this.taskName = taskName;
+        this.instanceId = instanceId;
+    }
+
+    public String getTaskName() {
+        return taskName;
+    }
+
+    public String getInstanceId() {
+        return instanceId;
+    }
+}
diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/TaskInstanceNotFoundException.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/TaskInstanceNotFoundException.java
new file mode 100644
--- /dev/null
+++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/exceptions/TaskInstanceNotFoundException.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (C) Gustav Karlsson
+ *
+ * Licensed 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 com.github.kagkarlsson.scheduler.exceptions;
+
+public class TaskInstanceNotFoundException extends TaskInstanceException {
+    private static final long serialVersionUID = -3604591431614052358L;
+    public TaskInstanceNotFoundException(String taskName, String instanceId){
+        super("Failed to perform action on task because it was not found.", taskName, instanceId);
+    }
+}
diff --git a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/jdbc/JdbcTaskRepository.java b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/jdbc/JdbcTaskRepository.java
--- a/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/jdbc/JdbcTaskRepository.java
+++ b/db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/jdbc/JdbcTaskRepository.java
@@ -24,6 +24,8 @@
 import com.github.kagkarlsson.scheduler.TaskRepository;
 import com.github.kagkarlsson.scheduler.TaskResolver;
 import com.github.kagkarlsson.scheduler.TaskResolver.UnresolvedTask;
+import com.github.kagkarlsson.scheduler.exceptions.ExecutionException;
+import com.github.kagkarlsson.scheduler.exceptions.TaskInstanceException;
 import com.github.kagkarlsson.scheduler.task.Execution;
 import com.github.kagkarlsson.scheduler.task.Task;
 import com.github.kagkarlsson.scheduler.task.TaskInstance;
@@ -69,10 +71,14 @@ public JdbcTaskRepository(DataSource dataSource, boolean commitWhenAutocommitDis
     }
 
     public JdbcTaskRepository(DataSource dataSource, boolean commitWhenAutocommitDisabled, JdbcCustomization jdbcCustomization, String tableName, TaskResolver taskResolver, SchedulerName schedulerSchedulerName, Serializer serializer) {
+        this(jdbcCustomization, tableName, taskResolver, schedulerSchedulerName, serializer, new JdbcRunner(dataSource, commitWhenAutocommitDisabled));
+    }
+
+    protected JdbcTaskRepository(JdbcCustomization jdbcCustomization, String tableName, TaskResolver taskResolver, SchedulerName schedulerSchedulerName, Serializer serializer, JdbcRunner jdbcRunner) {
         this.tableName = tableName;
         this.taskResolver = taskResolver;
         this.schedulerSchedulerName = schedulerSchedulerName;
-        this.jdbcRunner = new JdbcRunner(dataSource, commitWhenAutocommitDisabled);
+        this.jdbcRunner = jdbcRunner;
         this.serializer = serializer;
         this.jdbcCustomization = jdbcCustomization;
     }
@@ -103,7 +109,7 @@ public boolean createIfNotExists(Execution execution) {
             LOG.debug("Exception when inserting execution. Assuming it to be a constraint violation.", e);
             Optional<Execution> existingExecution = getExecution(execution.taskInstance);
             if (!existingExecution.isPresent()) {
-                throw new RuntimeException("Failed to add new execution.", e);
+                throw new ExecutionException("Failed to add new execution.", execution, e);
             }
             LOG.debug("Execution not created, another thread created it.");
             return false;
@@ -169,7 +175,7 @@ public void remove(Execution execution) {
         );
 
         if (removed != 1) {
-            throw new RuntimeException("Expected one execution to be removed, but removed " + removed + ". Indicates a bug.");
+            throw new ExecutionException("Expected one execution to be removed, but removed " + removed + ". Indicates a bug.", execution);
         }
     }
 
@@ -217,7 +223,7 @@ private boolean rescheduleInternal(Execution execution, Instant nextExecutionTim
                 });
 
         if (updated != 1) {
-            throw new RuntimeException("Expected one execution to be updated, but updated " + updated + ". Indicates a bug.");
+            throw new ExecutionException("Expected one execution to be updated, but updated " + updated + ". Indicates a bug.", execution);
         }
         return updated > 0;
     }
@@ -328,7 +334,7 @@ public Optional<Execution> getExecution(String taskName, String taskInstanceId)
                 new ExecutionResultSetMapper()
         );
         if (executions.size() > 1) {
-            throw new RuntimeException(String.format("Found more than one matching execution for task name/id combination: '%s'/'%s'", taskName, taskInstanceId));
+            throw new TaskInstanceException("Found more than one matching execution for task name/id combination.", taskName, taskInstanceId);
         }
 
         return executions.size() == 1 ? ofNullable(executions.get(0)) : Optional.empty();


Test patch:
diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/ScheduledExecutionTest.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/ScheduledExecutionTest.java
--- a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/ScheduledExecutionTest.java
+++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/ScheduledExecutionTest.java
@@ -1,5 +1,6 @@
 package com.github.kagkarlsson.scheduler;
 
+import com.github.kagkarlsson.scheduler.exceptions.DataClassMismatchException;
 import com.github.kagkarlsson.scheduler.task.Execution;
 import com.github.kagkarlsson.scheduler.task.helper.OneTimeTask;
 import org.junit.jupiter.api.Test;
@@ -38,13 +39,16 @@ public void test_data_class_type_equals() {
 
     @Test
     public void test_data_class_type_not_equals() {
-        assertThrows(ScheduledExecution.DataClassMismatchException.class, () -> {
+        DataClassMismatchException dataClassMismatchException = assertThrows(DataClassMismatchException.class, () -> {
 
             Instant now = Instant.now();
-            OneTimeTask<Integer> task = TestTasks.oneTime("OneTime", Integer.class, (instance, executionContext) -> {});
+            OneTimeTask<Integer> task = TestTasks.oneTime("OneTime", Integer.class, (instance, executionContext) -> {
+            });
             Execution execution = new Execution(now, task.instance("id1", new Integer(1))); // Data class is an integer
 
             new ScheduledExecution<>(String.class, execution).getData(); // Instantiate with incorrect type
         });
+
+        assertEquals("Task data mismatch. Expected class : class java.lang.String, actual : class java.lang.Integer", dataClassMismatchException.getMessage());
     }
 }
diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/SchedulerClientExceptionsTest.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/SchedulerClientExceptionsTest.java
new file mode 100644
--- /dev/null
+++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/SchedulerClientExceptionsTest.java
@@ -0,0 +1,98 @@
+package com.github.kagkarlsson.scheduler;
+
+import java.time.Instant;
+import java.util.Optional;
+
+import com.github.kagkarlsson.scheduler.task.TaskInstanceId;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import com.github.kagkarlsson.scheduler.exceptions.TaskInstanceCurrentlyExecutingException;
+import com.github.kagkarlsson.scheduler.exceptions.TaskInstanceNotFoundException;
+import com.github.kagkarlsson.scheduler.task.Execution;
+import com.github.kagkarlsson.scheduler.task.TaskInstance;
+
+import static com.github.kagkarlsson.scheduler.task.TaskInstanceId.StandardTaskInstanceId;
+import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class SchedulerClientExceptionsTest {
+
+    @InjectMocks
+    SchedulerClient.StandardSchedulerClient schedulerClient;
+
+    @Mock
+    TaskRepository taskRepository;
+
+    @Test
+    public void failsToRescheduleWhenTaskIsNotFound() {
+        StandardTaskInstanceId taskInstance = new StandardTaskInstanceId(randomAlphanumeric(10), randomAlphanumeric(10));
+        when(taskRepository.getExecution(taskInstance.getTaskName(), taskInstance.getId())).thenReturn(Optional.empty());
+
+        TaskInstanceNotFoundException actualException = assertThrows(TaskInstanceNotFoundException.class, () -> {
+            schedulerClient.reschedule(taskInstance, Instant.now(), null);
+        });
+        assertEquals("Failed to perform action on task because it was not found. (task name: " + taskInstance.getTaskName() + ", instance id: " + taskInstance.getId() + ")",
+            actualException.getMessage());
+    }
+
+    @Test
+    public void failsToRescheduleWhenATaskIsPickedAndExecuting() {
+        StandardTaskInstanceId taskInstance = new StandardTaskInstanceId(randomAlphanumeric(10), randomAlphanumeric(10));
+        Execution expectedExecution = createExecutingExecution(taskInstance);
+
+        when(taskRepository.getExecution(taskInstance.getTaskName(), taskInstance.getId())).thenReturn(Optional.of(expectedExecution));
+
+        TaskInstanceCurrentlyExecutingException actualException = assertThrows(TaskInstanceCurrentlyExecutingException.class, () -> {
+            schedulerClient.reschedule(taskInstance, Instant.now(), null);
+        });
+        assertEquals("Cannot use SchedulerClient to modify a task-instance that is currently executing. If this occurs often, consider only modifying executions not picked that has the next execution-time a certain time into the future. (task name: " + taskInstance.getTaskName() + ", instance id: " + taskInstance.getId() + ")",
+            actualException.getMessage());
+    }
+
+    @Test
+    public void failsToCancelWhenTaskIsNotFound() {
+        StandardTaskInstanceId taskInstance = new StandardTaskInstanceId(randomAlphanumeric(10), randomAlphanumeric(10));
+        when(taskRepository.getExecution(taskInstance.getTaskName(), taskInstance.getId())).thenReturn(Optional.empty());
+
+        TaskInstanceNotFoundException actualException = assertThrows(TaskInstanceNotFoundException.class, () -> {
+            schedulerClient.cancel(taskInstance);
+        });
+        assertEquals("Failed to perform action on task because it was not found. (task name: " + taskInstance.getTaskName() + ", instance id: " + taskInstance.getId() + ")",
+            actualException.getMessage());
+    }
+
+    @Test
+    public void failsToCancelWhenATaskIsPickedAndExecuting() {
+        StandardTaskInstanceId taskInstance = new StandardTaskInstanceId(randomAlphanumeric(10), randomAlphanumeric(10));
+        Execution expectedExecution = createExecutingExecution(taskInstance);
+
+        when(taskRepository.getExecution(taskInstance.getTaskName(), taskInstance.getId())).thenReturn(Optional.of(expectedExecution));
+
+        TaskInstanceCurrentlyExecutingException actualException = assertThrows(TaskInstanceCurrentlyExecutingException.class, () -> {
+            schedulerClient.cancel(taskInstance);
+        });
+        assertEquals("Cannot use SchedulerClient to modify a task-instance that is currently executing. If this occurs often, consider only modifying executions not picked that has the next execution-time a certain time into the future. (task name: " + taskInstance.getTaskName() + ", instance id: " + taskInstance.getId() + ")",
+            actualException.getMessage());
+    }
+
+    private Execution createExecutingExecution(StandardTaskInstanceId taskInstance) {
+        return new Execution(
+            Instant.now(),
+            new TaskInstance(taskInstance.getTaskName(), taskInstance.getId()),
+            true,
+            randomAlphanumeric(5),
+            null,
+            null,
+            0,
+            null,
+            1
+        );
+    }
+}
diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/SerializerExceptionsTest.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/SerializerExceptionsTest.java
new file mode 100644
--- /dev/null
+++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/SerializerExceptionsTest.java
@@ -0,0 +1,37 @@
+package com.github.kagkarlsson.scheduler;
+
+import java.io.NotSerializableException;
+import java.io.StreamCorruptedException;
+
+import org.junit.jupiter.api.Test;
+
+import com.github.kagkarlsson.scheduler.exceptions.SerializationException;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.*;
+
+class SerializerExceptionsTest {
+    class NotSerializable{}
+
+    @Test
+    public void onDeserializeFailureWillThrowExpectedException(){
+        SerializationException serializationException = assertThrows(SerializationException.class, () -> {
+            Serializer.DEFAULT_JAVA_SERIALIZER.serialize(new NotSerializable());
+        });
+
+        assertEquals("Failed to serialize object", serializationException.getMessage());
+        assertThat(serializationException.getCause(), is(instanceOf(NotSerializableException.class)));
+    }
+
+    @Test
+    public void onSerializeFailureWillThrowExpectedException(){
+        SerializationException serializationException = assertThrows(SerializationException.class, () -> {
+            Serializer.DEFAULT_JAVA_SERIALIZER.deserialize(NotSerializable.class, new byte[]{1, 2, 3, 4});
+        });
+
+        assertEquals("Failed to deserialize object", serializationException.getMessage());
+        assertThat(serializationException.getCause(), is(instanceOf(StreamCorruptedException.class)));
+    }
+}
diff --git a/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/jdbc/JdbcTaskRepositoryExceptionsTest.java b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/jdbc/JdbcTaskRepositoryExceptionsTest.java
new file mode 100644
--- /dev/null
+++ b/db-scheduler/src/test/java/com/github/kagkarlsson/scheduler/jdbc/JdbcTaskRepositoryExceptionsTest.java
@@ -0,0 +1,179 @@
+package com.github.kagkarlsson.scheduler.jdbc;
+
+import java.lang.reflect.Field;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.List;
+
+import com.github.kagkarlsson.scheduler.jdbc.JdbcCustomization;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.junit.platform.commons.util.ReflectionUtils;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import com.github.kagkarlsson.jdbc.JdbcRunner;
+import com.github.kagkarlsson.jdbc.PreparedStatementSetter;
+import com.github.kagkarlsson.jdbc.ResultSetMapper;
+import com.github.kagkarlsson.jdbc.SQLRuntimeException;
+import com.github.kagkarlsson.scheduler.exceptions.ExecutionException;
+import com.github.kagkarlsson.scheduler.exceptions.TaskInstanceException;
+import com.github.kagkarlsson.scheduler.jdbc.JdbcTaskRepository;
+import com.github.kagkarlsson.scheduler.task.Execution;
+import com.github.kagkarlsson.scheduler.task.TaskInstance;
+import com.google.common.collect.Lists;
+
+import static java.util.Collections.emptyList;
+import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class JdbcTaskRepositoryExceptionsTest {
+    JdbcTaskRepository jdbcTaskRepository;
+
+    @Mock
+    JdbcRunner mockJdbcRunner;
+
+    private String expectedTableName;
+
+    @BeforeEach
+    public void setup() throws NoSuchFieldException, IllegalAccessException {
+        expectedTableName = randomAlphanumeric(5);
+        jdbcTaskRepository = new JdbcTaskRepository(   null,  expectedTableName, null, null, null, mockJdbcRunner);
+    }
+
+    @Test
+    public void createIfNotExistsFailsToAddNewTask() {
+        when(mockJdbcRunner.query(ArgumentMatchers.eq("select * from " + expectedTableName + " where task_name = ? and task_instance = ?"), any(PreparedStatementSetter.class), (ResultSetMapper<List<Execution>>) any(ResultSetMapper.class)))
+            .thenReturn(emptyList());
+        SQLRuntimeException rootCause = new SQLRuntimeException("SQL GO BOOM!!!");
+        when(mockJdbcRunner.execute(ArgumentMatchers.eq("insert into " + expectedTableName + "(task_name, task_instance, task_data, execution_time, picked, version) values(?, ?, ?, ?, ?, ?)"), any(PreparedStatementSetter.class)))
+            .thenThrow(rootCause);
+
+        TaskInstance taskInstance = new TaskInstance(randomAlphanumeric(10), randomAlphanumeric(10));
+        Execution execution = new Execution(Instant.now(), taskInstance);
+        ExecutionException actualException = assertThrows(ExecutionException.class, () -> {
+            jdbcTaskRepository.createIfNotExists(execution);
+        });
+        assertEquals("Failed to add new execution. (task name: " + taskInstance.getTaskName() + ", instance id: " + taskInstance.getId() + ")", actualException.getMessage());
+        assertEquals(rootCause, actualException.getCause());
+        assertEquals(execution.version, actualException.getVersion());
+        assertEquals(execution.taskInstance.getTaskName(), actualException.getTaskName());
+        assertEquals(execution.taskInstance.getId(), actualException.getInstanceId());
+    }
+
+    @Test
+    public void getExecutionIsMoreThanOne() {
+        TaskInstance expectedTaskInstance = new TaskInstance(randomAlphanumeric(10), randomAlphanumeric(10));
+
+        when(mockJdbcRunner.query(ArgumentMatchers.eq("select * from " + expectedTableName + " where task_name = ? and task_instance = ?"), any(PreparedStatementSetter.class), (ResultSetMapper<List<Execution>>) any(ResultSetMapper.class)))
+            .thenReturn(Lists.newArrayList(new Execution(Instant.now(), expectedTaskInstance), new Execution(Instant.now(), expectedTaskInstance)));
+
+        TaskInstanceException actualException = assertThrows(TaskInstanceException.class, () -> {
+            jdbcTaskRepository.getExecution(expectedTaskInstance);
+        });
+        assertEquals("Found more than one matching execution for task name/id combination. (task name: " + expectedTaskInstance.getTaskName() + ", instance id: " + expectedTaskInstance.getId() + ")",
+            actualException.getMessage());
+    }
+
+    @ParameterizedTest(name = "Remove ends up removing {0} records")
+    @ValueSource(ints = {0, 2})
+    public void removesUnexpectedNumberOfRows(int removalCount) {
+        when(mockJdbcRunner.execute(ArgumentMatchers.eq("delete from " + expectedTableName + " where task_name = ? and task_instance = ? and version = ?"), any(PreparedStatementSetter.class)))
+            .thenReturn(removalCount);
+
+        TaskInstance taskInstance = new TaskInstance(randomAlphanumeric(10), randomAlphanumeric(10));
+        Execution execution = new Execution(Instant.now(), taskInstance);
+        ExecutionException actualException = assertThrows(ExecutionException.class, () -> {
+            jdbcTaskRepository.remove(execution);
+        });
+        assertEquals(
+            "Expected one execution to be removed, but removed " + removalCount + ". Indicates a bug. (task name: " + taskInstance.getTaskName() + ", instance id: " + taskInstance.getId() + ")",
+            actualException.getMessage());
+        assertEquals(execution.version, actualException.getVersion());
+        assertEquals(execution.taskInstance.getTaskName(), actualException.getTaskName());
+        assertEquals(execution.taskInstance.getId(), actualException.getInstanceId());
+    }
+
+    @ParameterizedTest(name = "Reschedule without new data ends up modifying {0} records")
+    @ValueSource(ints = {0, 2})
+    public void rescheduleUpdatesUnexpectedNumberOfRowsWithoutNewData(int updateCount) {
+        when(mockJdbcRunner.execute(ArgumentMatchers.eq("update " + expectedTableName + " set " +
+                "picked = ?, " +
+                "picked_by = ?, " +
+                "last_heartbeat = ?, " +
+                "last_success = ?, " +
+                "last_failure = ?, " +
+                "consecutive_failures = ?, " +
+                "execution_time = ?, " +
+                "version = version + 1 " +
+                "where task_name = ? " +
+                "and task_instance = ? " +
+                "and version = ?"),
+            any(PreparedStatementSetter.class)))
+            .thenReturn(updateCount);
+
+        TaskInstance taskInstance = new TaskInstance(randomAlphanumeric(10), randomAlphanumeric(10));
+        Execution execution = new Execution(Instant.now(), taskInstance);
+        ExecutionException actualException = assertThrows(ExecutionException.class, () -> {
+            jdbcTaskRepository.reschedule(execution,
+                Instant.now(),
+                null,
+                null,
+                0
+            );
+        });
+        assertEquals(
+            "Expected one execution to be updated, but updated " + updateCount + ". Indicates a bug. (task name: " + taskInstance.getTaskName() + ", instance id: " + taskInstance.getId() + ")",
+            actualException.getMessage());
+        assertEquals(execution.version, actualException.getVersion());
+        assertEquals(execution.taskInstance.getTaskName(), actualException.getTaskName());
+        assertEquals(execution.taskInstance.getId(), actualException.getInstanceId());
+    }
+
+    @ParameterizedTest(name = "Reschedule with new data ends up modifying {0} records")
+    @ValueSource(ints = {0, 2})
+    public void rescheduleUpdatesUnexpectedNumberOfRowsWithNewData(int updateCount) {
+        when(mockJdbcRunner.execute(ArgumentMatchers.eq("update " + expectedTableName + " set " +
+                "picked = ?, " +
+                "picked_by = ?, " +
+                "last_heartbeat = ?, " +
+                "last_success = ?, " +
+                "last_failure = ?, " +
+                "consecutive_failures = ?, " +
+                "execution_time = ?, " +
+                "task_data = ?, " +
+                "version = version + 1 " +
+                "where task_name = ? " +
+                "and task_instance = ? " +
+                "and version = ?"),
+            any(PreparedStatementSetter.class)))
+            .thenReturn(updateCount);
+
+        TaskInstance taskInstance = new TaskInstance(randomAlphanumeric(10), randomAlphanumeric(10));
+        Execution execution = new Execution(Instant.now(), taskInstance);
+        ExecutionException actualException = assertThrows(ExecutionException.class, () -> {
+            jdbcTaskRepository.reschedule(execution,
+                Instant.now(),
+                new HashMap(),
+                null,
+                null,
+                0
+            );
+        });
+        assertEquals(
+            "Expected one execution to be updated, but updated " + updateCount + ". Indicates a bug. (task name: " + taskInstance.getTaskName() + ", instance id: " + taskInstance.getId() + ")",
+            actualException.getMessage());
+        assertEquals(execution.version, actualException.getVersion());
+        assertEquals(execution.taskInstance.getTaskName(), actualException.getTaskName());
+        assertEquals(execution.taskInstance.getId(), actualException.getInstanceId());
+    }
+
+}