CE-936 - dynamic scheduling of records from ScheduledJob table; quartz re-pause if paused before rescheduling

This commit is contained in:
2024-03-14 11:18:18 -05:00
parent 60ffac4646
commit 2fec4891d3
14 changed files with 1815 additions and 95 deletions

View File

@ -23,10 +23,12 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
/*******************************************************************************
@ -89,4 +91,18 @@ public class QueryOutput extends AbstractActionOutput implements Serializable
return storage.getRecords();
}
/*******************************************************************************
**
*******************************************************************************/
public <T extends QRecordEntity> List<T> getRecordEntities(Class<T> entityClass) throws QException
{
List<T> rs = new ArrayList<>();
for(QRecord record : storage.getRecords())
{
rs.add(QRecordEntity.fromQRecord(entityClass, record));
}
return (rs);
}
}

View File

@ -0,0 +1,78 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.common;
import java.util.ArrayList;
import java.util.List;
import java.util.TimeZone;
import java.util.function.Function;
import java.util.function.Predicate;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
/*******************************************************************************
**
*******************************************************************************/
public class TimeZonePossibleValueSourceMetaDataProvider
{
public static final String NAME = "timeZones";
/*******************************************************************************
**
*******************************************************************************/
public QPossibleValueSource produce()
{
return (produce(null, null));
}
/*******************************************************************************
**
*******************************************************************************/
public QPossibleValueSource produce(Predicate<String> filter, Function<String, String> labelMapper)
{
QPossibleValueSource possibleValueSource = new QPossibleValueSource()
.withName("timeZones")
.withType(QPossibleValueSourceType.ENUM)
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY);
List<QPossibleValue<?>> enumValues = new ArrayList<>();
for(String availableID : TimeZone.getAvailableIDs())
{
if(filter == null || filter.test(availableID))
{
String label = labelMapper == null ? availableID : labelMapper.apply(availableID);
enumValues.add(new QPossibleValue<>(availableID, label));
}
}
possibleValueSource.withEnumValues(enumValues);
return (possibleValueSource);
}
}

View File

@ -0,0 +1,461 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.scheduledjobs;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.common.TimeZonePossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.data.QAssociation;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MutableMap;
/*******************************************************************************
**
*******************************************************************************/
public class ScheduledJob extends QRecordEntity
{
public static final String TABLE_NAME = "scheduledJob";
@QField(isEditable = false)
private Integer id;
@QField(isEditable = false)
private Instant createDate;
@QField(isEditable = false)
private Instant modifyDate;
@QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE)
private String label;
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
private String description;
@QField(isRequired = true, label = "Scheduler", maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR, possibleValueSourceName = SchedulersPossibleValueSource.NAME)
private String schedulerName;
@QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
private String cronExpression;
@QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR, possibleValueSourceName = TimeZonePossibleValueSourceMetaDataProvider.NAME)
private String cronTimeZoneId;
@QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR, possibleValueSourceName = ScheduledJobType.NAME)
private String type;
@QField(isRequired = true)
private Boolean isActive;
@QAssociation(name = ScheduledJobParameter.TABLE_NAME)
private List<ScheduledJobParameter> jobParameters;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ScheduledJob()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ScheduledJob(QRecord qRecord) throws QException
{
populateFromQRecord(qRecord);
}
/*******************************************************************************
** Getter for id
*******************************************************************************/
public Integer getId()
{
return (this.id);
}
/*******************************************************************************
** Setter for id
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Fluent setter for id
*******************************************************************************/
public ScheduledJob withId(Integer id)
{
this.id = id;
return (this);
}
/*******************************************************************************
** Getter for createDate
*******************************************************************************/
public Instant getCreateDate()
{
return (this.createDate);
}
/*******************************************************************************
** Setter for createDate
*******************************************************************************/
public void setCreateDate(Instant createDate)
{
this.createDate = createDate;
}
/*******************************************************************************
** Fluent setter for createDate
*******************************************************************************/
public ScheduledJob withCreateDate(Instant createDate)
{
this.createDate = createDate;
return (this);
}
/*******************************************************************************
** Getter for modifyDate
*******************************************************************************/
public Instant getModifyDate()
{
return (this.modifyDate);
}
/*******************************************************************************
** Setter for modifyDate
*******************************************************************************/
public void setModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
}
/*******************************************************************************
** Fluent setter for modifyDate
*******************************************************************************/
public ScheduledJob withModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
return (this);
}
/*******************************************************************************
** Getter for label
*******************************************************************************/
public String getLabel()
{
return (this.label);
}
/*******************************************************************************
** Setter for label
*******************************************************************************/
public void setLabel(String label)
{
this.label = label;
}
/*******************************************************************************
** Fluent setter for label
*******************************************************************************/
public ScheduledJob withLabel(String label)
{
this.label = label;
return (this);
}
/*******************************************************************************
** Getter for description
*******************************************************************************/
public String getDescription()
{
return (this.description);
}
/*******************************************************************************
** Setter for description
*******************************************************************************/
public void setDescription(String description)
{
this.description = description;
}
/*******************************************************************************
** Fluent setter for description
*******************************************************************************/
public ScheduledJob withDescription(String description)
{
this.description = description;
return (this);
}
/*******************************************************************************
** Getter for cronExpression
*******************************************************************************/
public String getCronExpression()
{
return (this.cronExpression);
}
/*******************************************************************************
** Setter for cronExpression
*******************************************************************************/
public void setCronExpression(String cronExpression)
{
this.cronExpression = cronExpression;
}
/*******************************************************************************
** Fluent setter for cronExpression
*******************************************************************************/
public ScheduledJob withCronExpression(String cronExpression)
{
this.cronExpression = cronExpression;
return (this);
}
/*******************************************************************************
** Getter for cronTimeZoneId
*******************************************************************************/
public String getCronTimeZoneId()
{
return (this.cronTimeZoneId);
}
/*******************************************************************************
** Setter for cronTimeZoneId
*******************************************************************************/
public void setCronTimeZoneId(String cronTimeZoneId)
{
this.cronTimeZoneId = cronTimeZoneId;
}
/*******************************************************************************
** Fluent setter for cronTimeZoneId
*******************************************************************************/
public ScheduledJob withCronTimeZoneId(String cronTimeZoneId)
{
this.cronTimeZoneId = cronTimeZoneId;
return (this);
}
/*******************************************************************************
** Getter for isActive
*******************************************************************************/
public Boolean getIsActive()
{
return (this.isActive);
}
/*******************************************************************************
** Setter for isActive
*******************************************************************************/
public void setIsActive(Boolean isActive)
{
this.isActive = isActive;
}
/*******************************************************************************
** Fluent setter for isActive
*******************************************************************************/
public ScheduledJob withIsActive(Boolean isActive)
{
this.isActive = isActive;
return (this);
}
/*******************************************************************************
** Getter for schedulerName
*******************************************************************************/
public String getSchedulerName()
{
return (this.schedulerName);
}
/*******************************************************************************
** Setter for schedulerName
*******************************************************************************/
public void setSchedulerName(String schedulerName)
{
this.schedulerName = schedulerName;
}
/*******************************************************************************
** Fluent setter for schedulerName
*******************************************************************************/
public ScheduledJob withSchedulerName(String schedulerName)
{
this.schedulerName = schedulerName;
return (this);
}
/*******************************************************************************
** Getter for type
*******************************************************************************/
public String getType()
{
return (this.type);
}
/*******************************************************************************
** Setter for type
*******************************************************************************/
public void setType(String type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
*******************************************************************************/
public ScheduledJob withType(String type)
{
this.type = type;
return (this);
}
/*******************************************************************************
** Getter for jobParameters
*******************************************************************************/
public List<ScheduledJobParameter> getJobParameters()
{
return (this.jobParameters);
}
/*******************************************************************************
** Getter for jobParameters - but a map of just the key=value pairs.
*******************************************************************************/
public Map<String, String> getJobParametersMap()
{
if(CollectionUtils.nullSafeIsEmpty(this.jobParameters))
{
return (new HashMap<>());
}
///////////////////////////////////////////////////////////////////////////////////////
// wrap in mutable map, just to avoid any immutable or other bs from toMap's default //
///////////////////////////////////////////////////////////////////////////////////////
return new MutableMap<>(jobParameters.stream().collect(Collectors.toMap(ScheduledJobParameter::getKey, ScheduledJobParameter::getValue)));
}
/*******************************************************************************
** Setter for jobParameters
*******************************************************************************/
public void setJobParameters(List<ScheduledJobParameter> jobParameters)
{
this.jobParameters = jobParameters;
}
/*******************************************************************************
** Fluent setter for jobParameters
*******************************************************************************/
public ScheduledJob withJobParameters(List<ScheduledJobParameter> jobParameters)
{
this.jobParameters = jobParameters;
return (this);
}
}

