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
35 changes: 20 additions & 15 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,48 @@
# Changelog

## v1.5.0

- Adjusted transaction handling for trigger life cycle events
- Base event entry is only written for done/finished trigger
- Base statistics added for a task

## v1.4.6 - (2025-01-08)

- Trigger history with more details - not waiting for the transaction
- Trigger history with more details - not waiting for the transaction

## v1.4.5 - (2025-01-08)

- Adjusted path matching to support sub routes for an SPA web app
- Adjusted path matching to support sub routes for an SPA web app

## v1.4.4 - (2025-01-08)

- Fixed UI routing
- added support for thymeleaf - adding index.html to template folder
- Fixed UI routing
- added support for thymeleaf - adding index.html to template folder

## v1.4.3 - (2025-01-08)

- Scheduler service leaves current transaction before executing task
- Scheduler service leaves current transaction before executing task

## v1.4.2 - (2025-01-06)

- Fixed count by TaskId
- added search by ID to the UI
- added search by task to history
- Fixed count by TaskId
- added search by ID to the UI
- added search by task to history

## v1.4.1 - (2025-01-06)

- Added state to the TriggerLifeCycleEvent
- @Transactional annotation is taken from the method first
- Added state to the TriggerLifeCycleEvent
- @Transactional annotation is taken from the method first

## v1.4.0 - (2025-01-05)

- @Transactional Annotation support
- PersistentTask instead of Task or SpringBeanTask

- @Transactional Annotation support
- PersistentTask instead of Task or SpringBeanTask

## v1.3.1 - (2025-01-02)

- Bugfixes
- Sprign Transaction Template support
- Bugfixes
- Sprign Transaction Template support

## v1.3.0 - (2025-01-01)

Expand Down
10 changes: 7 additions & 3 deletions RUN_AND_BUILD.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
mvn versions:display-dependency-updates
mvn versions:set -DnewVersion=1.4.6 -DgenerateBackupPoms=false
git tag -a v1.4.6 -m "v1.4.6 release"
mvn versions:set -DnewVersion=1.4.7-SNAPSHOT -DgenerateBackupPoms=false
mvn versions:set -DnewVersion=1.5.0 -DgenerateBackupPoms=false
git tag -a v1.5.0 -m "v1.5.0 release"
mvn versions:set -DnewVersion=1.5.1-SNAPSHOT -DgenerateBackupPoms=false

## postgres

docker run --name pg-container -e POSTGRES_USER=sa -e POSTGRES_PASSWORD=veryStrong123 -p 5432:5432 -d postgres

## azure-sql-edge

docker run --cap-add SYS_PTRACE -e 'ACCEPT_EULA=Y' -e 'MSSQL_SA_PASSWORD=veryStrong123' -p 1433:1433 --name azuresqledge -d mcr.microsoft.com/azure-sql-edge

## MariaDB

docker run -e MYSQL_ROOT_PASSWORD=veryStrong123 -e MYSQL_DATABASE=testdb -e MYSQL_USER=sa -e MYSQL_PASSWORD=veryStrong123 -p 3306:3306 -d mariadb:latest

## MySQL

docker run -e MYSQL_ROOT_PASSWORD=veryStrong123 -e MYSQL_DATABASE=testdb -e MYSQL_USER=sa -e MYSQL_PASSWORD=veryStrong123 -p 3306:3306 -d mysql
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.util.concurrent.Future;

import org.springframework.context.event.EventListener;
import org.springframework.data.domain.Pageable;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -55,30 +56,48 @@ public Optional<TriggerData> getLastTriggerData(TriggerKey key) {
}
}

public Optional<TriggerData> getLastDetailData(TriggerKey key) {
var data = historyService.findAllDetailsForKey(key, Pageable.ofSize(1));
if (data.isEmpty()) return Optional.empty();
return Optional.of(data.getContent().get(0).getData());
}

@EventListener
void queue(TriggerTaskCommand<? extends Serializable> event) {
if (event.triggers().size() == 1) {
runOrQueue(event.triggers().iterator().next());
} else {
queueAll(event.triggers());
queue(event.triggers());
}
}

