Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,34 @@ jobs:
- name: PMD with Maven
run: mvn pmd:pmd --file pom.xml

test-mssql:
runs-on: ubuntu-latest
needs: build
services:
sql-edge:
image: mcr.microsoft.com/azure-sql-edge
options: --cap-add SYS_PTRACE --name azuresqledge
env:
ACCEPT_EULA: Y
MSSQL_SA_PASSWORD: "veryStrong123"
ports:
- 1433:1433

steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: maven
- name: Run Tests with MSSQL
run: mvn test -pl core -am -Dspring.profiles.active=mssql


java-doc:
runs-on: ubuntu-latest
needs: [build]
if: ${{ github.ref == 'refs/heads/main' }}
permissions:
contents: write # if you have a protection rule on your repository, you'll need to give write permission to the workflow.
Expand All @@ -56,9 +82,9 @@ jobs:
target-folder: javadoc-core # url will be https://<username>.github.io/<repo>/javadoc-core
project: maven # or gradle

deploy:
maven-deploy:
runs-on: ubuntu-latest
needs: [build]
needs: [build, test-mssql]
if: ${{ github.ref == 'refs/heads/main' }}
steps:
- uses: actions/checkout@v4
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## v1.1.1

- Run now button in the UI
- Offline Schedulers are deleted from the registry

## v1.1.0 - (2024-12-30)

- Showing trigger history entries
Expand Down
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ Focus is the usage with spring boot and JPA.

