diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fd310a9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM gradle:7.6.4-jdk17 AS builder +COPY . /project +WORKDIR /project +RUN gradle bootJar + +FROM openjdk:17-jdk-slim-bullseye +ARG JAR_FILE=/project/build/libs/*.jar +COPY --from=builder ${JAR_FILE} ./application.jar +ENV TZ=America/Chicago +ENV TASKS_LOCK_API_ENABLED=true +ENV SPRING_PROFILES_ACTIVE=tasks-lock-api +ENTRYPOINT ["java", "-jar", "application.jar"] \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8128009..3f01056 100644 --- a/build.gradle +++ b/build.gradle @@ -6,11 +6,11 @@ plugins { id 'java-library' } -def build = '2' +def build = '9' group = 'rjojjr.com.github' //version = "1.0.0.$build-SNAPSHOT" -version = "1.0.0-RELEASE" +version = "1.1.0-RELEASE" sourceCompatibility = '17' @@ -29,6 +29,7 @@ dependencies { api 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok:1.18.32' annotationProcessor 'org.projectlombok:lombok:1.18.32' + runtimeOnly 'com.mysql:mysql-connector-j' } java { diff --git a/src/main/java/rjojjr/com/github/taskslock/EmbeddedTasksLockService.java b/src/main/java/rjojjr/com/github/taskslock/EmbeddedTasksLockService.java index f1c96e0..822a779 100644 --- a/src/main/java/rjojjr/com/github/taskslock/EmbeddedTasksLockService.java +++ b/src/main/java/rjojjr/com/github/taskslock/EmbeddedTasksLockService.java @@ -6,10 +6,13 @@ import org.springframework.stereotype.Service; import rjojjr.com.github.taskslock.entity.TaskLockEntity; import rjojjr.com.github.taskslock.entity.TaskLockEntityRepository; +import rjojjr.com.github.taskslock.exception.AcquireLockFailureException; +import rjojjr.com.github.taskslock.exception.ReleaseLockFailureException; +import rjojjr.com.github.taskslock.exception.TasksLockShutdownFailure; import rjojjr.com.github.taskslock.models.TaskLock; import rjojjr.com.github.taskslock.util.HostUtil; import rjojjr.com.github.taskslock.util.ThreadUtil; - +import org.hibernate.exception.DataException; import java.util.Date; import java.util.HashSet; import java.util.Set; @@ -34,64 +37,83 @@ public TaskLock acquireLock(String taskName, String contextId, boolean waitForLo @Override public TaskLock acquireLock(String taskName, String hostName, String contextId, boolean waitForLock) { - log.debug("Acquiring lock for task {}", taskName); - synchronized (releaseLock) { - var lockedAt = new Date(); - var entity = taskLockEntityRepository.findById(taskName).orElseGet(() -> new TaskLockEntity(taskName, false, hostName, contextId, new Date())); - if (!entity.getIsLocked()) { - entity.setIsLocked(true); - entity.setLockedAt(lockedAt); - entity.setIsLockedByHost(hostName); - entity.setContextId(contextId); - - taskLockEntityRepository.save(entity); - var taskLock = new TaskLock( - taskName, - contextId, - lockedAt, - () -> releaseLock(taskName) - ); - taskLocks.add(taskLock); - log.debug("Task lock acquired for task {}", taskName); - return taskLock; + try { + log.debug("Acquiring lock for task {}", taskName); + synchronized (releaseLock) { + var lockedAt = new Date(); + var entity = taskLockEntityRepository.findById(taskName).orElseGet(() -> new TaskLockEntity(taskName, false, hostName, contextId, new Date())); + try { + if (!entity.getIsLocked()) { + entity.setIsLocked(true); + entity.setLockedAt(lockedAt); + entity.setIsLockedByHost(hostName); + entity.setContextId(contextId); + taskLockEntityRepository.save(entity); + var taskLock = new TaskLock( + taskName, + contextId, + true, + lockedAt, + () -> releaseLock(taskName) + ); + taskLocks.add(taskLock); + log.debug("Task lock acquired for task {}", taskName); + return taskLock; + } + } catch (DataException e) { + log.debug("Task lock not acquired for task {} because this worker lost in a race condition", taskName); + } } - } - if (waitForLock) { - log.debug("Task lock not acquired for task {}, retrying in {}ms", taskName, RETRY_INTERVAL); - ThreadUtil.sleep(RETRY_INTERVAL); - return acquireLock(taskName, hostName, contextId, waitForLock); + if (waitForLock) { + log.debug("Task lock not acquired for task {}, retrying in {}ms", taskName, RETRY_INTERVAL); + ThreadUtil.sleep(RETRY_INTERVAL); + return acquireLock(taskName, hostName, contextId, true); + } + log.debug("Task lock not acquired for task {}", taskName); + return new TaskLock(taskName, contextId, false, null, () -> {}); + } catch (Exception e) { + log.error("Error acquiring lock for task {}: {}", taskName, e.getMessage()); + throw new AcquireLockFailureException(taskName, e); } - log.debug("Task lock not acquired for task {}", taskName); - return null; } @Override public void releaseLock(String taskName) { log.debug("Releasing lock for task {}", taskName); - synchronized (releaseLock) { - var entity = taskLockEntityRepository.findById(taskName).orElseGet(() -> new TaskLockEntity(taskName, false, null, null, new Date())); - if (entity.getIsLocked()) { - entity.setIsLocked(false); - entity.setLockedAt(null); - entity.setIsLockedByHost(null); - entity.setContextId(null); + try { + synchronized (releaseLock) { + var entity = taskLockEntityRepository.findById(taskName).orElseGet(() -> new TaskLockEntity(taskName, false, null, null, new Date())); + if (entity.getIsLocked()) { + entity.setIsLocked(false); + entity.setLockedAt(null); + entity.setIsLockedByHost(null); + entity.setContextId(null); - taskLockEntityRepository.save(entity); - taskLocks = taskLocks.stream().filter(taskLock -> !taskLock.getTaskName().equals(taskName)).collect(Collectors.toSet()); + taskLockEntityRepository.save(entity); + taskLocks = taskLocks.stream().filter(taskLock -> !taskLock.getTaskName().equals(taskName)).collect(Collectors.toSet()); + } } + } catch (Exception e) { + log.error("Error releasing lock for task {}: {}", taskName, e.getMessage()); + throw new ReleaseLockFailureException(taskName, e); } } @Override public void onShutdown() { log.info("Shutting down TasksLockService and releasing task-locks"); - synchronized (releaseLock) { - for(TaskLock lock : taskLocks) { - lock.getRelease().run(); + try { + synchronized (releaseLock) { + for(TaskLock lock : taskLocks) { + lock.getRelease().run(); + } } + log.info("Shut down TasksLockService and released task-locks"); + } catch (Exception e) { + log.error("error shutting down TasksLock API and releasing task-locks: {}", e.getMessage()); + throw new TasksLockShutdownFailure(e); } - log.info("Shut down TasksLockService and released task-locks"); } } diff --git a/src/main/java/rjojjr/com/github/taskslock/TasksLocksApiClientService.java b/src/main/java/rjojjr/com/github/taskslock/TasksLocksApiClientService.java index 3d7a768..ce5ca85 100644 --- a/src/main/java/rjojjr/com/github/taskslock/TasksLocksApiClientService.java +++ b/src/main/java/rjojjr/com/github/taskslock/TasksLocksApiClientService.java @@ -6,6 +6,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import rjojjr.com.github.taskslock.exception.AcquireLockFailureException; +import rjojjr.com.github.taskslock.exception.ReleaseLockFailureException; +import rjojjr.com.github.taskslock.exception.TasksLockShutdownFailure; import rjojjr.com.github.taskslock.models.TaskLock; import rjojjr.com.github.taskslock.models.TasksLockApiResponse; @@ -29,23 +32,33 @@ public class TasksLocksApiClientService implements TasksLockService { @Override public TaskLock acquireLock(String taskName, String contextId, boolean waitForLock) { - var response = restTemplate.getForObject(String.format("%s/tasks-lock/api/v1/acquire?taskName=%s&contextId=%s&waitForLock=%s", apiProtoAndHost, taskName, contextId, waitForLock ? "true" : "false"), TasksLockApiResponse.class); - if(!response.getIsLockAcquired()){ - return null; - } - var taskLock = new TaskLock(taskName, contextId, response.getLockedAt(), () -> releaseLock(taskName)); - synchronized (releaseLock) { - taskLocks.add(taskLock); + try { + var response = restTemplate.getForObject(String.format("%s/tasks-lock/api/v1/acquire?taskName=%s&contextId=%s&waitForLock=%s", apiProtoAndHost, taskName, contextId, waitForLock ? "true" : "false"), TasksLockApiResponse.class); + if(!response.getIsLockAcquired()){ + return new TaskLock(taskName, contextId, false, null, () -> {}); + } + var taskLock = new TaskLock(taskName, contextId, true, response.getLockedAt(), () -> releaseLock(taskName)); + synchronized (releaseLock) { + taskLocks.add(taskLock); + } + return taskLock; + } catch (Exception e) { + log.error("Error acquiring lock from TasksLock API: {}", e.getMessage()); + throw new AcquireLockFailureException(taskName, e); } - return taskLock; } @Override public void releaseLock(String taskName) { - restTemplate.getForObject(String.format("%s/tasks-lock/api/v1/acquire?taskName=%s", apiProtoAndHost, taskName), TasksLockApiResponse.class); - synchronized (releaseLock) { - taskLocks = taskLocks.stream().filter(taskLock -> !taskLock.getTaskName().equals(taskName)) - .collect(Collectors.toSet()); + try { + restTemplate.getForObject(String.format("%s/tasks-lock/api/v1/release?taskName=%s", apiProtoAndHost, taskName), TasksLockApiResponse.class); + synchronized (releaseLock) { + taskLocks = taskLocks.stream().filter(taskLock -> !taskLock.getTaskName().equals(taskName)) + .collect(Collectors.toSet()); + } + } catch (Exception e) { + log.error("Error releasing lock from TasksLock API: {}", e.getMessage()); + throw new ReleaseLockFailureException(taskName, e); } } @@ -57,11 +70,17 @@ public TaskLock acquireLock(String taskName, String hostName, String contextId, @Override public void onShutdown() { log.info("Shutting down TasksLockService and releasing task-locks"); - synchronized (releaseLock) { - for(TaskLock lock : taskLocks) { - lock.getRelease().run(); + try { + synchronized (releaseLock) { + for(TaskLock lock : taskLocks) { + lock.getRelease().run(); + } } + log.info("Shut down TasksLockService and released task-locks"); + } catch (Exception e) { + log.error("error shutting down TasksLock API and releasing task-locks: {}", e.getMessage()); + throw new TasksLockShutdownFailure(e); } - log.info("Shut down TasksLockService and released task-locks"); + } } diff --git a/src/main/java/rjojjr/com/github/taskslock/controllers/TasksLockApiController.java b/src/main/java/rjojjr/com/github/taskslock/controllers/TasksLockApiController.java index 372f0c5..7874587 100644 --- a/src/main/java/rjojjr/com/github/taskslock/controllers/TasksLockApiController.java +++ b/src/main/java/rjojjr/com/github/taskslock/controllers/TasksLockApiController.java @@ -23,7 +23,7 @@ public class TasksLockApiController { @GetMapping("/acquire") public TasksLockApiResponse acquire(@RequestParam String taskName, @RequestParam String contextId, @RequestParam(defaultValue = "true") Boolean waitForLock, HttpServletRequest request) { var lock = tasksLockService.acquireLock(taskName, request.getRemoteHost(), contextId, waitForLock); - return new TasksLockApiResponse(taskName, lock != null, lock != null ? lock.getLockedAt() : null); + return new TasksLockApiResponse(taskName, lock.getIsLocked(), lock.getLockedAt()); } @GetMapping("/release") diff --git a/src/main/java/rjojjr/com/github/taskslock/exception/AcquireLockFailureException.java b/src/main/java/rjojjr/com/github/taskslock/exception/AcquireLockFailureException.java new file mode 100644 index 0000000..bd7b6aa --- /dev/null +++ b/src/main/java/rjojjr/com/github/taskslock/exception/AcquireLockFailureException.java @@ -0,0 +1,7 @@ +package rjojjr.com.github.taskslock.exception; + +public class AcquireLockFailureException extends TasksLockApiException { + public AcquireLockFailureException(String taskName, Exception cause) { + super(String.format("error while acquiring lock for task %s: %s", taskName, cause), cause); + } +} diff --git a/src/main/java/rjojjr/com/github/taskslock/exception/ReleaseLockFailureException.java b/src/main/java/rjojjr/com/github/taskslock/exception/ReleaseLockFailureException.java new file mode 100644 index 0000000..eb0028d --- /dev/null +++ b/src/main/java/rjojjr/com/github/taskslock/exception/ReleaseLockFailureException.java @@ -0,0 +1,7 @@ +package rjojjr.com.github.taskslock.exception; + +public class ReleaseLockFailureException extends TasksLockApiException { + public ReleaseLockFailureException(String taskName, Exception cause) { + super(String.format("error while releasing lock for %s: %s", taskName, cause.getMessage()), cause); + } +} diff --git a/src/main/java/rjojjr/com/github/taskslock/exception/TasksLockApiException.java b/src/main/java/rjojjr/com/github/taskslock/exception/TasksLockApiException.java new file mode 100644 index 0000000..13d0fb3 --- /dev/null +++ b/src/main/java/rjojjr/com/github/taskslock/exception/TasksLockApiException.java @@ -0,0 +1,7 @@ +package rjojjr.com.github.taskslock.exception; + +public class TasksLockApiException extends RuntimeException { + public TasksLockApiException(String message, Exception cause) { + super(message, cause); + } +} diff --git a/src/main/java/rjojjr/com/github/taskslock/exception/TasksLockShutdownFailure.java b/src/main/java/rjojjr/com/github/taskslock/exception/TasksLockShutdownFailure.java new file mode 100644 index 0000000..bc5d5d5 --- /dev/null +++ b/src/main/java/rjojjr/com/github/taskslock/exception/TasksLockShutdownFailure.java @@ -0,0 +1,7 @@ +package rjojjr.com.github.taskslock.exception; + +public class TasksLockShutdownFailure extends TasksLockApiException { + public TasksLockShutdownFailure(Exception cause) { + super("failed to run TasksLock API shutdown procedure", cause); + } +} diff --git a/src/main/java/rjojjr/com/github/taskslock/models/TaskLock.java b/src/main/java/rjojjr/com/github/taskslock/models/TaskLock.java index 4ed8465..c18b872 100644 --- a/src/main/java/rjojjr/com/github/taskslock/models/TaskLock.java +++ b/src/main/java/rjojjr/com/github/taskslock/models/TaskLock.java @@ -12,6 +12,7 @@ public class TaskLock { private String taskName; private String contextId; + private Boolean isLocked; private Date lockedAt; private Runnable release; } diff --git a/src/main/java/rjojjr/com/github/taskslock/util/HostUtil.java b/src/main/java/rjojjr/com/github/taskslock/util/HostUtil.java index fac5934..df52c68 100644 --- a/src/main/java/rjojjr/com/github/taskslock/util/HostUtil.java +++ b/src/main/java/rjojjr/com/github/taskslock/util/HostUtil.java @@ -2,12 +2,18 @@ import lombok.AccessLevel; import lombok.NoArgsConstructor; +import rjojjr.com.github.taskslock.exception.TasksLockApiException; import java.net.InetAddress; +import java.net.UnknownHostException; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class HostUtil { - public static String getRemoteHost(){ - return InetAddress.getLoopbackAddress().getHostName(); + public static String getRemoteHost() { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + throw new TasksLockApiException(e.getMessage(), e); + } } } diff --git a/src/main/resources/application-tasks-lock-api.properties b/src/main/resources/application-tasks-lock-api.properties new file mode 100644 index 0000000..63601b0 --- /dev/null +++ b/src/main/resources/application-tasks-lock-api.properties @@ -0,0 +1,5 @@ +spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect +spring.jpa.hibernate.ddl-auto=update +spring.datasource.url=jdbc:mysql://${MYSQL_HOST:mysql}:${MYSQL_PORT:3306}/${MYSQL_DB:default}?useSSL=false&allowPublicKeyRetrieval=true +spring.datasource.username=${MYSQL_USERNAME:user} +spring.datasource.password=${MYSQL_PW:password} \ No newline at end of file