View File

@ -0,0 +1,266 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.scheduledjobs;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
/*******************************************************************************
**
*******************************************************************************/
public class ScheduledJobParameter extends QRecordEntity
{
public static final String TABLE_NAME = "scheduledJobParameter";
@QField(isEditable = false)
private Integer id;
@QField(isEditable = false)
private Instant createDate;
@QField(isEditable = false)
private Instant modifyDate;
@QField(possibleValueSourceName = ScheduledJob.TABLE_NAME, isRequired = true)
private Integer scheduledJobId;
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR, isRequired = true)
private String key;
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
private String value;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ScheduledJobParameter()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ScheduledJobParameter(QRecord qRecord) throws QException
{
populateFromQRecord(qRecord);
}
/*******************************************************************************
** Getter for id
*******************************************************************************/
public Integer getId()
{
return (this.id);
}
/*******************************************************************************
** Setter for id
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Fluent setter for id
*******************************************************************************/
public ScheduledJobParameter withId(Integer id)
{
this.id = id;
return (this);
}
/*******************************************************************************
** Getter for createDate
*******************************************************************************/
public Instant getCreateDate()
{
return (this.createDate);
}
/*******************************************************************************
** Setter for createDate
*******************************************************************************/
public void setCreateDate(Instant createDate)
{
this.createDate = createDate;
}
/*******************************************************************************
** Fluent setter for createDate
*******************************************************************************/
public ScheduledJobParameter withCreateDate(Instant createDate)
{
this.createDate = createDate;
return (this);
}
/*******************************************************************************
** Getter for modifyDate
*******************************************************************************/
public Instant getModifyDate()
{
return (this.modifyDate);
}
/*******************************************************************************
** Setter for modifyDate
*******************************************************************************/
public void setModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
}
/*******************************************************************************
** Fluent setter for modifyDate
*******************************************************************************/
public ScheduledJobParameter withModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
return (this);
}
/*******************************************************************************
** Getter for scheduledJobId
*******************************************************************************/
public Integer getScheduledJobId()
{
return (this.scheduledJobId);
}
/*******************************************************************************
** Setter for scheduledJobId
*******************************************************************************/
public void setScheduledJobId(Integer scheduledJobId)
{
this.scheduledJobId = scheduledJobId;
}
/*******************************************************************************
** Fluent setter for scheduledJobId
*******************************************************************************/
public ScheduledJobParameter withScheduledJobId(Integer scheduledJobId)
{
this.scheduledJobId = scheduledJobId;
return (this);
}
/*******************************************************************************
** Getter for key
*******************************************************************************/
public String getKey()
{
return (this.key);
}
/*******************************************************************************
** Setter for key
*******************************************************************************/
public void setKey(String key)
{
this.key = key;
}
/*******************************************************************************
** Fluent setter for key
*******************************************************************************/
public ScheduledJobParameter withKey(String key)
{
this.key = key;
return (this);
}
/*******************************************************************************
** Getter for value
*******************************************************************************/
public String getValue()
{
return (this.value);
}
/*******************************************************************************
** Setter for value
*******************************************************************************/
public void setValue(String value)
{
this.value = value;
}
/*******************************************************************************
** Fluent setter for value
*******************************************************************************/
public ScheduledJobParameter withValue(String value)
{
this.value = value;
return (this);
}
}