/**
* Queues the given triggers.
* Queues/updates the given triggers, if the {@link TriggerKey} is already present
*
* @param <T> the state type
* @param triggers the triggers to add
* @return the {@link TriggerKey}
*/
@Transactional(timeout = 10)
@NonNull
public <T extends Serializable> List<TriggerKey> queueAll(Collection<AddTriggerRequest<T>> triggers) {
public <T extends Serializable> List<TriggerKey> queue(Collection<AddTriggerRequest<T>> triggers) {
return triggers.stream() //
.map(t -> triggerService.queue(t)) //
.map(TriggerEntity::getKey) //
.toList();
}
/**
* Queues/updates the given trigger, if the {@link TriggerKey} is already present.
*
* @param <T> the state type
* @param trigger the trigger to add
* @return the {@link TriggerKey}
*/
@Transactional(timeout = 5)
@NonNull
public <T extends Serializable> TriggerKey queue(AddTriggerRequest<T> trigger) {
return triggerService.queue(trigger).getKey();
}

/**
* Runs the given trigger if a free threads are available
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import org.springframework.boot.autoconfigure.AutoConfigurationPackage;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

@Configuration
@EnableScheduling
@EnableAsync
@AutoConfigurationPackage(basePackageClasses = EnableSpringPersistentTasks.class)
@ComponentScan(basePackageClasses = EnableSpringPersistentTasks.class)
public class SpringPersistentTasksConfig {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.sterl.spring.persistent_tasks.api;

import java.time.OffsetDateTime;

public record TaskStatusHistoryOverview(
String taskName,
TriggerStatus status,
Long executionCount,
OffsetDateTime firstRun,
OffsetDateTime lastRun,
Number maxDurationMs,
Number minDurationMs,
Number avgDurationMs,
Number avgExecutionCount
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import java.time.OffsetDateTime;

import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus;

import lombok.Data;

@Data
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.sterl.spring.persistent_tasks.shared.model;
package org.sterl.spring.persistent_tasks.api;

import java.util.EnumSet;
import java.util.Set;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.lang.Nullable;
import org.sterl.spring.persistent_tasks.api.TaskStatusHistoryOverview;
import org.sterl.spring.persistent_tasks.api.TriggerKey;
import org.sterl.spring.persistent_tasks.api.TriggerStatus;
import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryDetailEntity;
import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryLastStateEntity;
import org.sterl.spring.persistent_tasks.history.repository.TriggerHistoryDetailRepository;
import org.sterl.spring.persistent_tasks.history.repository.TriggerHistoryLastStateRepository;
import org.sterl.spring.persistent_tasks.shared.model.TriggerStatus;
import org.sterl.spring.persistent_tasks.shared.stereotype.TransactionalService;
import org.sterl.spring.persistent_tasks.trigger.TriggerService;
import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity;
Expand Down Expand Up @@ -64,7 +65,7 @@ public Page<TriggerHistoryDetailEntity> findAllDetailsForKey(TriggerKey key) {
return findAllDetailsForKey(key, PageRequest.of(0, 100));
}
public Page<TriggerHistoryDetailEntity> findAllDetailsForKey(TriggerKey key, Pageable page) {
page = sortByIdIfNeeded(page);
page = applyDefaultSortIfNeeded(page);
return triggerHistoryDetailRepository.listKnownStatusFor(key, page);
}

Expand Down Expand Up @@ -97,7 +98,7 @@ public long countTriggers(TriggerKey key) {
public Page<TriggerHistoryLastStateEntity> findTriggerState(
@Nullable TriggerKey key, Pageable page) {

page = sortByIdIfNeeded(page);
page = applyDefaultSortIfNeeded(page);
if (key == null) return triggerHistoryLastStateRepository.findAll(page);
if (key.getId() == null && key.getTaskName() == null) return triggerHistoryLastStateRepository.findAll(page);
if (key.getId() == null && key.getTaskName() != null) {
Expand All @@ -109,11 +110,15 @@ public Page<TriggerHistoryLastStateEntity> findTriggerState(
page);
}

private Pageable sortByIdIfNeeded(Pageable page) {
private Pageable applyDefaultSortIfNeeded(Pageable page) {
if (page.getSort() == Sort.unsorted()) {
return PageRequest.of(page.getPageNumber(), page.getPageSize(),
Sort.by(Direction.DESC, "id"));
Sort.by(Direction.DESC, "data.createdTime", "id"));
}
return page;
}

public List<TaskStatusHistoryOverview> taskStatusHistory() {
return triggerHistoryLastStateRepository.listTriggerStatus();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.web.PageableDefault;
import org.springframework.data.web.PagedModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.sterl.spring.persistent_tasks.api.TaskStatusHistoryOverview;
import org.sterl.spring.persistent_tasks.api.Trigger;
import org.sterl.spring.persistent_tasks.api.TriggerKey;
import org.sterl.spring.persistent_tasks.history.HistoryService;
Expand All @@ -32,12 +32,16 @@ public List<Trigger> listInstances(@PathVariable("instanceId") long instanceId)
return FromTriggerStateDetailEntity.INSTANCE.convert( //
historyService.findAllDetailsForInstance(instanceId));
}
@GetMapping("task-status-history")
public List<TaskStatusHistoryOverview> taskStatusHistory() {
return historyService.taskStatusHistory();
}

@GetMapping("history")
public PagedModel<Trigger> list(
@RequestParam(name = "id", required = false) String id,
@RequestParam(name = "taskName", required = false) String taskName,
@PageableDefault(size = 100, direction = Direction.DESC, sort = "id") Pageable pageable) {
@PageableDefault(size = 100) Pageable pageable) {

return FromLastTriggerStateEntity.INSTANCE.toPage( //
historyService.findTriggerState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
import java.time.OffsetDateTime;

import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryDetailEntity;
import org.sterl.spring.persistent_tasks.history.model.TriggerHistoryLastStateEntity;
import org.sterl.spring.persistent_tasks.history.repository.TriggerHistoryDetailRepository;
import org.sterl.spring.persistent_tasks.history.repository.TriggerHistoryLastStateRepository;
import org.sterl.spring.persistent_tasks.shared.model.TriggerData;
import org.sterl.spring.persistent_tasks.shared.stereotype.TransactionalCompontant;
import org.sterl.spring.persistent_tasks.trigger.event.TriggerLifeCycleEvent;
import org.sterl.spring.persistent_tasks.trigger.model.TriggerEntity;
import org.sterl.spring.persistent_tasks.trigger.event.TriggerRunningEvent;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -23,27 +27,46 @@ public class TriggerHistoryComponent {
private final TriggerHistoryLastStateRepository triggerHistoryLastStateRepository;
private final TriggerHistoryDetailRepository triggerHistoryDetailRepository;

public void write(TriggerEntity e) {
var state = new TriggerHistoryLastStateEntity();
state.setId(e.getId());
state.setData(e.getData().toBuilder().build());
triggerHistoryLastStateRepository.save(state);
// we have to ensure to run in an own transaction
// as if the trigger fails, a rollback would also remove this entry
// furthermore async to ensure that we would not block
// as REQURES_NEW would block two DB connections ...
@Async
@Transactional(timeout = 10)
@EventListener
public void onRunning(TriggerRunningEvent e) {
log.debug("Received event={} for {} new status={}",
e.getClass().getSimpleName(),
e.key(), e.status());

execute(e.id(), e.data(), false);
}

@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
void onPersistentTaskEvent(TriggerLifeCycleEvent e) {
if (e instanceof TriggerRunningEvent) return; // we have an own listener for that
log.debug("Received event={} for {} new status={}",
e.getClass().getSimpleName(),
e.key(), e.status());


execute(e.id(), e.data(), e.isDone());
}

public void execute(final long triggerId, final TriggerData data, boolean isDone) {
if (isDone) {
final var state = new TriggerHistoryLastStateEntity();
state.setId(triggerId);
state.setData(data.copy());
triggerHistoryLastStateRepository.save(state);
}

var detail = new TriggerHistoryDetailEntity();
detail.setInstanceId(e.getId());
detail.setData(e.getData().toBuilder()
detail.setInstanceId(triggerId);
detail.setData(data.toBuilder()
.state(null)
.createdTime(OffsetDateTime.now())
.build());
detail.getData().setCreatedTime(OffsetDateTime.now());
triggerHistoryDetailRepository.save(detail);
}

@Transactional(timeout = 10)
@EventListener
public void onPersistentTaskEvent(TriggerLifeCycleEvent triggerLifeCycleEvent) {
log.debug("Received event={} for {} new status={}",
triggerLifeCycleEvent.getClass().getSimpleName(),
triggerLifeCycleEvent.key(), triggerLifeCycleEvent.status());
write(triggerLifeCycleEvent.trigger());
}
}
Loading
Loading