Skip to content

Commit 8cf01aa

Browse files
committed
added transaction behavior test
1 parent f828d24 commit 8cf01aa

File tree

9 files changed

+205
-147
lines changed

9 files changed

+205
-147
lines changed

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
import java.io.Serializable;
44

55
/**
6-
* A spring persistentTask which state is saved in a {@link Trigger}.
7-
*
8-
* @param <T> the state type
6+
* A Spring persistent task whose state is saved in a {@link Trigger}.
7+
*
8+
* <p>This interface defines a task that accepts a state of type <code>T</code> and
9+
* provides default implementations for retry strategies.
10+
*
11+
* @param <T> the type of the state, which must be {@link Serializable}
912
*/
1013
@FunctionalInterface
1114
public interface PersistentTask<T extends Serializable> {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.sterl.spring.persistent_tasks.api;
2+
3+
import java.io.Serializable;
4+
5+
/**
6+
* Similar to {@link PersistentTask} but specifically for transactional workloads.
7+
* Use this interface when the task execution should be wrapped in a transaction.
8+
*
9+
* <p>This interface ensures that the task's execution is transactional, meaning that it will
10+
* be executed within a transaction context, along with the state update and the dispatching of
11+
* relevant events.
12+
*
13+
* @param <T> the type of the state, which must be {@link Serializable}
14+
*/
15+
@FunctionalInterface
16+
public interface TransactionalTask<T extends Serializable> extends PersistentTask<T> {
17+
/**
18+
* Whether the persistentTask is transaction or not. If <code>true</code> the execution
19+
* is wrapped into the default transaction template together with the state update
20+
* and the following events:
21+
* <ol>
22+
* <li>org.sterl.spring.persistent_tasks.trigger.event.TriggerRunningEvent</li>
23+
* <li>org.sterl.spring.persistent_tasks.trigger.event.TriggerSuccessEvent</li>
24+
* </ol>
25+
* @return {@code true} if the persistentTask is transactional; {@code false} otherwise.
26+
*/
27+
default boolean isTransactional() {
28+
return true;
29+
}
30+
}

core/src/main/java/org/sterl/spring/persistent_tasks/task/TaskService.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ public Set<TaskId<? extends Serializable>> findAllTaskIds() {
2727
return this.taskRepository.all();
2828
}
2929

30-
3130
public <T extends Serializable> Optional<PersistentTask<T>> get(TaskId<T> id) {
3231
return taskRepository.get(id);
3332
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package org.sterl.spring.persistent_tasks.task.util;
2+
3+
import java.io.Serializable;
4+
import java.lang.annotation.Annotation;
5+
6+
import org.springframework.aop.framework.AopProxyUtils;
7+
import org.springframework.core.annotation.AnnotationUtils;
8+
import org.springframework.util.ReflectionUtils;
9+
import org.sterl.spring.persistent_tasks.api.PersistentTask;
10+
11+
public abstract class ReflectionUtil {
12+
13+
public static <A extends Annotation> A getAnnotation(PersistentTask<? extends Serializable> inTask, Class<A> searchFor) {
14+
var task = AopProxyUtils.ultimateTargetClass(inTask);
15+
A result = AnnotationUtils.findAnnotation(task, searchFor);
16+
if (result != null) return result;
17+
18+
var targetMethod = ReflectionUtils.findMethod(task, "accept", Serializable.class);
19+
if (targetMethod == null) return null;
20+
21+
result = AnnotationUtils.findAnnotation(targetMethod, searchFor);
22+
return result;
23+
}
24+
}

core/src/test/java/org/sterl/spring/persistent_tasks/AbstractSpringTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,14 @@ protected Optional<TriggerEntity> runNextTrigger() {
163163

164164
@BeforeEach
165165
public void beforeEach() throws Exception {
166-
hibernateAsserts.reset();
167166
triggerService.deleteAll();
168167
historyService.deleteAll();
169168
asserts.clear();
170169
schedulerA.setMaxThreads(10);
171170
schedulerB.setMaxThreads(20);
172171
schedulerA.start();
173172
schedulerB.start();
173+
hibernateAsserts.reset();
174174
}
175175

176176
@AfterEach
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package org.sterl.spring.persistent_tasks.task;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import org.junit.jupiter.api.Test;
6+
import org.junit.jupiter.params.ParameterizedTest;
7+
import org.junit.jupiter.params.provider.ValueSource;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.beans.factory.annotation.Qualifier;
10+
import org.springframework.context.annotation.Bean;
11+
import org.springframework.context.annotation.Configuration;
12+
import org.springframework.stereotype.Component;
13+
import org.springframework.transaction.annotation.Propagation;
14+
import org.springframework.transaction.annotation.Transactional;
15+
import org.springframework.transaction.support.DefaultTransactionDefinition;
16+
import org.sterl.spring.persistent_tasks.AbstractSpringTest;
17+
import org.sterl.spring.persistent_tasks.api.PersistentTask;
18+
import org.sterl.spring.persistent_tasks.api.TaskId.TaskTriggerBuilder;
19+
import org.sterl.spring.persistent_tasks.api.TransactionalTask;
20+
import org.sterl.spring.persistent_tasks.task.util.ReflectionUtil;
21+
import org.sterl.spring.sample_app.person.PersonBE;
22+
import org.sterl.spring.sample_app.person.PersonRepository;
23+
24+
import lombok.RequiredArgsConstructor;
25+
26+
class TaskTransactionTest extends AbstractSpringTest {
27+
28+
@Component("transactionalClass")
29+
@Transactional(timeout = 5, propagation = Propagation.MANDATORY)
30+
@RequiredArgsConstructor
31+
static class TransactionalClass implements PersistentTask<String> {
32+
private final PersonRepository personRepository;
33+
@Override
34+
public void accept(String name) {
35+
personRepository.save(new PersonBE(name));
36+
}
37+
}
38+
@Component("transactionalMethod")
39+
@RequiredArgsConstructor
40+
static class TransactionalMethod implements PersistentTask<String> {
41+
private final PersonRepository personRepository;
42+
@Transactional(timeout = 6, propagation = Propagation.MANDATORY)
43+
@Override
44+
public void accept(String name) {
45+
personRepository.save(new PersonBE(name));
46+
}
47+
}
48+
49+
/**
50+
* A closure cannot be annotated, so we use a anonymous class
51+
*/
52+
@Configuration
53+
static class Config {
54+
@Bean("transactionalAnonymous")
55+
PersistentTask<String> transactionalAnonymous(PersonRepository personRepository) {
56+
return new PersistentTask<String>() {
57+
@Transactional(timeout = 7, propagation = Propagation.MANDATORY)
58+
@Override
59+
public void accept(String name) {
60+
personRepository.save(new PersonBE(name));
61+
}
62+
};
63+
}
64+
@Bean("transactionalClosure")
65+
TransactionalTask<String> transactionalClosure(PersonRepository personRepository) {
66+
return name -> {
67+
personRepository.save(new PersonBE(name));
68+
personRepository.save(new PersonBE(name));
69+
};
70+
}
71+
}
72+
73+
@Autowired PersonRepository personRepository;
74+
75+
@Autowired @Qualifier("transactionalClass")
76+
PersistentTask<String> transactionalClass;
77+
@Autowired @Qualifier("transactionalMethod")
78+
PersistentTask<String> transactionalMethod;
79+
@Autowired @Qualifier("transactionalAnonymous")
80+
PersistentTask<String> transactionalAnonymous;
81+
82+
@Test
83+
void testFindTransactionAnnotation() {
84+
var a = ReflectionUtil.getAnnotation(transactionalClass, Transactional.class);
85+
assertThat(a).isNotNull();
86+
assertThat(a.timeout()).isEqualTo(5);
87+
88+
a = ReflectionUtil.getAnnotation(transactionalMethod, Transactional.class);
89+
assertThat(a).isNotNull();
90+
assertThat(a.timeout()).isEqualTo(6);
91+
92+
a = ReflectionUtil.getAnnotation(transactionalAnonymous, Transactional.class);
93+
assertThat(a).isNotNull();
94+
assertThat(a.timeout()).isEqualTo(7);
95+
}
96+
97+
@ParameterizedTest
98+
@ValueSource(strings = {"transactionalClass", "transactionalMethod", "transactionalClosure"})
99+
void testTransactionalTask(String task) {
100+
// GIVEN
101+
var t = triggerService.queue(TaskTriggerBuilder
102+
.newTrigger(task, "test").build());
103+
104+
// WHEN
105+
personRepository.deleteAllInBatch();
106+
hibernateAsserts.reset();
107+
triggerService.run(t).get();
108+
109+
// THEN
110+
hibernateAsserts.assertTrxCount(1);
111+
assertThat(personRepository.count()).isEqualTo(2);
112+
}
113+
114+
public static DefaultTransactionDefinition convertTransactionalToDefinition(Transactional transactional) {
115+
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
116+
117+
// Map Transactional attributes to DefaultTransactionDefinition
118+
def.setIsolationLevel(transactional.isolation().value());
119+
def.setPropagationBehavior(transactional.propagation().value());
120+
def.setTimeout(transactional.timeout());
121+
def.setReadOnly(transactional.readOnly());
122+
// No direct mapping for 'rollbackFor' or 'noRollbackFor'
123+
// Set a name if desired (e.g., based on transactional class/method)
124+
def.setName("TransactionalDefinition");
125+
126+
return def;
127+
}
128+
129+
}

core/src/test/java/org/sterl/spring/persistent_tasks/task/TransactionAnnotationTest.java

Lines changed: 0 additions & 133 deletions
This file was deleted.

core/src/test/java/org/sterl/spring/persistent_tasks/trigger/TriggerServiceTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ void testAddTrigger() throws Exception {
5757
final var triggerId = subject.queue(trigger).getKey();
5858

5959
// THEN
60+
hibernateAsserts.assertTrxCount(1);
61+
// one for the trigger and two for the history
62+
hibernateAsserts.assertInsertCount(3);
63+
// AND
6064
final var e = subject.get(triggerId);
6165
assertThat(e).isPresent();
6266
assertThat(e.get().getData().getRunAt().toEpochSecond()).isEqualTo(triggerTime.toEpochSecond());

0 commit comments

Comments
 (0)