mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-21 14:38:43 +00:00
Compare commits
26 Commits
snapshot-f
...
snapshot-i
Author | SHA1 | Date | |
---|---|---|---|
ecc925715b | |||
d7dec3e560 | |||
0c4dc1d4cb | |||
40e8a85977 | |||
30ee8ce3bf | |||
3354717fd6 | |||
0d6538593b | |||
1019218762 | |||
7e6a3c528f | |||
02d068dad7 | |||
c9f921c148 | |||
2545d03f20 | |||
92b8211f20 | |||
8afbbfb4da | |||
24b1daa110 | |||
17899c3fdc | |||
605578d661 | |||
cb382c3f4b | |||
f15277f23b | |||
af757ea1fd | |||
d480027aeb | |||
5963a706b0 | |||
c8c7051628 | |||
7015322bf3 | |||
d6edbfa06b | |||
a10992226a |
@ -26,6 +26,16 @@ include::metaData/Reports.adoc[leveloffset=+1]
|
|||||||
include::metaData/Icons.adoc[leveloffset=+1]
|
include::metaData/Icons.adoc[leveloffset=+1]
|
||||||
include::metaData/PermissionRules.adoc[leveloffset=+1]
|
include::metaData/PermissionRules.adoc[leveloffset=+1]
|
||||||
|
|
||||||
|
== Services
|
||||||
|
|
||||||
|
include::misc/ScheduledJobs.adoc[leveloffset=+1]
|
||||||
|
|
||||||
|
=== Web server (Javalin)
|
||||||
|
#todo#
|
||||||
|
|
||||||
|
=== API server (OpenAPI)
|
||||||
|
#todo#
|
||||||
|
|
||||||
== Custom Application Code
|
== Custom Application Code
|
||||||
include::misc/QContext.adoc[leveloffset=+1]
|
include::misc/QContext.adoc[leveloffset=+1]
|
||||||
include::misc/QRecords.adoc[leveloffset=+1]
|
include::misc/QRecords.adoc[leveloffset=+1]
|
||||||
|
141
docs/misc/ScheduledJobs.adoc
Normal file
141
docs/misc/ScheduledJobs.adoc
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
== Schedulers and Scheduled Jobs
|
||||||
|
include::../variables.adoc[]
|
||||||
|
|
||||||
|
QQQ has the ability to automatically run various types of jobs on schedules,
|
||||||
|
either defined in your instance's meta-data,
|
||||||
|
or optionally via data in your application, in a `scheduledJob` table.
|
||||||
|
|
||||||
|
=== Schedulers and QSchedulerMetaData
|
||||||
|
2 types of schedulers are included in QQQ by default (though an application can define its own schedulers):
|
||||||
|
|
||||||
|
* `SimpleScheduler` - is (as its name suggests) a simple class which uses java's `ScheduledExecutorService`
|
||||||
|
to run jobs on repeating intervals.
|
||||||
|
** Cannot run cron schedules - only repeating intervals.
|
||||||
|
** If multiple servers are running, each will potentially run the same job concurrently
|
||||||
|
** Has no configurations, e.g., to limit the number of threads.
|
||||||
|
|
||||||
|
* `QuartzScheduler` - uses the 3rd party https://www.quartz-scheduler.org/[Quartz Scheduler] library to provide
|
||||||
|
a much more capable, though admittedly more complex, scheduling solution.
|
||||||
|
** Can run both cron schedules and repeating intervals.
|
||||||
|
** By default, will not allow concurrent executions of the same job.
|
||||||
|
** Supports multiple configurations, e.g., to limit the number of threads.
|
||||||
|
|
||||||
|
An application can define its own scheduler by providing a class which implements the `QSchedulerInterface`.
|
||||||
|
|
||||||
|
A `QInstance` can work with 0 or more schedulers, as defined by adding `QSchedulerMetaData` objects
|
||||||
|
to the instance.
|
||||||
|
|
||||||
|
This meta-data class is `abstract`, and is extended by the 2 built-in schedulers
|
||||||
|
(e.g., `SimpleSchedulerMetaData` and `QuartzSchedulerMetaData`). As such,
|
||||||
|
these concrete subclasses are what you need to instantiate and add to your instance.
|
||||||
|
|
||||||
|
To configure a QuartzScheduler, you can add a `Properties` object to the `QuartzSchedulerMetaData` object.
|
||||||
|
See https://www.quartz-scheduler.org/documentation/[Quartz's documentation] for available configuration properties.
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
.Defining SchedulerMetaData
|
||||||
|
----
|
||||||
|
qInstance.addScheduler(new SimpleSchedulerMetaData().withName("mySimpleScheduler"));
|
||||||
|
|
||||||
|
qInstance.addScheduler(new QuartzSchedulerMetaData()
|
||||||
|
.withName("myQuartzScheduler")
|
||||||
|
.withProperties(myQuartzProperties);
|
||||||
|
----
|
||||||
|
|
||||||
|
=== SchedulableTypes
|
||||||
|
The types of jobs which can be scheduled in a QQQ application are defined in the `QInstance` by
|
||||||
|
instances of the `SchedulableType` meta-data class.
|
||||||
|
These objects contain a name, along with a `QCodeReference` to the `runner`,
|
||||||
|
which must be a class that implements the `SchedulableRunner` interface.
|
||||||
|
|
||||||
|
By default, (in the `QInstanceEnricher`), QQQ will make 3 `SchedulableType` options available:
|
||||||
|
|
||||||
|
* `PROCESS` - Any Process defined in the `QInstance` can be scheduled.
|
||||||
|
* `QUEUE_PROCESSOR` - A Queue defined in the `QInstance`, which requires polling (e.g., SQS), can be scheduled.
|
||||||
|
* `TABLE_AUTOMATIONS` - A Table in the `QInstance`, with `AutomationDetails` referring to an
|
||||||
|
AutomationProvider which requires polling, can be scheduled.
|
||||||
|
|
||||||
|
If an application only wants to use a subset of these `SchedulableType` options,
|
||||||
|
or to add custom `SchedulableType` options,
|
||||||
|
the `QInstance` will need to have 1 or more `SchedulableType` objects in it before the `QInstanceEnricher` runs.
|
||||||
|
|
||||||
|
=== User-defined Scheduled Jobs
|
||||||
|
To allow users to schedule jobs (rather than using programmer-defined schedules (in meta-data)),
|
||||||
|
you can add a set of tables to your `QInstance`, using the `ScheduledJobsMetaDataProvider` class:
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
.Adding the ScheduledJob tables and related meta-data to a QInstance
|
||||||
|
----
|
||||||
|
new ScheduledJobsMetaDataProvider().defineAll(
|
||||||
|
qInstance, backendName, table -> tableEnricher(table));
|
||||||
|
----
|
||||||
|
|
||||||
|
This meta-data provider adds a "scheduledJob" and "scheduledJobParameter" table, along with
|
||||||
|
some PossibleValueSources.
|
||||||
|
These tables include post-action customizers, which manage (re-, un-) scheduling jobs based on
|
||||||
|
changes made to records in this these tables.
|
||||||
|
|
||||||
|
Also, when `QScheduleManager` is started, it will query these tables,and will schedule jobs as defined therein.
|
||||||
|
|
||||||
|
_You can use a mix of user-defined and meta-data defined scheduled jobs in your instance.
|
||||||
|
However, if a ScheduledJob record references a process, queue, or table automation with a
|
||||||
|
meta-data defined schedule, the ScheduledJob will NOT be started by ScheduleManager --
|
||||||
|
rather, the meta-data definition will "win"._
|
||||||
|
|
||||||
|
[source,sql]
|
||||||
|
.Example of inserting scheduled jobs records directly into an SQL backend
|
||||||
|
----
|
||||||
|
-- A process:
|
||||||
|
INSERT INTO scheduled_job (label, scheduler_name, cron_expression, cron_time_zone_id, repeat_seconds, type, is_active) VALUES
|
||||||
|
('myProcess', 'QuartzScheduler', null, null, 300, 'PROCESS', 1);
|
||||||
|
INSERT INTO scheduled_job_parameter (scheduled_job_id, `key`, value) VALUES
|
||||||
|
((SELECT id FROM scheduled_job WHERE label = 'myProcess'), 'processName', 'myProcess');
|
||||||
|
|
||||||
|
-- A table's insert & update automations:
|
||||||
|
INSERT INTO scheduled_job (label, scheduler_name, cron_expression, cron_time_zone_id, repeat_seconds, type, is_active) VALUES
|
||||||
|
('myTable.PENDING_INSERT_AUTOMATIONS', 'QuartzScheduler', null, null, 15, 'TABLE_AUTOMATIONS', 1),
|
||||||
|
('myTable.PENDING_UPDATE_AUTOMATIONS', 'QuartzScheduler', null, null, 15, 'TABLE_AUTOMATIONS', 1);
|
||||||
|
INSERT INTO scheduled_job_parameter (scheduled_job_id, `key`, value) VALUES
|
||||||
|
((SELECT id FROM scheduled_job WHERE label = 'myTable.PENDING_INSERT_AUTOMATIONS'), 'tableName', 'myTable'),
|
||||||
|
((SELECT id FROM scheduled_job WHERE label = 'myTable.PENDING_INSERT_AUTOMATIONS'), 'automationStatus', 'PENDING_INSERT_AUTOMATIONS'),
|
||||||
|
((SELECT id FROM scheduled_job WHERE label = 'myTable.PENDING_UPDATE_AUTOMATIONS'), 'tableName', 'myTable'),
|
||||||
|
((SELECT id FROM scheduled_job WHERE label = 'myTable.PENDING_UPDATE_AUTOMATIONS'), 'automationStatus', 'PENDING_UPDATE_AUTOMATIONS');
|
||||||
|
|
||||||
|
-- A queue processor:
|
||||||
|
INSERT INTO scheduled_job (label, scheduler_name, cron_expression, cron_time_zone_id, repeat_seconds, type, is_active) VALUES
|
||||||
|
('mySqsQueue', 'QuartzScheduler', null, null, 60, 'QUEUE_PROCESSOR', 1);
|
||||||
|
INSERT INTO scheduled_job_parameter (scheduled_job_id, `key`, value) VALUES
|
||||||
|
((SELECT id FROM scheduled_job WHERE label = 'mySqsQueue'), 'queueName', 'mySqsQueue');
|
||||||
|
----
|
||||||
|
|
||||||
|
=== Running Scheduled Jobs
|
||||||
|
In a server running QQQ, if you wish to start running scheduled jobs, you need to initialize
|
||||||
|
the `QScheduleManger` singleton class, then call its `start()` method.
|
||||||
|
|
||||||
|
Note that internally, this class will check for a system property of `qqq.scheduleManager.enabled`
|
||||||
|
or an environment variable of `QQQ_SCHEDULE_MANAGER_ENABLED`, and if the first value found is
|
||||||
|
`"false"`, then the scheduler will not actually run its jobs (but, in the case of the `QuartzSchdeuler`,
|
||||||
|
it will be available for managing scheduled jobs).
|
||||||
|
|
||||||
|
The method `QScheduleManager.initInstance` requires 2 parameters: Your `QInstance`, and a
|
||||||
|
`Supplier<QSession>` lambda, which returns the session that will be used for scheduled jobs when they
|
||||||
|
are executed.
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
.Starting the Schedule Manager service
|
||||||
|
----
|
||||||
|
QScheduleManager.initInstance(qInstance, () -> systemUserSession).start();
|
||||||
|
----
|
||||||
|
|
||||||
|
=== Examples
|
||||||
|
[source,java]
|
||||||
|
.Attach a schedule in meta-data to a Process
|
||||||
|
----
|
||||||
|
QProcessMetaData myProcess = new QProcessMetaData()
|
||||||
|
// ...
|
||||||
|
.withSchedule(new QScheduleMetaData()
|
||||||
|
.withSchedulerName("myScheduler")
|
||||||
|
.withDescription("Run myProcess every five minutes")
|
||||||
|
.withRepeatSeconds(300))
|
||||||
|
----
|
||||||
|
|
@ -159,7 +159,6 @@ public class AsyncJobManager
|
|||||||
private <T extends Serializable> T runAsyncJob(String jobName, AsyncJob<T> asyncJob, UUIDAndTypeStateKey uuidAndTypeStateKey, AsyncJobStatus asyncJobStatus)
|
private <T extends Serializable> T runAsyncJob(String jobName, AsyncJob<T> asyncJob, UUIDAndTypeStateKey uuidAndTypeStateKey, AsyncJobStatus asyncJobStatus)
|
||||||
{
|
{
|
||||||
String originalThreadName = Thread.currentThread().getName();
|
String originalThreadName = Thread.currentThread().getName();
|
||||||
// Thread.currentThread().setName("Job:" + jobName + ":" + uuidAndTypeStateKey.getUuid().toString().substring(0, 8));
|
|
||||||
Thread.currentThread().setName("Job:" + jobName);
|
Thread.currentThread().setName("Job:" + jobName);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -370,6 +370,14 @@ public class QPossibleValueTranslator
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private String translatePossibleValueCustom(Serializable value, QPossibleValueSource possibleValueSource)
|
private String translatePossibleValueCustom(Serializable value, QPossibleValueSource possibleValueSource)
|
||||||
{
|
{
|
||||||
|
/////////////////////////////////
|
||||||
|
// null input gets null output //
|
||||||
|
/////////////////////////////////
|
||||||
|
if(value == null)
|
||||||
|
{
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource);
|
QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource);
|
||||||
|
@ -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.bulk.insert.BulkInsertTransformStep;
|
||||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
|
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.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.CollectionUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
@ -159,6 +160,18 @@ public class QInstanceEnricher
|
|||||||
}
|
}
|
||||||
|
|
||||||
enrichJoins();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,6 +50,16 @@ public abstract class QSchedulerMetaData implements TopLevelMetaDataInterface
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public boolean mayUseInScheduledJobsTable()
|
||||||
|
{
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -68,6 +68,16 @@ public class QuartzSchedulerMetaData extends QSchedulerMetaData
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public boolean mayUseInScheduledJobsTable()
|
||||||
|
{
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -61,6 +61,16 @@ public class SimpleSchedulerMetaData extends QSchedulerMetaData
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public boolean mayUseInScheduledJobsTable()
|
||||||
|
{
|
||||||
|
return (false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -73,7 +73,7 @@ public class ScheduledJob extends QRecordEntity
|
|||||||
@QField(displayFormat = DisplayFormat.COMMAS)
|
@QField(displayFormat = DisplayFormat.COMMAS)
|
||||||
private Integer repeatSeconds;
|
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;
|
private String type;
|
||||||
|
|
||||||
@QField(isRequired = true)
|
@QField(isRequired = true)
|
||||||
|
@ -22,102 +22,16 @@
|
|||||||
package com.kingsrook.qqq.backend.core.model.scheduledjobs;
|
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<String>
|
public enum ScheduledJobType
|
||||||
{
|
{
|
||||||
PROCESS,
|
PROCESS,
|
||||||
QUEUE_PROCESSOR,
|
QUEUE_PROCESSOR,
|
||||||
TABLE_AUTOMATIONS,
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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.scheduler.schedulable.SchedulableType;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public class ScheduledJobTypePossibleValueSource implements QCustomPossibleValueProvider<String>
|
||||||
|
{
|
||||||
|
public static final String NAME = "scheduledJobType";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public QPossibleValue<String> getPossibleValue(Serializable idValue)
|
||||||
|
{
|
||||||
|
SchedulableType schedulableType = QContext.getQInstance().getSchedulableType(String.valueOf(idValue));
|
||||||
|
if(schedulableType != null)
|
||||||
|
{
|
||||||
|
return schedulableTypeToPossibleValue(schedulableType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public List<QPossibleValue<String>> search(SearchPossibleValueSourceInput input) throws QException
|
||||||
|
{
|
||||||
|
List<QPossibleValue<String>> rs = new ArrayList<>();
|
||||||
|
for(SchedulableType schedulableType : CollectionUtils.nonNullMap(QContext.getQInstance().getSchedulableTypes()).values())
|
||||||
|
{
|
||||||
|
rs.add(schedulableTypeToPossibleValue(schedulableType));
|
||||||
|
}
|
||||||
|
return rs;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static QPossibleValue<String> schedulableTypeToPossibleValue(SchedulableType schedulableType)
|
||||||
|
{
|
||||||
|
return new QPossibleValue<>(schedulableType.getName(), schedulableType.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -41,9 +41,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRule
|
|||||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
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.possiblevalues.QPossibleValueSourceType;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||||
|
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.QFieldSection;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
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.metadata.tables.Tier;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.scheduledjobs.customizers.ScheduledJobParameterTableCustomizer;
|
||||||
import com.kingsrook.qqq.backend.core.model.scheduledjobs.customizers.ScheduledJobTableCustomizer;
|
import com.kingsrook.qqq.backend.core.model.scheduledjobs.customizers.ScheduledJobTableCustomizer;
|
||||||
|
|
||||||
|
|
||||||
@ -63,7 +65,7 @@ public class ScheduledJobsMetaDataProvider
|
|||||||
{
|
{
|
||||||
defineStandardTables(instance, backendName, backendDetailEnricher);
|
defineStandardTables(instance, backendName, backendDetailEnricher);
|
||||||
instance.addPossibleValueSource(QPossibleValueSource.newForTable(ScheduledJob.TABLE_NAME));
|
instance.addPossibleValueSource(QPossibleValueSource.newForTable(ScheduledJob.TABLE_NAME));
|
||||||
instance.addPossibleValueSource(QPossibleValueSource.newForEnum(ScheduledJobType.NAME, ScheduledJobType.values()));
|
instance.addPossibleValueSource(defineScheduledJobTypePossibleValueSource());
|
||||||
instance.addPossibleValueSource(defineSchedulersPossibleValueSource());
|
instance.addPossibleValueSource(defineSchedulersPossibleValueSource());
|
||||||
defineStandardJoins(instance);
|
defineStandardJoins(instance);
|
||||||
defineStandardWidgets(instance);
|
defineStandardWidgets(instance);
|
||||||
@ -183,6 +185,11 @@ public class ScheduledJobsMetaDataProvider
|
|||||||
.withAssociatedTableName(ScheduledJobParameter.TABLE_NAME)
|
.withAssociatedTableName(ScheduledJobParameter.TABLE_NAME)
|
||||||
.withJoinName(JOB_PARAMETER_JOIN_NAME));
|
.withJoinName(JOB_PARAMETER_JOIN_NAME));
|
||||||
|
|
||||||
|
tableMetaData.withExposedJoin(new ExposedJoin()
|
||||||
|
.withJoinTable(ScheduledJobParameter.TABLE_NAME)
|
||||||
|
.withJoinPath(List.of(JOB_PARAMETER_JOIN_NAME))
|
||||||
|
.withLabel("Parameters"));
|
||||||
|
|
||||||
return (tableMetaData);
|
return (tableMetaData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,11 +206,35 @@ public class ScheduledJobsMetaDataProvider
|
|||||||
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "scheduledJobId", "key", "value")))
|
.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")));
|
.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))
|
||||||
|
.withLabel("Scheduled Job"));
|
||||||
|
|
||||||
return (tableMetaData);
|
return (tableMetaData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private QPossibleValueSource defineScheduledJobTypePossibleValueSource()
|
||||||
|
{
|
||||||
|
return (new QPossibleValueSource()
|
||||||
|
.withName(ScheduledJobTypePossibleValueSource.NAME)
|
||||||
|
.withType(QPossibleValueSourceType.CUSTOM)
|
||||||
|
.withCustomCodeReference(new QCodeReference(ScheduledJobTypePossibleValueSource.class)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -213,7 +244,6 @@ public class ScheduledJobsMetaDataProvider
|
|||||||
.withName(SchedulersPossibleValueSource.NAME)
|
.withName(SchedulersPossibleValueSource.NAME)
|
||||||
.withType(QPossibleValueSourceType.CUSTOM)
|
.withType(QPossibleValueSourceType.CUSTOM)
|
||||||
.withCustomCodeReference(new QCodeReference(SchedulersPossibleValueSource.class)));
|
.withCustomCodeReference(new QCodeReference(SchedulersPossibleValueSource.class)));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -68,9 +68,12 @@ public class SchedulersPossibleValueSource implements QCustomPossibleValueProvid
|
|||||||
{
|
{
|
||||||
List<QPossibleValue<String>> rs = new ArrayList<>();
|
List<QPossibleValue<String>> rs = new ArrayList<>();
|
||||||
for(QSchedulerMetaData scheduler : CollectionUtils.nonNullMap(QContext.getQInstance().getSchedulers()).values())
|
for(QSchedulerMetaData scheduler : CollectionUtils.nonNullMap(QContext.getQInstance().getSchedulers()).values())
|
||||||
|
{
|
||||||
|
if(scheduler.mayUseInScheduledJobsTable())
|
||||||
{
|
{
|
||||||
rs.add(schedulerToPossibleValue(scheduler));
|
rs.add(schedulerToPossibleValue(scheduler));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return rs;
|
return rs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<QRecord> postInsert(InsertInput insertInput, List<QRecord> 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<QRecord> records, Optional<List<QRecord>> 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<Integer, QRecord> recordsByJobId = new ListingHash<>();
|
||||||
|
for(QRecord record : records)
|
||||||
|
{
|
||||||
|
recordsByJobId.add(record.getValueInteger("scheduledJobId"), record);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Integer> 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<QRecord> postUpdate(UpdateInput updateInput, List<QRecord> records, Optional<List<QRecord>> 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<QRecord> postDelete(DeleteInput deleteInput, List<QRecord> 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<AbstractActionInput> firstActionInStack = QContext.getFirstActionInStack();
|
||||||
|
if(firstActionInStack.isPresent())
|
||||||
|
{
|
||||||
|
if(firstActionInStack.get() instanceof AbstractTableActionInput tableActionInput)
|
||||||
|
{
|
||||||
|
if(!ScheduledJobParameter.TABLE_NAME.equals(tableActionInput.getTableName()))
|
||||||
|
{
|
||||||
|
return (false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -22,10 +22,12 @@
|
|||||||
package com.kingsrook.qqq.backend.core.model.scheduledjobs.customizers;
|
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.List;
|
||||||
import java.util.ListIterator;
|
import java.util.ListIterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
@ -43,9 +45,11 @@ 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.data.QRecord;
|
||||||
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJob;
|
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJob;
|
||||||
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
|
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
|
||||||
import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager;
|
import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager;
|
||||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
|
import org.quartz.CronScheduleBuilder;
|
||||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||||
|
|
||||||
|
|
||||||
@ -61,7 +65,7 @@ public class ScheduledJobTableCustomizer implements TableCustomizerInterface
|
|||||||
@Override
|
@Override
|
||||||
public List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
|
public List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
|
||||||
{
|
{
|
||||||
validateConditionalFields(records);
|
validateConditionalFields(records, Collections.emptyMap());
|
||||||
return (records);
|
return (records);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +89,9 @@ public class ScheduledJobTableCustomizer implements TableCustomizerInterface
|
|||||||
@Override
|
@Override
|
||||||
public List<QRecord> preUpdate(UpdateInput updateInput, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
|
public List<QRecord> preUpdate(UpdateInput updateInput, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
|
||||||
{
|
{
|
||||||
validateConditionalFields(records);
|
Map<Integer, QRecord> freshOldRecordsWithAssociationsMap = CollectionUtils.recordsToMap(freshlyQueryForRecordsWithAssociations(oldRecordList.get()), "id", Integer.class);
|
||||||
|
|
||||||
|
validateConditionalFields(records, freshOldRecordsWithAssociationsMap);
|
||||||
|
|
||||||
if(isPreview || oldRecordList.isEmpty())
|
if(isPreview || oldRecordList.isEmpty())
|
||||||
{
|
{
|
||||||
@ -95,7 +101,6 @@ 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 //
|
// 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();
|
ListIterator<QRecord> iterator = oldRecordList.get().listIterator();
|
||||||
while(iterator.hasNext())
|
while(iterator.hasNext())
|
||||||
{
|
{
|
||||||
@ -115,15 +120,42 @@ public class ScheduledJobTableCustomizer implements TableCustomizerInterface
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private static void validateConditionalFields(List<QRecord> records)
|
private static void validateConditionalFields(List<QRecord> records, Map<Integer, QRecord> freshOldRecordsWithAssociationsMap)
|
||||||
{
|
{
|
||||||
|
QRecord blankRecord = new QRecord();
|
||||||
for(QRecord record : records)
|
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(repeatSeconds))
|
||||||
|
{
|
||||||
|
record.addError(new BadInputStatusMessage("Either Cron Expression or Repeat Seconds must be given."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -189,6 +221,7 @@ public class ScheduledJobTableCustomizer implements TableCustomizerInterface
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
Map<Integer, QRecord> originalRecordMap = recordsWithoutErrors.stream().collect(Collectors.toMap(r -> r.getValueInteger("id"), r -> r));
|
||||||
List<QRecord> freshRecordListWithAssociations = freshlyQueryForRecordsWithAssociations(recordsWithoutErrors);
|
List<QRecord> freshRecordListWithAssociations = freshlyQueryForRecordsWithAssociations(recordsWithoutErrors);
|
||||||
|
|
||||||
QScheduleManager scheduleManager = QScheduleManager.getInstance();
|
QScheduleManager scheduleManager = QScheduleManager.getInstance();
|
||||||
@ -200,7 +233,11 @@ public class ScheduledJobTableCustomizer implements TableCustomizerInterface
|
|||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
LOG.info("Caught exception while scheduling a job in post-action", e, logPair("id", record.getValue("id")));
|
LOG.warn("Caught exception while scheduling a job in post-action", e, logPair("id", record.getValue("id")));
|
||||||
|
if(originalRecordMap.containsKey(record.getValueInteger("id")))
|
||||||
|
{
|
||||||
|
originalRecordMap.get(record.getValueInteger("id")).addWarning(new QWarningMessage("Error scheduling job: " + e.getMessage()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -265,7 +302,7 @@ public class ScheduledJobTableCustomizer implements TableCustomizerInterface
|
|||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
LOG.info("Caught exception while scheduling a job in post-action", e, logPair("id", record.getValue("id")));
|
LOG.warn("Caught exception while un-scheduling a job in post-action", e, logPair("id", record.getValue("id")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,6 +113,10 @@ public class ExtractViaQueryStep extends AbstractExtractStep
|
|||||||
{
|
{
|
||||||
queryInput.setShouldFetchHeavyFields(true);
|
queryInput.setShouldFetchHeavyFields(true);
|
||||||
}
|
}
|
||||||
|
if(runBackendStepInput.getValuePrimitiveBoolean(StreamedETLWithFrontendProcess.FIELD_INCLUDE_ASSOCIATIONS))
|
||||||
|
{
|
||||||
|
queryInput.setIncludeAssociations(true);
|
||||||
|
}
|
||||||
|
|
||||||
customizeInputPreQuery(queryInput);
|
customizeInputPreQuery(queryInput);
|
||||||
|
|
||||||
|
@ -84,6 +84,7 @@ public class StreamedETLWithFrontendProcess
|
|||||||
public static final String FIELD_RECORD_COUNT = "recordCount"; // Integer
|
public static final String FIELD_RECORD_COUNT = "recordCount"; // Integer
|
||||||
public static final String FIELD_DEFAULT_QUERY_FILTER = "defaultQueryFilter"; // QQueryFilter or String (json, of q QQueryFilter)
|
public static final String FIELD_DEFAULT_QUERY_FILTER = "defaultQueryFilter"; // QQueryFilter or String (json, of q QQueryFilter)
|
||||||
public static final String FIELD_FETCH_HEAVY_FIELDS = "fetchHeavyFields"; // Boolean
|
public static final String FIELD_FETCH_HEAVY_FIELDS = "fetchHeavyFields"; // Boolean
|
||||||
|
public static final String FIELD_INCLUDE_ASSOCIATIONS = "includeAssociations"; // Boolean
|
||||||
|
|
||||||
public static final String FIELD_SUPPORTS_FULL_VALIDATION = "supportsFullValidation"; // Boolean
|
public static final String FIELD_SUPPORTS_FULL_VALIDATION = "supportsFullValidation"; // Boolean
|
||||||
public static final String FIELD_DO_FULL_VALIDATION = "doFullValidation"; // Boolean
|
public static final String FIELD_DO_FULL_VALIDATION = "doFullValidation"; // Boolean
|
||||||
@ -145,6 +146,7 @@ public class StreamedETLWithFrontendProcess
|
|||||||
.withCode(new QCodeReference(StreamedETLPreviewStep.class))
|
.withCode(new QCodeReference(StreamedETLPreviewStep.class))
|
||||||
.withInputData(new QFunctionInputMetaData()
|
.withInputData(new QFunctionInputMetaData()
|
||||||
.withField(new QFieldMetaData(FIELD_SOURCE_TABLE, QFieldType.STRING).withDefaultValue(defaultFieldValues.get(FIELD_SOURCE_TABLE)))
|
.withField(new QFieldMetaData(FIELD_SOURCE_TABLE, QFieldType.STRING).withDefaultValue(defaultFieldValues.get(FIELD_SOURCE_TABLE)))
|
||||||
|
.withField(new QFieldMetaData(FIELD_INCLUDE_ASSOCIATIONS, QFieldType.BOOLEAN).withDefaultValue(defaultFieldValues.getOrDefault(FIELD_INCLUDE_ASSOCIATIONS, false)))
|
||||||
.withField(new QFieldMetaData(FIELD_FETCH_HEAVY_FIELDS, QFieldType.BOOLEAN).withDefaultValue(defaultFieldValues.getOrDefault(FIELD_FETCH_HEAVY_FIELDS, false)))
|
.withField(new QFieldMetaData(FIELD_FETCH_HEAVY_FIELDS, QFieldType.BOOLEAN).withDefaultValue(defaultFieldValues.getOrDefault(FIELD_FETCH_HEAVY_FIELDS, false)))
|
||||||
.withField(new QFieldMetaData(FIELD_DESTINATION_TABLE, QFieldType.STRING).withDefaultValue(defaultFieldValues.get(FIELD_DESTINATION_TABLE)))
|
.withField(new QFieldMetaData(FIELD_DESTINATION_TABLE, QFieldType.STRING).withDefaultValue(defaultFieldValues.get(FIELD_DESTINATION_TABLE)))
|
||||||
.withField(new QFieldMetaData(FIELD_SUPPORTS_FULL_VALIDATION, QFieldType.BOOLEAN).withDefaultValue(defaultFieldValues.getOrDefault(FIELD_SUPPORTS_FULL_VALIDATION, true)))
|
.withField(new QFieldMetaData(FIELD_SUPPORTS_FULL_VALIDATION, QFieldType.BOOLEAN).withDefaultValue(defaultFieldValues.getOrDefault(FIELD_SUPPORTS_FULL_VALIDATION, true)))
|
||||||
|
@ -102,15 +102,6 @@ public class QScheduleManager
|
|||||||
{
|
{
|
||||||
qScheduleManager = new QScheduleManager(qInstance, systemUserSessionSupplier);
|
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 //
|
// 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 //
|
// 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)
|
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.PROCESS.name()).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.QUEUE_PROCESSOR.name()).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.TABLE_AUTOMATIONS.name()).withRunner(new QCodeReference(SchedulableTableAutomationsRunner.class)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -330,8 +321,8 @@ public class QScheduleManager
|
|||||||
throw (new QException("Missing a type " + exceptionSuffix));
|
throw (new QException("Missing a type " + exceptionSuffix));
|
||||||
}
|
}
|
||||||
|
|
||||||
ScheduledJobType scheduledJobType = ScheduledJobType.getById(scheduledJob.getType());
|
SchedulableType schedulableType = qInstance.getSchedulableType(scheduledJob.getType());
|
||||||
if(scheduledJobType == null)
|
if(schedulableType == null)
|
||||||
{
|
{
|
||||||
throw (new QException("Unrecognized type [" + scheduledJob.getType() + "] " + exceptionSuffix));
|
throw (new QException("Unrecognized type [" + scheduledJob.getType() + "] " + exceptionSuffix));
|
||||||
}
|
}
|
||||||
@ -339,8 +330,6 @@ public class QScheduleManager
|
|||||||
QSchedulerInterface scheduler = getScheduler(scheduledJob.getSchedulerName());
|
QSchedulerInterface scheduler = getScheduler(scheduledJob.getSchedulerName());
|
||||||
Map<String, Serializable> paramMap = new HashMap<>(scheduledJob.getJobParametersMap());
|
Map<String, Serializable> paramMap = new HashMap<>(scheduledJob.getJobParametersMap());
|
||||||
|
|
||||||
SchedulableType schedulableType = qInstance.getSchedulableType(scheduledJob.getType());
|
|
||||||
|
|
||||||
SchedulableRunner runner = QCodeLoader.getAdHoc(SchedulableRunner.class, schedulableType.getRunner());
|
SchedulableRunner runner = QCodeLoader.getAdHoc(SchedulableRunner.class, schedulableType.getRunner());
|
||||||
runner.validateParams(schedulableIdentity, new HashMap<>(paramMap));
|
runner.validateParams(schedulableIdentity, new HashMap<>(paramMap));
|
||||||
|
|
||||||
@ -396,7 +385,7 @@ public class QScheduleManager
|
|||||||
Map<String, String> paramMap = new HashMap<>();
|
Map<String, String> paramMap = new HashMap<>();
|
||||||
paramMap.put("processName", process.getName());
|
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()))
|
if(process.getVariantBackend() == null || VariantRunStrategy.SERIAL.equals(process.getVariantRunStrategy()))
|
||||||
{
|
{
|
||||||
@ -446,7 +435,7 @@ public class QScheduleManager
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private void setupTableAutomations(QTableMetaData table) throws QException
|
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();
|
QTableAutomationDetails automationDetails = table.getAutomationDetails();
|
||||||
QSchedulerInterface scheduler = getScheduler(automationDetails.getSchedule().getSchedulerName());
|
QSchedulerInterface scheduler = getScheduler(automationDetails.getSchedule().getSchedulerName());
|
||||||
|
|
||||||
@ -475,7 +464,7 @@ public class QScheduleManager
|
|||||||
{
|
{
|
||||||
SchedulableIdentity schedulableIdentity = SchedulableIdentityFactory.of(queue);
|
SchedulableIdentity schedulableIdentity = SchedulableIdentityFactory.of(queue);
|
||||||
QSchedulerInterface scheduler = getScheduler(queue.getSchedule().getSchedulerName());
|
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());
|
boolean allowedToStart = SchedulerUtils.allowedToStart(queue.getName());
|
||||||
|
|
||||||
Map<String, String> paramMap = new HashMap<>();
|
Map<String, String> paramMap = new HashMap<>();
|
||||||
|
@ -30,6 +30,8 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
|
|||||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||||
import com.kingsrook.qqq.backend.core.scheduler.schedulable.SchedulableType;
|
import com.kingsrook.qqq.backend.core.scheduler.schedulable.SchedulableType;
|
||||||
import com.kingsrook.qqq.backend.core.scheduler.schedulable.runner.SchedulableRunner;
|
import com.kingsrook.qqq.backend.core.scheduler.schedulable.runner.SchedulableRunner;
|
||||||
|
import org.apache.logging.log4j.Level;
|
||||||
|
import org.quartz.DisallowConcurrentExecution;
|
||||||
import org.quartz.Job;
|
import org.quartz.Job;
|
||||||
import org.quartz.JobExecutionContext;
|
import org.quartz.JobExecutionContext;
|
||||||
import org.quartz.JobExecutionException;
|
import org.quartz.JobExecutionException;
|
||||||
@ -39,10 +41,13 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@DisallowConcurrentExecution
|
||||||
public class QuartzJobRunner implements Job
|
public class QuartzJobRunner implements Job
|
||||||
{
|
{
|
||||||
private static final QLogger LOG = QLogger.getLogger(QuartzJobRunner.class);
|
private static final QLogger LOG = QLogger.getLogger(QuartzJobRunner.class);
|
||||||
|
|
||||||
|
private static Level logLevel = null;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -52,21 +57,38 @@ public class QuartzJobRunner implements Job
|
|||||||
public void execute(JobExecutionContext context) throws JobExecutionException
|
public void execute(JobExecutionContext context) throws JobExecutionException
|
||||||
{
|
{
|
||||||
CapturedContext capturedContext = QContext.capture();
|
CapturedContext capturedContext = QContext.capture();
|
||||||
|
|
||||||
|
String name = null;
|
||||||
|
SchedulableType schedulableType = null;
|
||||||
|
Map<String, Object> params = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
name = context.getJobDetail().getKey().getName();
|
||||||
|
|
||||||
QuartzScheduler quartzScheduler = QuartzScheduler.getInstance();
|
QuartzScheduler quartzScheduler = QuartzScheduler.getInstance();
|
||||||
QInstance qInstance = quartzScheduler.getQInstance();
|
QInstance qInstance = quartzScheduler.getQInstance();
|
||||||
QContext.init(qInstance, quartzScheduler.getSessionSupplier().get());
|
QContext.init(qInstance, quartzScheduler.getSessionSupplier().get());
|
||||||
|
|
||||||
SchedulableType schedulableType = qInstance.getSchedulableType(context.getJobDetail().getJobDataMap().getString("type"));
|
schedulableType = qInstance.getSchedulableType(context.getJobDetail().getJobDataMap().getString("type"));
|
||||||
Map<String, Object> params = (Map<String, Object>) context.getJobDetail().getJobDataMap().get("params");
|
params = (Map<String, Object>) context.getJobDetail().getJobDataMap().get("params");
|
||||||
|
|
||||||
SchedulableRunner schedulableRunner = QCodeLoader.getAdHoc(SchedulableRunner.class, schedulableType.getRunner());
|
SchedulableRunner schedulableRunner = QCodeLoader.getAdHoc(SchedulableRunner.class, schedulableType.getRunner());
|
||||||
|
|
||||||
|
if(logLevel != null)
|
||||||
|
{
|
||||||
|
LOG.log(logLevel, "Running QuartzJob", null, logPair("name", name), logPair("type", schedulableType.getName()), logPair("params", params));
|
||||||
|
}
|
||||||
|
|
||||||
schedulableRunner.run(params);
|
schedulableRunner.run(params);
|
||||||
|
|
||||||
|
if(logLevel != null)
|
||||||
|
{
|
||||||
|
LOG.log(logLevel, "Finished QuartzJob", null, logPair("name", name), logPair("type", schedulableType.getName()), logPair("params", params));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
LOG.warn("Error running QuartzJob", e, logPair("jobContext", context));
|
LOG.warn("Error running QuartzJob", e, logPair("name", name), logPair("type", schedulableType == null ? null : schedulableType.getName()), logPair("params", params));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -74,4 +96,14 @@ public class QuartzJobRunner implements Job
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static void setLogLevel(Level level)
|
||||||
|
{
|
||||||
|
logLevel = level;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -52,11 +52,12 @@ public class PauseQuartzJobsProcess extends AbstractLoadStep implements MetaData
|
|||||||
@Override
|
@Override
|
||||||
public QProcessMetaData produce(QInstance qInstance) throws QException
|
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||||
{
|
{
|
||||||
String tableName = "quartzTriggers";
|
String tableName = "quartzJobDetails";
|
||||||
|
|
||||||
return StreamedETLWithFrontendProcess.processMetaDataBuilder()
|
return StreamedETLWithFrontendProcess.processMetaDataBuilder()
|
||||||
.withName(getClass().getSimpleName())
|
.withName(getClass().getSimpleName())
|
||||||
.withLabel("Pause Quartz Jobs")
|
.withLabel("Pause Quartz Jobs")
|
||||||
|
.withPreviewMessage("This is a preview of the jobs that will be paused.")
|
||||||
.withTableName(tableName)
|
.withTableName(tableName)
|
||||||
.withSourceTable(tableName)
|
.withSourceTable(tableName)
|
||||||
.withDestinationTable(tableName)
|
.withDestinationTable(tableName)
|
||||||
|
@ -52,11 +52,12 @@ public class ResumeQuartzJobsProcess extends AbstractLoadStep implements MetaDat
|
|||||||
@Override
|
@Override
|
||||||
public QProcessMetaData produce(QInstance qInstance) throws QException
|
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||||
{
|
{
|
||||||
String tableName = "quartzTriggers";
|
String tableName = "quartzJobDetails";
|
||||||
|
|
||||||
return StreamedETLWithFrontendProcess.processMetaDataBuilder()
|
return StreamedETLWithFrontendProcess.processMetaDataBuilder()
|
||||||
.withName(getClass().getSimpleName())
|
.withName(getClass().getSimpleName())
|
||||||
.withLabel("Resume Quartz Jobs")
|
.withLabel("Resume Quartz Jobs")
|
||||||
|
.withPreviewMessage("This is a preview of the jobs that will be resumed.")
|
||||||
.withTableName(tableName)
|
.withTableName(tableName)
|
||||||
.withSourceTable(tableName)
|
.withSourceTable(tableName)
|
||||||
.withDestinationTable(tableName)
|
.withDestinationTable(tableName)
|
||||||
|
@ -22,12 +22,17 @@
|
|||||||
package com.kingsrook.qqq.backend.core.scheduler.quartz.tables;
|
package com.kingsrook.qqq.backend.core.scheduler.quartz.tables;
|
||||||
|
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCustomizer;
|
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCustomizer;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
||||||
|
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||||
import org.apache.commons.lang3.SerializationUtils;
|
import org.apache.commons.lang3.SerializationUtils;
|
||||||
|
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -67,8 +72,35 @@ public class QuartzJobDataPostQueryCustomizer extends AbstractPostQueryCustomize
|
|||||||
LOG.info("Error deserializing quartz job data", e);
|
LOG.info("Error deserializing quartz job data", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formatEpochTime(record, "nextFireTime");
|
||||||
|
formatEpochTime(record, "prevFireTime");
|
||||||
|
formatEpochTime(record, "startTime");
|
||||||
}
|
}
|
||||||
return (records);
|
return (records);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static void formatEpochTime(QRecord record, String fieldName)
|
||||||
|
{
|
||||||
|
Long value = record.getValueLong(fieldName);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if(value != null && value > 0)
|
||||||
|
{
|
||||||
|
Instant instant = Instant.ofEpochMilli(value);
|
||||||
|
record.setDisplayValue(fieldName, String.format("%,d", value) + " (" + QValueFormatter.formatDateTimeWithZone(instant.atZone(ZoneId.of(QContext.getQInstance().getDefaultTimeZoneId()))) + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
LOG.info("Error formatting an epoc time value", e, logPair("fieldName", fieldName), logPair("value", value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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.processes.QProcessMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData;
|
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.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.SchedulableType;
|
||||||
import com.kingsrook.qqq.backend.core.scheduler.schedulable.runner.SchedulableRunner;
|
import com.kingsrook.qqq.backend.core.scheduler.schedulable.runner.SchedulableRunner;
|
||||||
|
|
||||||
@ -46,18 +45,17 @@ public class SchedulableIdentityFactory
|
|||||||
public static BasicSchedulableIdentity of(ScheduledJob scheduledJob)
|
public static BasicSchedulableIdentity of(ScheduledJob scheduledJob)
|
||||||
{
|
{
|
||||||
String description = "";
|
String description = "";
|
||||||
ScheduledJobType scheduledJobType = ScheduledJobType.getById(scheduledJob.getType());
|
SchedulableType schedulableType = QContext.getQInstance().getSchedulableType(scheduledJob.getType());
|
||||||
if(scheduledJobType != null)
|
if(schedulableType != null)
|
||||||
{
|
{
|
||||||
try
|
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()));
|
description = runner.getDescription(new HashMap<>(scheduledJob.getJobParametersMap()));
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
description = "type: " + scheduledJobType;
|
description = "type: " + schedulableType.getName();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,10 +75,6 @@ public class SchedulableTableAutomationsRunner implements SchedulableRunner
|
|||||||
|
|
||||||
AutomationStatus automationStatus = AutomationStatus.valueOf(ValueUtils.getValueAsString(params.get("automationStatus")));
|
AutomationStatus automationStatus = AutomationStatus.valueOf(ValueUtils.getValueAsString(params.get("automationStatus")));
|
||||||
|
|
||||||
/////////////
|
|
||||||
// run it. //
|
|
||||||
/////////////
|
|
||||||
LOG.debug("Running table automations", logPair("tableName", tableName), logPair(""));
|
|
||||||
QTableAutomationDetails automationDetails = table.getAutomationDetails();
|
QTableAutomationDetails automationDetails = table.getAutomationDetails();
|
||||||
if(automationDetails == null)
|
if(automationDetails == null)
|
||||||
{
|
{
|
||||||
|
@ -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 <T extends Serializable> Map<T, QRecord> recordsToMap(Collection<QRecord> records, String keyFieldName, Class<T> type)
|
||||||
|
{
|
||||||
|
Map<T, QRecord> rs = new HashMap<>();
|
||||||
|
|
||||||
|
for(QRecord record : nonNullCollection(records))
|
||||||
|
{
|
||||||
|
rs.put(ValueUtils.getValueAsType(type, record.getValue(keyFieldName)), record);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (rs);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -1,64 +0,0 @@
|
|||||||
#
|
|
||||||
# QQQ - Low-code Application Framework for Engineers.
|
|
||||||
# Copyright (C) 2021-2023. 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/>.
|
|
||||||
|
|
||||||
#
|
|
||||||
#org.quartz.scheduler.instanceName = MyScheduler
|
|
||||||
#org.quartz.threadPool.threadCount = 3
|
|
||||||
#org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#============================================================================
|
|
||||||
# Configure Main Scheduler Properties
|
|
||||||
#============================================================================
|
|
||||||
org.quartz.scheduler.instanceName = MyClusteredScheduler
|
|
||||||
org.quartz.scheduler.instanceId = AUTO
|
|
||||||
|
|
||||||
#============================================================================
|
|
||||||
# Configure ThreadPool
|
|
||||||
#============================================================================
|
|
||||||
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
|
|
||||||
org.quartz.threadPool.threadCount = 5
|
|
||||||
org.quartz.threadPool.threadPriority = 5
|
|
||||||
|
|
||||||
#============================================================================
|
|
||||||
# Configure JobStore
|
|
||||||
#============================================================================
|
|
||||||
org.quartz.jobStore.misfireThreshold = 60000
|
|
||||||
|
|
||||||
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
|
|
||||||
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
|
|
||||||
org.quartz.jobStore.useProperties = false
|
|
||||||
org.quartz.jobStore.dataSource = myDS
|
|
||||||
org.quartz.jobStore.tablePrefix = QUARTZ_
|
|
||||||
|
|
||||||
org.quartz.jobStore.isClustered = true
|
|
||||||
org.quartz.jobStore.clusterCheckinInterval = 20000
|
|
||||||
|
|
||||||
#============================================================================
|
|
||||||
# Configure Datasources
|
|
||||||
#============================================================================
|
|
||||||
org.quartz.dataSource.myDS.driver = com.mysql.cj.jdbc.Driver
|
|
||||||
org.quartz.dataSource.myDS.URL = jdbc:mysql://localhost:3306/nutrifresh_one
|
|
||||||
org.quartz.dataSource.myDS.user = root
|
|
||||||
org.quartz.dataSource.myDS.password = BXca6Bubxf!ECt7sua6L
|
|
||||||
org.quartz.dataSource.myDS.maxConnections = 5
|
|
||||||
org.quartz.dataSource.myDS.validationQuery=select 1
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Consumer<ScheduledJob>, 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<QRecord, AbstractStringAssert<?>> assertOneErrorExtractingMessage = qRecord -> assertThat(qRecord.getErrors()).hasSize(1).first().extracting("message").asString();
|
||||||
|
Consumer<QRecord> 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<QuartzJobAndTriggerWrapper> 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<QuartzJobAndTriggerWrapper> 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<Consumer<QRecord>, 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<QRecord, AbstractStringAssert<?>> assertOneErrorExtractingMessage = qRecord -> assertThat(qRecord.getErrors()).hasSize(1).first().extracting("message").asString();
|
||||||
|
Consumer<QRecord> 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<QuartzJobAndTriggerWrapper> 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<QuartzJobAndTriggerWrapper> 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<QuartzJobAndTriggerWrapper> 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -92,7 +92,7 @@ class QScheduleManagerTest extends BaseTest
|
|||||||
.withId(1)
|
.withId(1)
|
||||||
.withIsActive(true)
|
.withIsActive(true)
|
||||||
.withSchedulerName(TestUtils.SIMPLE_SCHEDULER_NAME)
|
.withSchedulerName(TestUtils.SIMPLE_SCHEDULER_NAME)
|
||||||
.withType(type.getId())
|
.withType(type.name())
|
||||||
.withRepeatSeconds(1)
|
.withRepeatSeconds(1)
|
||||||
.withJobParameters(new ArrayList<>());
|
.withJobParameters(new ArrayList<>());
|
||||||
|
|
||||||
|
@ -62,27 +62,7 @@ class QuartzSchedulerTest extends BaseTest
|
|||||||
@AfterEach
|
@AfterEach
|
||||||
void afterEach()
|
void afterEach()
|
||||||
{
|
{
|
||||||
try
|
QuartzTestUtils.afterEach();
|
||||||
{
|
|
||||||
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 //
|
|
||||||
/////////////////////////////////////////////////////////////////
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -120,7 +100,7 @@ class QuartzSchedulerTest extends BaseTest
|
|||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
// give a moment for the job to run a few times //
|
// give a moment for the job to run a few times //
|
||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
SleepUtils.sleep(50, TimeUnit.MILLISECONDS);
|
SleepUtils.sleep(150, TimeUnit.MILLISECONDS);
|
||||||
qScheduleManager.stopAsync();
|
qScheduleManager.stopAsync();
|
||||||
|
|
||||||
System.out.println("Ran: " + BasicStep.counter + " times");
|
System.out.println("Ran: " + BasicStep.counter + " times");
|
||||||
@ -156,7 +136,6 @@ class QuartzSchedulerTest extends BaseTest
|
|||||||
void testRemovingNoLongerNeededJobsDuringSetupSchedules() throws SchedulerException
|
void testRemovingNoLongerNeededJobsDuringSetupSchedules() throws SchedulerException
|
||||||
{
|
{
|
||||||
QInstance qInstance = QContext.getQInstance();
|
QInstance qInstance = QContext.getQInstance();
|
||||||
QScheduleManager.defineDefaultSchedulableTypesInInstance(qInstance);
|
|
||||||
QuartzTestUtils.setupInstanceForQuartzTests();
|
QuartzTestUtils.setupInstanceForQuartzTests();
|
||||||
|
|
||||||
////////////////////////////
|
////////////////////////////
|
||||||
@ -167,7 +146,7 @@ class QuartzSchedulerTest extends BaseTest
|
|||||||
qInstance.addProcess(test1);
|
qInstance.addProcess(test1);
|
||||||
qInstance.addProcess(test2);
|
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 quartzScheduler = QuartzScheduler.initInstance(qInstance, QuartzTestUtils.QUARTZ_SCHEDULER_NAME, QuartzTestUtils.getQuartzProperties(), () -> QContext.getQSession());
|
||||||
quartzScheduler.start();
|
quartzScheduler.start();
|
||||||
|
@ -27,6 +27,7 @@ import java.util.Properties;
|
|||||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
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.QInstance;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.quartz.QuartzSchedulerMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.quartz.QuartzSchedulerMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager;
|
||||||
import org.quartz.SchedulerException;
|
import org.quartz.SchedulerException;
|
||||||
|
|
||||||
|
|
||||||
@ -102,4 +103,35 @@ public class QuartzTestUtils
|
|||||||
{
|
{
|
||||||
return QuartzScheduler.getInstance().queryQuartz();
|
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 //
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,7 @@ class QuartzJobsProcessTest extends BaseTest
|
|||||||
{
|
{
|
||||||
QInstance qInstance = QContext.getQInstance();
|
QInstance qInstance = QContext.getQInstance();
|
||||||
qInstance.addTable(new QTableMetaData()
|
qInstance.addTable(new QTableMetaData()
|
||||||
.withName("quartzTriggers")
|
.withName("quartzJobDetails")
|
||||||
.withBackendName(TestUtils.MEMORY_BACKEND_NAME)
|
.withBackendName(TestUtils.MEMORY_BACKEND_NAME)
|
||||||
.withPrimaryKeyField("id")
|
.withPrimaryKeyField("id")
|
||||||
.withField(new QFieldMetaData("id", QFieldType.LONG)));
|
.withField(new QFieldMetaData("id", QFieldType.LONG)));
|
||||||
@ -92,28 +92,7 @@ class QuartzJobsProcessTest extends BaseTest
|
|||||||
@AfterEach
|
@AfterEach
|
||||||
void afterEach()
|
void afterEach()
|
||||||
{
|
{
|
||||||
try
|
QuartzTestUtils.afterEach();
|
||||||
{
|
|
||||||
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 //
|
|
||||||
/////////////////////////////////////////////////////////////////
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -183,7 +162,7 @@ class QuartzJobsProcessTest extends BaseTest
|
|||||||
// pause just one //
|
// pause just one //
|
||||||
////////////////////
|
////////////////////
|
||||||
List<QuartzJobAndTriggerWrapper> quartzJobAndTriggerWrappers = QuartzTestUtils.queryQuartz();
|
List<QuartzJobAndTriggerWrapper> 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("jobName", quartzJobAndTriggerWrappers.get(0).jobDetail().getKey().getName())
|
||||||
.withValue("jobGroup", quartzJobAndTriggerWrappers.get(0).jobDetail().getKey().getGroup())
|
.withValue("jobGroup", quartzJobAndTriggerWrappers.get(0).jobDetail().getKey().getGroup())
|
||||||
));
|
));
|
||||||
@ -231,7 +210,7 @@ class QuartzJobsProcessTest extends BaseTest
|
|||||||
// pause just one //
|
// pause just one //
|
||||||
////////////////////
|
////////////////////
|
||||||
List<QuartzJobAndTriggerWrapper> quartzJobAndTriggerWrappers = QuartzTestUtils.queryQuartz();
|
List<QuartzJobAndTriggerWrapper> 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("jobName", quartzJobAndTriggerWrappers.get(0).jobDetail().getKey().getName())
|
||||||
.withValue("jobGroup", quartzJobAndTriggerWrappers.get(0).jobDetail().getKey().getGroup())
|
.withValue("jobGroup", quartzJobAndTriggerWrappers.get(0).jobDetail().getKey().getGroup())
|
||||||
));
|
));
|
||||||
|
@ -1403,7 +1403,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
{
|
{
|
||||||
String associatedTableName = association.getAssociatedTableName();
|
String associatedTableName = association.getAssociatedTableName();
|
||||||
QTableMetaData associatedTable = QContext.getQInstance().getTable(associatedTableName);
|
QTableMetaData associatedTable = QContext.getQInstance().getTable(associatedTableName);
|
||||||
ApiTableMetaData associatedApiTableMetaData = ObjectUtils.tryElse(() -> ApiTableMetaDataContainer.of(associatedTable).getApiTableMetaData(apiName), new ApiTableMetaData());
|
ApiTableMetaData associatedApiTableMetaData = ObjectUtils.tryAndRequireNonNullElse(() -> ApiTableMetaDataContainer.of(associatedTable).getApiTableMetaData(apiName), new ApiTableMetaData());
|
||||||
String associatedTableApiName = StringUtils.hasContent(associatedApiTableMetaData.getApiTableName()) ? associatedApiTableMetaData.getApiTableName() : associatedTableName;
|
String associatedTableApiName = StringUtils.hasContent(associatedApiTableMetaData.getApiTableName()) ? associatedApiTableMetaData.getApiTableName() : associatedTableName;
|
||||||
|
|
||||||
ApiAssociationMetaData apiAssociationMetaData = thisApiTableMetaData.getApiAssociationMetaData().get(association.getName());
|
ApiAssociationMetaData apiAssociationMetaData = thisApiTableMetaData.getApiAssociationMetaData().get(association.getName());
|
||||||
|
@ -710,10 +710,15 @@ public class QJavalinImplementation
|
|||||||
{
|
{
|
||||||
throw (new QUserFacingException("Error updating " + tableMetaData.getLabel() + ": " + joinErrorsWithCommasAndAnd(outputRecord.getErrors())));
|
throw (new QUserFacingException("Error updating " + tableMetaData.getLabel() + ": " + joinErrorsWithCommasAndAnd(outputRecord.getErrors())));
|
||||||
}
|
}
|
||||||
if(CollectionUtils.nullSafeHasContents(outputRecord.getWarnings()))
|
|
||||||
{
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
throw (new QUserFacingException("Warning updating " + tableMetaData.getLabel() + ": " + joinErrorsWithCommasAndAnd(outputRecord.getWarnings())));
|
// at one time, we threw upon warning - but //
|
||||||
}
|
// on insert we need to return the record (e.g., to get a generated id), so, make update do the same. //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if(CollectionUtils.nullSafeHasContents(outputRecord.getWarnings()))
|
||||||
|
// {
|
||||||
|
// throw (new QUserFacingException("Warning updating " + tableMetaData.getLabel() + ": " + joinErrorsWithCommasAndAnd(outputRecord.getWarnings())));
|
||||||
|
// }
|
||||||
|
|
||||||
QJavalinAccessLogger.logEndSuccess();
|
QJavalinAccessLogger.logEndSuccess();
|
||||||
context.result(JsonUtils.toJson(updateOutput));
|
context.result(JsonUtils.toJson(updateOutput));
|
||||||
@ -902,10 +907,16 @@ public class QJavalinImplementation
|
|||||||
{
|
{
|
||||||
throw (new QUserFacingException("Error inserting " + table.getLabel() + ": " + joinErrorsWithCommasAndAnd(outputRecord.getErrors())));
|
throw (new QUserFacingException("Error inserting " + table.getLabel() + ": " + joinErrorsWithCommasAndAnd(outputRecord.getErrors())));
|
||||||
}
|
}
|
||||||
if(CollectionUtils.nullSafeHasContents(outputRecord.getWarnings()))
|
|
||||||
{
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
throw (new QUserFacingException("Warning inserting " + table.getLabel() + ": " + joinErrorsWithCommasAndAnd(outputRecord.getWarnings())));
|
// at one time, we threw upon warning - but //
|
||||||
}
|
// our use-case is, the frontend, it wants to get the record, and show a success (with the generated id) //
|
||||||
|
// and then to also show a warning message - so - let it all be returned and handled on the frontend. //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if(CollectionUtils.nullSafeHasContents(outputRecord.getWarnings()))
|
||||||
|
// {
|
||||||
|
// throw (new QUserFacingException("Warning inserting " + table.getLabel() + ": " + joinErrorsWithCommasAndAnd(outputRecord.getWarnings())));
|
||||||
|
// }
|
||||||
|
|
||||||
QJavalinAccessLogger.logEndSuccess(logPair("primaryKey", () -> (outputRecord.getValue(table.getPrimaryKeyField()))));
|
QJavalinAccessLogger.logEndSuccess(logPair("primaryKey", () -> (outputRecord.getValue(table.getPrimaryKeyField()))));
|
||||||
context.result(JsonUtils.toJson(insertOutput));
|
context.result(JsonUtils.toJson(insertOutput));
|
||||||
|
@ -514,6 +514,42 @@ class QJavalinImplementationTest extends QJavalinTestBase
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test an insert that returns a warning
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_dataInsertWithWarning()
|
||||||
|
{
|
||||||
|
Map<String, Serializable> body = new HashMap<>();
|
||||||
|
body.put("firstName", "Warning");
|
||||||
|
body.put("lastName", "Kelkhoff");
|
||||||
|
body.put("email", "warning@kelkhoff.com");
|
||||||
|
|
||||||
|
HttpResponse<String> response = Unirest.post(BASE_URL + "/data/person")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(body)
|
||||||
|
.asString();
|
||||||
|
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
assertTrue(jsonObject.has("records"));
|
||||||
|
JSONArray records = jsonObject.getJSONArray("records");
|
||||||
|
assertEquals(1, records.length());
|
||||||
|
JSONObject record0 = records.getJSONObject(0);
|
||||||
|
assertTrue(record0.has("values"));
|
||||||
|
JSONObject values0 = record0.getJSONObject("values");
|
||||||
|
assertTrue(values0.has("id"));
|
||||||
|
assertEquals(7, values0.getInt("id"));
|
||||||
|
|
||||||
|
assertTrue(record0.has("warnings"));
|
||||||
|
JSONArray warnings = record0.getJSONArray("warnings");
|
||||||
|
assertEquals(1, warnings.length());
|
||||||
|
assertTrue(warnings.getJSONObject(0).has("message"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** test an insert - posting a multipart form.
|
** test an insert - posting a multipart form.
|
||||||
**
|
**
|
||||||
@ -594,6 +630,52 @@ class QJavalinImplementationTest extends QJavalinTestBase
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test an update - with a warning returned
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_dataUpdateWithWarning()
|
||||||
|
{
|
||||||
|
Map<String, Serializable> body = new HashMap<>();
|
||||||
|
body.put("firstName", "Warning");
|
||||||
|
body.put("birthDate", "");
|
||||||
|
|
||||||
|
HttpResponse<String> response = Unirest.patch(BASE_URL + "/data/person/4")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(body)
|
||||||
|
.asString();
|
||||||
|
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
assertTrue(jsonObject.has("records"));
|
||||||
|
JSONArray records = jsonObject.getJSONArray("records");
|
||||||
|
assertEquals(1, records.length());
|
||||||
|
JSONObject record0 = records.getJSONObject(0);
|
||||||
|
assertTrue(record0.has("values"));
|
||||||
|
assertEquals("person", record0.getString("tableName"));
|
||||||
|
JSONObject values0 = record0.getJSONObject("values");
|
||||||
|
assertEquals(4, values0.getInt("id"));
|
||||||
|
assertEquals("Warning", values0.getString("firstName"));
|
||||||
|
|
||||||
|
assertTrue(record0.has("warnings"));
|
||||||
|
JSONArray warnings = record0.getJSONArray("warnings");
|
||||||
|
assertEquals(1, warnings.length());
|
||||||
|
assertTrue(warnings.getJSONObject(0).has("message"));
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////
|
||||||
|
// re-GET the record, and validate that birthDate was nulled out //
|
||||||
|
///////////////////////////////////////////////////////////////////
|
||||||
|
response = Unirest.get(BASE_URL + "/data/person/4").asString();
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
assertTrue(jsonObject.has("values"));
|
||||||
|
JSONObject values = jsonObject.getJSONObject("values");
|
||||||
|
assertFalse(values.has("birthDate"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** test an update - posting the data as a multipart form
|
** test an update - posting the data as a multipart form
|
||||||
**
|
**
|
||||||
|
@ -25,6 +25,10 @@ package com.kingsrook.qqq.backend.javalin;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
@ -35,6 +39,7 @@ 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.QCriteriaOperator;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
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.QQueryFilter;
|
||||||
|
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.data.QRecord;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
|
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||||
@ -68,6 +73,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
|
|||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.savedviews.SavedViewsMetaDataProvider;
|
import com.kingsrook.qqq.backend.core.model.savedviews.SavedViewsMetaDataProvider;
|
||||||
import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
|
import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
|
||||||
import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep;
|
import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep;
|
||||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
|
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
|
||||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
|
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
|
||||||
@ -255,6 +261,9 @@ public class TestUtils
|
|||||||
.withScriptTypeId(1)
|
.withScriptTypeId(1)
|
||||||
.withScriptTester(new QCodeReference(TestScriptAction.class)));
|
.withScriptTester(new QCodeReference(TestScriptAction.class)));
|
||||||
|
|
||||||
|
qTableMetaData.withCustomizer(TableCustomizers.POST_INSERT_RECORD, new QCodeReference(PersonTableCustomizer.class));
|
||||||
|
qTableMetaData.withCustomizer(TableCustomizers.POST_UPDATE_RECORD, new QCodeReference(PersonTableCustomizer.class));
|
||||||
|
|
||||||
qTableMetaData.getField("photo")
|
qTableMetaData.getField("photo")
|
||||||
.withIsHeavy(true)
|
.withIsHeavy(true)
|
||||||
.withFieldAdornment(new FieldAdornment(AdornmentType.FILE_DOWNLOAD)
|
.withFieldAdornment(new FieldAdornment(AdornmentType.FILE_DOWNLOAD)
|
||||||
@ -265,6 +274,51 @@ public class TestUtils
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static class PersonTableCustomizer implements TableCustomizerInterface
|
||||||
|
{
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public List<QRecord> postInsert(InsertInput insertInput, List<QRecord> records) throws QException
|
||||||
|
{
|
||||||
|
return warnPostInsertOrUpdate(records);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public List<QRecord> postUpdate(UpdateInput updateInput, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
|
||||||
|
{
|
||||||
|
return warnPostInsertOrUpdate(records);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private List<QRecord> warnPostInsertOrUpdate(List<QRecord> records)
|
||||||
|
{
|
||||||
|
for(QRecord record : records)
|
||||||
|
{
|
||||||
|
if(Objects.requireNonNullElse(record.getValueString("firstName"), "").toLowerCase().contains("warn"))
|
||||||
|
{
|
||||||
|
record.addWarning(new QWarningMessage("Warning in firstName."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
|
Reference in New Issue
Block a user