View File

@ -0,0 +1,123 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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;
/*******************************************************************************
**
*******************************************************************************/
public enum ScheduledJobType implements PossibleValueEnum<String>
{
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);
}
}

View File

@ -0,0 +1,219 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.scheduledjobs;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ChildRecordListRenderer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
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.ScheduledJobTableCustomizer;
/*******************************************************************************
**
*******************************************************************************/
public class ScheduledJobsMetaDataProvider
{
private static final String JOB_PARAMETER_JOIN_NAME = QJoinMetaData.makeInferredJoinName(ScheduledJob.TABLE_NAME, ScheduledJobParameter.TABLE_NAME);
/*******************************************************************************
**
*******************************************************************************/
public void defineAll(QInstance instance, String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
defineStandardTables(instance, backendName, backendDetailEnricher);
instance.addPossibleValueSource(QPossibleValueSource.newForTable(ScheduledJob.TABLE_NAME));
instance.addPossibleValueSource(QPossibleValueSource.newForEnum(ScheduledJobType.NAME, ScheduledJobType.values()));
instance.addPossibleValueSource(defineSchedulersPossibleValueSource());
defineStandardJoins(instance);
defineStandardScriptsWidgets(instance);
}
/*******************************************************************************
**
*******************************************************************************/
public void defineStandardScriptsWidgets(QInstance instance)
{
QJoinMetaData join = instance.getJoin(JOB_PARAMETER_JOIN_NAME);
instance.addWidget(ChildRecordListRenderer.widgetMetaDataBuilder(join)
.withCanAddChildRecord(true)
.withLabel("Parameters")
.getWidgetMetaData()
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED)));
}
/*******************************************************************************
**
*******************************************************************************/
public void defineStandardJoins(QInstance instance)
{
instance.addJoin(new QJoinMetaData()
.withType(JoinType.ONE_TO_MANY)
.withLeftTable(ScheduledJob.TABLE_NAME)
.withRightTable(ScheduledJobParameter.TABLE_NAME)
.withJoinOn(new JoinOn("id", "scheduledJobId"))
.withOrderBy(new QFilterOrderBy("id"))
.withInferredName());
}
/*******************************************************************************
**
*******************************************************************************/
public void defineStandardTables(QInstance instance, String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
for(QTableMetaData tableMetaData : defineStandardTables(backendName, backendDetailEnricher))
{
instance.addTable(tableMetaData);
}
}
/*******************************************************************************
**
*******************************************************************************/
private List<QTableMetaData> defineStandardTables(String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
List<QTableMetaData> rs = new ArrayList<>();
rs.add(enrich(backendDetailEnricher, defineScheduledJobTable(backendName)));
rs.add(enrich(backendDetailEnricher, defineScheduledJobParameterTable(backendName)));
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
private QTableMetaData enrich(Consumer<QTableMetaData> backendDetailEnricher, QTableMetaData table)
{
if(backendDetailEnricher != null)
{
backendDetailEnricher.accept(table);
}
return (table);
}
/*******************************************************************************
**
*******************************************************************************/
private QTableMetaData defineStandardTable(String backendName, String name, Class<? extends QRecordEntity> fieldsFromEntity) throws QException
{
return new QTableMetaData()
.withName(name)
.withBackendName(backendName)
.withPrimaryKeyField("id")
.withFieldsFromEntity(fieldsFromEntity);
}
/*******************************************************************************
**
*******************************************************************************/
private QTableMetaData defineScheduledJobTable(String backendName) throws QException
{
QTableMetaData tableMetaData = defineStandardTable(backendName, ScheduledJob.TABLE_NAME, ScheduledJob.class)
.withRecordLabelFormat("%s")
.withRecordLabelFields("label")
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "label", "description")))
.withSection(new QFieldSection("schedule", new QIcon().withName("alarm"), Tier.T2, List.of("cronExpression", "cronTimeZoneId")))
.withSection(new QFieldSection("settings", new QIcon().withName("tune"), Tier.T2, List.of("type", "isActive", "schedulerName")))
.withSection(new QFieldSection("parameters", new QIcon().withName("list"), Tier.T2).withWidgetName(JOB_PARAMETER_JOIN_NAME))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
QCodeReference customizerReference = new QCodeReference(ScheduledJobTableCustomizer.class);
tableMetaData.withCustomizer(TableCustomizers.PRE_INSERT_RECORD, customizerReference);
tableMetaData.withCustomizer(TableCustomizers.POST_INSERT_RECORD, customizerReference);
tableMetaData.withCustomizer(TableCustomizers.POST_UPDATE_RECORD, customizerReference);
tableMetaData.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD, customizerReference);
tableMetaData.withCustomizer(TableCustomizers.POST_DELETE_RECORD, customizerReference);
tableMetaData.withAssociation(new Association()
.withAssociatedTableName(ScheduledJobParameter.TABLE_NAME)
.withJoinName(JOB_PARAMETER_JOIN_NAME)
.withName(ScheduledJobParameter.TABLE_NAME));
return (tableMetaData);
}
/*******************************************************************************
**
*******************************************************************************/
private QTableMetaData defineScheduledJobParameterTable(String backendName) throws QException
{
QTableMetaData tableMetaData = defineStandardTable(backendName, ScheduledJobParameter.TABLE_NAME, ScheduledJobParameter.class)
.withRecordLabelFormat("%s - %s")
.withRecordLabelFields("scheduledJobId", "key")
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "scheduledJobId", "key")))
.withSection(new QFieldSection("value", new QIcon().withName("dataset"), Tier.T2, List.of("value")))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
return (tableMetaData);
}
/*******************************************************************************
**
*******************************************************************************/
private QPossibleValueSource defineSchedulersPossibleValueSource()
{
return (new QPossibleValueSource()
.withName(SchedulersPossibleValueSource.NAME)
.withType(QPossibleValueSourceType.CUSTOM)
.withCustomCodeReference(new QCodeReference(SchedulersPossibleValueSource.class)));
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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.model.metadata.scheduleing.QSchedulerMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
**
*******************************************************************************/
public class SchedulersPossibleValueSource implements QCustomPossibleValueProvider<String>
{
public static final String NAME = "schedulers";
/*******************************************************************************
**
*******************************************************************************/
@Override
public QPossibleValue<String> getPossibleValue(Serializable idValue)
{
QSchedulerMetaData scheduler = QContext.getQInstance().getScheduler(String.valueOf(idValue));
if(scheduler != null)
{
return schedulerToPossibleValue(scheduler);
}
return null;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QPossibleValue<String>> search(SearchPossibleValueSourceInput input) throws QException
{
List<QPossibleValue<String>> rs = new ArrayList<>();
for(QSchedulerMetaData scheduler : CollectionUtils.nonNullMap(QContext.getQInstance().getSchedulers()).values())
{
rs.add(schedulerToPossibleValue(scheduler));
}
return rs;
}
/*******************************************************************************
**
*******************************************************************************/
private static QPossibleValue<String> schedulerToPossibleValue(QSchedulerMetaData scheduler)
{
return new QPossibleValue<>(scheduler.getName(), scheduler.getName());
}
}

View File

@ -0,0 +1,278 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.scheduledjobs.customizers;
import java.io.Serializable;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
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.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
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.statusmessages.BadInputStatusMessage;
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 static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
**
*******************************************************************************/
public class ScheduledJobTableCustomizer implements TableCustomizerInterface
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
{
validateConditionalFields(records);
return (records);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> postInsert(InsertInput insertInput, List<QRecord> records) throws QException
{
scheduleJobsForRecordList(records);
return (records);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> preUpdate(UpdateInput updateInput, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
{
validateConditionalFields(records);
if(isPreview || oldRecordList.isEmpty())
{
return (records);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// refresh the old-records w/ versions that have associations - so we can use those in the post-update to property unschedule things //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Map<Serializable, QRecord> freshOldRecordsWithAssociationsMap = CollectionUtils.recordsToMap(freshlyQueryForRecordsWithAssociations(oldRecordList.get()), "id");
ListIterator<QRecord> iterator = oldRecordList.get().listIterator();
while(iterator.hasNext())
{
QRecord record = iterator.next();
QRecord freshRecord = freshOldRecordsWithAssociationsMap.get(record.getValue("id"));
if(freshRecord != null)
{
iterator.set(freshRecord);
}
}
return (records);
}
/*******************************************************************************
**
*******************************************************************************/
private static void validateConditionalFields(List<QRecord> records)
{
for(QRecord record : records)
{
if(StringUtils.hasContent(record.getValueString("cronExpression")))
{
if(!StringUtils.hasContent(record.getValueString("cronTimeZoneId")))
{
record.addError(new BadInputStatusMessage("If a Cron Expression is given, then a Cron Time Zone Id is required."));
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> postUpdate(UpdateInput updateInput, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
{
if(oldRecordList.isPresent())
{
Set<Integer> idsWithErrors = getRecordIdsWithErrors(records);
unscheduleJobsForRecordList(oldRecordList.get(), idsWithErrors);
}
scheduleJobsForRecordList(records);
return (records);
}
/*******************************************************************************
**
*******************************************************************************/
private static Set<Integer> getRecordIdsWithErrors(List<QRecord> records)
{
return records.stream()
.filter(r -> !recordHasErrors().test(r))
.map(r -> r.getValueInteger("id"))
.collect(Collectors.toSet());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> postDelete(DeleteInput deleteInput, List<QRecord> records) throws QException
{
Set<Integer> idsWithErrors = getRecordIdsWithErrors(records);
unscheduleJobsForRecordList(records, idsWithErrors);
return (records);
}
/*******************************************************************************
**
*******************************************************************************/
private void scheduleJobsForRecordList(List<QRecord> records)
{
List<QRecord> recordsWithoutErrors = records.stream().filter(recordHasErrors()).toList();
if(CollectionUtils.nullSafeIsEmpty(recordsWithoutErrors))
{
return;
}
try
{
List<QRecord> freshRecordListWithAssociations = freshlyQueryForRecordsWithAssociations(recordsWithoutErrors);
QScheduleManager scheduleManager = QScheduleManager.getInstance();
for(QRecord record : freshRecordListWithAssociations)
{
try
{
scheduleManager.setupScheduledJob(new ScheduledJob(record));
}
catch(Exception e)
{
LOG.info("Caught exception while scheduling a job in post-action", e, logPair("id", record.getValue("id")));
}
}
}
catch(Exception e)
{
LOG.warn("Error scheduling jobs in post-action", e);
}
}
/*******************************************************************************
**
*******************************************************************************/
private static Predicate<QRecord> recordHasErrors()
{
return r -> CollectionUtils.nullSafeIsEmpty(r.getErrors());
}
/*******************************************************************************
**
*******************************************************************************/
private static List<QRecord> freshlyQueryForRecordsWithAssociations(List<QRecord> records) throws QException
{
List<Integer> idList = records.stream().map(r -> r.getValueInteger("id")).toList();
return new QueryAction().execute(new QueryInput(ScheduledJob.TABLE_NAME)
.withIncludeAssociations(true)
.withFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.IN, idList))))
.getRecords();
}
/*******************************************************************************
**
*******************************************************************************/
private void unscheduleJobsForRecordList(List<QRecord> oldRecords, Set<Integer> exceptIdsWithErrors)
{
try
{
QScheduleManager scheduleManager = QScheduleManager.getInstance();
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// for un-schedule - use the old records as they are - don't re-query them (they may not exist anymore!) //
///////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QRecord record : oldRecords)
{
try
{
ScheduledJob scheduledJob = new ScheduledJob(record);
if(exceptIdsWithErrors.contains(scheduledJob.getId()))
{
LOG.info("Will not unschedule the job for a record that had an error", logPair("id", scheduledJob.getId()));
continue;
}
scheduleManager.unscheduleScheduledJob(scheduledJob);
}
catch(Exception e)
{
LOG.info("Caught exception while scheduling a job in post-action", e, logPair("id", record.getValue("id")));
}
}
}
catch(Exception e)
{
LOG.warn("Error scheduling jobs in post-action", e);
}
}
}

View File

@ -28,12 +28,15 @@ import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.actions.automation.polling.PollingAutomationPerTableRunner;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.CapturedContext;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -44,11 +47,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueProviderMetaDa
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QSchedulerMetaData;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJob;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobType;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import org.apache.commons.lang.NotImplementedException;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -112,21 +117,25 @@ public class QScheduleManager
*******************************************************************************/
public void start() throws QException
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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 //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QSchedulerMetaData schedulerMetaData : CollectionUtils.nonNullMap(qInstance.getSchedulers()).values())
{
QSchedulerInterface scheduler = schedulerMetaData.initSchedulerInstance(qInstance, systemUserSessionSupplier);
schedulers.put(schedulerMetaData.getName(), scheduler);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// now, exist w/o setting up schedules and not starting schedules, if schedule manager isn't enabled here //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(!new QMetaDataVariableInterpreter().getBooleanFromPropertyOrEnvironment("qqq.scheduleManager.enabled", "QQQ_SCHEDULE_MANAGER_ENABLED", true))
{
LOG.info("Not starting ScheduleManager per settings.");
return;
}
/////////////////////////////////////////////////////////
// initialize the scheduler(s) we're configured to use //
/////////////////////////////////////////////////////////
for(QSchedulerMetaData schedulerMetaData : qInstance.getSchedulers().values())
{
QSchedulerInterface scheduler = schedulerMetaData.initSchedulerInstance(qInstance, systemUserSessionSupplier);
schedulers.put(schedulerMetaData.getName(), scheduler);
}
/////////////////////////////////////////////////////////////////////////////////////////////////
// ensure that everything which should be scheduled is scheduled, in the appropriate scheduler //
/////////////////////////////////////////////////////////////////////////////////////////////////
@ -165,6 +174,26 @@ public class QScheduleManager
*******************************************************************************/
private void setupSchedules()
{
/////////////////////////////////////////////
// read dynamic schedules //
// e.g., user-scheduled processes, reports //
/////////////////////////////////////////////
List<ScheduledJob> scheduledJobList = null;
try
{
if(QContext.getQInstance().getTables().containsKey(ScheduledJob.TABLE_NAME))
{
scheduledJobList = new QueryAction()
.execute(new QueryInput(ScheduledJob.TABLE_NAME)
.withIncludeAssociations(true))
.getRecordEntities(ScheduledJob.class);
}
}
catch(Exception e)
{
throw (new QRuntimeException("Failed to query for scheduled jobs - will not set up scheduler!", e));
}
/////////////////////////////////////////////////////////
// let the schedulers know we're starting this process //
/////////////////////////////////////////////////////////
@ -192,6 +221,115 @@ public class QScheduleManager
for(QProcessMetaData process : qInstance.getProcesses().values())
{
if(process.getSchedule() != null)
{
setupProcess(process);
}
}
/////////////////////////////////////////////////////////////////////////////////////////////
// todo- before, or after meta-datas? //
// like quartz, it'd just re-schedule if a dupe - but, should we do our own dupe checking? //
/////////////////////////////////////////////////////////////////////////////////////////////
for(ScheduledJob scheduledJob : CollectionUtils.nonNullList(scheduledJobList))
{
try
{
setupScheduledJob(scheduledJob);
}
catch(Exception e)
{
LOG.info("Caught exception while scheduling a job", e, logPair("id", scheduledJob.getId()));
}
}
//////////////////////////////////////////////////////////
// let the schedulers know we're done with this process //
//////////////////////////////////////////////////////////
schedulers.values().forEach(s -> s.endOfSetupSchedules());
}
/*******************************************************************************
**
*******************************************************************************/
public void setupScheduledJob(ScheduledJob scheduledJob)
{
///////////////////////////////////////////////////////////////////////////////////////////
// non-active jobs should be deleted from the scheduler. they get re-added //
// if they get re-activated. but we don't want to rely on (e.g., for quartz) the paused //
// state to be drive by is-active. else, devops-pause & unpause ops would clobber //
// scheduled-job record facts //
///////////////////////////////////////////////////////////////////////////////////////////
if(!scheduledJob.getIsActive())
{
unscheduleScheduledJob(scheduledJob);
return;
}
QSchedulerInterface scheduler = getScheduler(scheduledJob.getSchedulerName());
QScheduleMetaData scheduleMetaData = new QScheduleMetaData();
scheduleMetaData.setCronExpression(scheduledJob.getCronExpression());
scheduleMetaData.setCronTimeZoneId(scheduledJob.getCronTimeZoneId());
switch(ScheduledJobType.getById(scheduledJob.getType()))
{
case PROCESS ->
{
Map<String, String> paramMap = scheduledJob.getJobParametersMap();
String processName = paramMap.get("processName");
QProcessMetaData process = qInstance.getProcess(processName);
// todo - variants... serial vs parallel?
scheduler.setupProcess(process, null, scheduleMetaData, true);
}
case QUEUE_PROCESSOR ->
{
throw new NotImplementedException("ScheduledJob queue processors are not yet implemented...");
}
case TABLE_AUTOMATIONS ->
{
throw new NotImplementedException("ScheduledJob table automations are not yet implemented...");
}
}
}
/*******************************************************************************
**
*******************************************************************************/
public void unscheduleScheduledJob(ScheduledJob scheduledJob)
{
QSchedulerInterface scheduler = getScheduler(scheduledJob.getSchedulerName());
switch(ScheduledJobType.getById(scheduledJob.getType()))
{
case PROCESS ->
{
Map<String, String> paramMap = scheduledJob.getJobParametersMap();
String processName = paramMap.get("processName");
QProcessMetaData process = qInstance.getProcess(processName);
scheduler.unscheduleProcess(process);
}
case QUEUE_PROCESSOR ->
{
throw new NotImplementedException("ScheduledJob queue processors are not yet implemented...");
}
case TABLE_AUTOMATIONS ->
{
throw new NotImplementedException("ScheduledJob table automations are not yet implemented...");
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private void setupProcess(QProcessMetaData process)
{
QScheduleMetaData scheduleMetaData = process.getSchedule();
if(process.getSchedule().getVariantBackend() == null || QScheduleMetaData.RunStrategy.SERIAL.equals(process.getSchedule().getVariantRunStrategy()))
@ -225,20 +363,6 @@ public class QScheduleManager
LOG.error("Unsupported Schedule Run Strategy [" + process.getSchedule().getVariantRunStrategy() + "] was provided.");
}
}
}
/////////////////////////////////////////////////////////////
// todo - read dynamic schedules and schedule those things //
// e.g., user-scheduled processes, reports //
/////////////////////////////////////////////////////////////
// ScheduledJob scheduledJob = new ScheduledJob();
// setupScheduledJob(scheduledJob);
//////////////////////////////////////////////////////////
// let the schedulers know we're done with this process //
//////////////////////////////////////////////////////////
schedulers.values().forEach(s -> s.endOfSetupSchedules());
}

View File

@ -52,6 +52,11 @@ public interface QSchedulerInterface
*******************************************************************************/
void setupTableAutomation(QAutomationProviderMetaData automationProvider, PollingAutomationPerTableRunner.TableActionsInterface tableActions, QScheduleMetaData schedule, boolean allowedToStart);
/*******************************************************************************
**
*******************************************************************************/
void unscheduleProcess(QProcessMetaData process);
/*******************************************************************************
**
*******************************************************************************/
@ -90,5 +95,4 @@ public interface QSchedulerInterface
{
}
}

View File

@ -83,6 +83,11 @@ public class QuartzScheduler implements QSchedulerInterface
private Scheduler scheduler;
private final static String GROUP_NAME_PROCESSES = "processes";
private final static String GROUP_NAME_SQS_QUEUES = "sqsQueues";
private final static String GROUP_NAME_TABLE_AUTOMATIONS = "tableAutomations";
/////////////////////////////////////////////////////////////////////////////////////////
// create memoization objects for some quartz-query functions, that we'll only want to //
// use during our setup routine, when we'd query it many times over and over again. //
@ -95,6 +100,11 @@ public class QuartzScheduler implements QSchedulerInterface
private Memoization<String, Set<JobKey>> jobKeyNamesMemoization = new Memoization<String, Set<JobKey>>()
.withTimeout(Duration.of(0, ChronoUnit.SECONDS));
private Memoization<AnyKey, List<QuartzJobAndTriggerWrapper>> queryQuartzMemoization = new Memoization<AnyKey, List<QuartzJobAndTriggerWrapper>>()
.withTimeout(Duration.of(0, ChronoUnit.SECONDS));
private List<Memoization<?, ?>> allMemoizations = List.of(jobGroupNamesMemoization, jobKeyNamesMemoization, queryQuartzMemoization);
///////////////////////////////////////////////////////////////////////////////
// vars used during the setup routine, to figure out what jobs need deleted. //
///////////////////////////////////////////////////////////////////////////////
@ -103,6 +113,7 @@ public class QuartzScheduler implements QSchedulerInterface
private List<QuartzJobAndTriggerWrapper> scheduledJobsAtEndOfSetup = new ArrayList<>();
/*******************************************************************************
** Constructor
**
@ -227,7 +238,7 @@ public class QuartzScheduler implements QSchedulerInterface
jobData.put("backendVariantData", backendVariantData);
}
scheduleJob(process.getName(), "processes", QuartzRunProcessJob.class, jobData, schedule, allowedToStart);
scheduleJob(process.getName(), GROUP_NAME_PROCESSES, QuartzRunProcessJob.class, jobData, schedule, allowedToStart);
}
@ -239,8 +250,7 @@ public class QuartzScheduler implements QSchedulerInterface
public void startOfSetupSchedules()
{
this.insideSetup = true;
this.jobGroupNamesMemoization.setTimeout(Duration.ofSeconds(5));
this.jobKeyNamesMemoization.setTimeout(Duration.ofSeconds(5));
this.allMemoizations.forEach(m -> m.setTimeout(Duration.ofSeconds(5)));
try
{
@ -253,6 +263,7 @@ public class QuartzScheduler implements QSchedulerInterface
}
/*******************************************************************************
**
*******************************************************************************/
@ -260,8 +271,7 @@ public class QuartzScheduler implements QSchedulerInterface
public void endOfSetupSchedules()
{
this.insideSetup = false;
this.jobGroupNamesMemoization.setTimeout(Duration.ofSeconds(0));
this.jobKeyNamesMemoization.setTimeout(Duration.ofSeconds(0));
this.allMemoizations.forEach(m -> m.setTimeout(Duration.ofSeconds(0)));
if(this.scheduledJobsAtStartOfSetup == null)
{
@ -343,6 +353,14 @@ public class QuartzScheduler implements QSchedulerInterface
{
startAt.setTime(startAt.getTime() + scheduleMetaData.getInitialDelaySeconds() * 1000);
}
else
{
//////////////////////////////////////////////////////////////////////////////////////////////////////
// by default, put a 3-second delay on everything we schedule //
// this gives us a chance to re-pause if the job was previously paused, but then we re-schedule it. //
//////////////////////////////////////////////////////////////////////////////////////////////////////
startAt.setTime(startAt.getTime() + 3000);
}
///////////////////////////////////////
// Define a Trigger for the schedule //
@ -359,18 +377,6 @@ public class QuartzScheduler implements QSchedulerInterface
///////////////////////////////////////
addOrReplaceJobAndTrigger(jobKey, jobDetail, trigger);
//////////////////////////////////////////////////////////
// either pause or resume, based on if allowed to start //
//////////////////////////////////////////////////////////
if(!allowedToStart)
{
pauseJob(jobKey.getName(), jobKey.getGroup());
}
else
{
resumeJob(jobKey.getName(), jobKey.getGroup());
}
///////////////////////////////////////////////////////////////////////////
// if we're inside the setup event (e.g., initial startup), then capture //
// this job as one that is currently active and should be kept. //
@ -404,10 +410,11 @@ public class QuartzScheduler implements QSchedulerInterface
jobData.put("queueProviderName", queueProvider.getName());
jobData.put("queueName", queue.getName());
scheduleJob(queue.getName(), "sqsQueue", QuartzSqsPollerJob.class, jobData, schedule, allowedToStart);
scheduleJob(queue.getName(), GROUP_NAME_SQS_QUEUES, QuartzSqsPollerJob.class, jobData, schedule, allowedToStart);
}
/*******************************************************************************
**
*******************************************************************************/
@ -422,10 +429,22 @@ public class QuartzScheduler implements QSchedulerInterface
jobData.put("tableName", tableActions.tableName());
jobData.put("automationStatus", tableActions.status().toString());
scheduleJob(tableActions.tableName() + "." + tableActions.status(), "tableAutomations", QuartzTableAutomationsJob.class, jobData, schedule, allowedToStart);
scheduleJob(tableActions.tableName() + "." + tableActions.status(), GROUP_NAME_TABLE_AUTOMATIONS, QuartzTableAutomationsJob.class, jobData, schedule, allowedToStart);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void unscheduleProcess(QProcessMetaData process)
{
deleteJob(new JobKey(process.getName(), GROUP_NAME_PROCESSES));
}
/*******************************************************************************
**
*******************************************************************************/
@ -434,17 +453,42 @@ public class QuartzScheduler implements QSchedulerInterface
boolean isJobAlreadyScheduled = isJobAlreadyScheduled(jobKey);
if(isJobAlreadyScheduled)
{
this.scheduler.addJob(jobDetail, true);
boolean wasPaused = wasExistingJobPaused(jobKey);
this.scheduler.addJob(jobDetail, true); // note, true flag here replaces if already present.
this.scheduler.rescheduleJob(trigger.getKey(), trigger);
LOG.info("Re-scheduled job: " + jobKey);
LOG.info("Re-scheduled job", logPair("jobKey", jobKey));
if(wasPaused)
{
LOG.info("Re-pausing job", logPair("jobKey", jobKey));
pauseJob(jobKey.getName(), jobKey.getGroup());
}
}
else
{
this.scheduler.scheduleJob(jobDetail, trigger);
LOG.info("Scheduled new job: " + jobKey);
LOG.info("Scheduled new job", logPair("jobKey", jobKey));
}
}
// todo - think about... clear memoization - but - when this is used in bulk, that's when we want the memo!
/*******************************************************************************
**
*******************************************************************************/
private boolean wasExistingJobPaused(JobKey jobKey) throws SchedulerException
{
List<QuartzJobAndTriggerWrapper> quartzJobAndTriggerWrappers = queryQuartz();
Optional<QuartzJobAndTriggerWrapper> existingWrapper = quartzJobAndTriggerWrappers.stream().filter(w -> w.jobDetail().getKey().equals(jobKey)).findFirst();
if(existingWrapper.isPresent())
{
if(Trigger.TriggerState.PAUSED.equals(existingWrapper.get().triggerState()))
{
return (true);
}
}
return(false);
}
@ -492,12 +536,15 @@ public class QuartzScheduler implements QSchedulerInterface
/////////////////////////////////////////////////////////////////////////////////////////////
if(isJobAlreadyScheduled(jobKey))
{
return scheduler.deleteJob(jobKey);
boolean result = scheduler.deleteJob(jobKey);
LOG.info("Attempted to delete quartz job", logPair("jobKey", jobKey), logPair("deleteJobResult", result));
return (result);
}
/////////////////////////////////////////
// return true to indicate, we're good //
/////////////////////////////////////////
LOG.info("Request to delete quartz job, but it is not already scheduled.", logPair("jobKey", jobKey));
return (true);
}
catch(Exception e)
@ -575,11 +622,12 @@ public class QuartzScheduler implements QSchedulerInterface
**
*******************************************************************************/
List<QuartzJobAndTriggerWrapper> queryQuartz() throws SchedulerException
{
return queryQuartzMemoization.getResultThrowing(AnyKey.getInstance(), (x) ->
{
List<QuartzJobAndTriggerWrapper> rs = new ArrayList<>();
List<String> jobGroupNames = scheduler.getJobGroupNames();
for(String group : jobGroupNames)
for(String group : scheduler.getJobGroupNames())
{
Set<JobKey> jobKeys = scheduler.getJobKeys(GroupMatcher.groupEquals(group));
for(JobKey jobKey : jobKeys)
@ -595,6 +643,7 @@ public class QuartzScheduler implements QSchedulerInterface
}
return (rs);
}).orElse(null);
}

View File

@ -33,9 +33,9 @@ import org.apache.commons.lang3.SerializationUtils;
/*******************************************************************************
**
*******************************************************************************/
public class QuartzJobDetailsPostQueryCustomizer extends AbstractPostQueryCustomizer
public class QuartzJobDataPostQueryCustomizer extends AbstractPostQueryCustomizer
{
private static final QLogger LOG = QLogger.getLogger(QuartzJobDetailsPostQueryCustomizer.class);
private static final QLogger LOG = QLogger.getLogger(QuartzJobDataPostQueryCustomizer.class);
@ -55,10 +55,13 @@ public class QuartzJobDetailsPostQueryCustomizer extends AbstractPostQueryCustom
// this field has a blob of essentially a serialized map - so, deserialize that, then convert to JSON //
////////////////////////////////////////////////////////////////////////////////////////////////////////
byte[] value = record.getValueByteArray("jobData");
if(value.length > 0)
{
Object deserialize = SerializationUtils.deserialize(value);
String json = JsonUtils.toJson(deserialize);
record.setValue("jobData", json);
}
}
catch(Exception e)
{
LOG.info("Error deserializing quartz job data", e);

View File

@ -39,6 +39,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaDa
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.scheduler.QSchedulerInterface;
import com.kingsrook.qqq.backend.core.scheduler.SchedulerUtils;
import org.apache.commons.lang.NotImplementedException;
/*******************************************************************************
@ -157,6 +158,17 @@ public class SimpleScheduler implements QSchedulerInterface
/*******************************************************************************
**
*******************************************************************************/
@Override
public void unscheduleProcess(QProcessMetaData process)
{
throw (new NotImplementedException("Unscheduling is not implemented in SimpleScheduler..."));
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -177,7 +177,7 @@ class QuartzJobsProcessTest extends BaseTest
List<QuartzJobAndTriggerWrapper> quartzJobAndTriggerWrappers = QuartzTestUtils.queryQuartz();
new InsertAction().execute(new InsertInput("quartzTriggers").withRecord(new QRecord()
.withValue("jobName", quartzJobAndTriggerWrappers.get(0).jobDetail().getKey().getName())
.withValue("groupName", quartzJobAndTriggerWrappers.get(0).jobDetail().getKey().getGroup())
.withValue("jobGroup", quartzJobAndTriggerWrappers.get(0).jobDetail().getKey().getGroup())
));
input = new RunProcessInput();
@ -224,7 +224,7 @@ class QuartzJobsProcessTest extends BaseTest
List<QuartzJobAndTriggerWrapper> quartzJobAndTriggerWrappers = QuartzTestUtils.queryQuartz();
new InsertAction().execute(new InsertInput("quartzTriggers").withRecord(new QRecord()
.withValue("jobName", quartzJobAndTriggerWrappers.get(0).jobDetail().getKey().getName())
.withValue("groupName", quartzJobAndTriggerWrappers.get(0).jobDetail().getKey().getGroup())
.withValue("jobGroup", quartzJobAndTriggerWrappers.get(0).jobDetail().getKey().getGroup())
));
RunProcessInput input = new RunProcessInput();