Skip to content

Commit

Permalink
MODSCHED-24 Integrate Internal Route Discovery into Sidecar with Dyna…
Browse files Browse the repository at this point in the history
…mic Routing Switch, Replacing Kong (#118)
  • Loading branch information
dmtkachenko authored Dec 31, 2024
1 parent 23c326d commit 9dfe5d3
Show file tree
Hide file tree
Showing 30 changed files with 501 additions and 150 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
package org.folio.scheduler.domain.entity;

import static org.apache.commons.lang3.StringUtils.isEmpty;

import io.hypersistence.utils.hibernate.type.json.JsonBinaryType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.UUID;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.folio.scheduler.domain.dto.RoutingEntry;
import org.folio.scheduler.domain.dto.TimerDescriptor;
import org.folio.scheduler.domain.model.TimerType;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.annotations.Type;
import org.hibernate.type.SqlTypes;

@Data
@Entity
Expand All @@ -19,8 +27,15 @@ public class TimerDescriptorEntity {

@Id private UUID id;

@Enumerated(EnumType.STRING)
@JdbcTypeCode(SqlTypes.NAMED_ENUM)
@Column(name = "type", columnDefinition = "timer_type")
private TimerType type;

private String moduleName;

private String moduleId;

private String naturalKey;

@Type(JsonBinaryType.class)
Expand All @@ -32,24 +47,18 @@ public void setTimerDescriptor(TimerDescriptor timerDescriptor) {
this.naturalKey = toNaturalKey(timerDescriptor);
}

public static TimerDescriptorEntity of(TimerDescriptor timerDescriptor) {
var entity = new TimerDescriptorEntity();
entity.id = timerDescriptor.getId();
entity.timerDescriptor = timerDescriptor;
entity.naturalKey = toNaturalKey(timerDescriptor);
return entity;
}

public static String toNaturalKey(TimerDescriptor timerDescriptor) {
if (timerDescriptor.getRoutingEntry() == null) {
public static String toNaturalKey(TimerDescriptor td) {
RoutingEntry re = td.getRoutingEntry();
if (re == null) {
return null;
}

var methods = timerDescriptor.getRoutingEntry().getMethods() != null ? String.join(",",
timerDescriptor.getRoutingEntry().getMethods()) : "";
var path = timerDescriptor.getRoutingEntry().getPath() != null ? timerDescriptor.getRoutingEntry().getPath()
: timerDescriptor.getRoutingEntry().getPathPattern();
return String.format("%s#%s#%s", timerDescriptor.getModuleName() != null ? timerDescriptor.getModuleName() : "",
methods, path);
if (isEmpty(td.getModuleName())) {
throw new IllegalArgumentException("Module name is required");
}

var methods = re.getMethods() != null ? String.join(",", re.getMethods()) : "";
var path = re.getPath() != null ? re.getPath() : re.getPathPattern();
return String.format("%s#%s#%s#%s", td.getType(), td.getModuleName(), methods, path);
}
}
6 changes: 6 additions & 0 deletions src/main/java/org/folio/scheduler/domain/model/TimerType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.folio.scheduler.domain.model;

public enum TimerType {
USER,
SYSTEM
}
11 changes: 7 additions & 4 deletions src/main/java/org/folio/scheduler/integration/OkapiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,39 +8,42 @@
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestHeader;

@FeignClient(name = "okapi")
public interface OkapiClient {

String MODULE_HINT = "x-okapi-module-hint";

/**
* Performs GET HTTP Request.
*
* @param uri - uniform resource identifier
*/
@GetMapping(consumes = APPLICATION_JSON_VALUE)
void doGet(URI uri);
void doGet(URI uri, @RequestHeader(MODULE_HINT) String moduleHint);

/**
* Performs POST HTTP Request.
*
* @param uri - uniform resource identifier
*/
@PostMapping(consumes = APPLICATION_JSON_VALUE)
void doPost(URI uri);
void doPost(URI uri, @RequestHeader(MODULE_HINT) String moduleHint);

/**
* Performs PUT HTTP Request.
*
* @param uri - uniform resource identifier
*/
@PutMapping(consumes = APPLICATION_JSON_VALUE)
void doPut(URI uri);
void doPut(URI uri, @RequestHeader(MODULE_HINT) String moduleHint);

/**
* Performs DELETE HTTP Request.
*
* @param uri - uniform resource identifier
*/
@DeleteMapping(consumes = APPLICATION_JSON_VALUE)
void doDelete(URI uri);
void doDelete(URI uri, @RequestHeader(MODULE_HINT) String moduleHint);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import static java.util.Collections.singletonList;
import static org.apache.commons.collections4.CollectionUtils.isEmpty;
import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
import static org.folio.common.utils.CollectionUtils.mapItems;
import static org.folio.scheduler.domain.model.TimerType.SYSTEM;
import static org.folio.scheduler.utils.OkapiRequestUtils.getStaticPath;
import static org.folio.spring.integration.XOkapiHeaders.TENANT;
import static org.folio.spring.integration.XOkapiHeaders.USER_ID;
Expand All @@ -18,6 +20,7 @@
import org.folio.common.utils.SemverUtils;
import org.folio.scheduler.domain.dto.RoutingEntry;
import org.folio.scheduler.domain.dto.TimerDescriptor;
import org.folio.scheduler.domain.dto.TimerType;
import org.folio.scheduler.integration.kafka.model.ResourceEvent;
import org.folio.scheduler.integration.keycloak.SystemUserService;
import org.folio.scheduler.service.SchedulerTimerService;
Expand Down Expand Up @@ -60,21 +63,24 @@ public void handleScheduledJobEvent(ConsumerRecord<String, ResourceEvent> consum

private void createTimers(ResourceEvent event) {
try (var ignored = new FolioExecutionContextSetter(folioModuleMetadata, prepareContextHeaders(event.getTenant()))) {
var moduleName = SemverUtils.getName(event.getNewValue().getModuleId());
var moduleId = event.getNewValue().getModuleId();
var moduleName = SemverUtils.getName(moduleId);
var timers = event.getNewValue().getTimers();

logCreatingTimers(timers);
for (var routingEntry : timers) {
var timerDescriptor = new TimerDescriptor().enabled(TRUE).moduleName(moduleName).routingEntry(routingEntry);
schedulerTimerService.create(timerDescriptor);
schedulerTimerService.create(createTimerDescriptor(routingEntry, moduleName, moduleId));
}
}
}

private void updateTimers(ResourceEvent resourceEvent) {
try (var ignored = new FolioExecutionContextSetter(folioModuleMetadata,
prepareContextHeaders(resourceEvent.getTenant()))) {
var moduleName = SemverUtils.getName(resourceEvent.getNewValue().getModuleId());
var timers = schedulerTimerService.findByModuleName(moduleName);
var moduleId = resourceEvent.getNewValue().getModuleId();
var moduleName = SemverUtils.getName(moduleId);

var timers = schedulerTimerService.findByModuleNameAndType(moduleName, SYSTEM);
if (isNotEmpty(timers)) {
logDeletingTimers(timers);
for (var timer : timers) {
Expand All @@ -85,8 +91,7 @@ private void updateTimers(ResourceEvent resourceEvent) {
var timerToCreate = resourceEvent.getNewValue();
logCreatingTimers(timerToCreate.getTimers());
for (var routingEntry : timerToCreate.getTimers()) {
var timerDescriptor = new TimerDescriptor().enabled(TRUE).moduleName(moduleName).routingEntry(routingEntry);
schedulerTimerService.create(timerDescriptor);
schedulerTimerService.create(createTimerDescriptor(routingEntry, moduleName, moduleId));
}
}
}
Expand All @@ -95,7 +100,7 @@ private void deleteTimers(ResourceEvent resourceEvent) {
try (var ignored = new FolioExecutionContextSetter(folioModuleMetadata,
prepareContextHeaders(resourceEvent.getTenant()))) {
var moduleName = SemverUtils.getName(resourceEvent.getOldValue().getModuleId());
var timers = schedulerTimerService.findByModuleName(moduleName);
var timers = schedulerTimerService.findByModuleNameAndType(moduleName, SYSTEM);
if (isEmpty(timers)) {
return;
}
Expand All @@ -114,18 +119,26 @@ private Map<String, Collection<String>> prepareContextHeaders(String tenant) {
return headers;
}

private static TimerDescriptor createTimerDescriptor(RoutingEntry routingEntry, String moduleName, String moduleId) {
return new TimerDescriptor().enabled(TRUE)
.type(TimerType.SYSTEM)
.moduleName(moduleName)
.moduleId(moduleId)
.routingEntry(routingEntry);
}

private static String getRoutingEntryKey(RoutingEntry routingEntry) {
var methods = String.join("|", routingEntry.getMethods());
return methods + " " + getStaticPath(routingEntry);
}

private static void logCreatingTimers(List<RoutingEntry> entries) {
var methods = entries.stream().map(KafkaMessageListener::getRoutingEntryKey).toList();
log.info("Processing scheduled job event from kafka: timers = {}", methods);
log.info("Processing scheduled job event from kafka: timers = {}",
() -> mapItems(entries, KafkaMessageListener::getRoutingEntryKey));
}

private static void logDeletingTimers(List<TimerDescriptor> timers) {
var methods = timers.stream().map(t -> getRoutingEntryKey(t.getRoutingEntry())).toList();
log.info("Deleting timers: timers = {}", methods);
log.info("Deleting timers: timers = {}",
() -> mapItems(timers, t -> getRoutingEntryKey(t.getRoutingEntry())));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,13 @@
@Mapper(componentModel = "spring")
public interface TimerDescriptorMapper {

/**
* Converts {@link TimerDescriptorEntity} to {@link TimerDescriptor} object.
*
* @param entity - {@link TimerDescriptorEntity} object
* @return converted {@link TimerDescriptor} object
*/
TimerDescriptor convert(TimerDescriptorEntity entity);

/**
* Converts {@link TimerDescriptor} to {@link TimerDescriptorEntity} object.
*
* @param descriptor - {@link TimerDescriptor} object
* @return converted {@link TimerDescriptorEntity} object
*/
@Mapping(target = "naturalKey", ignore = true)
@Mapping(target = "timerDescriptor", source = "descriptor")
TimerDescriptorEntity convert(TimerDescriptor descriptor);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
import java.util.Optional;
import java.util.UUID;
import org.folio.scheduler.domain.entity.TimerDescriptorEntity;
import org.folio.scheduler.domain.model.TimerType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface SchedulerTimerRepository extends JpaRepository<TimerDescriptorEntity, UUID> {

List<TimerDescriptorEntity> findByModuleName(String moduleName);
List<TimerDescriptorEntity> findByModuleNameAndType(String moduleName, TimerType type);

Optional<TimerDescriptorEntity> findByNaturalKey(String naturalKey);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package org.folio.scheduler.service;

import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.folio.common.utils.CollectionUtils.mapItems;
import static org.folio.scheduler.utils.TimerDescriptorUtils.evalModuleName;

import jakarta.persistence.EntityExistsException;
import jakarta.persistence.EntityNotFoundException;
Expand All @@ -13,6 +16,7 @@
import org.folio.scheduler.domain.dto.TimerDescriptor;
import org.folio.scheduler.domain.entity.TimerDescriptorEntity;
import org.folio.scheduler.domain.model.SearchResult;
import org.folio.scheduler.domain.model.TimerType;
import org.folio.scheduler.exception.RequestValidationException;
import org.folio.scheduler.mapper.TimerDescriptorMapper;
import org.folio.scheduler.repository.SchedulerTimerRepository;
Expand Down Expand Up @@ -41,9 +45,9 @@ public Optional<TimerDescriptor> findById(UUID uuid) {
}

@Transactional(readOnly = true)
public List<TimerDescriptor> findByModuleName(String moduleName) {
return schedulerTimerRepository.findByModuleName(moduleName).stream().map(TimerDescriptorEntity::getTimerDescriptor)
.toList();
public List<TimerDescriptor> findByModuleNameAndType(String moduleName, TimerType type) {
return mapItems(schedulerTimerRepository.findByModuleNameAndType(moduleName, type),
TimerDescriptorEntity::getTimerDescriptor);
}

/**
Expand Down Expand Up @@ -89,16 +93,15 @@ public TimerDescriptor create(TimerDescriptor timerDescriptor) {
validate(timerDescriptor);

timerDescriptor.setId(defaultIfNull(id, UUID.randomUUID()));
timerDescriptor.setModuleName(evalModuleName(timerDescriptor));

var naturalKey = TimerDescriptorEntity.toNaturalKey(timerDescriptor);
return schedulerTimerRepository.findByNaturalKey(naturalKey).map(existingTimer -> {
timerDescriptor.setId(existingTimer.getId());
return doUpdate(timerDescriptor);
}).orElseGet(() -> {
var entity = timerDescriptorMapper.convert(timerDescriptor);
var savedEntity = schedulerTimerRepository.save(entity);
jobSchedulingService.schedule(timerDescriptor);
return savedEntity.getTimerDescriptor();
});
return schedulerTimerRepository.findByNaturalKey(naturalKey)
.map(existingTimer -> {
timerDescriptor.setId(existingTimer.getId());
return doUpdate(timerDescriptor);
})
.orElseGet(() -> doCreate(timerDescriptor));
}

/**
Expand All @@ -120,25 +123,11 @@ public TimerDescriptor update(UUID uuid, TimerDescriptor newDescriptor) {
}

validate(newDescriptor);
newDescriptor.setModuleName(evalModuleName(newDescriptor));

return doUpdate(newDescriptor);
}

protected TimerDescriptor doUpdate(TimerDescriptor newDescriptor) {
var oldTimerDescriptor =
schedulerTimerRepository.findById(newDescriptor.getId()).map(TimerDescriptorEntity::getTimerDescriptor)
.orElseThrow(
() -> new EntityNotFoundException("Unable to find timer descriptor with id " + newDescriptor.getId()));

newDescriptor.modified(true);
var convertedValue = timerDescriptorMapper.convert(newDescriptor);
var updatedEntity = schedulerTimerRepository.save(convertedValue);
var timerDescriptor = updatedEntity.getTimerDescriptor();
jobSchedulingService.reschedule(oldTimerDescriptor, timerDescriptor);

return timerDescriptor;
}

/**
* Deletes timer descriptor by id.
*
Expand All @@ -164,10 +153,40 @@ public void deleteAll() {
}
}

protected void validate(TimerDescriptor timerDescriptor) {
private void validate(TimerDescriptor timerDescriptor) {
if (timerDescriptor.getRoutingEntry().getMethods() != null
&& timerDescriptor.getRoutingEntry().getMethods().size() > 1) {
throw new IllegalArgumentException("Only 1 method is allowed per timer");
}

if (isEmpty(timerDescriptor.getModuleId()) && isEmpty(timerDescriptor.getModuleName())) {
throw new IllegalArgumentException("Module id or module name is required");
}

if (timerDescriptor.getType() == null) {
throw new IllegalArgumentException("Timer type is required");
}
}

private TimerDescriptor doCreate(TimerDescriptor timerDescriptor) {
var entity = timerDescriptorMapper.convert(timerDescriptor);
var savedEntity = schedulerTimerRepository.save(entity);
jobSchedulingService.schedule(timerDescriptor);
return savedEntity.getTimerDescriptor();
}

private TimerDescriptor doUpdate(TimerDescriptor newDescriptor) {
var oldTimerDescriptor =
schedulerTimerRepository.findById(newDescriptor.getId()).map(TimerDescriptorEntity::getTimerDescriptor)
.orElseThrow(
() -> new EntityNotFoundException("Unable to find timer descriptor with id " + newDescriptor.getId()));

newDescriptor.modified(true);
var convertedValue = timerDescriptorMapper.convert(newDescriptor);
var updatedEntity = schedulerTimerRepository.save(convertedValue);
var timerDescriptor = updatedEntity.getTimerDescriptor();
jobSchedulingService.reschedule(oldTimerDescriptor, timerDescriptor);

return timerDescriptor;
}
}
Loading

0 comments on commit 9dfe5d3

Please sign in to comment.