diff --git a/README.md b/README.md index dacfb12..6908fca 100644 --- a/README.md +++ b/README.md @@ -199,3 +199,10 @@ Disables Spring OAuth2 resource server for testing. ## s3-storage Amazon S3 support. + +# spring-workers-agent + +Framework for clustered scheduling based on Quartz and Spring Data. +Stores all data in a database (Postgres by default). + +Used as manageable alternative for Spring's `@Scheduled`. diff --git a/settings.gradle.kts b/settings.gradle.kts index 1887c54..afa55c2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -51,3 +51,6 @@ include("security-resource-server-custom-jwt-configuration") include("security-resource-server-test-jwt-configuration") include("security-jwt-common") include("s3-storage") +include("spring-workers-manager-core") +include("spring-workers-manager-agent") +include("spring-workers-manager-api") diff --git a/spring-workers-manager-agent/README.md b/spring-workers-manager-agent/README.md new file mode 100644 index 0000000..0d91ad8 --- /dev/null +++ b/spring-workers-manager-agent/README.md @@ -0,0 +1,77 @@ +# Workers Agent + +Framework for clustered scheduling based on Quartz and Spring Data. +Stores all data in a database (Postgres by default). + +Used as manageable alternative for Spring's `@Scheduled`. + +## How to +1. Add dependency + ``` + implementation project(":spring-workers-manager-agent") + ``` +1. Define a job by annotations to be scheduled in any component. + ``` + package ru.touchin + + @Component + class MyJob { + + @ScheduledAction + @InitTrigger(type = "CRON", expression = "0 15 * * * ?") + fun sayHello(){ + println("Hello, world!") + } + + } + ``` + +1. Enable job in `application.properties` + + ``` + workers.names=ru.touchin.MyJob + ``` + or: + ``` + workers.names=* + ``` + +1. Start the application. + +## Annotations +### @ScheduledAction +Registers method as action of some job. + +Parameters: +- `name` - name of job. Defaults to class full name. + Must be unique in application scope. + +### @Trigger +Declares default trigger for the job. +Default triggers are created when launching job first time. + +Parameters: +- `name` - Optional name for trigger. Defaults to some (maybe random) string. + Name must be unique in scope of corresponding job. +- `type` - Trigger type. See triggers types. + SpEL expressions are supported like in `@Scheduled` annotation of Spring. +- `expression` - The value for trigger. + SpEL expressions are supported like in `@Scheduled` annotation of Spring. + +## Configuration +### Enabling workers + +Agent ignores workers by default. To enable worker add its name to `worker.names` property. +Example: +``` +worker.names=com.eample.Job1,\ + com.example.Job2 +``` + +#### Patterns for names +`workers.names` support Glob-like patterns. +- Asterisk (`*`) symbol is for "zero or more any symbols" (as `.*` in regex) +- Question mark (`?`) is for "any single symbol" (as `.` in regex) + +## TODO +- External data source, unrelated to application code. diff --git a/spring-workers-manager-agent/build.gradle.kts b/spring-workers-manager-agent/build.gradle.kts new file mode 100644 index 0000000..6c473be --- /dev/null +++ b/spring-workers-manager-agent/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + id("kotlin") + id("kotlin-spring") + id("maven-publish") +} + +dependencies { + implementation(project(":common-spring")) + implementation(project(":common-spring-jpa")) + implementation(project(":spring-workers-manager-core")) + + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + implementation("org.springframework.data:spring-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-quartz") + + testImplementation(project(":common-spring-test-jpa")) + + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin") +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/AgentInitializer.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/AgentInitializer.kt new file mode 100644 index 0000000..fc5840f --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/AgentInitializer.kt @@ -0,0 +1,30 @@ +package ru.touchin.spring.workers.manager.agent + +import org.springframework.boot.context.event.ApplicationStartedEvent +import org.springframework.context.event.EventListener +import org.springframework.core.annotation.Order +import org.springframework.stereotype.Component +import ru.touchin.common.spring.Ordered +import ru.touchin.spring.workers.manager.agent.config.WorkerInitializer +import ru.touchin.spring.workers.manager.agent.scheduled.WorkerManagerWatcher +import ru.touchin.spring.workers.manager.core.config.LiquibaseRunner + +/** + * Prepares required resources and initializes agent. + */ +@Component +class AgentInitializer( + private val liquibase: LiquibaseRunner, + private val workerInitializer: WorkerInitializer, + private val workerWatcher: WorkerManagerWatcher +) { + + @EventListener(value = [ApplicationStartedEvent::class]) + @Order(Ordered.HIGH) + fun execute() { + liquibase.run() + workerInitializer.init() + workerWatcher.init() + } + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/AnnotationConfigCollectingBeanPostProcessor.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/AnnotationConfigCollectingBeanPostProcessor.kt new file mode 100644 index 0000000..7b89ed9 --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/AnnotationConfigCollectingBeanPostProcessor.kt @@ -0,0 +1,80 @@ +package ru.touchin.spring.workers.manager.agent.annotation_config + +import org.springframework.beans.factory.config.BeanPostProcessor +import org.springframework.stereotype.Component +import ru.touchin.spring.workers.manager.agent.annotation_config.job_factory.AnnotationConfigJobFactory +import ru.touchin.spring.workers.manager.agent.common.base.BaseJob +import java.lang.reflect.Method + +/** + * 1. Scans components for [ScheduledAction] annotation + * 2. Keeps metadata of that components + * 3. Creates [BaseJob] for methods created + */ +@Component +class AnnotationConfigCollectingBeanPostProcessor( + private val jobFactories: List +) : BeanPostProcessor { + + val jobs: MutableList = ArrayList() + + val jobName2Method: MutableMap = HashMap() + + /** + * Bean name -> class of this bean. + * + * Contains only entries for classes with [ScheduledAction] annotation. + */ + private val beanName2OriginalClass: MutableMap> = HashMap() + + override fun postProcessBeforeInitialization(bean: Any, beanName: String): Any? { + val hasMethodsForScheduling = bean.javaClass.declaredMethods + .any { it.isAnnotationPresent(ScheduledAction::class.java) } + + if (hasMethodsForScheduling) { + beanName2OriginalClass[beanName] = bean.javaClass + } + + return bean + } + + override fun postProcessAfterInitialization(bean: Any, beanName: String): Any? { + val clazz = beanName2OriginalClass[beanName] + ?: return bean + + val actionMethod = findActionMethod(clazz) + + val createdJobs = jobFactories.flatMap { it.create(bean, actionMethod) } + + createdJobs.forEach { + jobName2Method[it.getName()] = actionMethod + } + + jobs.addAll(createdJobs) + + return bean + } + + companion object { + + private fun findActionMethod(clazz: Class<*>): Method { + return clazz.declaredMethods + .filter { it.isAnnotationPresent(ScheduledAction::class.java) } + .also { annotatedMethods -> + check(annotatedMethods.size <= 1) { + "Class `${clazz.name}` has more that one methods with annotation @Scheduled. " + + "Methods: $annotatedMethods" + } + } + .onEach { annotatedMethod -> + check(annotatedMethod.parameters.isEmpty()) { + "Method ${clazz.name}:${annotatedMethod.name}' must not have arguments for scheduling, " + + "but requires ${annotatedMethod.parameters.size} parameters" + } + } + .single() + } + + } + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/AnnotationConfigInitialTriggerDescriptorsProvider.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/AnnotationConfigInitialTriggerDescriptorsProvider.kt new file mode 100644 index 0000000..70e9b58 --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/AnnotationConfigInitialTriggerDescriptorsProvider.kt @@ -0,0 +1,41 @@ +package ru.touchin.spring.workers.manager.agent.annotation_config + +import org.springframework.stereotype.Component +import org.springframework.util.LinkedMultiValueMap +import org.springframework.util.MultiValueMap +import ru.touchin.spring.workers.manager.agent.annotation_config.trigger_factory.AnnotationConfigTriggerFactory +import ru.touchin.spring.workers.manager.agent.trigger.InitialTriggerDescriptorsProvider +import ru.touchin.spring.workers.manager.core.trigger.dto.TriggerDescriptor +import ru.touchin.spring.workers.manager.core.trigger.models.TriggerDescriptorEntity +import ru.touchin.spring.workers.manager.core.trigger.services.dto.CreateTriggerDescriptor +import ru.touchin.spring.workers.manager.core.worker.dto.Worker +import ru.touchin.spring.workers.manager.core.worker.models.WorkerEntity + +@Component +class AnnotationConfigInitialTriggerDescriptorsProvider( + private val triggersCollector: AnnotationConfigCollectingBeanPostProcessor, + private val triggerFactories: List +) : InitialTriggerDescriptorsProvider { + + val jobName2Triggers: MultiValueMap = LinkedMultiValueMap() + + override fun applicableFor(worker: Worker): Boolean { + val actionMethod = triggersCollector.jobName2Method[worker.name] + ?: return false + + val triggers = triggerFactories.flatMap { it.create(worker, actionMethod) } + + if (triggers.isEmpty()) { + return false + } + + jobName2Triggers.addAll(worker.name, triggers) + + return true + } + + override fun createInitialTriggerDescriptors(worker: Worker): List { + return jobName2Triggers[worker.name].orEmpty() + } + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/AnnotationConfigJobProvider.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/AnnotationConfigJobProvider.kt new file mode 100644 index 0000000..18688c6 --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/AnnotationConfigJobProvider.kt @@ -0,0 +1,14 @@ +package ru.touchin.spring.workers.manager.agent.annotation_config + +import org.springframework.stereotype.Component +import ru.touchin.spring.workers.manager.agent.common.base.BaseJob +import ru.touchin.spring.workers.manager.agent.registry.JobProvider + +@Component +class AnnotationConfigJobProvider( + private val jobsCollector: AnnotationConfigCollectingBeanPostProcessor +) : JobProvider { + + override fun getJobs(): List = jobsCollector.jobs + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/InitTrigger.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/InitTrigger.kt new file mode 100644 index 0000000..5186f68 --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/InitTrigger.kt @@ -0,0 +1,15 @@ +package ru.touchin.spring.workers.manager.agent.annotation_config + +import java.lang.annotation.Inherited + +/** + * Adds default trigger to [ScheduledAction]. + * Default trigger is submitted if job is launched first time. + */ +@Inherited +@Target(AnnotationTarget.FUNCTION) +annotation class InitTrigger( + val name: String = "", + val type: String, + val expression: String +) diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/ScheduledAction.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/ScheduledAction.kt new file mode 100644 index 0000000..28f4c29 --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/ScheduledAction.kt @@ -0,0 +1,10 @@ +package ru.touchin.spring.workers.manager.agent.annotation_config + +@Target(AnnotationTarget.FUNCTION) +annotation class ScheduledAction( + /** + * Job name. Defaults to class name. + */ + val name: String = "" +) + diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/job_factory/AnnotationConfigJobFactory.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/job_factory/AnnotationConfigJobFactory.kt new file mode 100644 index 0000000..873258e --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/job_factory/AnnotationConfigJobFactory.kt @@ -0,0 +1,19 @@ +package ru.touchin.spring.workers.manager.agent.annotation_config.job_factory + +import ru.touchin.spring.workers.manager.agent.common.base.BaseJob +import java.lang.reflect.Method + +/** + * Invoked when found method with [ScheduledAction] annotation in some bean. + * + * Used to read jobs settings from annotations in components. + */ +interface AnnotationConfigJobFactory { + + /** + * Warning: As Spring could substitute actual beans with proxy-objects, + * you must carefully check if [actionMethod] is applicable for [bean]. + */ + fun create(bean: Any, actionMethod: Method): List + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/job_factory/ScheduledActionAnnotationConfigJobFactory.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/job_factory/ScheduledActionAnnotationConfigJobFactory.kt new file mode 100644 index 0000000..243b0fc --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/job_factory/ScheduledActionAnnotationConfigJobFactory.kt @@ -0,0 +1,50 @@ +package ru.touchin.spring.workers.manager.agent.annotation_config.job_factory + +import org.springframework.stereotype.Component +import ru.touchin.spring.workers.manager.agent.annotation_config.ScheduledAction +import ru.touchin.spring.workers.manager.agent.common.base.BaseJob +import java.lang.reflect.Method + +/** + * Creates job instances for every annotated action method. + */ +@Component +class ScheduledActionAnnotationConfigJobFactory : AnnotationConfigJobFactory { + + override fun create(bean: Any, actionMethod: Method): List { + val job = createJobForBean(bean, actionMethod) + + return listOf(job) + } + + companion object { + + private fun createJobForBean(bean: Any, annotatedMethod: Method): BaseJob { + val targetMethod = bean.javaClass.getMethod(annotatedMethod) + val annotation = annotatedMethod.getAnnotation(ScheduledAction::class.java) + + val jobName: String = annotation.name.takeIf { it.isNotBlank() } + ?: annotatedMethod.declaringClass.name + + return createJob(jobName) { targetMethod.invoke(bean) } + } + + private fun Class<*>.getMethod(sampleMethod: Method): Method { + return getMethod(sampleMethod.name, *sampleMethod.parameterTypes) + .apply { isAccessible = true } + } + + private fun createJob(jobName: String, func: () -> Unit): BaseJob = object : BaseJob { + + override fun getName() = jobName + + override fun run() { + func.invoke() + } + + } + + } + +} + diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/trigger_factory/AnnotationConfigTriggerFactory.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/trigger_factory/AnnotationConfigTriggerFactory.kt new file mode 100644 index 0000000..89906cd --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/trigger_factory/AnnotationConfigTriggerFactory.kt @@ -0,0 +1,14 @@ +package ru.touchin.spring.workers.manager.agent.annotation_config.trigger_factory + +import ru.touchin.spring.workers.manager.core.trigger.services.dto.CreateTriggerDescriptor +import ru.touchin.spring.workers.manager.core.worker.dto.Worker +import java.lang.reflect.Method + +/** + * Used to create initial triggers for new workers + */ +interface AnnotationConfigTriggerFactory { + + fun create(worker: Worker, actionMethod: Method): List + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/trigger_factory/TriggerAnnotationConfigTriggerFactory.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/trigger_factory/TriggerAnnotationConfigTriggerFactory.kt new file mode 100644 index 0000000..8d9b29a --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/annotation_config/trigger_factory/TriggerAnnotationConfigTriggerFactory.kt @@ -0,0 +1,49 @@ +package ru.touchin.spring.workers.manager.agent.annotation_config.trigger_factory + +import org.springframework.context.EmbeddedValueResolverAware +import org.springframework.stereotype.Component +import org.springframework.util.StringValueResolver +import ru.touchin.spring.workers.manager.agent.annotation_config.InitTrigger +import ru.touchin.spring.workers.manager.core.trigger.enums.TriggerType +import ru.touchin.spring.workers.manager.core.trigger.services.dto.CreateTriggerDescriptor +import ru.touchin.spring.workers.manager.core.worker.dto.Worker +import java.lang.reflect.Method + +/** + * Creates triggers for methods annotated with [ru.touchin.spring.workers.manager.agent.annotation_config.InitTrigger] annotation + */ +@Component +class TriggerAnnotationConfigTriggerFactory + : AnnotationConfigTriggerFactory, + EmbeddedValueResolverAware { + + lateinit var valueResolver: StringValueResolver + + override fun setEmbeddedValueResolver(resolver: StringValueResolver) { + valueResolver = resolver + } + + override fun create(worker: Worker, actionMethod: Method): List { + val triggerAnnotation = actionMethod.getAnnotation(InitTrigger::class.java) + ?: return emptyList() + + val resolvedType = valueResolver.resolveStringValue(triggerAnnotation.type) + val triggerType = TriggerType.find(resolvedType) + ?: throw IllegalArgumentException("Trigger type for name $resolvedType dies not exist") + + val expression = valueResolver.resolveStringValue(triggerAnnotation.expression) + ?: throw NullPointerException("Trigger for worker '${worker.name}' has null expression") + + val trigger = CreateTriggerDescriptor( + name = "${triggerType.name}_${expression.replace(" ", "_")}", + type = triggerType, + workerName = worker.name, + expression = valueResolver.resolveStringValue(triggerAnnotation.expression) + ?: throw NullPointerException("Trigger for worker '${worker.name}' has null expression"), + disabledAt = null, + ) + + return listOf(trigger) + } + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/common/base/BaseJob.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/common/base/BaseJob.kt new file mode 100644 index 0000000..ec9d17e --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/common/base/BaseJob.kt @@ -0,0 +1,9 @@ +package ru.touchin.spring.workers.manager.agent.common.base + +interface BaseJob { + + fun run() + + fun getName(): String = this::class.java.name + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/common/utils/Glob.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/common/utils/Glob.kt new file mode 100644 index 0000000..5444663 --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/common/utils/Glob.kt @@ -0,0 +1,54 @@ +package ru.touchin.spring.workers.manager.agent.common.utils + +/** + * Glob is simple replacement for Regex. + * It uses only two special chars: * (any substring) and ? (any char) + * This implementation has no support for characters escaping. + * Glob has no capturing features. + */ +object Glob { + + private const val ANY_SUBSTRING_SYMBOL = '*' + + private const val ANY_CHAR_SYMBOL = '?' + + /** + * Example: "I love patterns" matches "I * pa?t?er?s" + * + * Based on [StackOverflow answer](https://stackoverflow.com/a/3687031) + */ + fun matches(text: String, pattern: String): Boolean { + val starPosition = pattern.indexOf(ANY_SUBSTRING_SYMBOL) + + val headPattern: String = if (starPosition == -1) pattern else pattern.substring(0, starPosition) + + if (headPattern.length > text.length) { + return false + } + + // handle the part up to the first * + for (i in headPattern.indices) { + if ( + headPattern[i] != ANY_CHAR_SYMBOL + && headPattern[i] != text[i] + ) { + return false + } + } + + if (starPosition == -1) { + return headPattern.length == text.length + } + + val tailPattern: String = pattern.substring(starPosition + 1) + + for (i in headPattern.length..text.length) { + if (matches(text.substring(i), tailPattern)) { + return true + } + } + + return false + } + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/config/EnableWorkersManager.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/config/EnableWorkersManager.kt new file mode 100644 index 0000000..cbc6f34 --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/config/EnableWorkersManager.kt @@ -0,0 +1,10 @@ +package ru.touchin.spring.workers.manager.agent.config + +import org.springframework.context.annotation.Import +import ru.touchin.spring.workers.manager.WorkersManagerConfiguration + +/** + * Annotation to enable Workers Manager module in Spring components via annotations. + */ +@Import(WorkersManagerConfiguration::class) +annotation class EnableWorkersManager diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/config/WorkerInitializer.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/config/WorkerInitializer.kt new file mode 100644 index 0000000..2c1cd3b --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/config/WorkerInitializer.kt @@ -0,0 +1,39 @@ +package ru.touchin.spring.workers.manager.agent.config + +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional +import ru.touchin.spring.workers.manager.agent.trigger.services.TriggerDescriptorService +import ru.touchin.spring.workers.manager.agent.registry.JobDefinitionsRegistry +import ru.touchin.spring.workers.manager.core.worker.services.WorkerCoreService +import ru.touchin.spring.workers.manager.core.worker.services.WorkersStateService + +@Component +class WorkerInitializer( + private val triggerDescriptorAgentService: TriggerDescriptorService, + private val jobDefinitionsRegistry: JobDefinitionsRegistry, + private val workerCoreService: WorkerCoreService, + private val workerStateService: WorkersStateService, +) { + + @Transactional + fun init() { + initWorkers(jobDefinitionsRegistry.jobNames) + } + + private fun initWorkers(jobNames: Set) { + jobNames.forEach(this::getOrCreateWorkerWithTriggers) + } + + private fun getOrCreateWorkerWithTriggers(name: String) { + workerCoreService.getWithLock(name) + ?.let { workerStateService.start(it.name) } + ?: createWorkerWithTriggers(name) + } + + private fun createWorkerWithTriggers(name: String) { + val worker = workerCoreService.create(name) + + triggerDescriptorAgentService.createDefaultTriggerDescriptors(worker) + } + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/quartz/RunnableJob.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/quartz/RunnableJob.kt new file mode 100644 index 0000000..7ae3968 --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/quartz/RunnableJob.kt @@ -0,0 +1,36 @@ +package ru.touchin.spring.workers.manager.agent.quartz + +import org.quartz.Job +import org.quartz.JobBuilder +import org.quartz.JobDataMap +import org.quartz.JobExecutionContext +import org.quartz.JobExecutionException + +typealias JobFunction = (JobExecutionContext) -> Unit + +class RunnableJob : Job { + + override fun execute(context: JobExecutionContext) { + try { + @Suppress("UNCHECKED_CAST") + (context.jobDetail.jobDataMap[ACTION] as JobFunction).also { action -> + action.invoke(context) + } + } catch (e: Exception) { + throw JobExecutionException(e) + } + } + + companion object { + + private const val ACTION = "ACTION" + + fun initJobBuilder(action: JobFunction): JobBuilder { + return JobBuilder + .newJob(RunnableJob::class.java) + .usingJobData(JobDataMap(mapOf(ACTION to action))) + } + + } + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/registry/JobDefinitionsRegistry.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/registry/JobDefinitionsRegistry.kt new file mode 100644 index 0000000..6e1c848 --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/registry/JobDefinitionsRegistry.kt @@ -0,0 +1,64 @@ +package ru.touchin.spring.workers.manager.agent.registry + +import org.quartz.JobDetail +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import org.springframework.util.LinkedMultiValueMap +import ru.touchin.spring.workers.manager.agent.worker.executors.WorkerActionExecutor +import ru.touchin.spring.workers.manager.agent.quartz.RunnableJob +import ru.touchin.spring.workers.manager.agent.common.base.BaseJob +import ru.touchin.spring.workers.manager.agent.common.utils.Glob + +@Component +class JobDefinitionsRegistry( + @Value("#{'\${workers.names}'.split(',')}") + workersNamesPatterns: Set, + providers: List, + private val workerActionExecutor: WorkerActionExecutor +) { + + final val jobs: Map + + private final val jobDetails: Map + + final val jobNames: Set + + init { + val allJobs = providers.flatMap(JobProvider::getJobs) + + val name2jobsList = LinkedMultiValueMap() + + allJobs.forEach { job -> + name2jobsList.add(job.getName(), job) + } + + name2jobsList.forEach { (name, jobs) -> + check(jobs.size <= 1) { "There are ${jobs.size} jobs with name '$name'. Job names must be unique" } + } + + val name2job = name2jobsList + .toSingleValueMap() + .filterKeys { name -> workersNamesPatterns.any { pattern -> Glob.matches(name, pattern) } } + + jobNames = name2job.keys + + jobs = name2job + + jobDetails = name2job + .mapValues { (_, job) -> + createJobDetail(job) + } + } + + fun getJobDetail(jobName: String): JobDetail? = jobDetails[jobName] + + private fun createJobDetail(job: BaseJob): JobDetail { + return RunnableJob + .initJobBuilder { context -> + workerActionExecutor.executeJobAction(job, context.trigger) + } + .withIdentity(job.getName()) + .build() + } + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/registry/JobProvider.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/registry/JobProvider.kt new file mode 100644 index 0000000..1aa5ed3 --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/registry/JobProvider.kt @@ -0,0 +1,9 @@ +package ru.touchin.spring.workers.manager.agent.registry + +import ru.touchin.spring.workers.manager.agent.common.base.BaseJob + +interface JobProvider { + + fun getJobs(): List + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/registry/SimpleJobProvider.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/registry/SimpleJobProvider.kt new file mode 100644 index 0000000..2265717 --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/registry/SimpleJobProvider.kt @@ -0,0 +1,15 @@ +package ru.touchin.spring.workers.manager.agent.registry + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Component +import ru.touchin.spring.workers.manager.agent.common.base.BaseJob + +@Component +class SimpleJobProvider : JobProvider { + + @Autowired(required = false) + var jobBeans: List = emptyList() + + override fun getJobs(): List = jobBeans + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/registry/TriggersRegistry.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/registry/TriggersRegistry.kt new file mode 100644 index 0000000..6dc3bbf --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/registry/TriggersRegistry.kt @@ -0,0 +1,37 @@ +package ru.touchin.spring.workers.manager.agent.registry + +import org.quartz.Trigger +import org.springframework.stereotype.Component +import ru.touchin.spring.workers.manager.core.trigger.dto.TriggerDescriptor +import java.util.concurrent.ConcurrentHashMap + +@Component +class TriggersRegistry { + + // Concurrent impl to prevent java.util.ConcurrentModificationException + private val descriptors2triggers: MutableMap = ConcurrentHashMap() + + fun getTriggerByDescriptor(descriptor: TriggerDescriptor): Trigger? { + return descriptors2triggers[descriptor] + } + + fun getDescriptorByTrigger(trigger: Trigger): TriggerDescriptor? { + return descriptors2triggers + .entries + .firstOrNull { it.value == trigger } + ?.key + } + + fun getDescriptors(): List { + return descriptors2triggers.keys.toList() + } + + fun putTrigger(descriptor: TriggerDescriptor, trigger: Trigger) { + descriptors2triggers[descriptor] = trigger + } + + fun remove(triggerDescriptors: List) { + return triggerDescriptors.forEach { descriptors2triggers.remove(it) } + } + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/scheduled/WorkerManagerWatcher.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/scheduled/WorkerManagerWatcher.kt new file mode 100644 index 0000000..957f78b --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/scheduled/WorkerManagerWatcher.kt @@ -0,0 +1,67 @@ +package ru.touchin.spring.workers.manager.agent.scheduled + +import org.quartz.Scheduler +import org.quartz.SimpleScheduleBuilder +import org.quartz.TriggerBuilder +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import ru.touchin.spring.workers.manager.agent.quartz.RunnableJob +import ru.touchin.spring.workers.manager.agent.registry.JobDefinitionsRegistry +import ru.touchin.spring.workers.manager.agent.scheduled.services.SchedulerService +import ru.touchin.spring.workers.manager.agent.registry.TriggersRegistry +import ru.touchin.spring.workers.manager.core.trigger.services.TriggerDescriptorCoreService + +/** + * Class is proceeding regular synchronisation trigger descriptors from db and quartz scheduled triggers + */ +@Component +class WorkerManagerWatcher( + @Value("\${workers.watcher.sync.interval}") + private val watcherSyncInterval: Long, + private val jobDefinitionsRegistry: JobDefinitionsRegistry, + private val scheduleTriggerService: SchedulerService, + private val triggersRegistry: TriggersRegistry, + private val triggerDescriptorCoreService: TriggerDescriptorCoreService, + private val quartzScheduler: Scheduler +) { + + fun init() { + val systemJobDetail = RunnableJob + .initJobBuilder { sync() } + .withIdentity(SYSTEM_JOB_NAME, SYSTEM_JOB_GROUP) + .build() + + val systemTrigger = TriggerBuilder.newTrigger() + .forJob(systemJobDetail) + .withIdentity("${SYSTEM_JOB_NAME}_trigger") + .withSchedule(SimpleScheduleBuilder + .simpleSchedule() + .repeatForever() + .withIntervalInMilliseconds(watcherSyncInterval) + ) + .build() + + quartzScheduler.scheduleJob(systemJobDetail, systemTrigger) + } + + fun sync() { + val currentTriggerDescriptors = triggersRegistry.getDescriptors() + + val actualTriggerDescriptors = jobDefinitionsRegistry.jobs + .flatMap { (jobName, _) -> triggerDescriptorCoreService.getByWorkerName(jobName) } + .filter { !it.isDisabled() } + + val deletedTriggerDescriptors = currentTriggerDescriptors - actualTriggerDescriptors.toSet() + scheduleTriggerService.unscheduleTriggers(deletedTriggerDescriptors) + triggersRegistry.remove(deletedTriggerDescriptors) + + val newTriggerDescriptors = actualTriggerDescriptors - currentTriggerDescriptors.toSet() + scheduleTriggerService.scheduleTriggers(newTriggerDescriptors) + } + + companion object { + private const val SYSTEM_JOB_GROUP = "SYSTEM" + private const val SYSTEM_JOB_NAME = "system_worker_check" + } + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/scheduled/services/SchedulerService.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/scheduled/services/SchedulerService.kt new file mode 100644 index 0000000..61b7294 --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/scheduled/services/SchedulerService.kt @@ -0,0 +1,10 @@ +package ru.touchin.spring.workers.manager.agent.scheduled.services + +import ru.touchin.spring.workers.manager.core.trigger.dto.TriggerDescriptor + +interface SchedulerService { + + fun scheduleTriggers(triggerDescriptors: List) + fun unscheduleTriggers(triggerDescriptors: List) + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/scheduled/services/SchedulerServiceImpl.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/scheduled/services/SchedulerServiceImpl.kt new file mode 100644 index 0000000..400d281 --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/scheduled/services/SchedulerServiceImpl.kt @@ -0,0 +1,85 @@ +package ru.touchin.spring.workers.manager.agent.scheduled.services + +import org.quartz.CronScheduleBuilder +import org.quartz.JobDetail +import org.quartz.ScheduleBuilder +import org.quartz.Scheduler +import org.quartz.SimpleScheduleBuilder +import org.quartz.Trigger +import org.quartz.TriggerBuilder +import org.springframework.stereotype.Service +import ru.touchin.spring.workers.manager.agent.registry.JobDefinitionsRegistry +import ru.touchin.spring.workers.manager.agent.registry.TriggersRegistry +import ru.touchin.spring.workers.manager.core.trigger.dto.TriggerDescriptor +import ru.touchin.spring.workers.manager.core.trigger.enums.TriggerType + +@Service +class SchedulerServiceImpl( + private val quartzScheduler: Scheduler, + private val jobDefinitionsRegistry: JobDefinitionsRegistry, + private val triggersRegistry: TriggersRegistry +) : SchedulerService { + + override fun scheduleTriggers( + triggerDescriptors: List + ) { + triggerDescriptors.forEach(this::scheduleTrigger) + } + + override fun unscheduleTriggers( + triggerDescriptors: List + ) { + triggerDescriptors.forEach { descriptor -> + val trigger = triggersRegistry.getTriggerByDescriptor(descriptor) + + if (trigger != null) { + quartzScheduler.unscheduleJob(trigger.key) + } + } + } + + private fun scheduleTrigger(descriptor: TriggerDescriptor) { + val jobDetail = jobDefinitionsRegistry.getJobDetail(descriptor.workerName) + + if (jobDetail != null) { + val trigger = createTrigger(jobDetail, descriptor) + + triggersRegistry.putTrigger(descriptor, trigger) + + if (!quartzScheduler.checkExists(trigger.key)) { + if (quartzScheduler.checkExists(jobDetail.key)) { + quartzScheduler.scheduleJob(trigger) + } else { + quartzScheduler.scheduleJob(jobDetail, trigger) + } + } + } + } + + private fun createTrigger(job: JobDetail, triggerDescriptor: TriggerDescriptor): Trigger { + return TriggerBuilder.newTrigger().forJob(job) + .withIdentity(createTriggerName(job, triggerDescriptor)) + .withSchedule(getScheduleBuilder(triggerDescriptor)) + .build() + } + + private fun createTriggerName(job: JobDetail, triggerDescriptor: TriggerDescriptor) = + "${job.key.name}_${triggerDescriptor.id}_trigger" + + private fun getScheduleBuilder(triggerDescriptor: TriggerDescriptor): ScheduleBuilder { + return when (triggerDescriptor.type) { + TriggerType.CRON -> cronSchedule(triggerDescriptor.expression) + TriggerType.FIXED_RATE -> fixedRateSchedule(triggerDescriptor.expression.toLong()) + TriggerType.FIXED_DELAY -> fixedRateSchedule(triggerDescriptor.expression.toLong()) + } + } + + private fun fixedRateSchedule(interval: Long): SimpleScheduleBuilder { + return SimpleScheduleBuilder.simpleSchedule().repeatForever().withIntervalInMilliseconds(interval) + } + + private fun cronSchedule(expression: String): CronScheduleBuilder { + return CronScheduleBuilder.cronSchedule(expression) + } + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/trigger/InitialTriggerDescriptorsProvider.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/trigger/InitialTriggerDescriptorsProvider.kt new file mode 100644 index 0000000..6823f42 --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/trigger/InitialTriggerDescriptorsProvider.kt @@ -0,0 +1,14 @@ +package ru.touchin.spring.workers.manager.agent.trigger + +import ru.touchin.spring.workers.manager.core.trigger.services.dto.CreateTriggerDescriptor +import ru.touchin.spring.workers.manager.core.worker.dto.Worker + +/** + * When the agent is starting first time, then initial (default) triggers should be created for new workers. + */ +interface InitialTriggerDescriptorsProvider { + + fun applicableFor(worker: Worker): Boolean + fun createInitialTriggerDescriptors(worker: Worker): List + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/trigger/NoInitialTriggersDescriptorsProvider.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/trigger/NoInitialTriggersDescriptorsProvider.kt new file mode 100644 index 0000000..5159714 --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/trigger/NoInitialTriggersDescriptorsProvider.kt @@ -0,0 +1,20 @@ +package ru.touchin.spring.workers.manager.agent.trigger + +import org.springframework.core.annotation.Order +import org.springframework.stereotype.Component +import ru.touchin.common.spring.Ordered +import ru.touchin.spring.workers.manager.core.trigger.services.dto.CreateTriggerDescriptor +import ru.touchin.spring.workers.manager.core.worker.dto.Worker + +/** + * When no triggers could be created by any provider, then no triggers created. + */ +@Component +@Order(Ordered.LOWER) +class NoInitialTriggersDescriptorsProvider : InitialTriggerDescriptorsProvider { + + override fun applicableFor(worker: Worker): Boolean = true + + override fun createInitialTriggerDescriptors(worker: Worker): List = emptyList() + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/trigger/services/TriggerDescriptorService.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/trigger/services/TriggerDescriptorService.kt new file mode 100644 index 0000000..b90a84b --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/trigger/services/TriggerDescriptorService.kt @@ -0,0 +1,10 @@ +package ru.touchin.spring.workers.manager.agent.trigger.services + +import ru.touchin.spring.workers.manager.core.trigger.dto.TriggerDescriptor +import ru.touchin.spring.workers.manager.core.worker.dto.Worker + +interface TriggerDescriptorService { + + fun createDefaultTriggerDescriptors(worker: Worker): List + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/trigger/services/TriggerDescriptorServiceImpl.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/trigger/services/TriggerDescriptorServiceImpl.kt new file mode 100644 index 0000000..ba022d8 --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/trigger/services/TriggerDescriptorServiceImpl.kt @@ -0,0 +1,25 @@ +package ru.touchin.spring.workers.manager.agent.trigger.services + +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import ru.touchin.spring.workers.manager.agent.trigger.InitialTriggerDescriptorsProvider +import ru.touchin.spring.workers.manager.core.trigger.dto.TriggerDescriptor +import ru.touchin.spring.workers.manager.core.trigger.services.TriggerDescriptorCoreService +import ru.touchin.spring.workers.manager.core.worker.dto.Worker + +@Service +class TriggerDescriptorServiceImpl( + private val triggerDescriptorCoreService: TriggerDescriptorCoreService, + private val initialTriggerDescriptorProviders: List, +) : TriggerDescriptorService { + + @Transactional + override fun createDefaultTriggerDescriptors(worker: Worker): List { + return triggerDescriptorCoreService.create( + initialTriggerDescriptorProviders + .filter { it.applicableFor(worker) } + .flatMap { it.createInitialTriggerDescriptors(worker) } + ) + } + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/worker/executors/WorkerActionExecutor.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/worker/executors/WorkerActionExecutor.kt new file mode 100644 index 0000000..749c78e --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/worker/executors/WorkerActionExecutor.kt @@ -0,0 +1,14 @@ +package ru.touchin.spring.workers.manager.agent.worker.executors + +import org.quartz.Trigger +import ru.touchin.spring.workers.manager.agent.common.base.BaseJob +import ru.touchin.spring.workers.manager.core.execution.dto.Execution +import ru.touchin.spring.workers.manager.core.execution.models.ExecutionEntity + +interface WorkerActionExecutor { + + fun prepareExecution(job: BaseJob, currentTrigger: Trigger): Execution? + fun finishWorkerProcessing(job: BaseJob) + fun executeJobAction(job: BaseJob, currentTrigger: Trigger) + +} diff --git a/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/worker/executors/WorkerActionExecutorImpl.kt b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/worker/executors/WorkerActionExecutorImpl.kt new file mode 100644 index 0000000..275cd31 --- /dev/null +++ b/spring-workers-manager-agent/src/main/kotlin/ru/touchin/spring/workers/manager/agent/worker/executors/WorkerActionExecutorImpl.kt @@ -0,0 +1,77 @@ +package ru.touchin.spring.workers.manager.agent.worker.executors + +import org.quartz.Trigger +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import ru.touchin.spring.workers.manager.agent.common.base.BaseJob +import ru.touchin.spring.workers.manager.agent.registry.TriggersRegistry +import ru.touchin.spring.workers.manager.core.execution.dto.Execution +import ru.touchin.spring.workers.manager.core.execution.services.ExecutionCoreService +import ru.touchin.spring.workers.manager.core.execution.services.dto.CreateExecution +import ru.touchin.spring.workers.manager.core.worker.enums.WorkerStatus +import ru.touchin.spring.workers.manager.core.worker.services.WorkerCoreService +import ru.touchin.spring.workers.manager.core.worker.services.dto.UpdateWorker + +@Service +class WorkerActionExecutorImpl( + private val executionCoreService: ExecutionCoreService, + private val workerCoreService: WorkerCoreService, + private val triggersRegistry: TriggersRegistry, +) : WorkerActionExecutor { + + @Autowired + lateinit var workerActionExecutor: WorkerActionExecutor + + override fun executeJobAction(job: BaseJob, currentTrigger: Trigger) { + val execution = workerActionExecutor.prepareExecution(job, currentTrigger) + + execution?.let { + job.run() + + executionCoreService.setFinished(execution.id) + } + + workerActionExecutor.finishWorkerProcessing(job) + } + + @Transactional + override fun prepareExecution(job: BaseJob, currentTrigger: Trigger): Execution? { + val currentWorker = workerCoreService.getWithLock(job.getName()) + + return currentWorker + ?.takeIf { !it.isStopped() } + ?.let { worker -> + val currentTriggerDescriptor = triggersRegistry.getDescriptorByTrigger(currentTrigger) + + currentTriggerDescriptor?.let { + setWorkerStatus(worker.name, WorkerStatus.PROCESSING) + + executionCoreService.create( + CreateExecution( + workerName = worker.name, + triggerId = currentTriggerDescriptor.id, + ) + ) + } + } + } + + @Transactional + override fun finishWorkerProcessing(job: BaseJob) { + val currentWorker = workerCoreService.getWithLock(job.getName()) + ?: return + + setWorkerStatus(currentWorker.name, WorkerStatus.IDLE) + } + + private fun setWorkerStatus(name: String, status: WorkerStatus) { + workerCoreService.update( + UpdateWorker( + name = name, + status = status, + ) + ) + } + +} diff --git a/spring-workers-manager-agent/src/test/kotlin/ru/touchin/spring/workers/manager/TestApplication.kt b/spring-workers-manager-agent/src/test/kotlin/ru/touchin/spring/workers/manager/TestApplication.kt new file mode 100644 index 0000000..02e2fc2 --- /dev/null +++ b/spring-workers-manager-agent/src/test/kotlin/ru/touchin/spring/workers/manager/TestApplication.kt @@ -0,0 +1,10 @@ +package ru.touchin.spring.workers.manager + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Import + +@SpringBootApplication +@TestConfiguration +@Import(WorkersManagerConfiguration::class) +class TestApplication diff --git a/spring-workers-manager-agent/src/test/kotlin/ru/touchin/spring/workers/manager/agent/common/utils/GlobTest.kt b/spring-workers-manager-agent/src/test/kotlin/ru/touchin/spring/workers/manager/agent/common/utils/GlobTest.kt new file mode 100644 index 0000000..8ed5fc4 --- /dev/null +++ b/spring-workers-manager-agent/src/test/kotlin/ru/touchin/spring/workers/manager/agent/common/utils/GlobTest.kt @@ -0,0 +1,32 @@ +package ru.touchin.spring.workers.manager.agent.common.utils + +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +internal class GlobTest { + + @Test + fun matches() { + assertMatches("", "*****") + assertMatches("my perfect text", "my*text") + assertMatches("my perfect text", "my*?text") + assertMatches("moon", "????") + assertMatches("(abc)", "?abc?") + assertMatches("****", "????") + + assertNotMatches("", "?") + assertNotMatches("mo", "????") + assertNotMatches("moonmoon", "????") + assertNotMatches("my perfect text", "our*text") + } + + private fun assertMatches(text: String, pattern: String) { + assertTrue(Glob.matches(text, pattern)) + } + + private fun assertNotMatches(text: String, pattern: String) { + assertFalse(Glob.matches(text, pattern)) + } + +} diff --git a/spring-workers-manager-agent/src/test/kotlin/ru/touchin/spring/workers/manager/agent/config/WorkerInitializerTest.kt b/spring-workers-manager-agent/src/test/kotlin/ru/touchin/spring/workers/manager/agent/config/WorkerInitializerTest.kt new file mode 100644 index 0000000..9c70063 --- /dev/null +++ b/spring-workers-manager-agent/src/test/kotlin/ru/touchin/spring/workers/manager/agent/config/WorkerInitializerTest.kt @@ -0,0 +1,85 @@ +package ru.touchin.spring.workers.manager.agent.config + +import com.nhaarman.mockitokotlin2.doNothing +import com.nhaarman.mockitokotlin2.mock +import org.junit.jupiter.api.Test +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import ru.touchin.spring.workers.manager.agent.common.base.BaseJob +import ru.touchin.spring.workers.manager.agent.registry.JobDefinitionsRegistry +import ru.touchin.spring.workers.manager.agent.registry.JobProvider +import ru.touchin.spring.workers.manager.agent.trigger.services.TriggerDescriptorService +import ru.touchin.spring.workers.manager.agent.worker.executors.WorkerActionExecutorImpl +import ru.touchin.spring.workers.manager.core.worker.dto.Worker +import ru.touchin.spring.workers.manager.core.worker.enums.WorkerStatus +import ru.touchin.spring.workers.manager.core.worker.services.WorkerCoreService +import ru.touchin.spring.workers.manager.core.worker.services.WorkersStateService +import ru.touchin.spring.workers.manager.utils.MockitoUtils.anyx + +internal class WorkerInitializerTest { + + private val baseJob = mock { + on(it.getName()).thenReturn(BASE_WORKER_NAME) + } + + private val simpleJobProvider = mock { + on(it.getJobs()).thenReturn(listOf(baseJob)) + } + + private val workerActionExecutor = Mockito.mock(WorkerActionExecutorImpl::class.java) + + private val jobDefinitionsRegistry = JobDefinitionsRegistry(setOf(BASE_WORKER_NAME), listOf(simpleJobProvider), workerActionExecutor) + private val workerCoreService = Mockito.mock(WorkerCoreService::class.java) + + private val workersStateService = Mockito.mock(WorkersStateService::class.java) + + private val triggerDescriptorService = Mockito.mock(TriggerDescriptorService::class.java) + + private val workerInitializer = WorkerInitializer( + triggerDescriptorService, + jobDefinitionsRegistry, + workerCoreService, + workersStateService, + ) + + private val baseWorker = Worker( + name = BASE_WORKER_NAME, + status = WorkerStatus.IDLE, + disabledAt = null, + stoppedAt = null, + parallelExecutionEnabled = false, + ) + + @Test + fun checkSyncWithExistingWorker() { + doAnswer { baseWorker }.`when`(workerCoreService).getWithLock(BASE_WORKER_NAME) + doNothing().`when`(workersStateService).start(anyx()) + + workerInitializer.init() + + verify(workersStateService).start(baseWorker.name) + verify(workerCoreService, never()).create(anyString()) + verify(triggerDescriptorService, never()).createDefaultTriggerDescriptors(baseWorker) + } + + @Test + fun checkSyncAndCreateNewWorker() { + doAnswer { null }.`when`(workerCoreService).getWithLock(BASE_WORKER_NAME) + doAnswer { baseWorker }.`when`(workerCoreService).create(BASE_WORKER_NAME) + + workerInitializer.init() + + verify(workerCoreService).create(BASE_WORKER_NAME) + verify(triggerDescriptorService).createDefaultTriggerDescriptors(baseWorker) + } + + companion object { + + private const val BASE_WORKER_NAME = "baseWorker" + + } + +} diff --git a/spring-workers-manager-agent/src/test/kotlin/ru/touchin/spring/workers/manager/agent/scheduled/WorkerManagerWatcherTest.kt b/spring-workers-manager-agent/src/test/kotlin/ru/touchin/spring/workers/manager/agent/scheduled/WorkerManagerWatcherTest.kt new file mode 100644 index 0000000..05a6efa --- /dev/null +++ b/spring-workers-manager-agent/src/test/kotlin/ru/touchin/spring/workers/manager/agent/scheduled/WorkerManagerWatcherTest.kt @@ -0,0 +1,134 @@ +package ru.touchin.spring.workers.manager.agent.scheduled + +import com.nhaarman.mockitokotlin2.mock +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.ArgumentMatchers +import org.mockito.Mockito +import org.mockito.Mockito.anyString +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.verify +import org.quartz.Scheduler +import ru.touchin.spring.workers.manager.agent.common.base.BaseJob +import ru.touchin.spring.workers.manager.agent.registry.JobDefinitionsRegistry +import ru.touchin.spring.workers.manager.agent.registry.JobProvider +import ru.touchin.spring.workers.manager.agent.registry.TriggersRegistry +import ru.touchin.spring.workers.manager.agent.scheduled.services.SchedulerServiceImpl +import ru.touchin.spring.workers.manager.agent.worker.executors.WorkerActionExecutorImpl +import ru.touchin.spring.workers.manager.core.trigger.dto.TriggerDescriptor +import ru.touchin.spring.workers.manager.core.trigger.enums.TriggerType +import ru.touchin.spring.workers.manager.core.trigger.services.TriggerDescriptorCoreService +import ru.touchin.spring.workers.manager.core.worker.dto.Worker +import ru.touchin.spring.workers.manager.core.worker.enums.WorkerStatus +import java.util.* + +internal class WorkerManagerWatcherTest { + + private val baseJob = mock { + on(it.getName()).thenReturn(BASE_WORKER_NAME) + } + + private val simpleJobProvider = mock { + on(it.getJobs()).thenReturn(listOf(baseJob)) + } + + private val workerActionExecutor = Mockito.mock(WorkerActionExecutorImpl::class.java) + + private val jobDefinitionsRegistry = JobDefinitionsRegistry(setOf(BASE_WORKER_NAME), listOf(simpleJobProvider), workerActionExecutor) + + private val triggerRegistry = Mockito.mock(TriggersRegistry::class.java) + private val watcherSyncInterval = 1000L + + private val triggerDescriptorCoreService = Mockito.mock(TriggerDescriptorCoreService::class.java) + private val scheduleTriggerService = Mockito.mock(SchedulerServiceImpl::class.java) + private val quartzScheduler = Mockito.mock(Scheduler::class.java) + + private val workerManagerWatcher = WorkerManagerWatcher( + watcherSyncInterval, + jobDefinitionsRegistry, + scheduleTriggerService, + triggerRegistry, + triggerDescriptorCoreService, + quartzScheduler + ) + + private val baseWorker = Worker( + name = BASE_WORKER_NAME, + status = WorkerStatus.IDLE, + disabledAt = null, + stoppedAt = null, + parallelExecutionEnabled = false, + ) + + private val triggerDescriptorId1 = UUID.fromString("514fb14b-d7ea-4ace-b200-4e06e80c37b7") + private val triggerDescriptorId2 = UUID.fromString("94d2ef98-6fc9-4d41-a66a-35b73a2448e0") + private val triggerDescriptorId3 = UUID.fromString("2a7719ec-6a65-481e-947b-08537ada2337") + private val currentTriggerDescriptor1 = createTriggerDescriptor(triggerDescriptorId1, TriggerType.CRON, baseWorker) + private val actualTriggerDescriptor1 = createTriggerDescriptor(triggerDescriptorId2, TriggerType.FIXED_RATE, baseWorker) + private val actualTriggerDescriptor2 = createTriggerDescriptor(triggerDescriptorId3, TriggerType.FIXED_RATE, baseWorker) + private val currentTriggers = listOf(currentTriggerDescriptor1) + + @BeforeEach + fun setUp() { + doAnswer { currentTriggers } + .`when`(triggerRegistry).getDescriptors() + } + + @Test + fun checkSyncWithOnlyNewTriggersWithoutRemoving() { + doAnswer { listOf(currentTriggerDescriptor1, actualTriggerDescriptor1) } + .`when`(triggerDescriptorCoreService).getByWorkerName(anyString()) + + workerManagerWatcher.sync() + + verify(triggerRegistry).remove(emptyList()) + verify(scheduleTriggerService).unscheduleTriggers(emptyList()) + + verify(scheduleTriggerService).scheduleTriggers(listOf(actualTriggerDescriptor1)) + } + + @Test + fun checkSyncWithOnlyRemoveIrrelevantTriggers() { + doAnswer { emptyList() } + .`when`(triggerDescriptorCoreService).getByWorkerName(ArgumentMatchers.anyString()) + + workerManagerWatcher.sync() + + verify(triggerRegistry).remove(currentTriggers) + verify(scheduleTriggerService).unscheduleTriggers(currentTriggers) + + verify(scheduleTriggerService).scheduleTriggers(emptyList()) + } + + @Test + fun checkSyncWithSaveNewAndRemoveIrrelevantTriggers() { + doAnswer { listOf(actualTriggerDescriptor2, actualTriggerDescriptor1) } + .`when`(triggerDescriptorCoreService).getByWorkerName(ArgumentMatchers.anyString()) + + workerManagerWatcher.sync() + + verify(triggerRegistry).remove(currentTriggers) + verify(scheduleTriggerService).unscheduleTriggers(currentTriggers) + + verify(scheduleTriggerService).scheduleTriggers(setOf(actualTriggerDescriptor2, actualTriggerDescriptor1).toList()) + } + + private fun createTriggerDescriptor(id: UUID, type: TriggerType, worker: Worker): TriggerDescriptor { + return TriggerDescriptor( + id = id, + name = id.toString(), + type = type, + expression = "expression", + workerName = worker.name, + disabledAt = null, + deletedAt = null, + ) + } + + companion object { + + private const val BASE_WORKER_NAME = "baseWorker" + + } + +} diff --git a/spring-workers-manager-agent/src/test/kotlin/ru/touchin/spring/workers/manager/utils/MockitoUtils.kt b/spring-workers-manager-agent/src/test/kotlin/ru/touchin/spring/workers/manager/utils/MockitoUtils.kt new file mode 100644 index 0000000..4e1e027 --- /dev/null +++ b/spring-workers-manager-agent/src/test/kotlin/ru/touchin/spring/workers/manager/utils/MockitoUtils.kt @@ -0,0 +1,17 @@ +package ru.touchin.spring.workers.manager.utils + +import org.mockito.ArgumentMatchers +import org.mockito.Mockito + +object MockitoUtils { + fun anyx(matcher: ((T) -> Boolean)? = null): T { + @Suppress("UNCHECKED_CAST") + return if (matcher == null) { + ArgumentMatchers.any() ?: (null as T) + } else { + Mockito.argThat(matcher) ?: (null as T) + } + } + + fun anyx(sample: T): T = anyx { it == sample } +} diff --git a/spring-workers-manager-agent/src/test/resources/application-test-slow.yml b/spring-workers-manager-agent/src/test/resources/application-test-slow.yml new file mode 100644 index 0000000..40fc4f3 --- /dev/null +++ b/spring-workers-manager-agent/src/test/resources/application-test-slow.yml @@ -0,0 +1,3 @@ +spring: + config: + import: "test-slow.yml" diff --git a/spring-workers-manager-agent/src/test/resources/application-test.yml b/spring-workers-manager-agent/src/test/resources/application-test.yml new file mode 100644 index 0000000..956da22 --- /dev/null +++ b/spring-workers-manager-agent/src/test/resources/application-test.yml @@ -0,0 +1,3 @@ +spring: + config: + import: "test.yml" diff --git a/spring-workers-manager-api/build.gradle.kts b/spring-workers-manager-api/build.gradle.kts new file mode 100644 index 0000000..afeec26 --- /dev/null +++ b/spring-workers-manager-api/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("kotlin") + id("kotlin-spring") + id("maven-publish") +} + +dependencies { + implementation(project(":spring-workers-manager-core")) + + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + implementation("org.springframework.data:spring-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-web") +} diff --git a/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/trigger/controllers/TriggerController.kt b/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/trigger/controllers/TriggerController.kt new file mode 100644 index 0000000..0341fb8 --- /dev/null +++ b/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/trigger/controllers/TriggerController.kt @@ -0,0 +1,60 @@ +package ru.touchin.spring.workers.manager.api.trigger.controllers + +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import ru.touchin.spring.workers.manager.api.trigger.controllers.dto.TriggerChangeRequest +import ru.touchin.spring.workers.manager.api.trigger.services.TriggerDescriptorApiService +import ru.touchin.spring.workers.manager.api.trigger.services.dto.CreateTrigger +import ru.touchin.spring.workers.manager.api.trigger.services.dto.UpdateTrigger + +@RestController +@RequestMapping("/workers/{workerName}/triggers") +class TriggerController( + private val triggerDescriptorApiService: TriggerDescriptorApiService +) { + + @PostMapping + fun createTrigger( + @PathVariable + workerName: String, + @RequestBody + body: TriggerChangeRequest + ) { + triggerDescriptorApiService.create( + CreateTrigger( + workerName = workerName, + triggerName = body.name, + type = body.type, + expression = body.expression, + disabled = body.disabled, + ) + ) + } + + @PutMapping("/{triggerName}") + fun changeTrigger( + @PathVariable + triggerName: String, + @PathVariable + workerName: String, + @RequestBody + body: TriggerChangeRequest + ) { + triggerDescriptorApiService.update( + UpdateTrigger( + workerName = workerName, + oldTriggerName = triggerName, + newTriggerName = body.name, + type = body.type, + expression = body.expression, + disabled = body.disabled, + ) + ) + } + +} + diff --git a/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/trigger/controllers/dto/TriggerChangeRequest.kt b/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/trigger/controllers/dto/TriggerChangeRequest.kt new file mode 100644 index 0000000..59f3742 --- /dev/null +++ b/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/trigger/controllers/dto/TriggerChangeRequest.kt @@ -0,0 +1,10 @@ +package ru.touchin.spring.workers.manager.api.trigger.controllers.dto + +import ru.touchin.spring.workers.manager.core.trigger.enums.TriggerType + +data class TriggerChangeRequest( + val name: String, + val type: TriggerType, + val expression: String, + val disabled: Boolean, +) diff --git a/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/trigger/services/TriggerDescriptorApiService.kt b/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/trigger/services/TriggerDescriptorApiService.kt new file mode 100644 index 0000000..81b68a3 --- /dev/null +++ b/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/trigger/services/TriggerDescriptorApiService.kt @@ -0,0 +1,11 @@ +package ru.touchin.spring.workers.manager.api.trigger.services + +import ru.touchin.spring.workers.manager.api.trigger.services.dto.CreateTrigger +import ru.touchin.spring.workers.manager.api.trigger.services.dto.UpdateTrigger + +interface TriggerDescriptorApiService { + + fun create(create: CreateTrigger) + fun update(update: UpdateTrigger) + +} diff --git a/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/trigger/services/TriggerDescriptorApiServiceImpl.kt b/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/trigger/services/TriggerDescriptorApiServiceImpl.kt new file mode 100644 index 0000000..ad38d3f --- /dev/null +++ b/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/trigger/services/TriggerDescriptorApiServiceImpl.kt @@ -0,0 +1,61 @@ +package ru.touchin.spring.workers.manager.api.trigger.services + +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import ru.touchin.spring.workers.manager.api.trigger.services.dto.CreateTrigger +import ru.touchin.spring.workers.manager.api.trigger.services.dto.UpdateTrigger +import ru.touchin.spring.workers.manager.core.trigger.dto.TriggerDescriptor +import ru.touchin.spring.workers.manager.core.trigger.exceptions.TriggerNotFoundException +import ru.touchin.spring.workers.manager.core.trigger.services.TriggerDescriptorCoreService +import ru.touchin.spring.workers.manager.core.trigger.services.dto.CreateTriggerDescriptor +import ru.touchin.spring.workers.manager.core.worker.dto.Worker +import ru.touchin.spring.workers.manager.core.worker.services.WorkerCoreService +import java.time.ZonedDateTime + +@Service +class TriggerDescriptorApiServiceImpl( + private val workerCoreService: WorkerCoreService, + private val triggerDescriptorCoreService: TriggerDescriptorCoreService, +) : TriggerDescriptorApiService { + + @Transactional + override fun create(create: CreateTrigger) { + val worker = workerCoreService.get(create.workerName) + + triggerDescriptorCoreService.create( + CreateTriggerDescriptor( + expression = create.expression, + name = create.triggerName, + type = create.type, + disabledAt = if (create.disabled) ZonedDateTime.now() else null, + workerName = worker.name, + ) + ) + } + + @Transactional + override fun update(update: UpdateTrigger) { + val worker = workerCoreService.get(update.workerName) + + val triggerDescriptor = getTriggerDescriptor(worker, update.oldTriggerName) + + triggerDescriptorCoreService.setDeleted(triggerDescriptor.id) + + triggerDescriptorCoreService.create( + CreateTriggerDescriptor( + expression = update.expression, + name = update.newTriggerName, + type = update.type, + disabledAt = if (update.disabled) ZonedDateTime.now() else null, + workerName = worker.name, + ) + ) + } + + private fun getTriggerDescriptor(worker: Worker, triggerName: String): TriggerDescriptor { + return triggerDescriptorCoreService.getByWorkerName(worker.name) + .firstOrNull { it.name == triggerName } + ?: throw TriggerNotFoundException(triggerName) + } + +} diff --git a/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/trigger/services/dto/CreateTrigger.kt b/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/trigger/services/dto/CreateTrigger.kt new file mode 100644 index 0000000..d58ef17 --- /dev/null +++ b/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/trigger/services/dto/CreateTrigger.kt @@ -0,0 +1,11 @@ +package ru.touchin.spring.workers.manager.api.trigger.services.dto + +import ru.touchin.spring.workers.manager.core.trigger.enums.TriggerType + +data class CreateTrigger( + val workerName: String, + val triggerName: String, + val type: TriggerType, + val expression: String, + val disabled: Boolean, +) diff --git a/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/trigger/services/dto/UpdateTrigger.kt b/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/trigger/services/dto/UpdateTrigger.kt new file mode 100644 index 0000000..6d79552 --- /dev/null +++ b/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/trigger/services/dto/UpdateTrigger.kt @@ -0,0 +1,12 @@ +package ru.touchin.spring.workers.manager.api.trigger.services.dto + +import ru.touchin.spring.workers.manager.core.trigger.enums.TriggerType + +data class UpdateTrigger( + val workerName: String, + val oldTriggerName: String, + val newTriggerName: String, + val type: TriggerType, + val expression: String, + val disabled: Boolean, +) diff --git a/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/worker/controllers/WorkerController.kt b/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/worker/controllers/WorkerController.kt new file mode 100644 index 0000000..1415d90 --- /dev/null +++ b/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/worker/controllers/WorkerController.kt @@ -0,0 +1,65 @@ +package ru.touchin.spring.workers.manager.api.worker.controllers + +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import ru.touchin.spring.workers.manager.api.worker.controllers.dto.WorkerResponse +import ru.touchin.spring.workers.manager.api.worker.services.WorkerApiService +import ru.touchin.spring.workers.manager.core.worker.services.WorkersStateService + +@RestController +@RequestMapping("/workers") +class WorkerController( + private val workerStateService: WorkersStateService, + private val workerApiService: WorkerApiService +) { + + @GetMapping + fun getWorkers(): List { + return workerApiService.getWorkers() + } + + @GetMapping("/{workerName}") + fun getWorker( + @PathVariable + workerName: String + ): WorkerResponse { + return workerApiService.getWorker(workerName) + } + + @PostMapping("/{workerName}/stop") + fun stop( + @PathVariable + workerName: String + ) { + workerStateService.stop(workerName) + } + + @PostMapping("/{workerName}/start") + fun start( + @PathVariable + workerName: String + ) { + workerStateService.start(workerName) + } + + @PostMapping("/{workerName}/disable") + fun disable( + @PathVariable + workerName: String + ) { + workerStateService.disable(workerName) + } + + @PostMapping("/{workerName}/enable") + fun enable( + @PathVariable + workerName: String + ) { + workerStateService.enable(workerName) + } + +} + diff --git a/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/worker/controllers/dto/WorkerResponse.kt b/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/worker/controllers/dto/WorkerResponse.kt new file mode 100644 index 0000000..eb95964 --- /dev/null +++ b/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/worker/controllers/dto/WorkerResponse.kt @@ -0,0 +1,21 @@ +package ru.touchin.spring.workers.manager.api.worker.controllers.dto + +import ru.touchin.spring.workers.manager.core.worker.dto.Worker +import ru.touchin.spring.workers.manager.core.worker.enums.WorkerStatus +import java.time.ZonedDateTime + +data class WorkerResponse( + val workerName: String, + val stoppedAt: ZonedDateTime?, + val disabledAt: ZonedDateTime?, + val status: WorkerStatus, + val parallelExecutionEnabled: Boolean +) + +fun Worker.toWorkerResponse() = WorkerResponse( + workerName = this.name, + stoppedAt = this.stoppedAt, + disabledAt = this.disabledAt, + status = this.status, + parallelExecutionEnabled = this.parallelExecutionEnabled, +) diff --git a/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/worker/services/WorkerApiService.kt b/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/worker/services/WorkerApiService.kt new file mode 100644 index 0000000..8268fd3 --- /dev/null +++ b/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/worker/services/WorkerApiService.kt @@ -0,0 +1,10 @@ +package ru.touchin.spring.workers.manager.api.worker.services + +import ru.touchin.spring.workers.manager.api.worker.controllers.dto.WorkerResponse + +interface WorkerApiService { + + fun getWorker(workerName: String): WorkerResponse + fun getWorkers(): List + +} diff --git a/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/worker/services/WorkerApiServiceImpl.kt b/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/worker/services/WorkerApiServiceImpl.kt new file mode 100644 index 0000000..99eb6d9 --- /dev/null +++ b/spring-workers-manager-api/src/main/kotlin/ru/touchin/spring/workers/manager/api/worker/services/WorkerApiServiceImpl.kt @@ -0,0 +1,22 @@ +package ru.touchin.spring.workers.manager.api.worker.services + +import org.springframework.stereotype.Service +import ru.touchin.spring.workers.manager.api.worker.controllers.dto.WorkerResponse +import ru.touchin.spring.workers.manager.api.worker.controllers.dto.toWorkerResponse +import ru.touchin.spring.workers.manager.core.worker.dto.Worker +import ru.touchin.spring.workers.manager.core.worker.services.WorkerCoreService + +@Service +class WorkerApiServiceImpl( + private val workerCoreService: WorkerCoreService +) : WorkerApiService { + + override fun getWorker(workerName: String): WorkerResponse { + return workerCoreService.get(workerName).toWorkerResponse() + } + + override fun getWorkers(): List { + return workerCoreService.getAll().map(Worker::toWorkerResponse) + } + +} diff --git a/spring-workers-manager-core/README.md b/spring-workers-manager-core/README.md new file mode 100644 index 0000000..c10bfb4 --- /dev/null +++ b/spring-workers-manager-core/README.md @@ -0,0 +1 @@ +# Touch Spring Workers Manager diff --git a/spring-workers-manager-core/build.gradle.kts b/spring-workers-manager-core/build.gradle.kts new file mode 100644 index 0000000..c68c369 --- /dev/null +++ b/spring-workers-manager-core/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("kotlin") + id("kotlin-spring") + id("maven-publish") +} + +dependencies { + api(project(":common")) + + implementation(project(":common-spring-jpa")) + + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + implementation("org.liquibase:liquibase-core") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-quartz") +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/WorkersManagerConfiguration.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/WorkersManagerConfiguration.kt new file mode 100644 index 0000000..7cb85cf --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/WorkersManagerConfiguration.kt @@ -0,0 +1,38 @@ +package ru.touchin.spring.workers.manager + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.context.properties.ConfigurationPropertiesScan +import org.springframework.cache.annotation.EnableCaching +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration +import org.springframework.data.jpa.repository.config.EnableJpaAuditing +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import ru.touchin.common.spring.jpa.EnableJpaAuditingExtra + +/** + * Configuration which brings to context all the components, required to support workers manager module via annotations. + * + * You could @[org.springframework.context.annotation.Import] this configuration into your application or use [EnableWorkersManager] annotation to do it automatically + */ +@ComponentScan +@EntityScan +@EnableJpaRepositories +@EnableCaching +@ConfigurationPropertiesScan +class WorkersManagerConfiguration { + + companion object { + const val SCHEMA: String = "workers" + } + + /** + * Applies `@EnableJpaAuditing` only if it was not already applied. + * Enabling `@EnableJpaAuditing` twice will lead to application context failure. + */ + @Configuration + @ConditionalOnMissingBean(name=["jpaAuditingHandler"]) + @EnableJpaAuditingExtra + class JpaAuditingNonConflictingDeclaration + +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/config/LiquibaseRunner.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/config/LiquibaseRunner.kt new file mode 100644 index 0000000..efcc373 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/config/LiquibaseRunner.kt @@ -0,0 +1,34 @@ +package ru.touchin.spring.workers.manager.core.config + +import liquibase.Contexts +import liquibase.Liquibase +import liquibase.database.Database +import liquibase.database.DatabaseFactory +import liquibase.database.jvm.JdbcConnection +import liquibase.resource.ClassLoaderResourceAccessor +import org.springframework.stereotype.Component +import ru.touchin.spring.workers.manager.WorkersManagerConfiguration.Companion.SCHEMA +import javax.sql.DataSource + +@Component +class LiquibaseRunner( + private val dataSource: DataSource +) { + + fun run() = dataSource.connection.use { connection -> + val database: Database = DatabaseFactory.getInstance() + .findCorrectDatabaseImplementation(JdbcConnection(connection)) + .apply { defaultSchemaName = SCHEMA } + + val liquibase = Liquibase(MASTER_CHANGELOG_PATH, ClassLoaderResourceAccessor(), database) + + liquibase.changeLogParameters.set("schemaName", SCHEMA) + + liquibase.update(Contexts()) + } + + companion object { + private const val MASTER_CHANGELOG_PATH = "workers/db/changelog/db.changelog-master.yaml" + } + +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/converters/ExecutionConverters.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/converters/ExecutionConverters.kt new file mode 100644 index 0000000..8041ec6 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/converters/ExecutionConverters.kt @@ -0,0 +1,15 @@ +package ru.touchin.spring.workers.manager.core.execution.converters + +import ru.touchin.spring.workers.manager.core.execution.dto.Execution +import ru.touchin.spring.workers.manager.core.execution.models.ExecutionEntity +import ru.touchin.spring.workers.manager.core.trigger.converters.toTriggerDescriptor + +fun ExecutionEntity.toExecution(): Execution { + return Execution( + id = id!!, + triggerDescriptor = triggerDescriptor?.toTriggerDescriptor(), + status = status, + startedAt = startedAt, + finishedAt = finishedAt, + ) +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/dto/Execution.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/dto/Execution.kt new file mode 100644 index 0000000..521d847 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/dto/Execution.kt @@ -0,0 +1,14 @@ +package ru.touchin.spring.workers.manager.core.execution.dto + +import ru.touchin.spring.workers.manager.core.execution.enums.ExecutionStatus +import ru.touchin.spring.workers.manager.core.trigger.dto.TriggerDescriptor +import java.time.ZonedDateTime +import java.util.* + +data class Execution( + val id: UUID, + val triggerDescriptor: TriggerDescriptor?, + val status: ExecutionStatus, + val startedAt: ZonedDateTime?, + val finishedAt: ZonedDateTime?, +) diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/enums/ExecutionStatus.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/enums/ExecutionStatus.kt new file mode 100644 index 0000000..eab45e3 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/enums/ExecutionStatus.kt @@ -0,0 +1,8 @@ +package ru.touchin.spring.workers.manager.core.execution.enums + +enum class ExecutionStatus { + + PROCESSING, + FINISHED + +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/exceptions/ExecutionNotFoundException.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/exceptions/ExecutionNotFoundException.kt new file mode 100644 index 0000000..acd15fe --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/exceptions/ExecutionNotFoundException.kt @@ -0,0 +1,6 @@ +package ru.touchin.spring.workers.manager.core.execution.exceptions + +import ru.touchin.common.exceptions.CommonNotFoundException +import java.util.* + +class ExecutionNotFoundException(id: UUID) : CommonNotFoundException("Execution not found id=$id") diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/models/ExecutionEntity.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/models/ExecutionEntity.kt new file mode 100644 index 0000000..8e0cea3 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/models/ExecutionEntity.kt @@ -0,0 +1,43 @@ +package ru.touchin.spring.workers.manager.core.execution.models + +import ru.touchin.common.spring.jpa.models.BaseUuidIdEntity +import ru.touchin.spring.workers.manager.WorkersManagerConfiguration.Companion.SCHEMA +import ru.touchin.spring.workers.manager.core.execution.enums.ExecutionStatus +import ru.touchin.spring.workers.manager.core.trigger.models.TriggerDescriptorEntity +import java.time.ZonedDateTime +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne +import javax.persistence.Table + +@Entity +@Table(name = "executions", schema = SCHEMA) +class ExecutionEntity : BaseUuidIdEntity() { + + @Column(name = "worker_name", nullable = false) + lateinit var workerName: String + + @ManyToOne + @JoinColumn(name = "trigger_id", nullable = true) + var triggerDescriptor: TriggerDescriptorEntity? = null + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + lateinit var status: ExecutionStatus + + @Column(name = "error_message", nullable = true) + var errorMessage: String? = null + + @Column(name = "error_code", nullable = true) + var errorCode: Int? = null + + @Column(name = "started_at", nullable = true) + var startedAt: ZonedDateTime? = null + + @Column(name = "finished_at", nullable = true) + var finishedAt: ZonedDateTime? = null + +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/repositories/ExecutionRepository.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/repositories/ExecutionRepository.kt new file mode 100644 index 0000000..5cddd4f --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/repositories/ExecutionRepository.kt @@ -0,0 +1,14 @@ +package ru.touchin.spring.workers.manager.core.execution.repositories + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.repository.findByIdOrNull +import ru.touchin.spring.workers.manager.core.execution.exceptions.ExecutionNotFoundException +import ru.touchin.spring.workers.manager.core.execution.models.ExecutionEntity +import java.util.* + +interface ExecutionRepository : JpaRepository + +fun ExecutionRepository.findByIdOrThrow(id: UUID): ExecutionEntity { + return findByIdOrNull(id) + ?: throw ExecutionNotFoundException(id) +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/services/ExecutionCoreService.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/services/ExecutionCoreService.kt new file mode 100644 index 0000000..25ec190 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/services/ExecutionCoreService.kt @@ -0,0 +1,12 @@ +package ru.touchin.spring.workers.manager.core.execution.services + +import ru.touchin.spring.workers.manager.core.execution.dto.Execution +import ru.touchin.spring.workers.manager.core.execution.services.dto.CreateExecution +import java.util.* + +interface ExecutionCoreService{ + + fun create(create: CreateExecution): Execution + fun setFinished(id: UUID): Execution + +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/services/ExecutionCoreServiceImpl.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/services/ExecutionCoreServiceImpl.kt new file mode 100644 index 0000000..a18d2d2 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/services/ExecutionCoreServiceImpl.kt @@ -0,0 +1,49 @@ +package ru.touchin.spring.workers.manager.core.execution.services + +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import ru.touchin.spring.workers.manager.core.execution.converters.toExecution +import ru.touchin.spring.workers.manager.core.execution.dto.Execution +import ru.touchin.spring.workers.manager.core.execution.enums.ExecutionStatus +import ru.touchin.spring.workers.manager.core.execution.models.ExecutionEntity +import ru.touchin.spring.workers.manager.core.execution.repositories.ExecutionRepository +import ru.touchin.spring.workers.manager.core.execution.repositories.findByIdOrThrow +import ru.touchin.spring.workers.manager.core.execution.services.dto.CreateExecution +import ru.touchin.spring.workers.manager.core.trigger.repositories.TriggerDescriptorRepository +import ru.touchin.spring.workers.manager.core.trigger.repositories.findByIdOrThrow +import java.time.ZonedDateTime +import java.util.* + +@Service +class ExecutionCoreServiceImpl( + private val executionRepository: ExecutionRepository, + private val triggerDescriptorRepository: TriggerDescriptorRepository, +) : ExecutionCoreService { + + @Transactional + override fun create(create: CreateExecution): Execution { + val entity = ExecutionEntity().apply { + startedAt = ZonedDateTime.now() + workerName = create.workerName + triggerDescriptor = triggerDescriptorRepository.findByIdOrThrow(create.triggerId) + status = ExecutionStatus.PROCESSING + } + + return executionRepository.save(entity) + .toExecution() + } + + @Transactional + override fun setFinished(id: UUID): Execution { + val entity = executionRepository.findByIdOrThrow(id) + + entity.apply { + finishedAt = ZonedDateTime.now() + status = ExecutionStatus.FINISHED + } + + return executionRepository.save(entity) + .toExecution() + } + +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/services/dto/CreateExecution.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/services/dto/CreateExecution.kt new file mode 100644 index 0000000..d889da5 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/execution/services/dto/CreateExecution.kt @@ -0,0 +1,8 @@ +package ru.touchin.spring.workers.manager.core.execution.services.dto + +import java.util.* + +data class CreateExecution( + val workerName: String, + val triggerId: UUID, +) diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/converters/TriggerDescriptorConverters.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/converters/TriggerDescriptorConverters.kt new file mode 100644 index 0000000..a6e5893 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/converters/TriggerDescriptorConverters.kt @@ -0,0 +1,16 @@ +package ru.touchin.spring.workers.manager.core.trigger.converters + +import ru.touchin.spring.workers.manager.core.trigger.dto.TriggerDescriptor +import ru.touchin.spring.workers.manager.core.trigger.models.TriggerDescriptorEntity + +fun TriggerDescriptorEntity.toTriggerDescriptor(): TriggerDescriptor { + return TriggerDescriptor( + id = id!!, + name = triggerName, + type = type, + expression = expression, + workerName = worker.workerName, + disabledAt = disabledAt, + deletedAt = deletedAt, + ) +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/dto/TriggerDescriptor.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/dto/TriggerDescriptor.kt new file mode 100644 index 0000000..0111bd8 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/dto/TriggerDescriptor.kt @@ -0,0 +1,36 @@ +package ru.touchin.spring.workers.manager.core.trigger.dto + +import ru.touchin.spring.workers.manager.core.trigger.enums.TriggerType +import java.time.ZonedDateTime +import java.util.* + +data class TriggerDescriptor( + val id: UUID, + val name: String, + val type: TriggerType, + val expression: String, + val workerName: String, + val disabledAt: ZonedDateTime?, + val deletedAt: ZonedDateTime?, +) { + + fun isDeleted() = deletedAt != null + + fun isDisabled() = disabledAt != null + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is TriggerDescriptor) return false + + if (id != other.id || expression != other.expression || type != other.type) { + return false + } + + return true + } + + override fun hashCode(): Int { + return id.hashCode() + } + +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/enums/TriggerType.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/enums/TriggerType.kt new file mode 100644 index 0000000..4a6fd27 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/enums/TriggerType.kt @@ -0,0 +1,49 @@ +package ru.touchin.spring.workers.manager.core.trigger.enums + +enum class TriggerType { + + /** + * Uses CRON expression for scheduling. + * Expression example: `0 * * * * *` + */ + CRON, + + /** + * Uses numeric expressions, which mean time period in milliseconds between + * end of previous execution and start of the next one. + * ``` + * ------------ TIMELINE ------------------- + * [ EXECUTION ]............................ + * .............< DELAY >................... + * ......................[ NEXT EXECUTION ] + * ----------------------------------------- + * ``` + */ + FIXED_DELAY, + + /** + * Uses numeric expressions, which mean time period in milliseconds between + * start of previous execution and start of the next one. + * ``` + * ------------ TIMELINE ------------------- + * [ EXECUTION ]............................ + * < DELAY >................................ + * .........[ NEXT EXECUTION ].............. + * .........< DELAY >....................... + * ..................[ 3RD EXECUTION ]..... + * ----------------------------------------- + * ``` + */ + FIXED_RATE; + + companion object { + + fun find(name: String?): TriggerType? { + name ?: return null + + return values().find { it.name == name } + } + + } + +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/exceptions/TriggerNotFoundException.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/exceptions/TriggerNotFoundException.kt new file mode 100644 index 0000000..cc8bd87 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/exceptions/TriggerNotFoundException.kt @@ -0,0 +1,11 @@ +package ru.touchin.spring.workers.manager.core.trigger.exceptions + +import ru.touchin.common.exceptions.CommonNotFoundException +import java.util.* + +class TriggerNotFoundException : CommonNotFoundException { + + constructor(id: UUID) : super("TriggerDescriptor not found id=$id") + constructor(name: String) : super("TriggerDescriptor code not found name=$name") + +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/models/TriggerDescriptorEntity.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/models/TriggerDescriptorEntity.kt new file mode 100644 index 0000000..a149b1f --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/models/TriggerDescriptorEntity.kt @@ -0,0 +1,40 @@ +package ru.touchin.spring.workers.manager.core.trigger.models + +import ru.touchin.common.spring.jpa.models.BaseUuidIdEntity +import ru.touchin.spring.workers.manager.WorkersManagerConfiguration.Companion.SCHEMA +import ru.touchin.spring.workers.manager.core.trigger.enums.TriggerType +import ru.touchin.spring.workers.manager.core.worker.models.WorkerEntity +import java.time.ZonedDateTime +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne +import javax.persistence.Table + +@Entity +@Table(name = "triggers", schema = SCHEMA) +class TriggerDescriptorEntity : BaseUuidIdEntity() { + + @Column(name = "trigger_name", nullable = false) + lateinit var triggerName: String + + @Enumerated(EnumType.STRING) + @Column(name = "type", nullable = false) + lateinit var type: TriggerType + + @ManyToOne + @JoinColumn(name = "worker_name", nullable = false) + lateinit var worker: WorkerEntity + + @Column(name = "expression", nullable = false) + lateinit var expression: String + + @Column(name = "disabled_at", nullable = true) + var disabledAt: ZonedDateTime? = null + + @Column(name = "deleted_at", nullable = true) + var deletedAt: ZonedDateTime? = null + +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/repositories/TriggerDescriptorRepository.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/repositories/TriggerDescriptorRepository.kt new file mode 100644 index 0000000..7f33d69 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/repositories/TriggerDescriptorRepository.kt @@ -0,0 +1,27 @@ +package ru.touchin.spring.workers.manager.core.trigger.repositories + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.findByIdOrNull +import ru.touchin.spring.workers.manager.core.trigger.exceptions.TriggerNotFoundException +import ru.touchin.spring.workers.manager.core.trigger.models.TriggerDescriptorEntity +import java.util.* + +interface TriggerDescriptorRepository : JpaRepository { + + @Query( + """ + SELECT td + FROM TriggerDescriptorEntity td + WHERE td.worker.workerName = :workerName + AND td.deletedAt IS NULL + """ + ) + fun findAll(workerName: String): List + +} + +fun TriggerDescriptorRepository.findByIdOrThrow(id: UUID): TriggerDescriptorEntity { + return findByIdOrNull(id) + ?: throw TriggerNotFoundException(id) +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/services/TriggerDescriptorCoreService.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/services/TriggerDescriptorCoreService.kt new file mode 100644 index 0000000..7e2c18d --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/services/TriggerDescriptorCoreService.kt @@ -0,0 +1,14 @@ +package ru.touchin.spring.workers.manager.core.trigger.services + +import ru.touchin.spring.workers.manager.core.trigger.dto.TriggerDescriptor +import ru.touchin.spring.workers.manager.core.trigger.services.dto.CreateTriggerDescriptor +import java.util.* + +interface TriggerDescriptorCoreService { + + fun create(create: CreateTriggerDescriptor): TriggerDescriptor + fun create(create: List): List + fun setDeleted(id: UUID): TriggerDescriptor + fun getByWorkerName(workerName: String): List + +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/services/TriggerDescriptorCoreServiceImpl.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/services/TriggerDescriptorCoreServiceImpl.kt new file mode 100644 index 0000000..2b20183 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/services/TriggerDescriptorCoreServiceImpl.kt @@ -0,0 +1,68 @@ +package ru.touchin.spring.workers.manager.core.trigger.services + +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import ru.touchin.spring.workers.manager.core.trigger.converters.toTriggerDescriptor +import ru.touchin.spring.workers.manager.core.trigger.dto.TriggerDescriptor +import ru.touchin.spring.workers.manager.core.trigger.models.TriggerDescriptorEntity +import ru.touchin.spring.workers.manager.core.trigger.repositories.TriggerDescriptorRepository +import ru.touchin.spring.workers.manager.core.trigger.repositories.findByIdOrThrow +import ru.touchin.spring.workers.manager.core.trigger.services.dto.CreateTriggerDescriptor +import ru.touchin.spring.workers.manager.core.worker.repositories.WorkerRepository +import ru.touchin.spring.workers.manager.core.worker.repositories.findByNameOrThrow +import java.time.ZonedDateTime +import java.util.* + +@Service +class TriggerDescriptorCoreServiceImpl( + private val triggerDescriptorRepository: TriggerDescriptorRepository, + private val workerRepository: WorkerRepository, +) : TriggerDescriptorCoreService { + + @Transactional + override fun create(create: CreateTriggerDescriptor): TriggerDescriptor { + val entity = TriggerDescriptorEntity().apply { + expression = create.expression + triggerName = create.name + type = create.type + worker = workerRepository.findByNameOrThrow(create.workerName) + disabledAt = create.disabledAt + } + + return triggerDescriptorRepository.save(entity) + .toTriggerDescriptor() + } + + @Transactional + override fun create(create: List): List { + val entities = create.map { dto -> + TriggerDescriptorEntity().apply { + expression = dto.expression + triggerName = dto.name + type = dto.type + worker = workerRepository.findByNameOrThrow(dto.workerName) + disabledAt = dto.disabledAt + } + } + + return triggerDescriptorRepository.saveAll(entities) + .map(TriggerDescriptorEntity::toTriggerDescriptor) + } + + @Transactional + override fun setDeleted(id: UUID): TriggerDescriptor { + val entity = triggerDescriptorRepository.findByIdOrThrow(id) + + entity.apply { deletedAt = ZonedDateTime.now() } + + return triggerDescriptorRepository.save(entity) + .toTriggerDescriptor() + } + + @Transactional(readOnly = true) + override fun getByWorkerName(workerName: String): List { + return triggerDescriptorRepository.findAll(workerName) + .map(TriggerDescriptorEntity::toTriggerDescriptor) + } + +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/services/dto/CreateTriggerDescriptor.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/services/dto/CreateTriggerDescriptor.kt new file mode 100644 index 0000000..2f3f545 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/trigger/services/dto/CreateTriggerDescriptor.kt @@ -0,0 +1,12 @@ +package ru.touchin.spring.workers.manager.core.trigger.services.dto + +import ru.touchin.spring.workers.manager.core.trigger.enums.TriggerType +import java.time.ZonedDateTime + +data class CreateTriggerDescriptor( + val name: String, + val type: TriggerType, + val expression: String, + val disabledAt: ZonedDateTime?, + val workerName: String, +) diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/converters/WorkerConverters.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/converters/WorkerConverters.kt new file mode 100644 index 0000000..5be2895 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/converters/WorkerConverters.kt @@ -0,0 +1,16 @@ +package ru.touchin.spring.workers.manager.core.worker.converters + +import ru.touchin.spring.workers.manager.core.trigger.converters.toTriggerDescriptor +import ru.touchin.spring.workers.manager.core.trigger.models.TriggerDescriptorEntity +import ru.touchin.spring.workers.manager.core.worker.dto.Worker +import ru.touchin.spring.workers.manager.core.worker.models.WorkerEntity + +fun WorkerEntity.toWorker(): Worker { + return Worker( + name = workerName, + stoppedAt = stoppedAt, + disabledAt = disabledAt, + status = status, + parallelExecutionEnabled = parallelExecutionEnabled, + ) +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/dto/Worker.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/dto/Worker.kt new file mode 100644 index 0000000..4421be4 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/dto/Worker.kt @@ -0,0 +1,19 @@ +package ru.touchin.spring.workers.manager.core.worker.dto + +import ru.touchin.spring.workers.manager.core.trigger.dto.TriggerDescriptor +import ru.touchin.spring.workers.manager.core.worker.enums.WorkerStatus +import java.time.ZonedDateTime + +data class Worker( + val name: String, + val stoppedAt: ZonedDateTime?, + val disabledAt: ZonedDateTime?, + val status: WorkerStatus, + val parallelExecutionEnabled: Boolean, +) { + + fun isStopped() = stoppedAt != null + + fun isDisabled() = disabledAt != null + +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/enums/WorkerStatus.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/enums/WorkerStatus.kt new file mode 100644 index 0000000..e047d24 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/enums/WorkerStatus.kt @@ -0,0 +1,8 @@ +package ru.touchin.spring.workers.manager.core.worker.enums + +enum class WorkerStatus { + + IDLE, + PROCESSING + +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/exceptions/WorkerNotFoundException.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/exceptions/WorkerNotFoundException.kt new file mode 100644 index 0000000..8938362 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/exceptions/WorkerNotFoundException.kt @@ -0,0 +1,7 @@ +package ru.touchin.spring.workers.manager.core.worker.exceptions + +import ru.touchin.common.exceptions.CommonNotFoundException + +class WorkerNotFoundException(name: String): CommonNotFoundException( + "Worker not found name=$name" +) diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/models/WorkerEntity.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/models/WorkerEntity.kt new file mode 100644 index 0000000..19f9c90 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/models/WorkerEntity.kt @@ -0,0 +1,41 @@ +package ru.touchin.spring.workers.manager.core.worker.models + +import ru.touchin.common.spring.jpa.models.BaseEntity +import ru.touchin.spring.workers.manager.WorkersManagerConfiguration.Companion.SCHEMA +import ru.touchin.spring.workers.manager.core.trigger.models.TriggerDescriptorEntity +import ru.touchin.spring.workers.manager.core.worker.enums.WorkerStatus +import java.time.ZonedDateTime +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated +import javax.persistence.FetchType +import javax.persistence.Id +import javax.persistence.OneToMany +import javax.persistence.Table + +@Entity +@Table(name = "workers", schema = SCHEMA) +class WorkerEntity : BaseEntity() { + + @Id + @Column(name = "worker_name", unique = true) + lateinit var workerName: String + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + lateinit var status: WorkerStatus + + @Column(name = "disabled_at", nullable = true) + var disabledAt: ZonedDateTime? = null + + @Column(name = "stopped_at", nullable = true) + var stoppedAt: ZonedDateTime? = null + + @Column(name = "parallel_execution_enabled", nullable = false) + var parallelExecutionEnabled: Boolean = false + + @OneToMany(mappedBy = "worker", orphanRemoval = true, fetch = FetchType.LAZY) + lateinit var triggerDescriptors: Set + +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/repositories/WorkerRepository.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/repositories/WorkerRepository.kt new file mode 100644 index 0000000..4fb3f0a --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/repositories/WorkerRepository.kt @@ -0,0 +1,36 @@ +package ru.touchin.spring.workers.manager.core.worker.repositories + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Lock +import org.springframework.data.jpa.repository.Query +import ru.touchin.spring.workers.manager.core.worker.exceptions.WorkerNotFoundException +import ru.touchin.spring.workers.manager.core.worker.models.WorkerEntity +import javax.persistence.LockModeType + +interface WorkerRepository : JpaRepository { + + @Query( + """ + SELECT w + FROM WorkerEntity w + WHERE w.workerName = :workerName + """ + ) + @Lock(LockModeType.PESSIMISTIC_WRITE) + fun findWithLock(workerName: String): WorkerEntity? + + @Query( + """ + SELECT w + FROM WorkerEntity w + WHERE w.workerName = :workerName + """ + ) + fun findByName(workerName: String): WorkerEntity? + +} + +fun WorkerRepository.findByNameOrThrow(name: String): WorkerEntity { + return findByName(name) + ?: throw WorkerNotFoundException(name) +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/services/WorkerCoreService.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/services/WorkerCoreService.kt new file mode 100644 index 0000000..7739b6b --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/services/WorkerCoreService.kt @@ -0,0 +1,17 @@ +package ru.touchin.spring.workers.manager.core.worker.services + +import ru.touchin.spring.workers.manager.core.worker.dto.Worker +import ru.touchin.spring.workers.manager.core.worker.exceptions.WorkerNotFoundException +import ru.touchin.spring.workers.manager.core.worker.models.WorkerEntity +import ru.touchin.spring.workers.manager.core.worker.services.dto.UpdateWorker + +interface WorkerCoreService { + + fun create(name: String): Worker + fun update(update: UpdateWorker): Worker + fun getWithLock(name: String): Worker? + fun get(name: String): Worker = getOrNull(name) ?: throw WorkerNotFoundException(name) + fun getOrNull(name: String): Worker? + fun getAll(): List + +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/services/WorkerCoreServiceImpl.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/services/WorkerCoreServiceImpl.kt new file mode 100644 index 0000000..b7d063c --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/services/WorkerCoreServiceImpl.kt @@ -0,0 +1,56 @@ +package ru.touchin.spring.workers.manager.core.worker.services + +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import ru.touchin.spring.workers.manager.core.worker.converters.toWorker +import ru.touchin.spring.workers.manager.core.worker.dto.Worker +import ru.touchin.spring.workers.manager.core.worker.enums.WorkerStatus +import ru.touchin.spring.workers.manager.core.worker.models.WorkerEntity +import ru.touchin.spring.workers.manager.core.worker.repositories.WorkerRepository +import ru.touchin.spring.workers.manager.core.worker.repositories.findByNameOrThrow +import ru.touchin.spring.workers.manager.core.worker.services.dto.UpdateWorker + +@Service +class WorkerCoreServiceImpl( + private val workerRepository: WorkerRepository, +) : WorkerCoreService { + + @Transactional + override fun create(name: String): Worker { + val entity = WorkerEntity().apply { + workerName = name + status = WorkerStatus.IDLE + } + + return workerRepository.save(entity) + .toWorker() + } + + @Transactional + override fun update(update: UpdateWorker): Worker { + val entity = workerRepository.findByNameOrThrow(update.name) + .apply { status = update.status } + + return workerRepository.save(entity) + .toWorker() + } + + @Transactional + override fun getWithLock(name: String): Worker? { + return workerRepository.findWithLock(name) + ?.toWorker() + } + + @Transactional(readOnly = true) + override fun getOrNull(name: String): Worker? { + return workerRepository.findByName(name) + ?.toWorker() + } + + @Transactional(readOnly = true) + override fun getAll(): List { + return workerRepository.findAll() + .map(WorkerEntity::toWorker) + } + +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/services/WorkerStateServiceImpl.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/services/WorkerStateServiceImpl.kt new file mode 100644 index 0000000..0f02056 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/services/WorkerStateServiceImpl.kt @@ -0,0 +1,51 @@ +package ru.touchin.spring.workers.manager.core.worker.services + + +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import ru.touchin.spring.workers.manager.core.worker.models.WorkerEntity +import ru.touchin.spring.workers.manager.core.worker.repositories.WorkerRepository +import java.time.ZonedDateTime + +@Service +class WorkerStateServiceImpl( + private val workerRepository: WorkerRepository +) : WorkersStateService { + + @Transactional + override fun stop(name: String) { + updateWorkerWithLock(name) { + it.stoppedAt = ZonedDateTime.now() + } + } + + @Transactional + override fun start(name: String) { + updateWorkerWithLock(name) { + it.stoppedAt = null + } + } + + @Transactional + override fun disable(name: String) { + updateWorkerWithLock(name) { + it.disabledAt = ZonedDateTime.now() + } + } + + @Transactional + override fun enable(name: String) { + updateWorkerWithLock(name) { + it.disabledAt = null + } + } + + private fun updateWorkerWithLock(name: String, updater: (WorkerEntity) -> Unit) { + val worker = workerRepository.findWithLock(name) + ?.apply(updater) + ?: throw NoSuchElementException("Worker with name $name not found") + + workerRepository.save(worker) + } + +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/services/WorkersStateService.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/services/WorkersStateService.kt new file mode 100644 index 0000000..46fd10b --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/services/WorkersStateService.kt @@ -0,0 +1,10 @@ +package ru.touchin.spring.workers.manager.core.worker.services + +interface WorkersStateService { + + fun stop(name: String) + fun start(name: String) + fun disable(name: String) + fun enable(name: String) + +} diff --git a/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/services/dto/UpdateWorker.kt b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/services/dto/UpdateWorker.kt new file mode 100644 index 0000000..899e4e2 --- /dev/null +++ b/spring-workers-manager-core/src/main/kotlin/ru/touchin/spring/workers/manager/core/worker/services/dto/UpdateWorker.kt @@ -0,0 +1,8 @@ +package ru.touchin.spring.workers.manager.core.worker.services.dto + +import ru.touchin.spring.workers.manager.core.worker.enums.WorkerStatus + +data class UpdateWorker( + val name: String, + val status: WorkerStatus, +) diff --git a/spring-workers-manager-core/src/main/resources/workers/db/changelog/db.changelog-master.yaml b/spring-workers-manager-core/src/main/resources/workers/db/changelog/db.changelog-master.yaml new file mode 100644 index 0000000..0ad2e94 --- /dev/null +++ b/spring-workers-manager-core/src/main/resources/workers/db/changelog/db.changelog-master.yaml @@ -0,0 +1,165 @@ +databaseChangeLog: + - changeSet: + id: create-table-workers + author: sonkate + preConditions: + - onFail: MARK_RAN + not: + tableExists: + tableName: workers + changes: + - createTable: + tableName: workers + columns: + - column: + name: worker_name + type: VARCHAR(250) + constraints: + nullable: false + primaryKey: true + primaryKeyName: PK_WORKERS + - column: + name: status + type: VARCHAR(250) + constraints: + nullable: false + - column: + name: parallel_execution_enabled + type: BOOLEAN + defaultValueBoolean: false + constraints: + nullable: false + - column: + name: created_at + type: TIMESTAMP WITH TIME ZONE + defaultValueDate: CURRENT_TIMESTAMP + constraints: + nullable: false + - column: + name: updated_at + type: TIMESTAMP WITH TIME ZONE + - column: + name: stopped_at + type: TIMESTAMP WITH TIME ZONE + - column: + name: disabled_at + type: TIMESTAMP WITH TIME ZONE + - changeSet: + id: create-table-triggers + author: sonkate + preConditions: + - onFail: MARK_RAN + not: + tableExists: + tableName: triggers + changes: + - createTable: + tableName: triggers + columns: + - column: + name: id + type: UUID + constraints: + nullable: false + primaryKey: true + primaryKeyName: PK_TRIGGERS + - column: + name: worker_name + type: VARCHAR(250) + constraints: + nullable: false + foreignKeyName: fk_trigger_worker + references: workers(worker_name) + - column: + name: type + type: VARCHAR(250) + constraints: + nullable: false + - column: + name: trigger_name + type: VARCHAR(250) + constraints: + nullable: false + - column: + name: expression + type: VARCHAR(250) + constraints: + nullable: false + - column: + name: created_at + type: TIMESTAMP WITH TIME ZONE + defaultValueDate: CURRENT_TIMESTAMP + constraints: + nullable: false + - column: + name: updated_at + type: TIMESTAMP WITH TIME ZONE + - column: + name: deleted_at + type: TIMESTAMP WITH TIME ZONE + - column: + name: disabled_at + type: TIMESTAMP WITH TIME ZONE + - changeSet: + id: create-table-executions + author: sonkate + preConditions: + - onFail: MARK_RAN + not: + tableExists: + tableName: executions + changes: + - createTable: + tableName: executions + columns: + - column: + name: id + type: UUID + constraints: + nullable: false + primaryKey: true + primaryKeyName: PK_EXECUTIONS + - column: + name: worker_name + type: VARCHAR(250) + constraints: + nullable: false + - column: + name: trigger_id + type: UUID + - column: + constraints: + nullable: false + name: status + type: VARCHAR(250) + - column: + name: error_message + type: VARCHAR(250) + - column: + name: error_code + type: int + - column: + name: created_at + type: TIMESTAMP WITH TIME ZONE + defaultValueDate: CURRENT_TIMESTAMP + constraints: + nullable: false + - column: + name: updated_at + type: TIMESTAMP WITH TIME ZONE + - column: + name: started_at + type: TIMESTAMP WITH TIME ZONE + - column: + name: finished_at + type: TIMESTAMP WITH TIME ZONE + - changeSet: + id: create-index-for-executions + author: sonkate + changes: + - sql: + sql: | + CREATE INDEX fk_trigger_worker_idx + ON ${schemaName}.triggers (worker_name ASC) + WHERE deleted_at IS NULL; + endDelimeter: ;