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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## v1.5.5 - (2025-01-19)

- Added MdcTriggerInterceptor

## v1.5.4 - (2025-01-14)

- adjusted trigger cols that the UI does not break
Expand Down
156 changes: 17 additions & 139 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ Focus is the usage with spring boot and JPA.

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

# Documentation

Use for more advanced doc the [WIKI](https://github.com/sterlp/spring-persistent-tasks/wiki).
The README contains a shorter how to use.

# DBs for storage

## Tested in the pipeline
Expand All @@ -27,14 +32,17 @@ Secondary goal is to support [Poor mans Workflow](https://github.com/sterlp/pmw)

- mySQL: sequences are not supported

# Setup and Run a Task
# JavaDoc

- [JavaDoc](https://sterlp.github.io/spring-persistent-tasks/javadoc-core/org/sterl/spring/persistent_tasks/PersistentTaskService.html)

## Maven
# Quickstart

- [Maven Central spring-persistent-tasks-core](https://central.sonatype.com/artifact/org.sterl.spring/spring-persistent-tasks-core/versions)

## Setup with Maven


```xml
<dependency>
<groupId>org.sterl.spring</groupId>
Expand All @@ -51,61 +59,7 @@ Secondary goal is to support [Poor mans Workflow](https://github.com/sterlp/pmw)
public class ExampleApplication {
```

## Setup a spring persistent task

### As a class

```java
@Component(BuildVehicleTask.NAME)
@RequiredArgsConstructor
@Slf4j
public class BuildVehicleTask implements PersistentTask<Vehicle> {

private static final String NAME = "buildVehicleTask";
public static final TaskId<Vehicle> ID = new TaskId<>(NAME);

private final VehicleRepository vehicleRepository;

@Override
public void accept(Vehicle vehicle) {
// do stuff
// save
vehicleRepository.save(vehicle);
}
// OPTIONAL
@Override
public RetryStrategy retryStrategy() {
// run 5 times, multiply the execution count with 4, add the result in HOURS to now.
return new MultiplicativeRetryStrategy(5, ChronoUnit.HOURS, 4)
}
// OPTIONAL
// if the task in accept requires a DB transaction, join them together with the framework
// if true the TransactionTemplate is used. Set here any timeouts.
@Override
public boolean isTransactional() {
return true;
}
}
```

Consider setting a timeout to the `TransactionTemplate`:

```java
@Bean
TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
TransactionTemplate template = new TransactionTemplate(transactionManager);
template.setTimeout(10);
return template;
}
```

### As a closure

Simple task will use defaults:

- Not a transactional task, e.g. HTTP calls
- 4 executions, one regular and 3 retries, linear
- using minutes with an offset of 1 which is added to now
## Create a Task

```java
@Bean
Expand All @@ -114,79 +68,22 @@ PersistentTask<Vehicle> task1(VehicleHttpConnector vehicleHttpConnector) {
}
```

### Task Transaction Management

[Transaction-Management Task](https://github.com/sterlp/spring-persistent-tasks/wiki/Transaction-Management)

## Queue a task execution

### Direct usage of the `TriggerService` or `PersistentTaskService`.

```java
private final TriggerService triggerService;
private final PersistentTaskService persistentTaskService;

public void buildVehicle() {
// Vehicle has to be Serializable
final var v = new Vehicle();
// set any data to v ...

// EITHER: queue it, will run later
triggerService.queue(BuildVehicleTask.ID.newUniqueTrigger(v));

// OR: will queue it and run it if possible.
// if the scheduler service is missing it is same as using the TriggerService
persistentTaskService.runOrQueue(BuildVehicleTask.ID.newUniqueTrigger(v));
}
```

### Build complex Trigger

```java
private final PersistentTaskService persistentTaskService;

public void buildVehicle() {
var trigger = TaskTriggerBuilder
.<Vehicle>newTrigger("task2")
.id("my-id") // will overwrite existing triggers
.state(new Vehicle("funny"))
.runAfter(Duration.ofHours(2))
.build();

persistentTaskService.runOrQueue(trigger);
}
```

### Use a Spring Event
## Trigger a task

```java
private final ApplicationEventPublisher eventPublisher;
@Autowired
PersistentTaskService persistentTaskService;

public void buildVehicle() {
// Vehicle has to be Serializable
final var v = new Vehicle();
// send an event with the trigger inside - same as calling the PersistentTaskService
eventPublisher.publishEvent(TriggerTaskCommand.of(BuildVehicleTask.ID.newUniqueTrigger(v)));
public void triggerTask1(Vehicle vehicle) {
persistentTaskService.runOrQueue(
TaskTriggerBuilder.newTrigger("task1").state(vehicle).build());
}
```

### JUnit Tests

- [Persistent Task and Testing](https://github.com/sterlp/spring-persistent-tasks/wiki/Triggers-and-Tasks-in-JUnit-Tests)


### Spring configuration options

| Property | Type | Description | Default Value |
| ---------------------------------------------- | -------------------- | ------------------------------------------------------------------------ | ------------------ |
| `spring.persistent-tasks.poll-rate` | `java.lang.Integer` | The interval at which the scheduler checks for new tasks, in seconds. | `30` |
| `spring.persistent-tasks.max-threads` | `java.lang.Integer` | The number of threads to use; set to 0 to disable task processing. | `10` |
| `spring.persistent-tasks.task-timeout` | `java.time.Duration` | The maximum time allowed for a task and scheduler to complete a task. | `PT5M` (5 minutes) |
| `spring.persistent-tasks.poll-task-timeout` | `java.lang.Integer` | The interval at which the system checks for abandoned tasks, in seconds. | `300` (5 minutes) |
| `spring.persistent-tasks.scheduler-enabled` | `java.lang.Boolean` | Indicates whether this node should handle triggers. | `true` |
| `spring.persistent-tasks.history.delete-after` | `java.time.Duration` | The max age for a trigger in the hstory. | `PT72H` (30 days) |
| `spring.persistent-tasks.history.delete-rate` | `java.time.Integer` | The interval at which old triggers are deleted, in hours. | `24` (24 hours) |

# Setup DB with Liquibase

Liquibase is supported. Either import all or just the required versions.
Expand Down Expand Up @@ -246,25 +143,6 @@ public class ExampleApplication {

![History](screenshots/history-screen.png)

## Spring Boot CSRF config for the UI

Axios should work with the following spring config out of the box with csrf:

```java
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.httpBasic(org.springframework.security.config.Customizer.withDefaults())
.csrf(c ->
c.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
);
return http.build();
}
```

more informations: https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html

# Alternatives

- quartz
Expand Down
2 changes: 1 addition & 1 deletion RUN_AND_BUILD.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mvn versions:display-dependency-updates
mvn versions:set -DnewVersion=1.5.3 -DgenerateBackupPoms=false
mvn versions:set -DnewVersion=1.5.5 -DgenerateBackupPoms=false
git tag -a v1.5.3 -m "v1.5.3 release"
mvn versions:set -DnewVersion=1.5.4-SNAPSHOT -DgenerateBackupPoms=false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

/**
* Unique key of a trigger during it's execution. But it after that the same key
* can be added if needed. Ensures that only one trigger with the same key
* is currently scheduled for execution.
*/
@Data
@Builder(toBuilder = true)
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TriggerKey implements Serializable {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.sterl.spring.persistent_tasks.exception;

import lombok.Getter;

public class SpringPersistentTaskException extends RuntimeException {
private static final long serialVersionUID = 1L;
@Getter
protected final Object state;

public SpringPersistentTaskException(String message, Object state, Throwable cause) {
super(message, cause);
this.state = state;
}

public SpringPersistentTaskException(String message, Object state) {
super(message);
this.state = state;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.util.concurrent.Future;

import org.springframework.lang.NonNull;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
Expand Down Expand Up @@ -105,6 +106,7 @@ public List<Future<TriggerKey>> triggerNextTasks() {
* This method should not be called in a transaction!
* </p>
*/
@Transactional(propagation = Propagation.NEVER)
@NonNull
public List<Future<TriggerKey>> triggerNextTasks(OffsetDateTime timeDue) {
if (taskExecutor.getFreeThreads() > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.sterl.spring.persistent_tasks.api.AddTriggerRequest;
import org.sterl.spring.persistent_tasks.api.TriggerKey;
Expand All @@ -18,6 +19,7 @@
import org.sterl.spring.persistent_tasks.trigger.event.TriggerAddedEvent;
import org.sterl.spring.persistent_tasks.trigger.event.TriggerCanceledEvent;
import org.sterl.spring.persistent_tasks.trigger.event.TriggerFailedEvent;
import org.sterl.spring.persistent_tasks.trigger.event.TriggerRunningEvent;
import org.sterl.spring.persistent_tasks.trigger.event.TriggerSuccessEvent;
import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity;
import org.sterl.spring.persistent_tasks.trigger.repository.TriggerRepository;
Expand All @@ -40,9 +42,9 @@ public Optional<TriggerEntity> completeTaskWithSuccess(TriggerKey key, Serializa

result.ifPresent(t -> {
t.complete(null);
log.debug("{} set to status={}", key, t.getData().getStatus());
publisher.publishEvent(new TriggerSuccessEvent(
t.getId(), t.copyData(), state));
log.debug("Setting {} to status={}", key, t.getData().getStatus());
triggerRepository.delete(t);
});
return result;
Expand Down Expand Up @@ -146,4 +148,11 @@ public int markTriggersAsRunning(Collection<TriggerKey> keys, String runOn) {
return triggerRepository.markTriggersAsRunning(keys, runOn,
OffsetDateTime.now(), TriggerStatus.RUNNING);
}

@Transactional(propagation = Propagation.SUPPORTS)
public void triggerIsNowRunning(TriggerEntity trigger, Serializable state) {
if (!trigger.isRunning()) trigger.runOn(trigger.getRunningOn());
publisher.publishEvent(new TriggerRunningEvent(
trigger.getId(), trigger.copyData(), state, trigger.getRunningOn()));
}
}
Loading
Loading