diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java index 4626ac8d..a6a23d86 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java @@ -370,6 +370,14 @@ public class QPossibleValueTranslator *******************************************************************************/ private String translatePossibleValueCustom(Serializable value, QPossibleValueSource possibleValueSource) { + ///////////////////////////////// + // null input gets null output // + ///////////////////////////////// + if(value == null) + { + return (null); + } + try { QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java index 45d249ed..4bcac23f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java @@ -77,6 +77,7 @@ import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.Bulk import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertTransformStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; +import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ListingHash; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -159,6 +160,18 @@ public class QInstanceEnricher } enrichJoins(); + + ////////////////////////////////////////////////////////////////////////////// + // if the instance DOES have 1 or more scheduler, but no schedulable types, // + // then go ahead and add the default set that qqq knows about // + ////////////////////////////////////////////////////////////////////////////// + if(CollectionUtils.nullSafeHasContents(qInstance.getSchedulers())) + { + if(CollectionUtils.nullSafeIsEmpty(qInstance.getSchedulableTypes())) + { + QScheduleManager.defineDefaultSchedulableTypesInInstance(qInstance); + } + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/scheduleing/QSchedulerMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/scheduleing/QSchedulerMetaData.java index 90fb8f92..9eda36b2 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/scheduleing/QSchedulerMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/scheduleing/QSchedulerMetaData.java @@ -50,6 +50,16 @@ public abstract class QSchedulerMetaData implements TopLevelMetaDataInterface + /******************************************************************************* + ** + *******************************************************************************/ + public boolean mayUseInScheduledJobsTable() + { + return (true); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/scheduleing/quartz/QuartzSchedulerMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/scheduleing/quartz/QuartzSchedulerMetaData.java index 12ef0361..3367cd25 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/scheduleing/quartz/QuartzSchedulerMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/scheduleing/quartz/QuartzSchedulerMetaData.java @@ -68,6 +68,16 @@ public class QuartzSchedulerMetaData extends QSchedulerMetaData + /******************************************************************************* + ** + *******************************************************************************/ + public boolean mayUseInScheduledJobsTable() + { + return (true); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/scheduleing/simple/SimpleSchedulerMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/scheduleing/simple/SimpleSchedulerMetaData.java index 92ef2c99..69882b41 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/scheduleing/simple/SimpleSchedulerMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/scheduleing/simple/SimpleSchedulerMetaData.java @@ -61,6 +61,16 @@ public class SimpleSchedulerMetaData extends QSchedulerMetaData + /******************************************************************************* + ** + *******************************************************************************/ + public boolean mayUseInScheduledJobsTable() + { + return (false); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/ScheduledJob.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/ScheduledJob.java index 54bfc182..e923d549 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/ScheduledJob.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/ScheduledJob.java @@ -73,7 +73,7 @@ public class ScheduledJob extends QRecordEntity @QField(displayFormat = DisplayFormat.COMMAS) private Integer repeatSeconds; - @QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR, possibleValueSourceName = ScheduledJobType.NAME) + @QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR, possibleValueSourceName = ScheduledJobTypePossibleValueSource.NAME) private String type; @QField(isRequired = true) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/ScheduledJobType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/ScheduledJobType.java index c8296e40..56114667 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/ScheduledJobType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/ScheduledJobType.java @@ -22,102 +22,16 @@ package com.kingsrook.qqq.backend.core.model.scheduledjobs; -import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher; -import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum; - - /******************************************************************************* + ** enum of core schedulable types that QQQ schedule manager directly knows about. ** + ** note though, that applications can define their own schedulable types, + ** by adding SchedulableType meta-data to the QInstance, and providing classes + ** that implement SchedulableRunner. *******************************************************************************/ -public enum ScheduledJobType implements PossibleValueEnum +public enum ScheduledJobType { PROCESS, QUEUE_PROCESSOR, - TABLE_AUTOMATIONS, - // todo - future - USER_REPORT - ; - - public static final String NAME = "scheduledJobType"; - - private final String label; - - - - /******************************************************************************* - ** Constructor - ** - *******************************************************************************/ - ScheduledJobType() - { - this.label = QInstanceEnricher.nameToLabel(QInstanceEnricher.inferNameFromBackendName(name())); - } - - - - /******************************************************************************* - ** Get instance by id - ** - *******************************************************************************/ - public static ScheduledJobType getById(String id) - { - if(id == null) - { - return (null); - } - - for(ScheduledJobType value : ScheduledJobType.values()) - { - if(value.name().equals(id)) - { - return (value); - } - } - - return (null); - } - - - - /******************************************************************************* - ** Getter for id - ** - *******************************************************************************/ - public String getId() - { - return name(); - } - - - - /******************************************************************************* - ** Getter for label - ** - *******************************************************************************/ - public String getLabel() - { - return label; - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public String getPossibleValueId() - { - return name(); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public String getPossibleValueLabel() - { - return (label); - } - + TABLE_AUTOMATIONS } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/ScheduledJobTypePossibleValueSource.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/ScheduledJobTypePossibleValueSource.java new file mode 100644 index 00000000..43d26dd5 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/ScheduledJobTypePossibleValueSource.java @@ -0,0 +1,87 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.scheduledjobs; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; +import com.kingsrook.qqq.backend.core.scheduler.schedulable.SchedulableType; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ScheduledJobTypePossibleValueSource implements QCustomPossibleValueProvider +{ + public static final String NAME = "scheduledJobType"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QPossibleValue getPossibleValue(Serializable idValue) + { + SchedulableType schedulableType = QContext.getQInstance().getSchedulableType(String.valueOf(idValue)); + if(schedulableType != null) + { + return schedulableTypeToPossibleValue(schedulableType); + } + + return null; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public List> search(SearchPossibleValueSourceInput input) throws QException + { + List> rs = new ArrayList<>(); + for(SchedulableType schedulableType : CollectionUtils.nonNullMap(QContext.getQInstance().getSchedulableTypes()).values()) + { + rs.add(schedulableTypeToPossibleValue(schedulableType)); + } + return rs; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QPossibleValue schedulableTypeToPossibleValue(SchedulableType schedulableType) + { + return new QPossibleValue<>(schedulableType.getName(), schedulableType.getName()); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/ScheduledJobsMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/ScheduledJobsMetaDataProvider.java index 9b10da45..f814ff88 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/ScheduledJobsMetaDataProvider.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/ScheduledJobsMetaDataProvider.java @@ -45,6 +45,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; +import com.kingsrook.qqq.backend.core.model.scheduledjobs.customizers.ScheduledJobParameterTableCustomizer; import com.kingsrook.qqq.backend.core.model.scheduledjobs.customizers.ScheduledJobTableCustomizer; @@ -64,7 +65,7 @@ public class ScheduledJobsMetaDataProvider { defineStandardTables(instance, backendName, backendDetailEnricher); instance.addPossibleValueSource(QPossibleValueSource.newForTable(ScheduledJob.TABLE_NAME)); - instance.addPossibleValueSource(QPossibleValueSource.newForEnum(ScheduledJobType.NAME, ScheduledJobType.values())); + instance.addPossibleValueSource(defineScheduledJobTypePossibleValueSource()); instance.addPossibleValueSource(defineSchedulersPossibleValueSource()); defineStandardJoins(instance); defineStandardWidgets(instance); @@ -205,6 +206,12 @@ public class ScheduledJobsMetaDataProvider .withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "scheduledJobId", "key", "value"))) .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate"))); + + QCodeReference customizerReference = new QCodeReference(ScheduledJobParameterTableCustomizer.class); + tableMetaData.withCustomizer(TableCustomizers.POST_INSERT_RECORD, customizerReference); + tableMetaData.withCustomizer(TableCustomizers.POST_UPDATE_RECORD, customizerReference); + tableMetaData.withCustomizer(TableCustomizers.POST_DELETE_RECORD, customizerReference); + tableMetaData.withExposedJoin(new ExposedJoin() .withJoinTable(ScheduledJob.TABLE_NAME) .withJoinPath(List.of(JOB_PARAMETER_JOIN_NAME)) @@ -215,6 +222,19 @@ public class ScheduledJobsMetaDataProvider + + /******************************************************************************* + ** + *******************************************************************************/ + private QPossibleValueSource defineScheduledJobTypePossibleValueSource() + { + return (new QPossibleValueSource() + .withName(ScheduledJobTypePossibleValueSource.NAME) + .withType(QPossibleValueSourceType.CUSTOM) + .withCustomCodeReference(new QCodeReference(ScheduledJobTypePossibleValueSource.class))); + } + + /******************************************************************************* ** *******************************************************************************/ @@ -224,7 +244,6 @@ public class ScheduledJobsMetaDataProvider .withName(SchedulersPossibleValueSource.NAME) .withType(QPossibleValueSourceType.CUSTOM) .withCustomCodeReference(new QCodeReference(SchedulersPossibleValueSource.class))); - } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/SchedulersPossibleValueSource.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/SchedulersPossibleValueSource.java index a2363267..e46c81c0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/SchedulersPossibleValueSource.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/SchedulersPossibleValueSource.java @@ -69,7 +69,10 @@ public class SchedulersPossibleValueSource implements QCustomPossibleValueProvid List> rs = new ArrayList<>(); for(QSchedulerMetaData scheduler : CollectionUtils.nonNullMap(QContext.getQInstance().getSchedulers()).values()) { - rs.add(schedulerToPossibleValue(scheduler)); + if(scheduler.mayUseInScheduledJobsTable()) + { + rs.add(schedulerToPossibleValue(scheduler)); + } } return rs; } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/customizers/ScheduledJobParameterTableCustomizer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/customizers/ScheduledJobParameterTableCustomizer.java new file mode 100644 index 00000000..798efdba --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/customizers/ScheduledJobParameterTableCustomizer.java @@ -0,0 +1,215 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.scheduledjobs.customizers; + + +import java.time.Instant; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; +import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJob; +import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobParameter; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ListingHash; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ScheduledJobParameterTableCustomizer implements TableCustomizerInterface +{ + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public List postInsert(InsertInput insertInput, List records) throws QException + { + /////////////////////////////////////////////////////////////////////////////////////// + // if we're in this insert as a result of an insert (or update) on a different table // + // (e.g., under a manageAssociations call), then return with noop - assume that the // + // parent table's customizer will do what needed to be done. // + /////////////////////////////////////////////////////////////////////////////////////// + if(!isThisAnActionDirectlyOnThisTable()) + { + return (records); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // else - this was an action directly on this table - so bump all of the parent records, to get them rescheduled // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + bumpParentRecords(records, Optional.empty()); + + return (records); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void bumpParentRecords(List records, Optional> oldRecordList) throws QException + { + try + { + /////////////////////////////////////////////////////////////////////////////////////////// + // (listing) hash up the records by scheduledJobId - we'll use this to have a set of the // + // job ids, and in case we need to add warnings to them later // + /////////////////////////////////////////////////////////////////////////////////////////// + ListingHash recordsByJobId = new ListingHash<>(); + for(QRecord record : records) + { + recordsByJobId.add(record.getValueInteger("scheduledJobId"), record); + } + + Set scheduledJobIds = new HashSet<>(recordsByJobId.keySet()); + + //////////////////////////////////////////////////////////////////////////////// + // if we have an old record list (e.g., is an edit), add any job ids that are // + // in those too, e.g., in case moving a param from one job to another... // + // note, we won't line these up for doing a proper warning on these... // + //////////////////////////////////////////////////////////////////////////////// + if(oldRecordList.isPresent()) + { + for(QRecord oldRecord : oldRecordList.get()) + { + scheduledJobIds.add(oldRecord.getValueInteger("scheduledJobId")); + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////////// + // update the modify date on the scheduled jobs - to get their post-actions to run, to reschedule // + //////////////////////////////////////////////////////////////////////////////////////////////////// + UpdateInput updateInput = new UpdateInput(); + updateInput.setTableName(ScheduledJob.TABLE_NAME); + updateInput.setRecords(scheduledJobIds.stream() + .map(id -> new QRecord().withValue("id", id).withValue("modifyDate", Instant.now())) + .toList()); + UpdateOutput updateOutput = new UpdateAction().execute(updateInput); + + //////////////////////////////////////////////////////////////////////////////////////// + // look for warnings on those jobs - and propagate them to the params we just stored. // + //////////////////////////////////////////////////////////////////////////////////////// + for(QRecord updatedScheduledJob : updateOutput.getRecords()) + { + if(CollectionUtils.nullSafeHasContents(updatedScheduledJob.getWarnings())) + { + for(QRecord paramToWarn : CollectionUtils.nonNullList(recordsByJobId.get(updatedScheduledJob.getValueInteger("id")))) + { + paramToWarn.setWarnings(updatedScheduledJob.getWarnings()); + } + } + } + } + catch(Exception e) + { + LOG.warn("Error in scheduledJobParameter post-crud", e); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public List postUpdate(UpdateInput updateInput, List records, Optional> oldRecordList) throws QException + { + ///////////////////////////////////////////////////////////////////////////// + // if we're in this update as a result of an update on a different table // + // (e.g., under a manageAssociations call), then return with noop - assume // + // that the parent table's customizer will do what needed to be done. // + ///////////////////////////////////////////////////////////////////////////// + if(!isThisAnActionDirectlyOnThisTable()) + { + return (records); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // else - this was an action directly on this table - so bump all of the parent records, to get them rescheduled // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + bumpParentRecords(records, oldRecordList); + + return (records); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public List postDelete(DeleteInput deleteInput, List records) throws QException + { + ///////////////////////////////////////////////////////////////////////////// + // if we're in this update as a result of an update on a different table // + // (e.g., under a manageAssociations call), then return with noop - assume // + // that the parent table's customizer will do what needed to be done. // + ///////////////////////////////////////////////////////////////////////////// + if(!isThisAnActionDirectlyOnThisTable()) + { + return (records); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // else - this was an action directly on this table - so bump all of the parent records, to get them rescheduled // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + bumpParentRecords(records, Optional.empty()); + + return (records); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean isThisAnActionDirectlyOnThisTable() + { + Optional firstActionInStack = QContext.getFirstActionInStack(); + if(firstActionInStack.isPresent()) + { + if(firstActionInStack.get() instanceof AbstractTableActionInput tableActionInput) + { + if(!ScheduledJobParameter.TABLE_NAME.equals(tableActionInput.getTableName())) + { + return (false); + } + } + } + return (true); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/customizers/ScheduledJobTableCustomizer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/customizers/ScheduledJobTableCustomizer.java index f3cc54cf..7203f500 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/customizers/ScheduledJobTableCustomizer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/customizers/ScheduledJobTableCustomizer.java @@ -22,10 +22,12 @@ package com.kingsrook.qqq.backend.core.model.scheduledjobs.customizers; -import java.io.Serializable; +import java.text.ParseException; +import java.util.Collections; import java.util.List; import java.util.ListIterator; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; @@ -47,6 +49,7 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage; import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; +import org.quartz.CronScheduleBuilder; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -62,7 +65,7 @@ public class ScheduledJobTableCustomizer implements TableCustomizerInterface @Override public List preInsert(InsertInput insertInput, List records, boolean isPreview) throws QException { - validateConditionalFields(records); + validateConditionalFields(records, Collections.emptyMap()); return (records); } @@ -86,7 +89,9 @@ public class ScheduledJobTableCustomizer implements TableCustomizerInterface @Override public List preUpdate(UpdateInput updateInput, List records, boolean isPreview, Optional> oldRecordList) throws QException { - validateConditionalFields(records); + Map freshOldRecordsWithAssociationsMap = CollectionUtils.recordsToMap(freshlyQueryForRecordsWithAssociations(oldRecordList.get()), "id", Integer.class); + + validateConditionalFields(records, freshOldRecordsWithAssociationsMap); if(isPreview || oldRecordList.isEmpty()) { @@ -96,8 +101,7 @@ public class ScheduledJobTableCustomizer implements TableCustomizerInterface /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // refresh the old-records w/ versions that have associations - so we can use those in the post-update to property unschedule things // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - Map freshOldRecordsWithAssociationsMap = CollectionUtils.recordsToMap(freshlyQueryForRecordsWithAssociations(oldRecordList.get()), "id"); - ListIterator iterator = oldRecordList.get().listIterator(); + ListIterator iterator = oldRecordList.get().listIterator(); while(iterator.hasNext()) { QRecord record = iterator.next(); @@ -116,20 +120,40 @@ public class ScheduledJobTableCustomizer implements TableCustomizerInterface /******************************************************************************* ** *******************************************************************************/ - private static void validateConditionalFields(List records) + private static void validateConditionalFields(List records, Map freshOldRecordsWithAssociationsMap) { + QRecord blankRecord = new QRecord(); for(QRecord record : records) { - if(StringUtils.hasContent(record.getValueString("cronExpression"))) + QRecord oldRecord = Objects.requireNonNullElse(freshOldRecordsWithAssociationsMap.get(record.getValueInteger("id")), blankRecord); + String cronExpression = record.getValues().containsKey("cronExpression") ? record.getValueString("cronExpression") : oldRecord.getValueString("cronExpression"); + String cronTimeZoneId = record.getValues().containsKey("cronTimeZoneId") ? record.getValueString("cronTimeZoneId") : oldRecord.getValueString("cronTimeZoneId"); + String repeatSeconds = record.getValues().containsKey("repeatSeconds") ? record.getValueString("repeatSeconds") : oldRecord.getValueString("repeatSeconds"); + + if(StringUtils.hasContent(cronExpression)) { - if(!StringUtils.hasContent(record.getValueString("cronTimeZoneId"))) + if(StringUtils.hasContent(repeatSeconds)) { - record.addError(new BadInputStatusMessage("If a Cron Expression is given, then a Cron Time Zone Id is required.")); + record.addError(new BadInputStatusMessage("Cron Expression and Repeat Seconds may not both be given.")); + } + + try + { + CronScheduleBuilder.cronScheduleNonvalidatedExpression(cronExpression); + } + catch(ParseException e) + { + record.addError(new BadInputStatusMessage("Cron Expression [" + cronExpression + "] is not valid: " + e.getMessage())); + } + + if(!StringUtils.hasContent(cronTimeZoneId)) + { + record.addError(new BadInputStatusMessage("If a Cron Expression is given, then a Cron Time Zone is required.")); } } else { - if(!StringUtils.hasContent(record.getValueString("repeatSeconds"))) + if(!StringUtils.hasContent(repeatSeconds)) { record.addError(new BadInputStatusMessage("Either Cron Expression or Repeat Seconds must be given.")); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/QScheduleManager.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/QScheduleManager.java index 747c366b..a51999ab 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/QScheduleManager.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/QScheduleManager.java @@ -102,15 +102,6 @@ public class QScheduleManager { qScheduleManager = new QScheduleManager(qInstance, systemUserSessionSupplier); - ///////////////////////////////////////////////////////////////// - // if the instance doesn't have any schedulable types defined, // - // then go ahead and add the default set that qqq knows about // - ///////////////////////////////////////////////////////////////// - if(CollectionUtils.nullSafeIsEmpty(qInstance.getSchedulableTypes())) - { - defineDefaultSchedulableTypesInInstance(qInstance); - } - ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // initialize the scheduler(s) we're configured to use // // do this, even if we won't start them - so, for example, a web server can still be aware of schedules in the application // @@ -131,9 +122,9 @@ public class QScheduleManager *******************************************************************************/ public static void defineDefaultSchedulableTypesInInstance(QInstance qInstance) { - qInstance.addSchedulableType(new SchedulableType().withName(ScheduledJobType.PROCESS.getId()).withRunner(new QCodeReference(SchedulableProcessRunner.class))); - qInstance.addSchedulableType(new SchedulableType().withName(ScheduledJobType.QUEUE_PROCESSOR.getId()).withRunner(new QCodeReference(SchedulableSQSQueueRunner.class))); - qInstance.addSchedulableType(new SchedulableType().withName(ScheduledJobType.TABLE_AUTOMATIONS.getId()).withRunner(new QCodeReference(SchedulableTableAutomationsRunner.class))); + qInstance.addSchedulableType(new SchedulableType().withName(ScheduledJobType.PROCESS.name()).withRunner(new QCodeReference(SchedulableProcessRunner.class))); + qInstance.addSchedulableType(new SchedulableType().withName(ScheduledJobType.QUEUE_PROCESSOR.name()).withRunner(new QCodeReference(SchedulableSQSQueueRunner.class))); + qInstance.addSchedulableType(new SchedulableType().withName(ScheduledJobType.TABLE_AUTOMATIONS.name()).withRunner(new QCodeReference(SchedulableTableAutomationsRunner.class))); } @@ -330,8 +321,8 @@ public class QScheduleManager throw (new QException("Missing a type " + exceptionSuffix)); } - ScheduledJobType scheduledJobType = ScheduledJobType.getById(scheduledJob.getType()); - if(scheduledJobType == null) + SchedulableType schedulableType = qInstance.getSchedulableType(scheduledJob.getType()); + if(schedulableType == null) { throw (new QException("Unrecognized type [" + scheduledJob.getType() + "] " + exceptionSuffix)); } @@ -339,8 +330,6 @@ public class QScheduleManager QSchedulerInterface scheduler = getScheduler(scheduledJob.getSchedulerName()); Map paramMap = new HashMap<>(scheduledJob.getJobParametersMap()); - SchedulableType schedulableType = qInstance.getSchedulableType(scheduledJob.getType()); - SchedulableRunner runner = QCodeLoader.getAdHoc(SchedulableRunner.class, schedulableType.getRunner()); runner.validateParams(schedulableIdentity, new HashMap<>(paramMap)); @@ -396,7 +385,7 @@ public class QScheduleManager Map paramMap = new HashMap<>(); paramMap.put("processName", process.getName()); - SchedulableType schedulableType = qInstance.getSchedulableType(ScheduledJobType.PROCESS.getId()); + SchedulableType schedulableType = qInstance.getSchedulableType(ScheduledJobType.PROCESS.name()); if(process.getVariantBackend() == null || VariantRunStrategy.SERIAL.equals(process.getVariantRunStrategy())) { @@ -446,7 +435,7 @@ public class QScheduleManager *******************************************************************************/ private void setupTableAutomations(QTableMetaData table) throws QException { - SchedulableType schedulableType = qInstance.getSchedulableType(ScheduledJobType.TABLE_AUTOMATIONS.getId()); + SchedulableType schedulableType = qInstance.getSchedulableType(ScheduledJobType.TABLE_AUTOMATIONS.name()); QTableAutomationDetails automationDetails = table.getAutomationDetails(); QSchedulerInterface scheduler = getScheduler(automationDetails.getSchedule().getSchedulerName()); @@ -475,7 +464,7 @@ public class QScheduleManager { SchedulableIdentity schedulableIdentity = SchedulableIdentityFactory.of(queue); QSchedulerInterface scheduler = getScheduler(queue.getSchedule().getSchedulerName()); - SchedulableType schedulableType = qInstance.getSchedulableType(ScheduledJobType.QUEUE_PROCESSOR.getId()); + SchedulableType schedulableType = qInstance.getSchedulableType(ScheduledJobType.QUEUE_PROCESSOR.name()); boolean allowedToStart = SchedulerUtils.allowedToStart(queue.getName()); Map paramMap = new HashMap<>(); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/PauseQuartzJobsProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/PauseQuartzJobsProcess.java index f74791cc..c5d3d38b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/PauseQuartzJobsProcess.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/PauseQuartzJobsProcess.java @@ -52,11 +52,12 @@ public class PauseQuartzJobsProcess extends AbstractLoadStep implements MetaData @Override public QProcessMetaData produce(QInstance qInstance) throws QException { - String tableName = "quartzTriggers"; + String tableName = "quartzJobDetails"; return StreamedETLWithFrontendProcess.processMetaDataBuilder() .withName(getClass().getSimpleName()) .withLabel("Pause Quartz Jobs") + .withPreviewMessage("This is a preview of the jobs that will be paused.") .withTableName(tableName) .withSourceTable(tableName) .withDestinationTable(tableName) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/ResumeQuartzJobsProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/ResumeQuartzJobsProcess.java index 687f29b3..c4a8f3b8 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/ResumeQuartzJobsProcess.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/ResumeQuartzJobsProcess.java @@ -52,11 +52,12 @@ public class ResumeQuartzJobsProcess extends AbstractLoadStep implements MetaDat @Override public QProcessMetaData produce(QInstance qInstance) throws QException { - String tableName = "quartzTriggers"; + String tableName = "quartzJobDetails"; return StreamedETLWithFrontendProcess.processMetaDataBuilder() .withName(getClass().getSimpleName()) .withLabel("Resume Quartz Jobs") + .withPreviewMessage("This is a preview of the jobs that will be resumed.") .withTableName(tableName) .withSourceTable(tableName) .withDestinationTable(tableName) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/schedulable/identity/SchedulableIdentityFactory.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/schedulable/identity/SchedulableIdentityFactory.java index 01b1d17b..0ad89408 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/schedulable/identity/SchedulableIdentityFactory.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/scheduler/schedulable/identity/SchedulableIdentityFactory.java @@ -29,7 +29,6 @@ import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData; import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJob; -import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobType; import com.kingsrook.qqq.backend.core.scheduler.schedulable.SchedulableType; import com.kingsrook.qqq.backend.core.scheduler.schedulable.runner.SchedulableRunner; @@ -45,19 +44,18 @@ public class SchedulableIdentityFactory *******************************************************************************/ public static BasicSchedulableIdentity of(ScheduledJob scheduledJob) { - String description = ""; - ScheduledJobType scheduledJobType = ScheduledJobType.getById(scheduledJob.getType()); - if(scheduledJobType != null) + String description = ""; + SchedulableType schedulableType = QContext.getQInstance().getSchedulableType(scheduledJob.getType()); + if(schedulableType != null) { try { - SchedulableType schedulableType = QContext.getQInstance().getSchedulableType(scheduledJob.getType()); - SchedulableRunner runner = QCodeLoader.getAdHoc(SchedulableRunner.class, schedulableType.getRunner()); + SchedulableRunner runner = QCodeLoader.getAdHoc(SchedulableRunner.class, schedulableType.getRunner()); description = runner.getDescription(new HashMap<>(scheduledJob.getJobParametersMap())); } catch(Exception e) { - description = "type: " + scheduledJobType; + description = "type: " + schedulableType.getName(); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CollectionUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CollectionUtils.java index 95b8d091..1e53c346 100755 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CollectionUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CollectionUtils.java @@ -518,6 +518,24 @@ public class CollectionUtils + /******************************************************************************* + ** Convert a collection of QRecords to a map, from one field's values out of + ** those records, to the records themselves. + *******************************************************************************/ + public static Map recordsToMap(Collection records, String keyFieldName, Class type) + { + Map rs = new HashMap<>(); + + for(QRecord record : nonNullCollection(records)) + { + rs.put(ValueUtils.getValueAsType(type, record.getValue(keyFieldName)), record); + } + + return (rs); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/customizers/ScheduledJobTableCustomizerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/customizers/ScheduledJobTableCustomizerTest.java new file mode 100644 index 00000000..7e926fe1 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/scheduledjobs/customizers/ScheduledJobTableCustomizerTest.java @@ -0,0 +1,383 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.scheduledjobs.customizers; + + +import java.time.Instant; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJob; +import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobParameter; +import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobType; +import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobsMetaDataProvider; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager; +import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzJobAndTriggerWrapper; +import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzTestUtils; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction; +import org.assertj.core.api.AbstractStringAssert; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.quartz.CronTrigger; +import org.quartz.SchedulerException; +import org.quartz.SimpleTrigger; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** Unit test for ScheduledJobTableCustomizer + *******************************************************************************/ +class ScheduledJobTableCustomizerTest extends BaseTest +{ + private static final String GOOD_CRON = "0 * * * * ?"; + private static final String GOOD_CRON_2 = "* * * * * ?"; + private static final String BAD_CRON = "* * * * * *"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeEach() throws QException + { + QInstance qInstance = QContext.getQInstance(); + QuartzTestUtils.setupInstanceForQuartzTests(); + + QSession qSession = QContext.getQSession(); + QScheduleManager qScheduleManager = QScheduleManager.initInstance(qInstance, () -> qSession); + qScheduleManager.start(); + + new ScheduledJobsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @AfterEach + void afterEach() + { + QuartzTestUtils.afterEach(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPreInsertAssertValidationErrors() throws QException + { + UnsafeFunction, QRecord, QException> tryToInsert = consumer -> + { + ScheduledJob scheduledJob = new ScheduledJob() + .withLabel("Test") + .withSchedulerName(QuartzTestUtils.QUARTZ_SCHEDULER_NAME) + .withType(ScheduledJobType.PROCESS.name()) + .withIsActive(true); + consumer.accept(scheduledJob); + InsertOutput insertOutput = new InsertAction().execute(new InsertInput(ScheduledJob.TABLE_NAME).withRecordEntity(scheduledJob)); + return (insertOutput.getRecords().get(0)); + }; + + ///////////////////////////////////////////////////////// + // lambdas to run a test and assert about no of errors // + ///////////////////////////////////////////////////////// + Function> assertOneErrorExtractingMessage = qRecord -> assertThat(qRecord.getErrors()).hasSize(1).first().extracting("message").asString(); + Consumer assertNoErrors = qRecord -> assertThat(qRecord.getErrors()).hasSize(0); + + assertOneErrorExtractingMessage.apply(tryToInsert.apply(sj -> sj.setId(null))) + .contains("Either Cron Expression or Repeat Seconds must be given"); + + assertOneErrorExtractingMessage.apply(tryToInsert.apply(sj -> sj.withRepeatSeconds(1).withCronExpression(GOOD_CRON).withCronTimeZoneId("UTC"))) + .contains("Cron Expression and Repeat Seconds may not both be given"); + + assertOneErrorExtractingMessage.apply(tryToInsert.apply(sj -> sj.withRepeatSeconds(null).withCronExpression(GOOD_CRON))) + .contains("If a Cron Expression is given, then a Cron Time Zone is required"); + + assertOneErrorExtractingMessage.apply(tryToInsert.apply(sj -> sj.withRepeatSeconds(null).withCronExpression(BAD_CRON).withCronTimeZoneId("UTC"))) + .contains("Support for specifying both a day-of-week AND a day-of-month parameter is not implemented"); + + /////////////////// + // success cases // + /////////////////// + assertNoErrors.accept(tryToInsert.apply(sj -> sj.withCronExpression(GOOD_CRON).withCronTimeZoneId("UTC"))); + assertNoErrors.accept(tryToInsert.apply(sj -> sj.withRepeatSeconds(1))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPostInsertActionSchedulesJob() throws QException, SchedulerException + { + List wrappers = QuartzTestUtils.queryQuartz(); + assertEquals(0, wrappers.size()); + + InsertOutput insertOutput = new InsertAction().execute(new InsertInput(ScheduledJob.TABLE_NAME).withRecordEntity(new ScheduledJob() + .withLabel("Test") + .withSchedulerName(QuartzTestUtils.QUARTZ_SCHEDULER_NAME) + .withType(ScheduledJobType.PROCESS.name()) + .withIsActive(true) + .withRepeatSeconds(1) + .withJobParameters(List.of( + new ScheduledJobParameter().withKey("processName").withValue(TestUtils.PROCESS_NAME_BASEPULL))))); + + assertTrue(CollectionUtils.nullSafeIsEmpty(insertOutput.getRecords().get(0).getErrors())); + assertTrue(CollectionUtils.nullSafeIsEmpty(insertOutput.getRecords().get(0).getWarnings())); + + wrappers = QuartzTestUtils.queryQuartz(); + assertEquals(1, wrappers.size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPostInsertActionIssuesWarnings() throws QException, SchedulerException + { + List wrappers = QuartzTestUtils.queryQuartz(); + assertEquals(0, wrappers.size()); + + InsertOutput insertOutput = new InsertAction().execute(new InsertInput(ScheduledJob.TABLE_NAME).withRecordEntity(new ScheduledJob() + .withLabel("Test") + .withSchedulerName(QuartzTestUtils.QUARTZ_SCHEDULER_NAME) + .withType(ScheduledJobType.PROCESS.name()) + .withIsActive(true) + .withRepeatSeconds(1) + .withJobParameters(List.of( + new ScheduledJobParameter().withKey("processName").withValue("notAProcess"))))); + + assertTrue(CollectionUtils.nullSafeIsEmpty(insertOutput.getRecords().get(0).getErrors())); + assertThat(insertOutput.getRecords().get(0).getWarnings()) + .hasSize(1).first().extracting("message").asString() + .contains("Error scheduling job: Unrecognized processName"); + + wrappers = QuartzTestUtils.queryQuartz(); + assertEquals(0, wrappers.size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPreUpdateAssertValidationErrors() throws QException + { + new InsertAction().execute(new InsertInput(ScheduledJob.TABLE_NAME).withRecordEntity(new ScheduledJob() + .withLabel("Test") + .withSchedulerName(QuartzTestUtils.QUARTZ_SCHEDULER_NAME) + .withType(ScheduledJobType.PROCESS.name()) + .withIsActive(true) + .withRepeatSeconds(1))); + + UnsafeFunction, QRecord, QException> tryToUpdate = consumer -> + { + QRecord record = new QRecord().withValue("id", 1); + consumer.accept(record); + UpdateOutput updateOutput = new UpdateAction().execute(new UpdateInput(ScheduledJob.TABLE_NAME).withRecord(record)); + return (updateOutput.getRecords().get(0)); + }; + + ///////////////////////////////////////////////////////// + // lambdas to run a test and assert about no of errors // + ///////////////////////////////////////////////////////// + Function> assertOneErrorExtractingMessage = qRecord -> assertThat(qRecord.getErrors()).hasSize(1).first().extracting("message").asString(); + Consumer assertNoErrors = qRecord -> assertThat(qRecord.getErrors()).hasSize(0); + + assertOneErrorExtractingMessage.apply(tryToUpdate.apply(r -> r.withValue("repeatSeconds", null))) + .contains("Either Cron Expression or Repeat Seconds must be given"); + + assertOneErrorExtractingMessage.apply(tryToUpdate.apply(r -> r.withValue("cronExpression", GOOD_CRON).withValue("cronTimeZoneId", "UTC"))) + .contains("Cron Expression and Repeat Seconds may not both be given"); + + assertOneErrorExtractingMessage.apply(tryToUpdate.apply(r -> r.withValue("repeatSeconds", null).withValue("cronExpression", GOOD_CRON))) + .contains("If a Cron Expression is given, then a Cron Time Zone is required"); + + assertOneErrorExtractingMessage.apply(tryToUpdate.apply(r -> r.withValue("repeatSeconds", null).withValue("cronExpression", BAD_CRON).withValue("cronTimeZoneId", "UTC"))) + .contains("Support for specifying both a day-of-week AND a day-of-month parameter is not implemented"); + + /////////////////// + // success cases // + /////////////////// + assertNoErrors.accept(tryToUpdate.apply(r -> r.withValue("modifyDate", Instant.now()))); + assertNoErrors.accept(tryToUpdate.apply(r -> r.withValue("repeatSeconds", null).withValue("cronExpression", GOOD_CRON).withValue("cronTimeZoneId", "UTC"))); + assertNoErrors.accept(tryToUpdate.apply(r -> r.withValue("repeatSeconds", null).withValue("cronExpression", GOOD_CRON_2).withValue("cronTimeZoneId", "UTC"))); + assertNoErrors.accept(tryToUpdate.apply(r -> r.withValue("repeatSeconds", null).withValue("cronExpression", GOOD_CRON).withValue("cronTimeZoneId", "America/Chicago"))); + assertNoErrors.accept(tryToUpdate.apply(r -> r.withValue("repeatSeconds", 1).withValue("cronExpression", null).withValue("cronTimeZoneId", null))); + assertNoErrors.accept(tryToUpdate.apply(r -> r.withValue("repeatSeconds", 2))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPostUpdateActionReSchedulesJob() throws QException, SchedulerException + { + List wrappers = QuartzTestUtils.queryQuartz(); + assertEquals(0, wrappers.size()); + + ////////////////////////////////////////////////// + // do an insert - this will originally schedule // + ////////////////////////////////////////////////// + new InsertAction().execute(new InsertInput(ScheduledJob.TABLE_NAME).withRecordEntity(new ScheduledJob() + .withLabel("Test") + .withSchedulerName(QuartzTestUtils.QUARTZ_SCHEDULER_NAME) + .withType(ScheduledJobType.PROCESS.name()) + .withIsActive(true) + .withRepeatSeconds(1) + .withJobParameters(List.of(new ScheduledJobParameter().withKey("processName").withValue(TestUtils.PROCESS_NAME_BASEPULL))))); + + wrappers = QuartzTestUtils.queryQuartz(); + assertEquals(1, wrappers.size()); + assertThat(wrappers.get(0).trigger()).isInstanceOf(SimpleTrigger.class); + + ////////////////////////////////////// + // now do an update, to re-schedule // + ////////////////////////////////////// + new UpdateAction().execute(new UpdateInput(ScheduledJob.TABLE_NAME).withRecordEntity(new ScheduledJob() + .withId(1) + .withLabel("Test") + .withSchedulerName(QuartzTestUtils.QUARTZ_SCHEDULER_NAME) + .withType(ScheduledJobType.PROCESS.name()) + .withIsActive(true) + .withRepeatSeconds(null) + .withCronExpression(GOOD_CRON) + .withCronTimeZoneId("UTC"))); + + wrappers = QuartzTestUtils.queryQuartz(); + assertEquals(1, wrappers.size()); + assertThat(wrappers.get(0).trigger()).isInstanceOf(CronTrigger.class); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPostUpdateActionIssuesWarnings() throws QException, SchedulerException + { + List wrappers = QuartzTestUtils.queryQuartz(); + assertEquals(0, wrappers.size()); + + ////////////////////////////////////////////////// + // do an insert - this will originally schedule // + ////////////////////////////////////////////////// + new InsertAction().execute(new InsertInput(ScheduledJob.TABLE_NAME).withRecordEntity(new ScheduledJob() + .withLabel("Test") + .withSchedulerName(QuartzTestUtils.QUARTZ_SCHEDULER_NAME) + .withType(ScheduledJobType.PROCESS.name()) + .withIsActive(true) + .withRepeatSeconds(1) + .withJobParameters(List.of(new ScheduledJobParameter().withKey("processName").withValue(TestUtils.PROCESS_NAME_BASEPULL))))); + + wrappers = QuartzTestUtils.queryQuartz(); + assertEquals(1, wrappers.size()); + assertThat(wrappers.get(0).trigger()).isInstanceOf(SimpleTrigger.class); + + ////////////////////////////////////// + // now do an update, to re-schedule // + ////////////////////////////////////// + UpdateOutput updateOutput = new UpdateAction().execute(new UpdateInput(ScheduledJob.TABLE_NAME).withRecordEntity(new ScheduledJob() + .withId(1) + .withLabel("Test") + .withSchedulerName(QuartzTestUtils.QUARTZ_SCHEDULER_NAME) + .withType(ScheduledJobType.PROCESS.name()) + .withIsActive(true) + .withRepeatSeconds(null) + .withCronExpression(GOOD_CRON) + .withCronTimeZoneId("UTC") + .withJobParameters(List.of(new ScheduledJobParameter().withKey("process").withValue("not"))))); + + assertTrue(CollectionUtils.nullSafeIsEmpty(updateOutput.getRecords().get(0).getErrors())); + assertThat(updateOutput.getRecords().get(0).getWarnings()) + .hasSize(1).first().extracting("message").asString() + .contains("Missing scheduledJobParameter with key [processName]"); + + wrappers = QuartzTestUtils.queryQuartz(); + assertEquals(0, wrappers.size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPostDeleteUnschedules() throws QException, SchedulerException + { + List wrappers = QuartzTestUtils.queryQuartz(); + assertEquals(0, wrappers.size()); + + ////////////////////////////////////////////////// + // do an insert - this will originally schedule // + ////////////////////////////////////////////////// + new InsertAction().execute(new InsertInput(ScheduledJob.TABLE_NAME).withRecordEntity(new ScheduledJob() + .withLabel("Test") + .withSchedulerName(QuartzTestUtils.QUARTZ_SCHEDULER_NAME) + .withType(ScheduledJobType.PROCESS.name()) + .withIsActive(true) + .withRepeatSeconds(1) + .withJobParameters(List.of(new ScheduledJobParameter().withKey("processName").withValue(TestUtils.PROCESS_NAME_BASEPULL))))); + + wrappers = QuartzTestUtils.queryQuartz(); + assertEquals(1, wrappers.size()); + + //////////////////////////////////// + // now do a delete, to unschedule // + //////////////////////////////////// + new DeleteAction().execute(new DeleteInput(ScheduledJob.TABLE_NAME).withPrimaryKeys(List.of(1))); + + wrappers = QuartzTestUtils.queryQuartz(); + assertEquals(0, wrappers.size()); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/scheduler/QScheduleManagerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/scheduler/QScheduleManagerTest.java index cef9d263..68a7c37b 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/scheduler/QScheduleManagerTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/scheduler/QScheduleManagerTest.java @@ -92,7 +92,7 @@ class QScheduleManagerTest extends BaseTest .withId(1) .withIsActive(true) .withSchedulerName(TestUtils.SIMPLE_SCHEDULER_NAME) - .withType(type.getId()) + .withType(type.name()) .withRepeatSeconds(1) .withJobParameters(new ArrayList<>()); diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/scheduler/quartz/QuartzSchedulerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/scheduler/quartz/QuartzSchedulerTest.java index 43fe0b1d..46f5c8cc 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/scheduler/quartz/QuartzSchedulerTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/scheduler/quartz/QuartzSchedulerTest.java @@ -62,27 +62,7 @@ class QuartzSchedulerTest extends BaseTest @AfterEach void afterEach() { - try - { - QScheduleManager.getInstance().unInit(); - } - catch(IllegalStateException ise) - { - ///////////////////////////////////////////////////////////////// - // ok, might just mean that this test didn't init the instance // - ///////////////////////////////////////////////////////////////// - } - - try - { - QuartzScheduler.getInstance().unInit(); - } - catch(IllegalStateException ise) - { - ///////////////////////////////////////////////////////////////// - // ok, might just mean that this test didn't init the instance // - ///////////////////////////////////////////////////////////////// - } + QuartzTestUtils.afterEach(); } @@ -120,7 +100,7 @@ class QuartzSchedulerTest extends BaseTest ////////////////////////////////////////////////// // give a moment for the job to run a few times // ////////////////////////////////////////////////// - SleepUtils.sleep(50, TimeUnit.MILLISECONDS); + SleepUtils.sleep(150, TimeUnit.MILLISECONDS); qScheduleManager.stopAsync(); System.out.println("Ran: " + BasicStep.counter + " times"); @@ -156,7 +136,6 @@ class QuartzSchedulerTest extends BaseTest void testRemovingNoLongerNeededJobsDuringSetupSchedules() throws SchedulerException { QInstance qInstance = QContext.getQInstance(); - QScheduleManager.defineDefaultSchedulableTypesInInstance(qInstance); QuartzTestUtils.setupInstanceForQuartzTests(); //////////////////////////// @@ -167,7 +146,7 @@ class QuartzSchedulerTest extends BaseTest qInstance.addProcess(test1); qInstance.addProcess(test2); - SchedulableType schedulableType = qInstance.getSchedulableType(ScheduledJobType.PROCESS.getId()); + SchedulableType schedulableType = qInstance.getSchedulableType(ScheduledJobType.PROCESS.name()); QuartzScheduler quartzScheduler = QuartzScheduler.initInstance(qInstance, QuartzTestUtils.QUARTZ_SCHEDULER_NAME, QuartzTestUtils.getQuartzProperties(), () -> QContext.getQSession()); quartzScheduler.start(); diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/scheduler/quartz/QuartzTestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/scheduler/quartz/QuartzTestUtils.java index 3e952b50..a82d163c 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/scheduler/quartz/QuartzTestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/scheduler/quartz/QuartzTestUtils.java @@ -27,6 +27,7 @@ import java.util.Properties; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.quartz.QuartzSchedulerMetaData; +import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager; import org.quartz.SchedulerException; @@ -102,4 +103,35 @@ public class QuartzTestUtils { return QuartzScheduler.getInstance().queryQuartz(); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void afterEach() + { + try + { + QScheduleManager.getInstance().stop(); + QScheduleManager.getInstance().unInit(); + } + catch(IllegalStateException ise) + { + ///////////////////////////////////////////////////////////////// + // ok, might just mean that this test didn't init the instance // + ///////////////////////////////////////////////////////////////// + } + + try + { + QuartzScheduler.getInstance().unInit(); + } + catch(IllegalStateException ise) + { + ///////////////////////////////////////////////////////////////// + // ok, might just mean that this test didn't init the instance // + ///////////////////////////////////////////////////////////////// + } + } } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/QuartzJobsProcessTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/QuartzJobsProcessTest.java index 5ce1c175..f2bf6067 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/QuartzJobsProcessTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/scheduler/quartz/processes/QuartzJobsProcessTest.java @@ -67,7 +67,7 @@ class QuartzJobsProcessTest extends BaseTest { QInstance qInstance = QContext.getQInstance(); qInstance.addTable(new QTableMetaData() - .withName("quartzTriggers") + .withName("quartzJobDetails") .withBackendName(TestUtils.MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.LONG))); @@ -92,28 +92,7 @@ class QuartzJobsProcessTest extends BaseTest @AfterEach void afterEach() { - try - { - QScheduleManager.getInstance().stop(); - QScheduleManager.getInstance().unInit(); - } - catch(IllegalStateException ise) - { - ///////////////////////////////////////////////////////////////// - // ok, might just mean that this test didn't init the instance // - ///////////////////////////////////////////////////////////////// - } - - try - { - QuartzScheduler.getInstance().unInit(); - } - catch(IllegalStateException ise) - { - ///////////////////////////////////////////////////////////////// - // ok, might just mean that this test didn't init the instance // - ///////////////////////////////////////////////////////////////// - } + QuartzTestUtils.afterEach(); } @@ -183,7 +162,7 @@ class QuartzJobsProcessTest extends BaseTest // pause just one // //////////////////// List quartzJobAndTriggerWrappers = QuartzTestUtils.queryQuartz(); - new InsertAction().execute(new InsertInput("quartzTriggers").withRecord(new QRecord() + new InsertAction().execute(new InsertInput("quartzJobDetails").withRecord(new QRecord() .withValue("jobName", quartzJobAndTriggerWrappers.get(0).jobDetail().getKey().getName()) .withValue("jobGroup", quartzJobAndTriggerWrappers.get(0).jobDetail().getKey().getGroup()) )); @@ -231,7 +210,7 @@ class QuartzJobsProcessTest extends BaseTest // pause just one // //////////////////// List quartzJobAndTriggerWrappers = QuartzTestUtils.queryQuartz(); - new InsertAction().execute(new InsertInput("quartzTriggers").withRecord(new QRecord() + new InsertAction().execute(new InsertInput("quartzJobDetails").withRecord(new QRecord() .withValue("jobName", quartzJobAndTriggerWrappers.get(0).jobDetail().getKey().getName()) .withValue("jobGroup", quartzJobAndTriggerWrappers.get(0).jobDetail().getKey().getGroup()) ));