Skip to content

Commit 76298d4

Browse files
authored
V1.1.0 actions in UI (#1)
- Showing trigger history entries - Added `PersistentTaskService` as a new abstraction - Added cancel trigger button to the UI - Retry is now 3 times as in the strategy name
1 parent f28a47a commit 76298d4

37 files changed

+564
-194
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
distribution: 'temurin'
2525
cache: maven
2626
- name: Build with Maven
27-
run: mvn -B package --file pom.xml
27+
run: mvn -B install --file pom.xml
2828
- name: PMD with Maven
2929
run: mvn pmd:pmd --file pom.xml
3030

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Changelog
22

3-
## [v1.0.1]
3+
## v1.1.0 - (2024-12-30)
44

55
- Showing trigger history entries
6-
- Added PersistentTaskService as a abstraction
6+
- Added `PersistentTaskService` as a new abstraction
7+
- Added cancel trigger button to the UI
8+
- Retry is now 3 times as in the strategy name

README.md

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,11 @@ SpringBeanTask<Vehicle> task1(VehicleRepository vehicleRepository) {
7070

7171
## Queue a task execution
7272

73-
### Direct usage of the TriggerService.
73+
### Direct usage of the `TriggerService` or `PersistentTaskService`.
7474

7575
```java
7676
private final TriggerService triggerService;
77+
private final PersistentTaskService persistentTaskService;
7778

7879
public void buildVehicle() {
7980
// Vehicle has to be Serializable
@@ -82,6 +83,9 @@ SpringBeanTask<Vehicle> task1(VehicleRepository vehicleRepository) {
8283

8384
// queue it
8485
triggerService.queue(BuildVehicleTask.ID.newUniqueTrigger(v));
86+
// will queue it and run it if possible.
87+
// if the scheduler service is missing it is same as above
88+
persistentTaskService.runOrQueue(BuildVehicleTask.ID.newUniqueTrigger(v));
8589
}
8690
```
8791

@@ -115,6 +119,102 @@ SpringBeanTask<Vehicle> task1(VehicleRepository vehicleRepository) {
115119
}
116120
```
117121

122+
### Triggers and Tasks in JUnit Tests
123+
124+
The `SchedulerService` can be disabled for unit testing, which ensures that no trigger will be
125+
executed automatically.
126+
127+
```yml
128+
spring:
129+
persistent-tasks:
130+
scheduler-enabled: false
131+
```
132+
133+
Now you can run any trigger manually using the `TriggerService`
134+
135+
```java
136+
@Autowired
137+
private TriggerService triggerService;
138+
139+
@Test
140+
void testRunTriggerDirectly() {
141+
// GIVEN
142+
// setup your test and create any triggers needed
143+
144+
// WHEN run any pending triggers
145+
triggerService.run(triggerService.queue(trigger));
146+
147+
// THEN
148+
// any asserts you might need
149+
}
150+
151+
@Test
152+
void testRunUnknownTriggersCreated() {
153+
// GIVEN
154+
// setup your test call any method which might create triggers
155+
156+
// WHEN run any pending triggers
157+
triggerService.run(triggerService.lockNextTrigger("test"));
158+
159+
// THEN
160+
// any asserts you might need
161+
}
162+
```
163+
164+
It is also possible to define a test scheduler and use the async way to execute any triggers (without the spring scheduler which would trigger them automatically):
165+
166+
```java
167+
@Configuration
168+
public static class TestConfig {
169+
170+
@Primary
171+
@SuppressWarnings("resource")
172+
SchedulerService schedulerService(TriggerService triggerService, EditSchedulerStatusComponent editSchedulerStatus,
173+
TransactionTemplate trx) throws UnknownHostException {
174+
175+
final var taskExecutor = new TaskExecutorComponent(triggerService, 10);
176+
taskExecutor.setMaxShutdownWaitTime(Duration.ofSeconds(0));
177+
return new SchedulerService("testScheduler", triggerService, taskExecutor, editSchedulerStatus, trx);
178+
}
179+
}
180+
```
181+
182+
Now the `PersistentTaskService` has a method to trigger or to trigger and to wait for the result:
183+
184+
```java
185+
@Autowired
186+
private PersistentTaskService persistentTaskService;
187+
188+
@Test
189+
void testFoo() {
190+
// GIVEN
191+
// setup your test and create any triggers needed
192+
193+
// WHEN run any pending triggers
194+
persistentTaskService.executeTriggersAndWait();
195+
196+
// THEN
197+
// any asserts you might need
198+
}
199+
```
200+
201+
During the setup and cleanup it is possible to cancel any pending stuff:
202+
203+
```java
204+
@BeforeEach
205+
public void beforeEach() throws Exception {
206+
triggerService.deleteAll();
207+
historyService.deleteAll();
208+
schedulerA.setMaxThreads(10);
209+
schedulerService.start();
210+
}
211+
212+
@AfterEach
213+
public void afterEach() throws Exception {
214+
schedulerService.stop();
215+
}
216+
```
217+
118218
### Spring configuration options
119219

120220
| Property | Type | Description | Default Value |

core/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<parent>
77
<groupId>org.sterl.spring</groupId>
88
<artifactId>spring-persistent-tasks-root</artifactId>
9-
<version>1.0.1-SNAPSHOT</version>
9+
<version>1.1.0-SNAPSHOT</version>
1010
<relativePath>../pom.xml</relativePath>
1111
</parent>
1212

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,133 @@
11
package org.sterl.spring.persistent_tasks;
22

33
import java.io.Serializable;
4+
import java.util.ArrayList;
5+
import java.util.Collection;
6+
import java.util.List;
7+
import java.util.Optional;
8+
import java.util.concurrent.ExecutionException;
9+
import java.util.concurrent.Future;
410

5-
import org.springframework.beans.factory.annotation.Autowired;
611
import org.springframework.context.event.EventListener;
12+
import org.springframework.lang.NonNull;
713
import org.springframework.stereotype.Service;
14+
import org.springframework.transaction.annotation.Transactional;
815
import org.sterl.spring.persistent_tasks.api.AddTriggerRequest;
916
import org.sterl.spring.persistent_tasks.api.TriggerKey;
1017
import org.sterl.spring.persistent_tasks.api.event.TriggerTaskCommand;
18+
import org.sterl.spring.persistent_tasks.history.HistoryService;
1119
import org.sterl.spring.persistent_tasks.scheduler.SchedulerService;
20+
import org.sterl.spring.persistent_tasks.shared.model.TriggerData;
1221
import org.sterl.spring.persistent_tasks.trigger.TriggerService;
22+
import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity;
23+
24+
import lombok.RequiredArgsConstructor;
25+
import lombok.SneakyThrows;
1326

1427
/**
1528
* Abstraction to {@link SchedulerService} or {@link TriggerService}
1629
* depends on if the {@link SchedulerService} is available.
1730
*/
1831
@Service
32+
@RequiredArgsConstructor
1933
public class PersistentTaskService {
2034

21-
@Autowired(required = false)
22-
private SchedulerService schedulerService;
23-
@Autowired
24-
private TriggerService triggerService;
35+
private final Optional<SchedulerService> schedulerService;
36+
private final List<SchedulerService> schedulers;
37+
private final TriggerService triggerService;
38+
private final HistoryService historyService;
39+
40+
/**
41+
* Returns the last known {@link TriggerData} to a given key. First running triggers are checked.
42+
* Maybe out of the history event from a retry execution of the very same id.
43+
*
44+
* @param key the {@link TriggerKey} to look for
45+
* @return the {@link TriggerData} to the {@link TriggerKey}
46+
*/
47+
public Optional<TriggerData> getLastTriggerData(TriggerKey key) {
48+
final Optional<TriggerEntity> trigger = triggerService.get(key);
49+
if (trigger.isEmpty()) {
50+
var history = historyService.findLastKnownStatus(key);
51+
if (history.isPresent()) return Optional.ofNullable(history.get().getData());
52+
return Optional.empty();
53+
} else {
54+
return Optional.ofNullable(trigger.get().getData());
55+
}
56+
}
2557

2658
@EventListener
2759
void queue(TriggerTaskCommand<? extends Serializable> event) {
2860
if (event.triggers().size() == 1) {
2961
runOrQueue(event.triggers().iterator().next());
3062
} else {
31-
triggerService.queueAll(event.triggers());
63+
queueAll(event.triggers());
3264
}
3365
}
3466

67+
/**
68+
* Queues the given triggers.
69+
*
70+
* @param <T> the state type
71+
* @param triggers the triggers to add
72+
* @return the {@link TriggerKey}
73+
*/
74+
@Transactional(timeout = 10)
75+
@NonNull
76+
public <T extends Serializable> List<TriggerKey> queueAll(Collection<AddTriggerRequest<T>> triggers) {
77+
return triggers.stream() //
78+
.map(t -> triggerService.queue(t)) //
79+
.map(TriggerEntity::getKey) //
80+
.toList();
81+
}
82+
3583
/**
3684
* Runs the given trigger if a free threads are available
3785
* and the runAt time is not in the future.
3886
* @return the reference to the {@link TriggerKey}
3987
*/
4088
public <T extends Serializable> TriggerKey runOrQueue(
4189
AddTriggerRequest<T> triggerRequest) {
42-
if (schedulerService == null) {
43-
schedulerService.runOrQueue(triggerRequest);
90+
if (schedulerService.isPresent()) {
91+
schedulerService.get().runOrQueue(triggerRequest);
4492
} else {
4593
triggerService.queue(triggerRequest);
4694
}
4795
return triggerRequest.key();
4896
}
97+
98+
/**
99+
* Triggers the execution of all pending triggers.
100+
*
101+
* @return the reference to the {@link TriggerKey} of the running tasks
102+
*/
103+
public List<Future<TriggerKey>> executeTriggers() {
104+
var result = new ArrayList<Future<TriggerKey>>();
105+
for (SchedulerService s : schedulers) {
106+
result.addAll(s.triggerNextTasks());
107+
}
108+
return result;
109+
}
110+
111+
/**
112+
* Triggers the execution of all pending triggers and wait for the result.
113+
*/
114+
@SneakyThrows
115+
public List<TriggerKey> executeTriggersAndWait() {
116+
final var result = new ArrayList<TriggerKey>();
117+
118+
List<Future<TriggerKey>> triggers;
119+
do {
120+
triggers = executeTriggers();
121+
for (Future<TriggerKey> future : triggers) {
122+
try {
123+
result.add(future.get());
124+
} catch (InterruptedException | ExecutionException e) {
125+
final Throwable cause = e.getCause();
126+
throw cause == null ? e : cause;
127+
}
128+
}
129+
} while (!triggers.isEmpty());
130+
131+
return result;
132+
}
49133
}

core/src/main/java/org/sterl/spring/persistent_tasks/api/RetryStrategy.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@
77
@FunctionalInterface
88
public interface RetryStrategy {
99
RetryStrategy NO_RETRY = (c, e) -> false;
10-
RetryStrategy THREE_RETRIES = (c, e) -> c < 3;
10+
/**
11+
* One initial execution and after that we will try it 3 more times. Overall 4 executions.
12+
*/
13+
RetryStrategy THREE_RETRIES = (c, e) -> c < 4;
14+
/**
15+
* One initial execution and after that we will try it 3 more times. Overall 4 executions.
16+
*/
1117
RetryStrategy THREE_RETRIES_IMMEDIATELY = new RetryStrategy() {
1218
@Override
1319
public boolean shouldRetry(int executionCount, Exception error) {
14-
return executionCount < 3;
20+
return executionCount < 4;
1521
}
1622
@Override
1723
public OffsetDateTime retryAt(int executionCount, Exception error) {

core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskId.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ public static class TaskTriggerBuilder<T extends Serializable> {
4141
public static <T extends Serializable> TaskTriggerBuilder<T> newTrigger(String name) {
4242
return new TaskTriggerBuilder<>(new TaskId<T>(name));
4343
}
44+
public static <T extends Serializable> TaskTriggerBuilder<T> newTrigger(String name, T state) {
45+
return new TaskTriggerBuilder<>(new TaskId<T>(name)).state(state);
46+
}
4447
public AddTriggerRequest<T> build() {
4548
var key = TriggerKey.of(id, taskId);
4649
return new AddTriggerRequest<>(key, state, when, priority);

core/src/main/java/org/sterl/spring/persistent_tasks/api/event/TriggerTaskCommand.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import java.util.Collection;
66
import java.util.Collections;
77

8-
import org.sterl.spring.persistent_tasks.api.TaskId.TaskTriggerBuilder;
98
import org.sterl.spring.persistent_tasks.api.AddTriggerRequest;
9+
import org.sterl.spring.persistent_tasks.api.TaskId.TaskTriggerBuilder;
1010

1111
/**
1212
* An event to trigger one or multiple task executions

core/src/main/java/org/sterl/spring/persistent_tasks/history/HistoryService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
import org.springframework.data.domain.Sort.Direction;
1111
import org.sterl.spring.persistent_tasks.api.TaskId;
1212
import org.sterl.spring.persistent_tasks.api.TriggerKey;
13-
import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryLastStateEntity;
1413
import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryDetailEntity;
14+
import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryLastStateEntity;
1515
import org.sterl.spring.persistent_tasks.history.repository.TriggerHistoryDetailRepository;
1616
import org.sterl.spring.persistent_tasks.history.repository.TriggerHistoryLastStateRepository;
1717
import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus;

core/src/main/java/org/sterl/spring/persistent_tasks/history/api/HistoryConverter.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package org.sterl.spring.persistent_tasks.history.api;
22

3+
import org.springframework.lang.NonNull;
34
import org.sterl.spring.persistent_tasks.api.Trigger;
4-
import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryLastStateEntity;
55
import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryDetailEntity;
6+
import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryLastStateEntity;
67
import org.sterl.spring.persistent_tasks.shared.ExtendetConvert;
78
import org.sterl.spring.persistent_tasks.shared.converter.ToTrigger;
89

@@ -11,8 +12,9 @@ interface HistoryConverter {
1112
enum FromLastTriggerStateEntity implements ExtendetConvert<TriggerHistoryLastStateEntity, Trigger> {
1213
INSTANCE;
1314

15+
@NonNull
1416
@Override
15-
public Trigger convert(TriggerHistoryLastStateEntity source) {
17+
public Trigger convert(@NonNull TriggerHistoryLastStateEntity source) {
1618
var result = ToTrigger.INSTANCE.convert(source);
1719
result.setId(source.getId());
1820
result.setInstanceId(source.getId());
@@ -23,8 +25,9 @@ public Trigger convert(TriggerHistoryLastStateEntity source) {
2325
enum FromTriggerStateDetailEntity implements ExtendetConvert<TriggerHistoryDetailEntity, Trigger> {
2426
INSTANCE;
2527

28+
@NonNull
2629
@Override
27-
public Trigger convert(TriggerHistoryDetailEntity source) {
30+
public Trigger convert(@NonNull TriggerHistoryDetailEntity source) {
2831
var result = ToTrigger.INSTANCE.convert(source);
2932
result.setId(source.getId());
3033
result.setInstanceId(source.getInstanceId());

0 commit comments

Comments
 (0)