Secondary goal is to support [Poor mans Workflow](https://github.com/sterlp/pmw)

# DBs for storage

## Tested in the pipeline

- H2
- azure-sql-edge (MSSQL)

## Supported in theory

- MSSQL
- mySQL
- PostgreSQL
- mySQL
- MariaDB

# Setup and Run a Task

- [JavaDoc](https://sterlp.github.io/spring-persistent-tasks/javadoc-core/org/sterl/spring/persistent_tasks/PersistentTaskService.html)
Expand Down Expand Up @@ -198,20 +213,21 @@ Now the `PersistentTaskService` has a method to trigger or to trigger and to wai
}
```

During the setup and cleanup it is possible to cancel any pending stuff:
During the setup and cleanup it is possible to cancel any pending triggers:

```java
@BeforeEach
public void beforeEach() throws Exception {
triggerService.deleteAll();
historyService.deleteAll();
schedulerA.setMaxThreads(10);
schedulerService.setMaxThreads(10);
schedulerService.start();
}

@AfterEach
public void afterEach() throws Exception {
schedulerService.stop();
// will cancel any pending tasks
schedulerService.shutdownNow(); // use .stop() if you want to wait
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public record AddTriggerRequest<T extends Serializable>(
OffsetDateTime runtAt,
int priority) {

@SuppressWarnings("unchecked")
public TaskId<T> taskId() {
return (TaskId<T>)key.toTaskId();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public List<Trigger> listInstances(@PathVariable("instanceId") long instanceId)

@GetMapping("history")
public PagedModel<Trigger> list(
@PageableDefault(size = 100, direction = Direction.ASC, sort = "data.runAt") Pageable pageable) {
@PageableDefault(size = 100, direction = Direction.DESC, sort = "id") Pageable pageable) {

return FromLastTriggerStateEntity.INSTANCE.toPage( //
historyService.findTriggerState(null, pageable));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,15 @@ public void setMaxThreads(int value) {
@PreDestroy
public void stop() {
taskExecutor.close();
var s = editSchedulerStatus.checkinToRegistry(name);
log.info("Stopped {}", s);
editSchedulerStatus.offline(name);
log.info("Stopped {}", name);
}

public void shutdownNow() {
var running = taskExecutor.getRunningTasks();
taskExecutor.shutdownNow();
var s = editSchedulerStatus.checkinToRegistry(name);
log.info("Force stop {}", s);
log.info("Force stop {} with {} running tasks", name, running);
editSchedulerStatus.offline(name);
}

public SchedulerEntity pingRegistry() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ public SchedulerEntity checkinToRegistry(String name) {
result.setLastPing(OffsetDateTime.now());
return schedulerRepository.save(result);
}

public void offline(String name) {
schedulerRepository.deleteById(name);
}

public SchedulerEntity get(String name) {
return schedulerRepository.findById(name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
import java.util.concurrent.atomic.AtomicInteger;

import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.sterl.spring.persistent_tasks.api.TriggerKey;
import org.sterl.spring.persistent_tasks.trigger.TriggerService;
import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity;

import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Getter;
Expand All @@ -34,6 +34,7 @@ public class TaskExecutorComponent implements Closeable {
@Getter
@Setter
private Duration maxShutdownWaitTime = Duration.ofSeconds(10);
@Nullable
private ExecutorService executor;
private final ConcurrentHashMap<TriggerEntity, Future<TriggerKey>> runningTasks = new ConcurrentHashMap<>();
private final AtomicBoolean stopped = new AtomicBoolean(true);
Expand All @@ -58,6 +59,8 @@ public Future<TriggerKey> submit(@Nullable TriggerEntity trigger) {
if (trigger == null) {
return CompletableFuture.completedFuture(null);
}
if (stopped.get()) throw new IllegalStateException("Executor is already stopped");

final var result = executor.submit(() -> runTrigger(trigger));
runningTasks.put(trigger, result);
return result;
Expand All @@ -72,6 +75,7 @@ private TriggerKey runTrigger(TriggerEntity trigger) {
}
}

@SuppressWarnings("resource")
@PostConstruct
public void start() {
if (stopped.compareAndExchange(true, false)) {
Expand All @@ -87,33 +91,37 @@ public void start() {
public void close() {
if (stopped.compareAndExchange(false, true)) {
synchronized (stopped) {
if (executor != null) {
executor.shutdown();
waitForRunningTasks();
executor = null;
}
doShutdown();
}
}
}

private void waitForRunningTasks() {
if (runningTasks.size() > 0) {
log.info("Shutdown executor with {} running tasks, waiting for {}.",
runningTasks.size(), maxShutdownWaitTime);

try {
executor.awaitTermination(maxShutdownWaitTime.getSeconds(), TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.warn("Failed to complete runnings tasks.", e.getCause());
} finally {
executor.shutdownNow();
private void doShutdown() {
if (executor != null) {
executor.shutdown();
if (runningTasks.size() > 0) {
log.info("Shutdown executor with {} running tasks, waiting for {}.",
runningTasks.size(), maxShutdownWaitTime);

try {
executor.awaitTermination(maxShutdownWaitTime.getSeconds(), TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.warn("Failed to complete runnings tasks.", e.getCause());
shutdownNow();
} finally {
executor = null;
runningTasks.clear();
}
} else {
executor = null;
}
}
}

public void shutdownNow() {
stopped.set(true);
executor.shutdownNow();
if (executor != null) executor.shutdownNow();
executor = null;
}

public int getFreeThreads() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public long count() {
@GetMapping("triggers")
public PagedModel<Trigger> list(
@RequestParam(name = "taskId", required = false) String taskId,
@PageableDefault(size = 100, direction = Direction.ASC, sort = "data.runAt")
@PageableDefault(size = 100, direction = Direction.DESC, sort = "id")
Pageable pageable) {
return FromTriggerEntity.INSTANCE.toPage(
triggerService.findAllTriggers(TaskId.of(taskId), pageable));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,8 @@ public void beforeEach() throws Exception {

@AfterEach
public void afterEach() throws Exception {
schedulerA.stop();
schedulerB.stop();
schedulerA.shutdownNow();
schedulerB.shutdownNow();
triggerService.deleteAll();
historyService.deleteAll();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,7 @@ void testRunSimpleTask() throws Exception {
final var historyEntity = historyService.findLastKnownStatus(triggerKey).get();
assertThat(historyEntity.getData().getExecutionCount()).isEqualTo(1);
assertThat(historyEntity.getData().getEnd()).isAfterOrEqualTo(historyEntity.getData().getStart());
assertThat(historyEntity.getData().getRunningDurationInMs())
.isEqualTo(Duration.between(
historyEntity.getData().getStart(),
historyEntity.getData().getEnd()).toMillis());
assertThat(historyEntity.getData().getRunningDurationInMs()).isNotNull();
assertThat(historyEntity.getData().getExecutionCount()).isEqualTo(1);
asserts.assertValue("foo");
asserts.assertMissing("bar");
Expand Down Expand Up @@ -197,6 +194,11 @@ void testFailedSavingException() throws Exception {
void testFailedTriggerHasDuration() throws Exception {
// GIVEN
TaskId<String> task = taskService.<String>replace("foo", c -> {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
throw new IllegalArgumentException("Nope! " + c);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

<!-- Define properties for database-specific types -->
<property name="offsetdatetime.type" value="datetimeoffset(2)" dbms="mssql" />
<property name="offsetdatetime.type" value="datetimeoffset(6)" dbms="mssql" />
<property name="offsetdatetime.type" value="TIMESTAMPTZ" dbms="postgresql" />
<property name="offsetdatetime.type" value="TIMESTAMP WITH TIME ZONE" dbms="h2" />
<property name="offsetdatetime.type" value="DATETIME(6)" dbms="mysql,mariadb" />
Expand Down
1 change: 1 addition & 0 deletions ui/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default tseslint.config(
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-unsafe-member-access": "warn",
},
}
);
Loading
Loading