[scheduler] cleaned up and extended scheduler support
- Added additional user friendly schedules - `hourly` - a simple to configure schedule that will fire every hour on one or more specific minutes in the hour - `daily` - a simple to configure schedule that will fire every day on one or more specific times in the day - `weekly` - a simple to configure schedule that will fire every week on one or more specific days + times in the week - `monthly` - a simple to configure schedule that will fire every month on one or more specific days + times in the month - `yearly` - a simple to configure schedule that will fire every year on one or more specific months + days + times in the year - `interval` - a simple interval based schedule that will fire every fixed configurable interval (supported units are: seconds, minutes, hours, days and weeks) - Added unit tests to all the schedules and the schedule registry - Introduced `Scheduler` as an interface and `InternalScheduler` for the quartz implementation. This will help unit testing other dependent services - `Scheduler` is now independent of `Alert`. It works with `Job` constructs (`Alert` now implements a `Job`). - Introduced `SchedulerMock` as a simple `Scheduler` implementation that can be used for unit tests - enables manual triggering of jobs. - introduced `@Slow` test annotation support in the `pom.xml` Original commit: elastic/x-pack-elasticsearch@94a8f5ddea
This commit is contained in:
parent
59f0883721
commit
89b7d085e1
|
@ -7,6 +7,7 @@ package org.elasticsearch.alerts;
|
|||
|
||||
import org.elasticsearch.alerts.actions.ActionRegistry;
|
||||
import org.elasticsearch.alerts.actions.Actions;
|
||||
import org.elasticsearch.alerts.scheduler.Scheduler;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.Schedule;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.ScheduleRegistry;
|
||||
import org.elasticsearch.alerts.throttle.AlertThrottler;
|
||||
|
@ -37,7 +38,7 @@ import java.util.Map;
|
|||
|
||||
import static org.elasticsearch.alerts.support.AlertsDateUtils.*;
|
||||
|
||||
public class Alert implements ToXContent {
|
||||
public class Alert implements Scheduler.Job, ToXContent {
|
||||
|
||||
private final String name;
|
||||
private final Schedule schedule;
|
||||
|
@ -366,8 +367,6 @@ public class Alert implements ToXContent {
|
|||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void writeTo(StreamOutput out) throws IOException {
|
||||
out.writeLong(version);
|
||||
|
|
|
@ -147,7 +147,7 @@ public class AlertsService extends AbstractComponent {
|
|||
try {
|
||||
AlertsStore.AlertPut result = alertsStore.putAlert(name, alertSource);
|
||||
if (result.previous() == null || !result.previous().schedule().equals(result.current().schedule())) {
|
||||
scheduler.schedule(result.current());
|
||||
scheduler.add(result.current());
|
||||
}
|
||||
return result.indexResponse();
|
||||
} finally {
|
||||
|
|
|
@ -7,17 +7,17 @@ package org.elasticsearch.alerts.scheduler;
|
|||
|
||||
import org.quartz.*;
|
||||
|
||||
public class FireAlertJob implements Job {
|
||||
public class FireAlertQuartzJob implements Job {
|
||||
|
||||
static final String SCHEDULER_KEY = "scheduler";
|
||||
|
||||
public FireAlertJob() {
|
||||
public FireAlertQuartzJob() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
|
||||
String alertName = jobExecutionContext.getJobDetail().getKey().getName();
|
||||
Scheduler scheduler = (Scheduler) jobExecutionContext.getJobDetail().getJobDataMap().get(SCHEDULER_KEY);
|
||||
InternalScheduler scheduler = (InternalScheduler) jobExecutionContext.getJobDetail().getJobDataMap().get(SCHEDULER_KEY);
|
||||
scheduler.notifyListeners(alertName, jobExecutionContext);
|
||||
}
|
||||
|
||||
|
@ -25,8 +25,8 @@ public class FireAlertJob implements Job {
|
|||
return new JobKey(alertName);
|
||||
}
|
||||
|
||||
static JobDetail jobDetail(String alertName, Scheduler scheduler) {
|
||||
JobDetail job = JobBuilder.newJob(FireAlertJob.class).withIdentity(alertName).build();
|
||||
static JobDetail jobDetail(String alertName, InternalScheduler scheduler) {
|
||||
JobDetail job = JobBuilder.newJob(FireAlertQuartzJob.class).withIdentity(alertName).build();
|
||||
job.getJobDataMap().put("scheduler", scheduler);
|
||||
return job;
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler;
|
||||
|
||||
import org.elasticsearch.alerts.AlertsPlugin;
|
||||
import org.elasticsearch.alerts.AlertsSettingsException;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.CronnableSchedule;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.IntervalSchedule;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.Schedule;
|
||||
import org.elasticsearch.common.component.AbstractComponent;
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.joda.time.DateTime;
|
||||
import org.elasticsearch.common.joda.time.DateTimeZone;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.util.concurrent.EsThreadPoolExecutor;
|
||||
import org.elasticsearch.threadpool.ThreadPool;
|
||||
import org.quartz.*;
|
||||
import org.quartz.impl.StdSchedulerFactory;
|
||||
import org.quartz.simpl.SimpleJobFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import static org.elasticsearch.alerts.scheduler.FireAlertQuartzJob.jobDetail;
|
||||
|
||||
public class InternalScheduler extends AbstractComponent implements Scheduler {
|
||||
|
||||
// Not happy about it, but otherwise we're stuck with Quartz's SimpleThreadPool
|
||||
private volatile static ThreadPool threadPool;
|
||||
|
||||
private volatile org.quartz.Scheduler scheduler;
|
||||
|
||||
private List<Listener> listeners;
|
||||
|
||||
private final DateTimeZone defaultTimeZone;
|
||||
|
||||
@Inject
|
||||
public InternalScheduler(Settings settings, ThreadPool threadPool) {
|
||||
super(settings);
|
||||
this.listeners = new CopyOnWriteArrayList<>();
|
||||
InternalScheduler.threadPool = threadPool;
|
||||
String timeZoneStr = componentSettings.get("time_zone", "UTC");
|
||||
try {
|
||||
this.defaultTimeZone = DateTimeZone.forID(timeZoneStr);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
throw new AlertsSettingsException("unrecognized time zone setting [" + timeZoneStr + "]", iae);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void start(Collection<? extends Job> jobs) {
|
||||
try {
|
||||
logger.info("Starting scheduler");
|
||||
// Can't start a scheduler that has been shutdown, so we need to re-create each time start() is invoked
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("org.quartz.threadPool.class", AlertQuartzThreadPool.class.getName());
|
||||
properties.setProperty(StdSchedulerFactory.PROP_SCHED_SKIP_UPDATE_CHECK, "true");
|
||||
properties.setProperty(StdSchedulerFactory.PROP_SCHED_INTERRUPT_JOBS_ON_SHUTDOWN, "true");
|
||||
properties.setProperty(StdSchedulerFactory.PROP_SCHED_INTERRUPT_JOBS_ON_SHUTDOWN_WITH_WAIT, "true");
|
||||
SchedulerFactory schFactory = new StdSchedulerFactory(properties);
|
||||
scheduler = schFactory.getScheduler();
|
||||
scheduler.setJobFactory(new SimpleJobFactory());
|
||||
Map<JobDetail, Set<? extends Trigger>> quartzJobs = new HashMap<>();
|
||||
for (Job alert : jobs) {
|
||||
quartzJobs.put(jobDetail(alert.name(), this), createTrigger(alert.schedule(), defaultTimeZone));
|
||||
}
|
||||
scheduler.scheduleJobs(quartzJobs, false);
|
||||
scheduler.start();
|
||||
} catch (org.quartz.SchedulerException se) {
|
||||
logger.error("Failed to start quartz scheduler", se);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void stop() {
|
||||
try {
|
||||
org.quartz.Scheduler scheduler = this.scheduler;
|
||||
if (scheduler != null) {
|
||||
logger.info("Stopping scheduler...");
|
||||
scheduler.shutdown(true);
|
||||
this.scheduler = null;
|
||||
logger.info("Stopped scheduler");
|
||||
}
|
||||
} catch (org.quartz.SchedulerException se){
|
||||
logger.error("Failed to stop quartz scheduler", se);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(Listener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
void notifyListeners(String alertName, JobExecutionContext ctx) {
|
||||
DateTime scheduledTime = new DateTime(ctx.getScheduledFireTime());
|
||||
DateTime fireTime = new DateTime(ctx.getFireTime());
|
||||
for (Listener listener : listeners) {
|
||||
listener.fire(alertName, scheduledTime, fireTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules the given alert
|
||||
*/
|
||||
public void add(Job job) {
|
||||
try {
|
||||
logger.trace("scheduling [{}] with schedule [{}]", job.name(), job.schedule());
|
||||
scheduler.scheduleJob(jobDetail(job.name(), this), createTrigger(job.schedule(), defaultTimeZone), true);
|
||||
} catch (org.quartz.SchedulerException se) {
|
||||
logger.error("Failed to schedule job",se);
|
||||
throw new SchedulerException("Failed to schedule job", se);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean remove(String jobName) {
|
||||
try {
|
||||
return scheduler.deleteJob(new JobKey(jobName));
|
||||
} catch (org.quartz.SchedulerException se){
|
||||
throw new SchedulerException("Failed to remove [" + jobName + "] from the scheduler", se);
|
||||
}
|
||||
}
|
||||
|
||||
static Set<Trigger> createTrigger(Schedule schedule, DateTimeZone timeZone) {
|
||||
HashSet<Trigger> triggers = new HashSet<>();
|
||||
if (schedule instanceof CronnableSchedule) {
|
||||
for (String cron : ((CronnableSchedule) schedule).crons()) {
|
||||
triggers.add(TriggerBuilder.newTrigger()
|
||||
.withSchedule(CronScheduleBuilder.cronSchedule(cron).inTimeZone(timeZone.toTimeZone()))
|
||||
.startNow()
|
||||
.build());
|
||||
}
|
||||
} else {
|
||||
// must be interval schedule
|
||||
IntervalSchedule.Interval interval = ((IntervalSchedule) schedule).interval();
|
||||
triggers.add(TriggerBuilder.newTrigger().withSchedule(SimpleScheduleBuilder.simpleSchedule()
|
||||
.withIntervalInSeconds((int) interval.seconds())
|
||||
.repeatForever())
|
||||
.startNow()
|
||||
.build());
|
||||
}
|
||||
return triggers;
|
||||
}
|
||||
|
||||
|
||||
// This Quartz thread pool will always accept. On this thread we will only index an alert action and add it to the work queue
|
||||
public static final class AlertQuartzThreadPool implements org.quartz.spi.ThreadPool {
|
||||
|
||||
private final EsThreadPoolExecutor executor;
|
||||
|
||||
public AlertQuartzThreadPool() {
|
||||
this.executor = (EsThreadPoolExecutor) threadPool.executor(AlertsPlugin.SCHEDULER_THREAD_POOL_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean runInThread(Runnable runnable) {
|
||||
executor.execute(runnable);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int blockForAvailableThreads() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() throws SchedulerConfigException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown(boolean waitForJobsToComplete) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPoolSize() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInstanceId(String schedInstId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInstanceName(String schedName) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -5,173 +5,49 @@
|
|||
*/
|
||||
package org.elasticsearch.alerts.scheduler;
|
||||
|
||||
import org.elasticsearch.alerts.Alert;
|
||||
import org.elasticsearch.alerts.AlertsPlugin;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.Schedule;
|
||||
import org.elasticsearch.common.component.AbstractComponent;
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.joda.time.DateTime;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.util.concurrent.EsThreadPoolExecutor;
|
||||
import org.elasticsearch.threadpool.ThreadPool;
|
||||
import org.quartz.*;
|
||||
import org.quartz.impl.StdSchedulerFactory;
|
||||
import org.quartz.simpl.SimpleJobFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
import static org.elasticsearch.alerts.scheduler.FireAlertJob.jobDetail;
|
||||
|
||||
public class Scheduler extends AbstractComponent {
|
||||
|
||||
// Not happy about it, but otherwise we're stuck with Quartz's SimpleThreadPool
|
||||
private volatile static ThreadPool threadPool;
|
||||
|
||||
private volatile org.quartz.Scheduler scheduler;
|
||||
|
||||
private List<Listener> listeners;
|
||||
|
||||
@Inject
|
||||
public Scheduler(Settings settings, ThreadPool threadPool) {
|
||||
super(settings);
|
||||
this.listeners = new CopyOnWriteArrayList<>();
|
||||
Scheduler.threadPool = threadPool;
|
||||
}
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public interface Scheduler {
|
||||
|
||||
/**
|
||||
* Starts the scheduler and schedules the specified alerts before returning.
|
||||
*
|
||||
* Both the start and stop are synchronized to avoid that scheduler gets stopped while previously stored alerts
|
||||
* are being loaded.
|
||||
* Starts the scheduler and schedules the specified jobs before returning.
|
||||
*/
|
||||
public synchronized void start(Collection<Alert> alerts) {
|
||||
try {
|
||||
logger.info("Starting scheduler");
|
||||
// Can't start a scheduler that has been shutdown, so we need to re-create each time start() is invoked
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("org.quartz.threadPool.class", AlertQuartzThreadPool.class.getName());
|
||||
properties.setProperty(StdSchedulerFactory.PROP_SCHED_SKIP_UPDATE_CHECK, "true");
|
||||
properties.setProperty(StdSchedulerFactory.PROP_SCHED_INTERRUPT_JOBS_ON_SHUTDOWN, "true");
|
||||
properties.setProperty(StdSchedulerFactory.PROP_SCHED_INTERRUPT_JOBS_ON_SHUTDOWN_WITH_WAIT, "true");
|
||||
SchedulerFactory schFactory = new StdSchedulerFactory(properties);
|
||||
scheduler = schFactory.getScheduler();
|
||||
scheduler.setJobFactory(new SimpleJobFactory());
|
||||
Map<JobDetail, Set<? extends Trigger>> jobs = new HashMap<>();
|
||||
for (Alert alert : alerts) {
|
||||
jobs.put(jobDetail(alert.name(), this), createTrigger(alert.schedule()));
|
||||
}
|
||||
scheduler.scheduleJobs(jobs, false);
|
||||
scheduler.start();
|
||||
} catch (org.quartz.SchedulerException se) {
|
||||
logger.error("Failed to start quartz scheduler", se);
|
||||
}
|
||||
}
|
||||
void start(Collection<? extends Job> jobs);
|
||||
|
||||
/**
|
||||
* Stops the scheduler.
|
||||
*/
|
||||
public synchronized void stop() {
|
||||
try {
|
||||
org.quartz.Scheduler scheduler = this.scheduler;
|
||||
if (scheduler != null) {
|
||||
logger.info("Stopping scheduler...");
|
||||
scheduler.shutdown(true);
|
||||
this.scheduler = null;
|
||||
logger.info("Stopped scheduler");
|
||||
}
|
||||
} catch (org.quartz.SchedulerException se){
|
||||
logger.error("Failed to stop quartz scheduler", se);
|
||||
}
|
||||
}
|
||||
|
||||
public void addListener(Listener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
void notifyListeners(String alertName, JobExecutionContext ctx) {
|
||||
DateTime scheduledTime = new DateTime(ctx.getScheduledFireTime());
|
||||
DateTime fireTime = new DateTime(ctx.getFireTime());
|
||||
for (Listener listener : listeners) {
|
||||
listener.fire(alertName, scheduledTime, fireTime);
|
||||
}
|
||||
}
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* Schedules the given alert
|
||||
* Adds and schedules the give job
|
||||
*/
|
||||
public void schedule(Alert alert) {
|
||||
try {
|
||||
logger.trace("scheduling [{}] with schedule [{}]", alert.name(), alert.schedule());
|
||||
scheduler.scheduleJob(jobDetail(alert.name(), this), createTrigger(alert.schedule()), true);
|
||||
} catch (org.quartz.SchedulerException se) {
|
||||
logger.error("Failed to schedule job",se);
|
||||
throw new SchedulerException("Failed to schedule job", se);
|
||||
}
|
||||
}
|
||||
void add(Job job);
|
||||
|
||||
public boolean remove(String alertName) {
|
||||
try {
|
||||
return scheduler.deleteJob(new JobKey(alertName));
|
||||
} catch (org.quartz.SchedulerException se){
|
||||
throw new SchedulerException("Failed to remove [" + alertName + "] from the scheduler", se);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Removes the scheduled job that is associated with the given name
|
||||
*/
|
||||
boolean remove(String jobName);
|
||||
|
||||
static Set<CronTrigger> createTrigger(Schedule schedule) {
|
||||
return new HashSet<>(Arrays.asList(
|
||||
TriggerBuilder.newTrigger().withSchedule(CronScheduleBuilder.cronSchedule(schedule.cron())).build()
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
// This Quartz thread pool will always accept. On this thread we will only index an alert action and add it to the work queue
|
||||
public static final class AlertQuartzThreadPool implements org.quartz.spi.ThreadPool {
|
||||
|
||||
private final EsThreadPoolExecutor executor;
|
||||
|
||||
public AlertQuartzThreadPool() {
|
||||
this.executor = (EsThreadPoolExecutor) threadPool.executor(AlertsPlugin.SCHEDULER_THREAD_POOL_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean runInThread(Runnable runnable) {
|
||||
executor.execute(runnable);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int blockForAvailableThreads() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() throws SchedulerConfigException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown(boolean waitForJobsToComplete) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPoolSize() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInstanceId(String schedInstId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInstanceName(String schedName) {
|
||||
}
|
||||
}
|
||||
void addListener(Listener listener);
|
||||
|
||||
public static interface Listener {
|
||||
|
||||
void fire(String alertName, DateTime scheduledFireTime, DateTime fireTime);
|
||||
void fire(String jobName, DateTime scheduledFireTime, DateTime fireTime);
|
||||
|
||||
}
|
||||
|
||||
public static interface Job {
|
||||
|
||||
String name();
|
||||
|
||||
Schedule schedule();
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,9 +5,7 @@
|
|||
*/
|
||||
package org.elasticsearch.alerts.scheduler;
|
||||
|
||||
import org.elasticsearch.alerts.scheduler.schedule.CronSchedule;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.Schedule;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.ScheduleRegistry;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.*;
|
||||
import org.elasticsearch.common.inject.AbstractModule;
|
||||
import org.elasticsearch.common.inject.multibindings.MapBinder;
|
||||
|
||||
|
@ -29,8 +27,20 @@ public class SchedulerModule extends AbstractModule {
|
|||
protected void configure() {
|
||||
|
||||
MapBinder<String, Schedule.Parser> mbinder = MapBinder.newMapBinder(binder(), String.class, Schedule.Parser.class);
|
||||
bind(IntervalSchedule.Parser.class).asEagerSingleton();
|
||||
mbinder.addBinding(IntervalSchedule.TYPE).to(IntervalSchedule.Parser.class);
|
||||
bind(CronSchedule.Parser.class).asEagerSingleton();
|
||||
mbinder.addBinding(CronSchedule.TYPE).to(CronSchedule.Parser.class);
|
||||
bind(HourlySchedule.Parser.class).asEagerSingleton();
|
||||
mbinder.addBinding(HourlySchedule.TYPE).to(HourlySchedule.Parser.class);
|
||||
bind(DailySchedule.Parser.class).asEagerSingleton();
|
||||
mbinder.addBinding(DailySchedule.TYPE).to(DailySchedule.Parser.class);
|
||||
bind(WeeklySchedule.Parser.class).asEagerSingleton();
|
||||
mbinder.addBinding(WeeklySchedule.TYPE).to(WeeklySchedule.Parser.class);
|
||||
bind(MonthlySchedule.Parser.class).asEagerSingleton();
|
||||
mbinder.addBinding(MonthlySchedule.TYPE).to(MonthlySchedule.Parser.class);
|
||||
bind(YearlySchedule.Parser.class).asEagerSingleton();
|
||||
mbinder.addBinding(YearlySchedule.TYPE).to(YearlySchedule.Parser.class);
|
||||
|
||||
for (Map.Entry<String, Class<? extends Schedule.Parser>> entry : parsers.entrySet()) {
|
||||
bind(entry.getValue()).asEagerSingleton();
|
||||
|
@ -38,6 +48,6 @@ public class SchedulerModule extends AbstractModule {
|
|||
}
|
||||
|
||||
bind(ScheduleRegistry.class).asEagerSingleton();
|
||||
bind(Scheduler.class).asEagerSingleton();
|
||||
bind(Scheduler.class).to(InternalScheduler.class).asEagerSingleton();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,22 +5,26 @@
|
|||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule;
|
||||
|
||||
import org.elasticsearch.alerts.AlertsSettingsException;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.quartz.CronExpression;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class CronSchedule implements Schedule {
|
||||
public class CronSchedule extends CronnableSchedule {
|
||||
|
||||
public static final String TYPE = "cron";
|
||||
|
||||
private final String cron;
|
||||
|
||||
public CronSchedule(String cron) {
|
||||
this.cron = cron;
|
||||
public CronSchedule(String... crons) {
|
||||
super(crons);
|
||||
validate(crons);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -29,13 +33,18 @@ public class CronSchedule implements Schedule {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String cron() {
|
||||
return cron;
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||
return crons.length == 1 ? builder.value(crons[0]) : builder.value(crons);
|
||||
}
|
||||
|
||||
@Override
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||
return builder.value(cron);
|
||||
static void validate(String... crons) {
|
||||
for (String cron :crons) {
|
||||
try {
|
||||
CronExpression.validateExpression(cron);
|
||||
} catch (ParseException pe) {
|
||||
throw new ValidationException(cron, pe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Parser implements Schedule.Parser<CronSchedule> {
|
||||
|
@ -47,26 +56,43 @@ public class CronSchedule implements Schedule {
|
|||
|
||||
@Override
|
||||
public CronSchedule parse(XContentParser parser) throws IOException {
|
||||
assert parser.currentToken() == XContentParser.Token.VALUE_STRING : "expecting a string value with cron expression";
|
||||
String cron = parser.text();
|
||||
return new CronSchedule(cron);
|
||||
try {
|
||||
|
||||
XContentParser.Token token = parser.currentToken();
|
||||
if (token == XContentParser.Token.VALUE_STRING) {
|
||||
return new CronSchedule(parser.text());
|
||||
} else if (token == XContentParser.Token.START_ARRAY) {
|
||||
List<String> crons = new ArrayList<>();
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
|
||||
switch (token) {
|
||||
case VALUE_STRING:
|
||||
crons.add(parser.text());
|
||||
break;
|
||||
default:
|
||||
throw new AlertsSettingsException("could not parse [cron] schedule. expected a string value in the cron array but found [" + token + "]");
|
||||
}
|
||||
}
|
||||
if (crons.isEmpty()) {
|
||||
throw new AlertsSettingsException("could not parse [cron] schedule. no cron expression found in cron array");
|
||||
}
|
||||
return new CronSchedule(crons.toArray(new String[crons.size()]));
|
||||
} else {
|
||||
throw new AlertsSettingsException("could not parse [cron] schedule. expected either a cron string value or an array of cron string values, but found [" + token + "]");
|
||||
}
|
||||
|
||||
} catch (ValidationException ve) {
|
||||
throw new AlertsSettingsException("could not parse [cron] schedule. invalid cron expression [" + ve.expression + "]", ve);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
public static class ValidationException extends AlertsSettingsException {
|
||||
|
||||
CronSchedule that = (CronSchedule) o;
|
||||
private String expression;
|
||||
|
||||
if (cron != null ? !cron.equals(that.cron) : that.cron != null) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return cron != null ? cron.hashCode() : 0;
|
||||
public ValidationException(String expression, ParseException cause) {
|
||||
super("invalid cron expression [" + expression + "]. " + cause.getMessage());
|
||||
this.expression = expression;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public abstract class CronnableSchedule implements Schedule {
|
||||
|
||||
protected final String[] crons;
|
||||
|
||||
public CronnableSchedule(String... crons) {
|
||||
this.crons = crons;
|
||||
Arrays.sort(crons);
|
||||
}
|
||||
|
||||
public String[] crons() {
|
||||
return crons;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash((Object[]) crons);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
final CronnableSchedule other = (CronnableSchedule) obj;
|
||||
return Objects.deepEquals(this.crons, other.crons);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule;
|
||||
|
||||
import org.elasticsearch.alerts.AlertsSettingsException;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.support.DayTimes;
|
||||
import org.elasticsearch.common.ParseField;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class DailySchedule extends CronnableSchedule {
|
||||
|
||||
public static final String TYPE = "daily";
|
||||
|
||||
public static final DayTimes[] DEFAULT_TIMES = new DayTimes[] { DayTimes.MIDNIGHT };
|
||||
|
||||
private final DayTimes[] times;
|
||||
|
||||
DailySchedule() {
|
||||
this(DEFAULT_TIMES);
|
||||
}
|
||||
|
||||
DailySchedule(DayTimes... times) {
|
||||
super(crons(times));
|
||||
this.times = times;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
public DayTimes[] times() {
|
||||
return times;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||
builder.startObject();
|
||||
if (params.paramAsBoolean("normalize", false) && times.length == 1) {
|
||||
builder.field(Parser.AT_FIELD.getPreferredName(), times[0]);
|
||||
} else {
|
||||
builder.field(Parser.AT_FIELD.getPreferredName(), (Object[]) times);
|
||||
}
|
||||
return builder.endObject();
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
static String[] crons(DayTimes[] times) {
|
||||
assert times.length > 0 : "at least one time must be defined";
|
||||
List<String> crons = new ArrayList<>(times.length);
|
||||
for (DayTimes time : times) {
|
||||
crons.add(time.cron());
|
||||
}
|
||||
return crons.toArray(new String[crons.size()]);
|
||||
}
|
||||
|
||||
public static class Parser implements Schedule.Parser<DailySchedule> {
|
||||
|
||||
static final ParseField AT_FIELD = new ParseField("at");
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DailySchedule parse(XContentParser parser) throws IOException {
|
||||
List<DayTimes> times = new ArrayList<>();
|
||||
String currentFieldName = null;
|
||||
XContentParser.Token token;
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
|
||||
if (token == XContentParser.Token.FIELD_NAME) {
|
||||
currentFieldName = parser.currentName();
|
||||
} else if (AT_FIELD.match(currentFieldName)) {
|
||||
if (token != XContentParser.Token.START_ARRAY) {
|
||||
try {
|
||||
times.add(DayTimes.parse(parser, token));
|
||||
} catch (DayTimes.ParseException pe) {
|
||||
throw new AlertsSettingsException("could not parse [daily] schedule. invalid time value for field [at] - [" + token + "]", pe);
|
||||
}
|
||||
} else {
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
|
||||
try {
|
||||
times.add(DayTimes.parse(parser, token));
|
||||
} catch (DayTimes.ParseException pe) {
|
||||
throw new AlertsSettingsException("could not parse [daily] schedule. invalid time value for field [at] - [" + token + "]", pe);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new AlertsSettingsException("could not parse [daily] schedule. unexpected field [" + currentFieldName + "]");
|
||||
}
|
||||
}
|
||||
|
||||
return times.isEmpty() ? new DailySchedule() : new DailySchedule(times.toArray(new DayTimes[times.size()]));
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private Set<DayTimes> times = new HashSet<>();
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
public Builder at(int hour, int minute) {
|
||||
times.add(new DayTimes(hour, minute));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder atRoundHour(int... hours) {
|
||||
times.add(new DayTimes(hours, new int[] { 0 }));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder atNoon() {
|
||||
times.add(DayTimes.NOON);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder atMidnight() {
|
||||
times.add(DayTimes.MIDNIGHT);
|
||||
return this;
|
||||
}
|
||||
|
||||
public DailySchedule build() {
|
||||
return times.isEmpty() ? new DailySchedule() : new DailySchedule(times.toArray(new DayTimes[times.size()]));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule;
|
||||
|
||||
import org.elasticsearch.alerts.AlertsSettingsException;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.support.DayTimes;
|
||||
import org.elasticsearch.common.ParseField;
|
||||
import org.elasticsearch.common.primitives.Ints;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class HourlySchedule extends CronnableSchedule {
|
||||
|
||||
public static final String TYPE = "hourly";
|
||||
|
||||
public static final int[] DEFAULT_MINUTES = new int[] { 0 };
|
||||
|
||||
private final int[] minutes;
|
||||
|
||||
HourlySchedule() {
|
||||
this(DEFAULT_MINUTES);
|
||||
}
|
||||
|
||||
HourlySchedule(int... minutes) {
|
||||
super(cron(minutes));
|
||||
this.minutes = minutes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
public int[] minutes() {
|
||||
return minutes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||
builder.startObject();
|
||||
if (params.paramAsBoolean("normalize", false) && minutes.length == 1) {
|
||||
builder.field(Parser.MINUTE_FIELD.getPreferredName(), minutes[0]);
|
||||
} else {
|
||||
builder.field(Parser.MINUTE_FIELD.getPreferredName(), minutes);
|
||||
}
|
||||
return builder.endObject();
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
static String cron(int[] minutes) {
|
||||
assert minutes.length > 0 : "at least one minute must be defined";
|
||||
StringBuilder sb = new StringBuilder("0 ");
|
||||
for (int i = 0; i < minutes.length; i++) {
|
||||
if (i != 0) {
|
||||
sb.append(",");
|
||||
}
|
||||
if (!validMinute(minutes[i])) {
|
||||
throw new AlertsSettingsException("invalid hourly minute [" + minutes[i] + "]. minute must be between 0 and 59 incl.");
|
||||
}
|
||||
sb.append(minutes[i]);
|
||||
}
|
||||
return sb.append(" * * * ?").toString();
|
||||
}
|
||||
|
||||
static boolean validMinute(int minute) {
|
||||
return minute >= 0 && minute < 60;
|
||||
}
|
||||
|
||||
public static class Parser implements Schedule.Parser<HourlySchedule> {
|
||||
|
||||
static final ParseField MINUTE_FIELD = new ParseField("minute");
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HourlySchedule parse(XContentParser parser) throws IOException {
|
||||
List<Integer> minutes = new ArrayList<>();
|
||||
|
||||
String currentFieldName = null;
|
||||
XContentParser.Token token;
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
|
||||
if (token == XContentParser.Token.FIELD_NAME) {
|
||||
currentFieldName = parser.currentName();
|
||||
} else if (MINUTE_FIELD.match(currentFieldName)) {
|
||||
if (token.isValue()) {
|
||||
try {
|
||||
minutes.add(DayTimes.parseMinuteValue(parser, token));
|
||||
} catch (DayTimes.ParseException pe) {
|
||||
throw new AlertsSettingsException("could not parse [hourly] schedule. invalid value for [minute]", pe);
|
||||
}
|
||||
} else if (token == XContentParser.Token.START_ARRAY) {
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
|
||||
try {
|
||||
minutes.add(DayTimes.parseMinuteValue(parser, token));
|
||||
} catch (DayTimes.ParseException pe) {
|
||||
throw new AlertsSettingsException("could not parse [hourly] schedule. invalid value for [minute]", pe);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new AlertsSettingsException("could not parse [hourly] schedule. invalid minute value. expected either string/value or an array of string/number values, but found [" + token + "]");
|
||||
}
|
||||
} else {
|
||||
throw new AlertsSettingsException("could not parse [hourly] schedule. unexpected field [" + currentFieldName + "]");
|
||||
}
|
||||
}
|
||||
|
||||
return minutes.isEmpty() ? new HourlySchedule() : new HourlySchedule(Ints.toArray(minutes));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private Set<Integer> minutes = new HashSet<>();
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
public Builder minutes(int... minutes) {
|
||||
for (int minute : minutes) {
|
||||
this.minutes.add(minute);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public HourlySchedule build() {
|
||||
return minutes.isEmpty() ? new HourlySchedule() : new HourlySchedule(Ints.toArray(minutes));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule;
|
||||
|
||||
import org.elasticsearch.alerts.AlertsSettingsException;
|
||||
import org.elasticsearch.common.xcontent.ToXContent;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class IntervalSchedule implements Schedule {
|
||||
|
||||
public static final String TYPE = "interval";
|
||||
|
||||
private final Interval interval;
|
||||
|
||||
public IntervalSchedule(Interval interval) {
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
public Interval interval() {
|
||||
return interval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||
return interval.toXContent(builder, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return interval.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
IntervalSchedule schedule = (IntervalSchedule) o;
|
||||
|
||||
if (!interval.equals(schedule.interval)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return interval.hashCode();
|
||||
}
|
||||
|
||||
public static class Parser implements Schedule.Parser<IntervalSchedule> {
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntervalSchedule parse(XContentParser parser) throws IOException {
|
||||
XContentParser.Token token = parser.currentToken();
|
||||
if (token == XContentParser.Token.VALUE_NUMBER) {
|
||||
return new IntervalSchedule(Interval.seconds(parser.longValue()));
|
||||
}
|
||||
if (token == XContentParser.Token.VALUE_STRING) {
|
||||
String value = parser.text();
|
||||
return new IntervalSchedule(Interval.parse(value));
|
||||
}
|
||||
throw new AlertsSettingsException("could not parse [interval] schedule. expected either a numeric value " +
|
||||
"(millis) or a string value representing time value (e.g. '5s'), but found [" + token + "]");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a time interval. Ideally we would have used TimeValue here, but we don't because:
|
||||
* 1. We should limit the time values that the user can configure (we don't want to support nanos & millis
|
||||
* 2. TimeValue formatting & parsing is inconsistent (it doesn't format to a value that it can parse)
|
||||
* 3. The equals of TimeValue is odd - it will only equate two time values that have the exact same unit & duration,
|
||||
* this interval on the other hand, equates based on the millis value.
|
||||
* 4. We have the advantage of making this interval construct a ToXContent
|
||||
*/
|
||||
public static class Interval implements ToXContent {
|
||||
|
||||
public static enum Unit {
|
||||
SECONDS(TimeUnit.SECONDS.toMillis(1), "s"),
|
||||
MINUTES(TimeUnit.MINUTES.toMillis(1), "m"),
|
||||
HOURS(TimeUnit.HOURS.toMillis(1), "h"),
|
||||
DAYS(TimeUnit.DAYS.toMillis(1), "d"),
|
||||
WEEK(TimeUnit.DAYS.toMillis(7), "w");
|
||||
|
||||
private final String suffix;
|
||||
private final long millis;
|
||||
|
||||
private Unit(long millis, String suffix) {
|
||||
this.millis = millis;
|
||||
this.suffix = suffix;
|
||||
}
|
||||
|
||||
public long millis(long duration) {
|
||||
return duration * millis;
|
||||
}
|
||||
|
||||
public long parse(String value) {
|
||||
assert value.endsWith(suffix);
|
||||
String num = value.substring(0, value.indexOf(suffix));
|
||||
try {
|
||||
return Long.parseLong(num);
|
||||
} catch (NumberFormatException nfe) {
|
||||
throw new AlertsSettingsException("could not parse [interval] schedule. could not parse ["
|
||||
+ num + "] as a " + name().toLowerCase(Locale.ROOT) + " duration");
|
||||
}
|
||||
}
|
||||
|
||||
public String format(long duration) {
|
||||
return duration + suffix;
|
||||
}
|
||||
}
|
||||
|
||||
private final long duration;
|
||||
private final Unit unit;
|
||||
|
||||
public Interval(long duration, Unit unit) {
|
||||
this.duration = duration;
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
public long seconds() {
|
||||
return unit.millis(duration) / Unit.SECONDS.millis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||
return builder.value(unit.format(duration));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return unit.format(duration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Interval interval = (Interval) o;
|
||||
|
||||
if (unit.millis(duration) != interval.unit.millis(duration)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
long millis = unit.millis(duration);
|
||||
int result = (int) (millis ^ (millis >>> 32));
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Interval seconds(long duration) {
|
||||
return new Interval(duration, Unit.SECONDS);
|
||||
}
|
||||
|
||||
public static Interval parse(String value) {
|
||||
for (Unit unit : Unit.values()) {
|
||||
if (value.endsWith(unit.suffix)) {
|
||||
return new Interval(unit.parse(value), unit);
|
||||
}
|
||||
}
|
||||
throw new AlertsSettingsException("could not parse [interval] schedule. unrecognized interval format [" + value + "]");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule;
|
||||
|
||||
import org.elasticsearch.alerts.AlertsSettingsException;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.support.MonthTimes;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class MonthlySchedule extends CronnableSchedule {
|
||||
|
||||
public static final String TYPE = "monthly";
|
||||
|
||||
public static final MonthTimes[] DEFAULT_TIMES = new MonthTimes[] { new MonthTimes() };
|
||||
|
||||
private final MonthTimes[] times;
|
||||
|
||||
MonthlySchedule() {
|
||||
this(DEFAULT_TIMES);
|
||||
}
|
||||
|
||||
MonthlySchedule(MonthTimes... times) {
|
||||
super(crons(times));
|
||||
this.times = times;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
public MonthTimes[] times() {
|
||||
return times;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||
if (params.paramAsBoolean("normalize", false) && times.length == 1) {
|
||||
return builder.value(times[0]);
|
||||
}
|
||||
return builder.value(times);
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
static String[] crons(MonthTimes[] times) {
|
||||
assert times.length > 0 : "at least one time must be defined";
|
||||
Set<String> crons = new HashSet<>(times.length);
|
||||
for (MonthTimes time : times) {
|
||||
crons.addAll(time.crons());
|
||||
}
|
||||
return crons.toArray(new String[crons.size()]);
|
||||
}
|
||||
|
||||
public static class Parser implements Schedule.Parser<MonthlySchedule> {
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MonthlySchedule parse(XContentParser parser) throws IOException {
|
||||
if (parser.currentToken() == XContentParser.Token.START_OBJECT) {
|
||||
try {
|
||||
return new MonthlySchedule(MonthTimes.parse(parser, parser.currentToken()));
|
||||
} catch (MonthTimes.ParseException pe) {
|
||||
throw new AlertsSettingsException("could not parse [monthly] schedule. invalid month times", pe);
|
||||
}
|
||||
}
|
||||
if (parser.currentToken() == XContentParser.Token.START_ARRAY) {
|
||||
List<MonthTimes> times = new ArrayList<>();
|
||||
XContentParser.Token token;
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
|
||||
try {
|
||||
times.add(MonthTimes.parse(parser, token));
|
||||
} catch (MonthTimes.ParseException pe) {
|
||||
throw new AlertsSettingsException("could not parse [monthly] schedule. invalid month times", pe);
|
||||
}
|
||||
}
|
||||
return times.isEmpty() ? new MonthlySchedule() : new MonthlySchedule(times.toArray(new MonthTimes[times.size()]));
|
||||
}
|
||||
throw new AlertsSettingsException("could not parse [monthly] schedule. expected either an object or an array " +
|
||||
"of objects representing month times, but found [" + parser.currentToken() + "] instead");
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private final Set<MonthTimes> times = new HashSet<>();
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
public Builder time(MonthTimes time) {
|
||||
times.add(time);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder time(MonthTimes.Builder builder) {
|
||||
return time(builder.build());
|
||||
}
|
||||
|
||||
public MonthlySchedule build() {
|
||||
return times.isEmpty() ? new MonthlySchedule() : new MonthlySchedule(times.toArray(new MonthTimes[times.size()]));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -17,8 +17,6 @@ public interface Schedule extends ToXContent {
|
|||
|
||||
String type();
|
||||
|
||||
String cron();
|
||||
|
||||
static interface Parser<S extends Schedule> {
|
||||
|
||||
String type();
|
||||
|
|
|
@ -32,21 +32,22 @@ public class ScheduleRegistry {
|
|||
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
|
||||
if (token == XContentParser.Token.FIELD_NAME) {
|
||||
type = parser.currentName();
|
||||
} else if ((token.isValue() || token == XContentParser.Token.START_OBJECT) && type != null) {
|
||||
Schedule.Parser scheduleParser = parsers.get(type);
|
||||
if (scheduleParser == null) {
|
||||
throw new AlertsSettingsException("unknown schedule type [" + type + "]");
|
||||
}
|
||||
schedule = scheduleParser.parse(parser);
|
||||
} else if (type != null) {
|
||||
schedule = parse(type, parser);
|
||||
} else {
|
||||
throw new AlertsSettingsException("could not parse schedule. expected a schedule type field, but found [" + token + "]");
|
||||
}
|
||||
}
|
||||
if (schedule == null) {
|
||||
throw new AlertsSettingsException("could not parse schedule. expected a schedule type field, but no fields were found");
|
||||
}
|
||||
return schedule;
|
||||
}
|
||||
|
||||
public Schedule parse(String type, XContentParser parser) throws IOException {
|
||||
Schedule.Parser scheduleParser = parsers.get(type);
|
||||
if (scheduleParser == null) {
|
||||
throw new AlertsSettingsException("unknown schedule type [" + type + "]");
|
||||
throw new AlertsSettingsException("could not parse schedule. unknown schedule type [" + type + "]");
|
||||
}
|
||||
return scheduleParser.parse(parser);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule;
|
||||
|
||||
/**
|
||||
* A static factory for all available schedules.
|
||||
*/
|
||||
public class Schedules {
|
||||
|
||||
private Schedules() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an interval schedule. The provided string can have the following format:
|
||||
* <ul>
|
||||
* <li>34s</li> - a 34 seconds long interval
|
||||
* <li>23m</li> - a 23 minutes long interval
|
||||
* <li>40h</li> - a 40 hours long interval
|
||||
* <li>63d</li> - a 63 days long interval
|
||||
* <li>27w</li> - a 27 weeks long interval
|
||||
* </ul>
|
||||
*
|
||||
* @param interval The fixed interval by which the schedule will trigger.
|
||||
* @return The newly created interval schedule
|
||||
*/
|
||||
public static IntervalSchedule interval(String interval) {
|
||||
return new IntervalSchedule(IntervalSchedule.Interval.parse(interval));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an interval schedule.
|
||||
*
|
||||
* @param duration The duration of the interval
|
||||
* @param unit The unit of the duration (seconds, minutes, hours, days or weeks)
|
||||
* @return The newly created interval schedule.
|
||||
*/
|
||||
public static IntervalSchedule interval(long duration, IntervalSchedule.Interval.Unit unit) {
|
||||
return new IntervalSchedule(new IntervalSchedule.Interval(duration, unit));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a cron schedule.
|
||||
*
|
||||
* @param cronExpressions one or more cron expressions
|
||||
* @return the newly created cron schedule.
|
||||
* @throws org.elasticsearch.alerts.scheduler.schedule.CronSchedule.ValidationException if any of the given expression is invalid
|
||||
*/
|
||||
public static CronSchedule cron(String... cronExpressions) {
|
||||
return new CronSchedule(cronExpressions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an hourly schedule.
|
||||
*
|
||||
* @param minutes the minutes within the hour that the schedule should trigger at. values must be
|
||||
* between 0 and 59 (inclusive).
|
||||
* @return the newly created hourly schedule
|
||||
* @throws org.elasticsearch.alerts.AlertsSettingsException if any of the provided minutes are out of valid range
|
||||
*/
|
||||
public static HourlySchedule hourly(int... minutes) {
|
||||
return new HourlySchedule(minutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A builder for an hourly schedule.
|
||||
*/
|
||||
public static HourlySchedule.Builder hourly() {
|
||||
return HourlySchedule.builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A builder for a daily schedule.
|
||||
*/
|
||||
public static DailySchedule.Builder daily() {
|
||||
return DailySchedule.builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A builder for a weekly schedule.
|
||||
*/
|
||||
public static WeeklySchedule.Builder weekly() {
|
||||
return WeeklySchedule.builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A builder for a monthly schedule.
|
||||
*/
|
||||
public static MonthlySchedule.Builder monthly() {
|
||||
return MonthlySchedule.builder();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule;
|
||||
|
||||
import org.elasticsearch.alerts.AlertsSettingsException;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.support.WeekTimes;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class WeeklySchedule extends CronnableSchedule {
|
||||
|
||||
public static final String TYPE = "weekly";
|
||||
|
||||
public static final WeekTimes[] DEFAULT_TIMES = new WeekTimes[] { new WeekTimes() };
|
||||
|
||||
private final WeekTimes[] times;
|
||||
|
||||
WeeklySchedule() {
|
||||
this(DEFAULT_TIMES);
|
||||
}
|
||||
|
||||
WeeklySchedule(WeekTimes... times) {
|
||||
super(crons(times));
|
||||
this.times = times;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
public WeekTimes[] times() {
|
||||
return times;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||
if (params.paramAsBoolean("normalize", false) && times.length == 1) {
|
||||
return builder.value(times[0]);
|
||||
}
|
||||
return builder.value(times);
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
static String[] crons(WeekTimes[] times) {
|
||||
assert times.length > 0 : "at least one time must be defined";
|
||||
List<String> crons = new ArrayList<>(times.length);
|
||||
for (WeekTimes time : times) {
|
||||
crons.addAll(time.crons());
|
||||
}
|
||||
return crons.toArray(new String[crons.size()]);
|
||||
}
|
||||
|
||||
public static class Parser implements Schedule.Parser<WeeklySchedule> {
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WeeklySchedule parse(XContentParser parser) throws IOException {
|
||||
if (parser.currentToken() == XContentParser.Token.START_OBJECT) {
|
||||
try {
|
||||
return new WeeklySchedule(WeekTimes.parse(parser, parser.currentToken()));
|
||||
} catch (WeekTimes.ParseException pe) {
|
||||
throw new AlertsSettingsException("could not parse [weekly] schedule. invalid weekly times", pe);
|
||||
}
|
||||
}
|
||||
if (parser.currentToken() == XContentParser.Token.START_ARRAY) {
|
||||
List<WeekTimes> times = new ArrayList<>();
|
||||
XContentParser.Token token;
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
|
||||
try {
|
||||
times.add(WeekTimes.parse(parser, token));
|
||||
} catch (WeekTimes.ParseException pe) {
|
||||
throw new AlertsSettingsException("could not parse [weekly] schedule. invalid weekly times", pe);
|
||||
}
|
||||
}
|
||||
return times.isEmpty() ? new WeeklySchedule() : new WeeklySchedule(times.toArray(new WeekTimes[times.size()]));
|
||||
}
|
||||
throw new AlertsSettingsException("could not parse [weekly] schedule. expected either an object or an array " +
|
||||
"of objects representing weekly times, but found [" + parser.currentToken() + "] instead");
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private final Set<WeekTimes> times = new HashSet<>();
|
||||
|
||||
public Builder time(WeekTimes time) {
|
||||
times.add(time);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder time(WeekTimes.Builder time) {
|
||||
return time(time.build());
|
||||
}
|
||||
|
||||
public WeeklySchedule build() {
|
||||
return times.isEmpty() ? new WeeklySchedule() : new WeeklySchedule(times.toArray(new WeekTimes[times.size()]));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule;
|
||||
|
||||
import org.elasticsearch.alerts.AlertsSettingsException;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.support.YearTimes;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class YearlySchedule extends CronnableSchedule {
|
||||
|
||||
public static final String TYPE = "yearly";
|
||||
|
||||
public static final YearTimes[] DEFAULT_TIMES = new YearTimes[] { new YearTimes() };
|
||||
|
||||
private final YearTimes[] times;
|
||||
|
||||
YearlySchedule() {
|
||||
this(DEFAULT_TIMES);
|
||||
}
|
||||
|
||||
YearlySchedule(YearTimes... times) {
|
||||
super(crons(times));
|
||||
this.times = times;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
public YearTimes[] times() {
|
||||
return times;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||
if (params.paramAsBoolean("normalize", false) && times.length == 1) {
|
||||
return builder.value(times[0]);
|
||||
}
|
||||
return builder.value(times);
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
static String[] crons(YearTimes[] times) {
|
||||
assert times.length > 0 : "at least one time must be defined";
|
||||
Set<String> crons = new HashSet<>(times.length);
|
||||
for (YearTimes time : times) {
|
||||
crons.addAll(time.crons());
|
||||
}
|
||||
return crons.toArray(new String[crons.size()]);
|
||||
}
|
||||
|
||||
public static class Parser implements Schedule.Parser<YearlySchedule> {
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public YearlySchedule parse(XContentParser parser) throws IOException {
|
||||
if (parser.currentToken() == XContentParser.Token.START_OBJECT) {
|
||||
try {
|
||||
return new YearlySchedule(YearTimes.parse(parser, parser.currentToken()));
|
||||
} catch (YearTimes.ParseException pe) {
|
||||
throw new AlertsSettingsException("could not parse [yearly] schedule. invalid year times", pe);
|
||||
}
|
||||
}
|
||||
if (parser.currentToken() == XContentParser.Token.START_ARRAY) {
|
||||
List<YearTimes> times = new ArrayList<>();
|
||||
XContentParser.Token token;
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
|
||||
try {
|
||||
times.add(YearTimes.parse(parser, token));
|
||||
} catch (YearTimes.ParseException pe) {
|
||||
throw new AlertsSettingsException("could not parse [yearly] schedule. invalid year times", pe);
|
||||
}
|
||||
}
|
||||
return times.isEmpty() ? new YearlySchedule() : new YearlySchedule(times.toArray(new YearTimes[times.size()]));
|
||||
}
|
||||
throw new AlertsSettingsException("could not parse [yearly] schedule. expected either an object or an array " +
|
||||
"of objects representing year times, but found [" + parser.currentToken() + "] instead");
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private final Set<YearTimes> times = new HashSet<>();
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
public Builder time(YearTimes time) {
|
||||
times.add(time);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder time(YearTimes.Builder builder) {
|
||||
return time(builder.build());
|
||||
}
|
||||
|
||||
public YearlySchedule build() {
|
||||
return times.isEmpty() ? new YearlySchedule() : new YearlySchedule(times.toArray(new YearTimes[times.size()]));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule.support;
|
||||
|
||||
import org.elasticsearch.common.xcontent.ToXContent;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public enum DayOfWeek implements ToXContent {
|
||||
|
||||
SUNDAY("SUN"),
|
||||
MONDAY("MON"),
|
||||
TUESDAY("TUE"),
|
||||
WEDNESDAY("WED"),
|
||||
THURSDAY("THU"),
|
||||
FRIDAY("FRI"),
|
||||
SATURDAY("SAT");
|
||||
|
||||
private final String cronKey;
|
||||
|
||||
private DayOfWeek(String cronKey) {
|
||||
this.cronKey = cronKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||
return builder.value(name().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
public static String cronPart(EnumSet<DayOfWeek> days) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (DayOfWeek day : days) {
|
||||
if (sb.length() != 0) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append(day.cronKey);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static DayOfWeek resolve(int day) {
|
||||
switch (day) {
|
||||
case 1: return SUNDAY;
|
||||
case 2: return MONDAY;
|
||||
case 3: return TUESDAY;
|
||||
case 4: return WEDNESDAY;
|
||||
case 5: return THURSDAY;
|
||||
case 6: return FRIDAY;
|
||||
case 7: return SATURDAY;
|
||||
default:
|
||||
throw new WeekTimes.ParseException("unknown day of week number [" + day + "]");
|
||||
}
|
||||
}
|
||||
|
||||
public static DayOfWeek resolve(String day) {
|
||||
switch (day.toLowerCase(Locale.ROOT)) {
|
||||
case "1":
|
||||
case "sun":
|
||||
case "sunday": return SUNDAY;
|
||||
case "2":
|
||||
case "mon":
|
||||
case "monday": return MONDAY;
|
||||
case "3":
|
||||
case "tue":
|
||||
case "tuesday": return TUESDAY;
|
||||
case "4":
|
||||
case "wed":
|
||||
case "wednesday": return WEDNESDAY;
|
||||
case "5":
|
||||
case "thu":
|
||||
case "thursday": return THURSDAY;
|
||||
case "6":
|
||||
case "fri":
|
||||
case "friday": return FRIDAY;
|
||||
case "7":
|
||||
case "sat":
|
||||
case "saturday": return SATURDAY;
|
||||
default:
|
||||
throw new WeekTimes.ParseException("unknown day of week [" + day + "]");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return cronKey;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,289 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule.support;
|
||||
|
||||
import org.elasticsearch.alerts.AlertsException;
|
||||
import org.elasticsearch.alerts.AlertsSettingsException;
|
||||
import org.elasticsearch.common.ParseField;
|
||||
import org.elasticsearch.common.primitives.Ints;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class DayTimes implements Times {
|
||||
|
||||
public static final DayTimes NOON = new DayTimes("noon", new int[] { 12 }, new int[] { 0 });
|
||||
public static final DayTimes MIDNIGHT = new DayTimes("midnight", new int[] { 0 }, new int[] { 0 });
|
||||
|
||||
final int[] hour;
|
||||
final int[] minute;
|
||||
final String time;
|
||||
|
||||
public DayTimes() {
|
||||
this(0, 0);
|
||||
}
|
||||
|
||||
public DayTimes(int hour, int minute) {
|
||||
this(new int[] { hour }, new int[] { minute });
|
||||
}
|
||||
|
||||
public DayTimes(int[] hour, int[] minute) {
|
||||
this(null, hour, minute);
|
||||
}
|
||||
|
||||
DayTimes(String time, int[] hour, int[] minute) {
|
||||
this.time = time;
|
||||
this.hour = hour;
|
||||
this.minute = minute;
|
||||
validate();
|
||||
}
|
||||
|
||||
public int[] hour() {
|
||||
return hour;
|
||||
}
|
||||
|
||||
public int[] minute() {
|
||||
return minute;
|
||||
}
|
||||
|
||||
public String time() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public static DayTimes parse(String time) throws ParseException {
|
||||
if (NOON.time.equals(time)) {
|
||||
return NOON;
|
||||
}
|
||||
if (MIDNIGHT.time.equals(time)) {
|
||||
return MIDNIGHT;
|
||||
}
|
||||
int[] hour;
|
||||
int[] minute;
|
||||
int i = time.indexOf(":");
|
||||
if (i < 0) {
|
||||
throw new ParseException("could not parse time [" + time + "]. time format must be in the form of hh:mm");
|
||||
}
|
||||
if (i == time.length() - 1 || time.indexOf(":", i + 1) >= 0) {
|
||||
throw new ParseException("could not parse time [" + time + "]. time format must be in the form of hh:mm");
|
||||
}
|
||||
String hrStr = time.substring(0, i);
|
||||
String minStr = time.substring(i + 1);
|
||||
if (hrStr.length() != 1 && hrStr.length() != 2) {
|
||||
throw new ParseException("could not parse time [" + time + "]. time format must be in the form of hh:mm");
|
||||
}
|
||||
if (minStr.length() != 2) {
|
||||
throw new ParseException("could not parse time [" + time + "]. time format must be in the form of hh:mm");
|
||||
}
|
||||
try {
|
||||
hour = new int[] { Integer.parseInt(hrStr) };
|
||||
} catch (NumberFormatException nfe) {
|
||||
throw new ParseException("could not parse time [" + time + "]. time hour [" + hrStr + "] is not a number ");
|
||||
}
|
||||
try {
|
||||
minute = new int[] { Integer.parseInt(minStr) };
|
||||
} catch (NumberFormatException nfe) {
|
||||
throw new ParseException("could not parse time [" + time + "]. time minute [" + minStr + "] is not a number ");
|
||||
}
|
||||
return new DayTimes(time, hour, minute);
|
||||
}
|
||||
|
||||
public void validate() {
|
||||
for (int i = 0; i < hour.length; i++) {
|
||||
if (!validHour(hour[i])) {
|
||||
throw new AlertsSettingsException("invalid time [" + this + "]. invalid time hour value [" + hour[i] + "]. time hours must be between 0 and 23 incl.");
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < minute.length; i++) {
|
||||
if (!validMinute(minute[i])) {
|
||||
throw new AlertsSettingsException("invalid time [" + this + "]. invalid time minute value [" + minute[i] + "]. time minutes must be between 0 and 59 incl.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean validHour(int hour) {
|
||||
return hour >= 0 && hour < 24;
|
||||
}
|
||||
|
||||
static boolean validMinute(int minute) {
|
||||
return minute >= 0 && minute < 60;
|
||||
}
|
||||
|
||||
public String cron() {
|
||||
String hrs = Ints.join(",", hour);
|
||||
String mins = Ints.join(",", minute);
|
||||
return "0 " + mins + " " + hrs + " * * ?";
|
||||
}
|
||||
|
||||
@Override
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||
if (time != null) {
|
||||
return builder.value(time);
|
||||
}
|
||||
return builder.startObject()
|
||||
.field(HOUR_FIELD.getPreferredName(), hour)
|
||||
.field(MINUTE_FIELD.getPreferredName(), minute)
|
||||
.endObject();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (time != null) {
|
||||
return time;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int h = 0; h < hour.length; h++) {
|
||||
for (int m = 0; m < minute.length; m++) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append(", ");
|
||||
}
|
||||
if (hour[h] < 10) {
|
||||
sb.append("0");
|
||||
}
|
||||
sb.append(hour[h]).append(":");
|
||||
if (minute[m] < 10) {
|
||||
sb.append("0");
|
||||
}
|
||||
sb.append(minute[m]);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
DayTimes time = (DayTimes) o;
|
||||
|
||||
if (!Arrays.equals(hour, time.hour)) return false;
|
||||
if (!Arrays.equals(minute, time.minute)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Arrays.hashCode(hour);
|
||||
result = 31 * result + Arrays.hashCode(minute);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static DayTimes parse(XContentParser parser, XContentParser.Token token) throws IOException, ParseException {
|
||||
if (token == XContentParser.Token.VALUE_STRING) {
|
||||
return DayTimes.parse(parser.text());
|
||||
}
|
||||
if (token != XContentParser.Token.START_OBJECT) {
|
||||
throw new ParseException("could not parse time. expected string/number value or an object, but found [" + token + "]");
|
||||
}
|
||||
List<Integer> hours = new ArrayList<>();
|
||||
List<Integer> minutes = new ArrayList<>();
|
||||
String currentFieldName = null;
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
|
||||
if (token == XContentParser.Token.FIELD_NAME) {
|
||||
currentFieldName = parser.currentName();
|
||||
} else if (HOUR_FIELD.match(currentFieldName)) {
|
||||
if (token.isValue()) {
|
||||
hours.add(parseHourValue(parser, token));
|
||||
} else if (token == XContentParser.Token.START_ARRAY) {
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
|
||||
hours.add(parseHourValue(parser, token));
|
||||
}
|
||||
} else {
|
||||
throw new ParseException("invalid time hour value. expected string/number value or an array of string/number values, but found [" + token + "]");
|
||||
}
|
||||
} else if (MINUTE_FIELD.match(currentFieldName)) {
|
||||
if (token.isValue()) {
|
||||
minutes.add(parseMinuteValue(parser, token));
|
||||
} else if (token == XContentParser.Token.START_ARRAY) {
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
|
||||
minutes.add(parseMinuteValue(parser, token));
|
||||
}
|
||||
} else {
|
||||
throw new ParseException("invalid time minute value. expected string/number value or an array of string/number values, but found [" + token + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hours.isEmpty()) {
|
||||
hours.add(0);
|
||||
}
|
||||
if (minutes.isEmpty()) {
|
||||
minutes.add(0);
|
||||
}
|
||||
return new DayTimes(Ints.toArray(hours), Ints.toArray(minutes));
|
||||
}
|
||||
|
||||
public static int parseHourValue(XContentParser parser, XContentParser.Token token) throws IOException, ParseException {
|
||||
switch (token) {
|
||||
case VALUE_NUMBER:
|
||||
int hour = parser.intValue();
|
||||
if (!DayTimes.validHour(hour)) {
|
||||
throw new ParseException("invalid time hour value [" + hour + "] (possible values may be between 0 and 23 incl.)");
|
||||
}
|
||||
return hour;
|
||||
|
||||
case VALUE_STRING:
|
||||
String value = parser.text();
|
||||
try {
|
||||
hour = Integer.valueOf(value);
|
||||
if (!DayTimes.validHour(hour)) {
|
||||
throw new ParseException("invalid time hour value [" + hour + "] (possible values may be between 0 and 23 incl.)");
|
||||
}
|
||||
return hour;
|
||||
} catch (NumberFormatException nfe) {
|
||||
throw new ParseException("invalid time hour value [" + value + "]");
|
||||
}
|
||||
|
||||
default:
|
||||
throw new ParseException("invalid hour value. expected string/number value, but found [" + token + "]");
|
||||
}
|
||||
}
|
||||
|
||||
public static int parseMinuteValue(XContentParser parser, XContentParser.Token token) throws IOException, ParseException {
|
||||
switch (token) {
|
||||
case VALUE_NUMBER:
|
||||
int minute = parser.intValue();
|
||||
if (!DayTimes.validMinute(minute)) {
|
||||
throw new ParseException("invalid time minute value [" + minute + "] (possible values may be between 0 and 59 incl.)");
|
||||
}
|
||||
return minute;
|
||||
|
||||
case VALUE_STRING:
|
||||
String value = parser.text();
|
||||
try {
|
||||
minute = Integer.valueOf(value);
|
||||
if (!DayTimes.validMinute(minute)) {
|
||||
throw new ParseException("invalid time minute value [" + minute + "] (possible values may be between 0 and 59 incl.)");
|
||||
}
|
||||
return minute;
|
||||
} catch (NumberFormatException nfe) {
|
||||
throw new ParseException("invalid time minute value [" + value + "]");
|
||||
}
|
||||
|
||||
default:
|
||||
throw new ParseException("invalid time minute value. expected string/number value, but found [" + token + "]");
|
||||
}
|
||||
}
|
||||
|
||||
public static class ParseException extends AlertsException {
|
||||
|
||||
public ParseException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public ParseException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule.support;
|
||||
|
||||
import org.elasticsearch.common.xcontent.ToXContent;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public enum Month implements ToXContent {
|
||||
|
||||
JANUARY("JAN"),
|
||||
FEBRUARY("FEB"),
|
||||
MARCH("MAR"),
|
||||
APRIL("APR"),
|
||||
MAY("MAY"),
|
||||
JUNE("JUN"),
|
||||
JULY("JUL"),
|
||||
AUGUST("AUG"),
|
||||
SEPTEMBER("SEP"),
|
||||
OCTOBER("OCT"),
|
||||
NOVEMBER("NOV"),
|
||||
DECEMBER("DEC");
|
||||
|
||||
private final String cronKey;
|
||||
|
||||
private Month(String cronKey) {
|
||||
this.cronKey = cronKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||
return builder.value(name().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
public static String cronPart(EnumSet<Month> days) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Month day : days) {
|
||||
if (sb.length() != 0) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append(day.cronKey);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static Month resolve(int month) {
|
||||
switch (month) {
|
||||
case 1: return JANUARY;
|
||||
case 2: return FEBRUARY;
|
||||
case 3: return MARCH;
|
||||
case 4: return APRIL;
|
||||
case 5: return MAY;
|
||||
case 6: return JUNE;
|
||||
case 7: return JULY;
|
||||
case 8: return AUGUST;
|
||||
case 9: return SEPTEMBER;
|
||||
case 10: return OCTOBER;
|
||||
case 11: return NOVEMBER;
|
||||
case 12: return DECEMBER;
|
||||
default:
|
||||
throw new YearTimes.ParseException("unknown month number [" + month + "]");
|
||||
}
|
||||
}
|
||||
|
||||
public static Month resolve(String day) {
|
||||
switch (day.toLowerCase(Locale.ROOT)) {
|
||||
case "1":
|
||||
case "jan":
|
||||
case "first":
|
||||
case "january": return JANUARY;
|
||||
case "2":
|
||||
case "feb":
|
||||
case "february": return FEBRUARY;
|
||||
case "3":
|
||||
case "mar":
|
||||
case "march": return MARCH;
|
||||
case "4":
|
||||
case "apr":
|
||||
case "april": return APRIL;
|
||||
case "5":
|
||||
case "may": return MAY;
|
||||
case "6":
|
||||
case "jun":
|
||||
case "june": return JUNE;
|
||||
case "7":
|
||||
case "jul":
|
||||
case "july": return JULY;
|
||||
case "8":
|
||||
case "aug":
|
||||
case "august": return AUGUST;
|
||||
case "9":
|
||||
case "sep":
|
||||
case "september": return SEPTEMBER;
|
||||
case "10":
|
||||
case "oct":
|
||||
case "october": return OCTOBER;
|
||||
case "11":
|
||||
case "nov":
|
||||
case "november": return NOVEMBER;
|
||||
case "12":
|
||||
case "dec":
|
||||
case "last":
|
||||
case "december": return DECEMBER;
|
||||
default:
|
||||
throw new YearTimes.ParseException("unknown month [" + day + "]");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return cronKey;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule.support;
|
||||
|
||||
import org.elasticsearch.alerts.AlertsException;
|
||||
import org.elasticsearch.alerts.AlertsSettingsException;
|
||||
import org.elasticsearch.common.base.Joiner;
|
||||
import org.elasticsearch.common.collect.ImmutableSet;
|
||||
import org.elasticsearch.common.primitives.Ints;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class MonthTimes implements Times {
|
||||
|
||||
public static final String LAST = "last_day";
|
||||
public static final String FIRST = "first_day";
|
||||
|
||||
public static final int[] DEFAULT_DAYS = new int[] { 1 };
|
||||
public static final DayTimes[] DEFAULT_TIMES = new DayTimes[] { new DayTimes() };
|
||||
|
||||
private final int[] days;
|
||||
private final DayTimes[] times;
|
||||
|
||||
public MonthTimes() {
|
||||
this(DEFAULT_DAYS, DEFAULT_TIMES);
|
||||
}
|
||||
|
||||
public MonthTimes(int[] days, DayTimes[] times) {
|
||||
this.days = days.length == 0 ? DEFAULT_DAYS : days;
|
||||
Arrays.sort(this.days);
|
||||
this.times = times.length == 0 ? DEFAULT_TIMES : times;
|
||||
validate();
|
||||
}
|
||||
|
||||
void validate() {
|
||||
for (int day : days) {
|
||||
if (day < 1 || day > 32) { //32 represents the last day of the month
|
||||
throw new AlertsSettingsException("invalid month day [" + day + "]");
|
||||
}
|
||||
}
|
||||
for (DayTimes dayTimes : times) {
|
||||
dayTimes.validate();
|
||||
}
|
||||
}
|
||||
|
||||
public int[] days() {
|
||||
return days;
|
||||
}
|
||||
|
||||
public DayTimes[] times() {
|
||||
return times;
|
||||
}
|
||||
|
||||
public Set<String> crons() {
|
||||
Set<String> crons = new HashSet<>();
|
||||
for (DayTimes times : this.times) {
|
||||
String hrsStr = Ints.join(",", times.hour);
|
||||
String minsStr = Ints.join(",", times.minute);
|
||||
String daysStr = Ints.join(",", this.days);
|
||||
daysStr = daysStr.replace("32", "L");
|
||||
crons.add("0 " + minsStr + " " + hrsStr + " " + daysStr + " * ?");
|
||||
}
|
||||
return crons;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
MonthTimes that = (MonthTimes) o;
|
||||
|
||||
if (!Arrays.equals(days, that.days)) return false;
|
||||
// order doesn't matter
|
||||
if (!ImmutableSet.copyOf(times).equals(ImmutableSet.copyOf(that.times))) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Arrays.hashCode(days);
|
||||
result = 31 * result + Arrays.hashCode(times);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "days [" + Ints.join(",", days) + "], times [" + Joiner.on(",").join(times) + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||
return builder.startObject()
|
||||
.field(DAY_FIELD.getPreferredName(), days)
|
||||
.field(TIME_FIELD.getPreferredName(), (Object[]) times)
|
||||
.endObject();
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static MonthTimes parse(XContentParser parser, XContentParser.Token token) throws IOException, ParseException {
|
||||
if (token != XContentParser.Token.START_OBJECT) {
|
||||
throw new ParseException("could not parse month times. expected an object, but found [" + token + "]");
|
||||
}
|
||||
Set<Integer> daysSet = new HashSet<>();
|
||||
Set<DayTimes> timesSet = new HashSet<>();
|
||||
String currentFieldName = null;
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
|
||||
if (token == XContentParser.Token.FIELD_NAME) {
|
||||
currentFieldName = parser.currentName();
|
||||
} else if (DAY_FIELD.match(currentFieldName)) {
|
||||
if (token.isValue()) {
|
||||
daysSet.add(parseDayValue(parser, token));
|
||||
} else if (token == XContentParser.Token.START_ARRAY) {
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
|
||||
daysSet.add(parseDayValue(parser, token));
|
||||
}
|
||||
} else {
|
||||
throw new ParseException("invalid month day value for [on] field. expected string/number value or an array of string/number values, but found [" + token + "]");
|
||||
}
|
||||
} else if (TIME_FIELD.match(currentFieldName)) {
|
||||
if (token != XContentParser.Token.START_ARRAY) {
|
||||
try {
|
||||
timesSet.add(DayTimes.parse(parser, token));
|
||||
} catch (DayTimes.ParseException pe) {
|
||||
throw new ParseException("invalid time value for field [at] - [" + token + "]", pe);
|
||||
}
|
||||
} else {
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
|
||||
try {
|
||||
timesSet.add(DayTimes.parse(parser, token));
|
||||
} catch (DayTimes.ParseException pe) {
|
||||
throw new ParseException("invalid time value for field [at] - [" + token + "]", pe);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
int[] days = daysSet.isEmpty() ? DEFAULT_DAYS : Ints.toArray(daysSet);
|
||||
DayTimes[] times = timesSet.isEmpty() ? new DayTimes[] { new DayTimes(0, 0) } : timesSet.toArray(new DayTimes[timesSet.size()]);
|
||||
return new MonthTimes(days, times);
|
||||
}
|
||||
|
||||
static int parseDayValue(XContentParser parser, XContentParser.Token token) throws IOException {
|
||||
if (token == XContentParser.Token.VALUE_STRING) {
|
||||
String value = parser.text().toLowerCase(Locale.ROOT);
|
||||
if (LAST.equals(value)) {
|
||||
return 32;
|
||||
}
|
||||
if (FIRST.equals(value)) {
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(value);
|
||||
} catch (NumberFormatException nfe) {
|
||||
throw new MonthTimes.ParseException("invalid month day value. string value [" + value + "] cannot be ");
|
||||
}
|
||||
}
|
||||
if (token == XContentParser.Token.VALUE_NUMBER) {
|
||||
return parser.intValue();
|
||||
}
|
||||
throw new MonthTimes.ParseException("invalid month day value. expected a string or a number value, but found [" + token + "]");
|
||||
}
|
||||
|
||||
public static class ParseException extends AlertsException {
|
||||
|
||||
public ParseException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public ParseException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private final Set<Integer> days = new HashSet<>();
|
||||
private final Set<DayTimes> times = new HashSet<>();
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
public Builder on(int... days) {
|
||||
this.days.addAll(Ints.asList(days));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder at(int hour, int minute) {
|
||||
times.add(new DayTimes(hour, minute));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder atRoundHour(int... hours) {
|
||||
times.add(new DayTimes(hours, new int[] { 0 }));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder atNoon() {
|
||||
times.add(DayTimes.NOON);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder atMidnight() {
|
||||
times.add(DayTimes.MIDNIGHT);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MonthTimes build() {
|
||||
return new MonthTimes(Ints.toArray(days), times.toArray(new DayTimes[times.size()]));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule.support;
|
||||
|
||||
import org.elasticsearch.common.ParseField;
|
||||
import org.elasticsearch.common.xcontent.ToXContent;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public interface Times extends ToXContent {
|
||||
|
||||
public static final ParseField MONTH_FIELD = new ParseField("in", "month");
|
||||
public static final ParseField DAY_FIELD = new ParseField("on", "day");
|
||||
public static final ParseField TIME_FIELD = new ParseField("at", "time");
|
||||
public static final ParseField HOUR_FIELD = new ParseField("hour");
|
||||
public static final ParseField MINUTE_FIELD = new ParseField("minute");
|
||||
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule.support;
|
||||
|
||||
import org.elasticsearch.alerts.AlertsException;
|
||||
import org.elasticsearch.common.collect.ImmutableSet;
|
||||
import org.elasticsearch.common.primitives.Ints;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class WeekTimes implements Times {
|
||||
|
||||
public static final EnumSet<DayOfWeek> DEFAULT_DAYS = EnumSet.of(DayOfWeek.MONDAY);
|
||||
public static final DayTimes[] DEFAULT_TIMES = new DayTimes[] { new DayTimes() };
|
||||
|
||||
private final EnumSet<DayOfWeek> days;
|
||||
private final DayTimes[] times;
|
||||
|
||||
public WeekTimes() {
|
||||
this(DEFAULT_DAYS, DEFAULT_TIMES);
|
||||
}
|
||||
|
||||
public WeekTimes(DayOfWeek day, DayTimes times) {
|
||||
this(day, new DayTimes[] { times });
|
||||
}
|
||||
|
||||
public WeekTimes(DayOfWeek day, DayTimes[] times) {
|
||||
this(EnumSet.of(day), times);
|
||||
}
|
||||
|
||||
public WeekTimes(EnumSet<DayOfWeek> days, DayTimes[] times) {
|
||||
this.days = days.isEmpty() ? DEFAULT_DAYS : days;
|
||||
this.times = times.length == 0 ? DEFAULT_TIMES : times;
|
||||
}
|
||||
|
||||
public EnumSet<DayOfWeek> days() {
|
||||
return days;
|
||||
}
|
||||
|
||||
public DayTimes[] times() {
|
||||
return times;
|
||||
}
|
||||
|
||||
public Set<String> crons() {
|
||||
Set<String> crons = new HashSet<>();
|
||||
for (DayTimes times : this.times) {
|
||||
String hrsStr = Ints.join(",", times.hour);
|
||||
String minsStr = Ints.join(",", times.minute);
|
||||
String daysStr = DayOfWeek.cronPart(this.days);
|
||||
crons.add("0 " + minsStr + " " + hrsStr + " ? * " + daysStr);
|
||||
}
|
||||
return crons;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
WeekTimes that = (WeekTimes) o;
|
||||
|
||||
if (!days.equals(that.days)) return false;
|
||||
|
||||
// we don't care about order
|
||||
if (!ImmutableSet.copyOf(times).equals(ImmutableSet.copyOf(that.times))) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = days.hashCode();
|
||||
result = 31 * result + Arrays.hashCode(times);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||
return builder.startObject()
|
||||
.field(DAY_FIELD.getPreferredName(), days)
|
||||
.field(TIME_FIELD.getPreferredName(), (Object[]) times)
|
||||
.endObject();
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static WeekTimes parse(XContentParser parser, XContentParser.Token token) throws IOException, ParseException {
|
||||
if (token != XContentParser.Token.START_OBJECT) {
|
||||
throw new ParseException("could not parse week times. expected an object, but found [" + token + "]");
|
||||
}
|
||||
Set<DayOfWeek> daysSet = new HashSet<>();
|
||||
Set<DayTimes> timesSet = new HashSet<>();
|
||||
String currentFieldName = null;
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
|
||||
if (token == XContentParser.Token.FIELD_NAME) {
|
||||
currentFieldName = parser.currentName();
|
||||
} else if (DAY_FIELD.match(currentFieldName)) {
|
||||
if (token.isValue()) {
|
||||
daysSet.add(parseDayValue(parser, token));
|
||||
} else if (token == XContentParser.Token.START_ARRAY) {
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
|
||||
daysSet.add(parseDayValue(parser, token));
|
||||
}
|
||||
} else {
|
||||
throw new ParseException("invalid week day value for [on] field. expected string/number value or an array of string/number values, but found [" + token + "]");
|
||||
}
|
||||
} else if (TIME_FIELD.match(currentFieldName)) {
|
||||
if (token != XContentParser.Token.START_ARRAY) {
|
||||
try {
|
||||
timesSet.add(DayTimes.parse(parser, token));
|
||||
} catch (DayTimes.ParseException pe) {
|
||||
throw new ParseException("invalid time value for field [at] - [" + token + "]", pe);
|
||||
}
|
||||
} else {
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
|
||||
try {
|
||||
timesSet.add(DayTimes.parse(parser, token));
|
||||
} catch (DayTimes.ParseException pe) {
|
||||
throw new ParseException("invalid time value for field [at] - [" + token + "]", pe);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EnumSet<DayOfWeek> days = daysSet.isEmpty() ? EnumSet.of(DayOfWeek.MONDAY) : EnumSet.copyOf(daysSet);
|
||||
DayTimes[] times = timesSet.isEmpty() ? new DayTimes[] { new DayTimes(0, 0) } : timesSet.toArray(new DayTimes[timesSet.size()]);
|
||||
return new WeekTimes(days, times);
|
||||
}
|
||||
|
||||
static DayOfWeek parseDayValue(XContentParser parser, XContentParser.Token token) throws IOException {
|
||||
if (token == XContentParser.Token.VALUE_STRING) {
|
||||
return DayOfWeek.resolve(parser.text());
|
||||
}
|
||||
if (token == XContentParser.Token.VALUE_NUMBER) {
|
||||
return DayOfWeek.resolve(parser.intValue());
|
||||
}
|
||||
throw new WeekTimes.ParseException("invalid weekly day value. expected a string or a number value, but found [" + token + "]");
|
||||
}
|
||||
|
||||
public static class ParseException extends AlertsException {
|
||||
|
||||
public ParseException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public ParseException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private final Set<DayOfWeek> days = new HashSet<>();
|
||||
private final Set<DayTimes> times = new HashSet<>();
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
public Builder on(DayOfWeek... days) {
|
||||
Collections.addAll(this.days, days);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder at(int hour, int minute) {
|
||||
times.add(new DayTimes(hour, minute));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder atRoundHour(int... hours) {
|
||||
times.add(new DayTimes(hours, new int[] { 0 }));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder atNoon() {
|
||||
times.add(DayTimes.NOON);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder atMidnight() {
|
||||
times.add(DayTimes.MIDNIGHT);
|
||||
return this;
|
||||
}
|
||||
|
||||
public WeekTimes build() {
|
||||
EnumSet<DayOfWeek> dow = days.isEmpty() ? WeekTimes.DEFAULT_DAYS : EnumSet.copyOf(days);
|
||||
return new WeekTimes(dow, times.toArray(new DayTimes[times.size()]));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule.support;
|
||||
|
||||
import org.elasticsearch.alerts.AlertsException;
|
||||
import org.elasticsearch.alerts.AlertsSettingsException;
|
||||
import org.elasticsearch.common.base.Joiner;
|
||||
import org.elasticsearch.common.collect.ImmutableSet;
|
||||
import org.elasticsearch.common.primitives.Ints;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class YearTimes implements Times {
|
||||
|
||||
public static final EnumSet<Month> DEFAULT_MONTHS = EnumSet.of(Month.JANUARY);
|
||||
public static final int[] DEFAULT_DAYS = new int[] { 1 };
|
||||
public static final DayTimes[] DEFAULT_TIMES = new DayTimes[] { new DayTimes() };
|
||||
|
||||
private final EnumSet<Month> months;
|
||||
private final int[] days;
|
||||
private final DayTimes[] times;
|
||||
|
||||
public YearTimes() {
|
||||
this(DEFAULT_MONTHS, DEFAULT_DAYS, DEFAULT_TIMES);
|
||||
}
|
||||
|
||||
public YearTimes(EnumSet<Month> months, int[] days, DayTimes[] times) {
|
||||
this.months = months.isEmpty() ? DEFAULT_MONTHS : months;
|
||||
this.days = days.length == 0 ? DEFAULT_DAYS : days;
|
||||
Arrays.sort(this.days);
|
||||
this.times = times.length == 0 ? DEFAULT_TIMES : times;
|
||||
validate();
|
||||
}
|
||||
|
||||
void validate() {
|
||||
for (int day : days) {
|
||||
if (day < 1 || day > 32) { //32 represents the last day of the month
|
||||
throw new AlertsSettingsException("invalid month day [" + day + "]");
|
||||
}
|
||||
}
|
||||
for (DayTimes dayTimes : times) {
|
||||
dayTimes.validate();
|
||||
}
|
||||
}
|
||||
|
||||
public EnumSet<Month> months() {
|
||||
return months;
|
||||
}
|
||||
|
||||
public int[] days() {
|
||||
return days;
|
||||
}
|
||||
|
||||
public DayTimes[] times() {
|
||||
return times;
|
||||
}
|
||||
|
||||
public Set<String> crons() {
|
||||
Set<String> crons = new HashSet<>();
|
||||
for (DayTimes times : this.times) {
|
||||
String hrsStr = Ints.join(",", times.hour);
|
||||
String minsStr = Ints.join(",", times.minute);
|
||||
String daysStr = Ints.join(",", this.days);
|
||||
daysStr = daysStr.replace("32", "L");
|
||||
String monthsStr = Joiner.on(",").join(months);
|
||||
crons.add("0 " + minsStr + " " + hrsStr + " " + daysStr + " " + monthsStr + " ?");
|
||||
}
|
||||
return crons;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
YearTimes that = (YearTimes) o;
|
||||
|
||||
if (!Arrays.equals(days, that.days)) return false;
|
||||
if (!months.equals(that.months)) return false;
|
||||
// order doesn't matter
|
||||
if (!ImmutableSet.copyOf(times).equals(ImmutableSet.copyOf(that.times))) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = months.hashCode();
|
||||
result = 31 * result + Arrays.hashCode(days);
|
||||
result = 31 * result + Arrays.hashCode(times);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "months [" + Joiner.on(",").join(months) + "], days [" + Ints.join(",", days) + "], times [" + Joiner.on(",").join(times) + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||
return builder.startObject()
|
||||
.field(MONTH_FIELD.getPreferredName(), months)
|
||||
.field(DAY_FIELD.getPreferredName(), days)
|
||||
.field(TIME_FIELD.getPreferredName(), (Object[]) times)
|
||||
.endObject();
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static YearTimes parse(XContentParser parser, XContentParser.Token token) throws IOException, ParseException {
|
||||
if (token != XContentParser.Token.START_OBJECT) {
|
||||
throw new ParseException("could not parse year times. expected an object, but found [" + token + "]");
|
||||
}
|
||||
Set<Month> monthsSet = new HashSet<>();
|
||||
Set<Integer> daysSet = new HashSet<>();
|
||||
Set<DayTimes> timesSet = new HashSet<>();
|
||||
String currentFieldName = null;
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
|
||||
if (token == XContentParser.Token.FIELD_NAME) {
|
||||
currentFieldName = parser.currentName();
|
||||
} else if (MONTH_FIELD.match(currentFieldName)) {
|
||||
if (token.isValue()) {
|
||||
monthsSet.add(parseMonthValue(parser, token));
|
||||
} else if (token == XContentParser.Token.START_ARRAY) {
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
|
||||
monthsSet.add(parseMonthValue(parser, token));
|
||||
}
|
||||
} else {
|
||||
throw new ParseException("invalid year month value for [" + currentFieldName + "] field. expected string/number value or an array of string/number values, but found [" + token + "]");
|
||||
}
|
||||
} else if (DAY_FIELD.match(currentFieldName)) {
|
||||
if (token.isValue()) {
|
||||
daysSet.add(MonthTimes.parseDayValue(parser, token));
|
||||
} else if (token == XContentParser.Token.START_ARRAY) {
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
|
||||
daysSet.add(MonthTimes.parseDayValue(parser, token));
|
||||
}
|
||||
} else {
|
||||
throw new ParseException("invalid year day value for [on] field. expected string/number value or an array of string/number values, but found [" + token + "]");
|
||||
}
|
||||
} else if (TIME_FIELD.match(currentFieldName)) {
|
||||
if (token != XContentParser.Token.START_ARRAY) {
|
||||
try {
|
||||
timesSet.add(DayTimes.parse(parser, token));
|
||||
} catch (DayTimes.ParseException pe) {
|
||||
throw new ParseException("invalid time value for field [at] - [" + token + "]", pe);
|
||||
}
|
||||
} else {
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
|
||||
try {
|
||||
timesSet.add(DayTimes.parse(parser, token));
|
||||
} catch (DayTimes.ParseException pe) {
|
||||
throw new ParseException("invalid time value for field [at] - [" + token + "]", pe);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EnumSet<Month> months = monthsSet.isEmpty() ? DEFAULT_MONTHS : EnumSet.copyOf(monthsSet);
|
||||
int[] days = daysSet.isEmpty() ? DEFAULT_DAYS : Ints.toArray(daysSet);
|
||||
DayTimes[] times = timesSet.isEmpty() ? new DayTimes[] { new DayTimes(0, 0) } : timesSet.toArray(new DayTimes[timesSet.size()]);
|
||||
return new YearTimes(months, days, times);
|
||||
}
|
||||
|
||||
static Month parseMonthValue(XContentParser parser, XContentParser.Token token) throws IOException {
|
||||
if (token == XContentParser.Token.VALUE_STRING) {
|
||||
return Month.resolve(parser.text());
|
||||
}
|
||||
if (token == XContentParser.Token.VALUE_NUMBER) {
|
||||
return Month.resolve(parser.intValue());
|
||||
}
|
||||
throw new YearTimes.ParseException("invalid year month value. expected a string or a number value, but found [" + token + "]");
|
||||
}
|
||||
|
||||
public static class ParseException extends AlertsException {
|
||||
|
||||
public ParseException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public ParseException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private final Set<Month> months = new HashSet<>();
|
||||
private final Set<Integer> days = new HashSet<>();
|
||||
private final Set<DayTimes> times = new HashSet<>();
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
public Builder in(Month... months) {
|
||||
Collections.addAll(this.months, months);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder on(int... days) {
|
||||
this.days.addAll(Ints.asList(days));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder at(int hour, int minute) {
|
||||
times.add(new DayTimes(hour, minute));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder atRoundHour(int... hours) {
|
||||
times.add(new DayTimes(hours, new int[] { 0 }));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder atNoon() {
|
||||
times.add(DayTimes.NOON);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder atMidnight() {
|
||||
times.add(DayTimes.MIDNIGHT);
|
||||
return this;
|
||||
}
|
||||
|
||||
public YearTimes build() {
|
||||
return new YearTimes(EnumSet.copyOf(months), Ints.toArray(days), times.toArray(new DayTimes[times.size()]));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,283 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler;
|
||||
|
||||
import org.apache.lucene.util.LuceneTestCase.Slow;
|
||||
import org.elasticsearch.alerts.AlertsPlugin;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.Schedule;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.support.DayOfWeek;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.support.WeekTimes;
|
||||
import org.elasticsearch.common.joda.time.DateTime;
|
||||
import org.elasticsearch.common.joda.time.DateTimeZone;
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.elasticsearch.threadpool.ThreadPool;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.BitSet;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.elasticsearch.alerts.scheduler.schedule.Schedules.*;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@Slow
|
||||
public class InternalSchedulerTests extends ElasticsearchTestCase {
|
||||
|
||||
private ThreadPool threadPool;
|
||||
private InternalScheduler scheduler;
|
||||
|
||||
@Before
|
||||
public void init() throws Exception {
|
||||
AlertsPlugin plugin = new AlertsPlugin(ImmutableSettings.EMPTY);
|
||||
Settings settings = ImmutableSettings.builder()
|
||||
.put(plugin.additionalSettings())
|
||||
.put("name", "test")
|
||||
.build();
|
||||
threadPool = new ThreadPool(settings, null);
|
||||
scheduler = new InternalScheduler(ImmutableSettings.EMPTY, threadPool);
|
||||
}
|
||||
|
||||
@After
|
||||
public void cleanup() throws Exception {
|
||||
scheduler.stop();
|
||||
threadPool.shutdownNow();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStart() throws Exception {
|
||||
int count = randomIntBetween(2, 5);
|
||||
final CountDownLatch latch = new CountDownLatch(count);
|
||||
List<Scheduler.Job> jobs = new ArrayList<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
jobs.add(new SimpleJob(String.valueOf(i), interval("3s")));
|
||||
}
|
||||
final BitSet bits = new BitSet(count);
|
||||
scheduler.addListener(new Scheduler.Listener() {
|
||||
@Override
|
||||
public void fire(String jobName, DateTime scheduledFireTime, DateTime fireTime) {
|
||||
int index = Integer.parseInt(jobName);
|
||||
if (!bits.get(index)) {
|
||||
logger.info("job [" + index + "] first fire: " + new DateTime());
|
||||
bits.set(index);
|
||||
} else {
|
||||
latch.countDown();
|
||||
logger.info("job [" + index + "] second fire: " + new DateTime());
|
||||
}
|
||||
}
|
||||
});
|
||||
scheduler.start(jobs);
|
||||
if (!latch.await(5, TimeUnit.SECONDS)) {
|
||||
fail("waiting too long for all alerts to be fired");
|
||||
}
|
||||
scheduler.stop();
|
||||
assertThat(bits.cardinality(), is(count));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdd_Hourly() throws Exception {
|
||||
final String name = "job_name";
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
scheduler.start(Collections.<Scheduler.Job>emptySet());
|
||||
scheduler.addListener(new Scheduler.Listener() {
|
||||
@Override
|
||||
public void fire(String jobName, DateTime scheduledFireTime, DateTime fireTime) {
|
||||
assertThat(jobName, is(name));
|
||||
logger.info("triggered job on [{}]", new DateTime());
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
DateTime now = new DateTime(DateTimeZone.UTC);
|
||||
Minute minOfHour = new Minute(now);
|
||||
if (now.getSecondOfMinute() < 58) {
|
||||
minOfHour.inc(1);
|
||||
} else {
|
||||
minOfHour.inc(2);
|
||||
}
|
||||
int minute = minOfHour.value;
|
||||
logger.info("scheduling hourly job [{}]", minute);
|
||||
logger.info("current date [{}]", now);
|
||||
scheduler.add(new SimpleJob(name, hourly(minute)));
|
||||
long secondsToWait = now.getSecondOfMinute() < 29 ? 62 - now.getSecondOfMinute() : 122 - now.getSecondOfMinute();
|
||||
logger.info("waiting at least [{}] seconds for response", secondsToWait);
|
||||
if (!latch.await(secondsToWait, TimeUnit.SECONDS)) {
|
||||
fail("waiting too long for alert to be fired");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdd_Daily() throws Exception {
|
||||
final String name = "job_name";
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
scheduler.start(Collections.<Scheduler.Job>emptySet());
|
||||
scheduler.addListener(new Scheduler.Listener() {
|
||||
@Override
|
||||
public void fire(String jobName, DateTime scheduledFireTime, DateTime fireTime) {
|
||||
assertThat(jobName, is(name));
|
||||
logger.info("triggered job on [{}]", new DateTime());
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
DateTime now = new DateTime(DateTimeZone.UTC);
|
||||
Minute minOfHour = new Minute(now);
|
||||
Hour hourOfDay = new Hour(now);
|
||||
boolean jumpedHour = now.getSecondOfMinute() < 29 ? minOfHour.inc(1) : minOfHour.inc(2);
|
||||
int minute = minOfHour.value;
|
||||
if (jumpedHour) {
|
||||
hourOfDay.inc(1);
|
||||
}
|
||||
int hour = hourOfDay.value;
|
||||
logger.info("scheduling hourly job [{}:{}]", hour, minute);
|
||||
logger.info("current date [{}]", now);
|
||||
scheduler.add(new SimpleJob(name, daily().at(hour, minute).build()));
|
||||
// 30 sec is the default idle time of quartz
|
||||
long secondsToWait = now.getSecondOfMinute() < 29 ? 62 - now.getSecondOfMinute() : 122 - now.getSecondOfMinute();
|
||||
logger.info("waiting at least [{}] seconds for response", secondsToWait);
|
||||
if (!latch.await(secondsToWait, TimeUnit.SECONDS)) {
|
||||
fail("waiting too long for alert to be fired");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdd_Weekly() throws Exception {
|
||||
final String name = "job_name";
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
scheduler.start(Collections.<Scheduler.Job>emptySet());
|
||||
scheduler.addListener(new Scheduler.Listener() {
|
||||
@Override
|
||||
public void fire(String jobName, DateTime scheduledFireTime, DateTime fireTime) {
|
||||
assertThat(jobName, is(name));
|
||||
logger.info("triggered job on [{}]", new DateTime());
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
DateTime now = new DateTime(DateTimeZone.UTC);
|
||||
Minute minOfHour = new Minute(now);
|
||||
Hour hourOfDay = new Hour(now);
|
||||
Day dayOfWeek = new Day(now);
|
||||
boolean jumpedHour = now.getSecondOfMinute() < 29 ? minOfHour.inc(1) : minOfHour.inc(2);
|
||||
int minute = minOfHour.value;
|
||||
if (jumpedHour && hourOfDay.inc(1)) {
|
||||
dayOfWeek.inc(1);
|
||||
}
|
||||
int hour = hourOfDay.value;
|
||||
DayOfWeek day = dayOfWeek.day();
|
||||
logger.info("scheduling hourly job [{} {}:{}]", day, hour, minute);
|
||||
logger.info("current date [{}]", now);
|
||||
scheduler.add(new SimpleJob(name, weekly().time(WeekTimes.builder().on(day).at(hour, minute).build()).build()));
|
||||
// 30 sec is the default idle time of quartz
|
||||
long secondsToWait = now.getSecondOfMinute() < 29 ? 62 - now.getSecondOfMinute() : 122 - now.getSecondOfMinute();
|
||||
logger.info("waiting at least [{}] seconds for response", secondsToWait);
|
||||
if (!latch.await(secondsToWait, TimeUnit.SECONDS)) {
|
||||
fail("waiting too long for alert to be fired");
|
||||
}
|
||||
}
|
||||
|
||||
static class SimpleJob implements Scheduler.Job {
|
||||
|
||||
private final String name;
|
||||
private final Schedule schedule;
|
||||
|
||||
public SimpleJob(String name, Schedule schedule) {
|
||||
this.name = name;
|
||||
this.schedule = schedule;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Schedule schedule() {
|
||||
return schedule;
|
||||
}
|
||||
}
|
||||
|
||||
static class Hour {
|
||||
|
||||
int value;
|
||||
|
||||
Hour(DateTime time) {
|
||||
value = time.getHourOfDay();
|
||||
}
|
||||
|
||||
/**
|
||||
* increments the hour and returns whether the day jumped. (note, only supports increment steps < 24)
|
||||
*/
|
||||
boolean inc(int inc) {
|
||||
value += inc;
|
||||
if (value > 23) {
|
||||
value %= 24;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static class Minute {
|
||||
|
||||
int value;
|
||||
|
||||
Minute(DateTime time) {
|
||||
value = time.getMinuteOfHour();
|
||||
}
|
||||
|
||||
/**
|
||||
* increments the minute and returns whether the hour jumped. (note, only supports increment steps < 60)
|
||||
*/
|
||||
boolean inc(int inc) {
|
||||
value += inc;
|
||||
if (value > 59) {
|
||||
value %= 60;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static class Day {
|
||||
|
||||
int value;
|
||||
|
||||
Day(DateTime time) {
|
||||
value = time.getDayOfWeek() - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* increments the minute and returns whether the week jumped. (note, only supports increment steps < 8)
|
||||
*/
|
||||
boolean inc(int inc) {
|
||||
value += inc;
|
||||
if (value > 6) {
|
||||
value %= 7;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
DayOfWeek day() {
|
||||
switch (value) {
|
||||
case 0 : return DayOfWeek.MONDAY;
|
||||
case 1 : return DayOfWeek.TUESDAY;
|
||||
case 2 : return DayOfWeek.WEDNESDAY;
|
||||
case 3 : return DayOfWeek.THURSDAY;
|
||||
case 4 : return DayOfWeek.FRIDAY;
|
||||
case 5 : return DayOfWeek.SATURDAY;
|
||||
default : return DayOfWeek.SUNDAY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule;
|
||||
|
||||
import org.elasticsearch.alerts.AlertsSettingsException;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.common.xcontent.json.JsonXContent;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class CronScheduleTests extends ScheduleTestCase {
|
||||
|
||||
@Test(expected = CronSchedule.ValidationException.class)
|
||||
public void testInvalid() throws Exception {
|
||||
new CronSchedule("0 * * *");
|
||||
fail("expecting a validation error to be thrown when creating a cron schedule with invalid cron expression");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParse_Single() throws Exception {
|
||||
XContentBuilder builder = jsonBuilder().value("0 0/5 * * * ?");
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken();
|
||||
CronSchedule schedule = new CronSchedule.Parser().parse(parser);
|
||||
assertThat(schedule.crons(), arrayWithSize(1));
|
||||
assertThat(schedule.crons()[0], is("0 0/5 * * * ?"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParse_Multiple() throws Exception {
|
||||
XContentBuilder builder = jsonBuilder().value(new String[] {
|
||||
"0 0/1 * * * ?",
|
||||
"0 0/2 * * * ?",
|
||||
"0 0/3 * * * ?"
|
||||
});
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken();
|
||||
CronSchedule schedule = new CronSchedule.Parser().parse(parser);
|
||||
assertThat(schedule.crons(), arrayWithSize(3));
|
||||
assertThat(schedule.crons(), hasItemInArray("0 0/1 * * * ?"));
|
||||
assertThat(schedule.crons(), hasItemInArray("0 0/2 * * * ?"));
|
||||
assertThat(schedule.crons(), hasItemInArray("0 0/3 * * * ?"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParse_Invalid_BadExpression() throws Exception {
|
||||
XContentBuilder builder = jsonBuilder().value("0 0/5 * * ?");
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken();
|
||||
try {
|
||||
new CronSchedule.Parser().parse(parser);
|
||||
fail("expected cron parsing to fail when using invalid cron expression");
|
||||
} catch (AlertsSettingsException ase) {
|
||||
// expected
|
||||
assertThat(ase.getCause(), instanceOf(CronSchedule.ValidationException.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class)
|
||||
public void testParse_Invalid_Empty() throws Exception {
|
||||
XContentBuilder builder = jsonBuilder();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken();
|
||||
new CronSchedule.Parser().parse(parser);
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class)
|
||||
public void testParse_Invalid_Object() throws Exception {
|
||||
XContentBuilder builder = jsonBuilder().startObject().endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken();
|
||||
new CronSchedule.Parser().parse(parser);
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class)
|
||||
public void testParse_Invalid_EmptyArray() throws Exception {
|
||||
XContentBuilder builder = jsonBuilder().value(new String[0]);
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken();
|
||||
new CronSchedule.Parser().parse(parser);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule;
|
||||
|
||||
import com.carrotsearch.randomizedtesting.annotations.Repeat;
|
||||
import org.elasticsearch.alerts.AlertsSettingsException;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.support.DayTimes;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.common.primitives.Ints;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.common.xcontent.json.JsonXContent;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class DailyScheduleTests extends ScheduleTestCase {
|
||||
|
||||
@Test
|
||||
public void test_Default() throws Exception {
|
||||
DailySchedule schedule = new DailySchedule();
|
||||
String[] crons = schedule.crons();
|
||||
assertThat(crons, arrayWithSize(1));
|
||||
assertThat(crons, arrayContaining("0 0 0 * * ?"));
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void test_SingleTime() throws Exception {
|
||||
DayTimes time = validDayTime();
|
||||
DailySchedule schedule = new DailySchedule(time);
|
||||
String[] crons = schedule.crons();
|
||||
assertThat(crons, arrayWithSize(1));
|
||||
assertThat(crons, arrayContaining("0 " + Ints.join(",", time.minute()) + " " + Ints.join(",", time.hour()) + " * * ?"));
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void test_SingleTime_Invalid() throws Exception {
|
||||
try {
|
||||
HourAndMinute ham = invalidDayTime();
|
||||
new DayTimes(ham.hour, ham.minute);
|
||||
fail("expected either a parse exception or an alerts settings exception on invalid time input");
|
||||
} catch (DayTimes.ParseException pe) {
|
||||
// expected
|
||||
} catch (AlertsSettingsException ase) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void test_MultipleTimes() throws Exception {
|
||||
DayTimes[] times = validDayTimes();
|
||||
DailySchedule schedule = new DailySchedule(times);
|
||||
String[] crons = schedule.crons();
|
||||
assertThat(crons, arrayWithSize(times.length));
|
||||
for (DayTimes time : times) {
|
||||
assertThat(crons, hasItemInArray("0 " + Ints.join(",", time.minute()) + " " + Ints.join(",", time.hour()) + " * * ?"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParser_Empty() throws Exception {
|
||||
XContentBuilder builder = jsonBuilder().startObject().endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
DailySchedule schedule = new DailySchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.times().length, is(1));
|
||||
assertThat(schedule.times()[0], is(new DayTimes(0, 0)));
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParser_SingleTime_Object() throws Exception {
|
||||
DayTimes time = validDayTime();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.startObject("at")
|
||||
.field("hour", time.hour())
|
||||
.field("minute", time.minute())
|
||||
.endObject()
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
DailySchedule schedule = new DailySchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.times().length, is(1));
|
||||
assertThat(schedule.times()[0], is(time));
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
|
||||
public void testParser_SingleTime_Object_Invalid() throws Exception {
|
||||
HourAndMinute time = invalidDayTime();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.startObject("at")
|
||||
.field("hour", time.hour)
|
||||
.field("minute", time.minute)
|
||||
.endObject()
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
new DailySchedule.Parser().parse(parser);
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParser_SingleTime_String() throws Exception {
|
||||
String timeStr = validDayTimeStr();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field("at", timeStr)
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
DailySchedule schedule = new DailySchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.times().length, is(1));
|
||||
assertThat(schedule.times()[0], is(DayTimes.parse(timeStr)));
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
|
||||
public void testParser_SingleTime_String_Invalid() throws Exception {
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field("at", invalidDayTimeStr())
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
new DailySchedule.Parser().parse(parser);
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParser_MultipleTimes_Objects() throws Exception {
|
||||
DayTimes[] times = validDayTimesFromNumbers();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.array("at", (Object[]) times)
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
DailySchedule schedule = new DailySchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.times().length, is(times.length));
|
||||
for (int i = 0; i < times.length; i++) {
|
||||
assertThat(schedule.times(), hasItemInArray(times[i]));
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
|
||||
public void testParser_MultipleTimes_Objects_Invalid() throws Exception {
|
||||
HourAndMinute[] times = invalidDayTimes();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.array("at", (Object[]) times)
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
new DailySchedule.Parser().parse(parser);
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParser_MultipleTimes_Strings() throws Exception {
|
||||
DayTimes[] times = validDayTimesFromStrings();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.array("at", (Object[]) times)
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
DailySchedule schedule = new DailySchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.times().length, is(times.length));
|
||||
for (int i = 0; i < times.length; i++) {
|
||||
assertThat(schedule.times(), hasItemInArray(times[i]));
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
|
||||
public void testParser_MultipleTimes_Strings_Invalid() throws Exception {
|
||||
String[] times = invalidDayTimesAsStrings();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field("at", times)
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
new DailySchedule.Parser().parse(parser);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule;
|
||||
|
||||
import com.carrotsearch.randomizedtesting.annotations.Repeat;
|
||||
import org.elasticsearch.alerts.AlertsSettingsException;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.common.collect.Collections2;
|
||||
import org.elasticsearch.common.primitives.Ints;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.common.xcontent.json.JsonXContent;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class HourlyScheduleTests extends ScheduleTestCase {
|
||||
|
||||
@Test
|
||||
public void test_Default() throws Exception {
|
||||
HourlySchedule schedule = new HourlySchedule();
|
||||
String[] crons = schedule.crons();
|
||||
assertThat(crons, arrayWithSize(1));
|
||||
assertThat(crons, arrayContaining("0 0 * * * ?"));
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void test_SingleMinute() throws Exception {
|
||||
int minute = validMinute();
|
||||
HourlySchedule schedule = new HourlySchedule(minute);
|
||||
String[] crons = schedule.crons();
|
||||
assertThat(crons, arrayWithSize(1));
|
||||
assertThat(crons, arrayContaining("0 " + minute + " * * * ?"));
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
|
||||
public void test_SingleMinute_Invalid() throws Exception {
|
||||
new HourlySchedule(invalidMinute());
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void test_MultipleMinutes() throws Exception {
|
||||
int[] minutes = validMinutes();
|
||||
String minutesStr = Ints.join(",", minutes);
|
||||
HourlySchedule schedule = new HourlySchedule(minutes);
|
||||
String[] crons = schedule.crons();
|
||||
assertThat(crons, arrayWithSize(1));
|
||||
assertThat(crons, arrayContaining("0 " + minutesStr + " * * * ?"));
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
|
||||
public void test_MultipleMinutes_Invalid() throws Exception {
|
||||
int[] minutes = invalidMinutes();
|
||||
new HourlySchedule(minutes);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParser_Empty() throws Exception {
|
||||
XContentBuilder builder = jsonBuilder().startObject().endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
HourlySchedule schedule = new HourlySchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.minutes().length, is(1));
|
||||
assertThat(schedule.minutes()[0], is(0));
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParser_SingleMinute_Number() throws Exception {
|
||||
int minute = validMinute();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field("minute", minute)
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
HourlySchedule schedule = new HourlySchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.minutes().length, is(1));
|
||||
assertThat(schedule.minutes()[0], is(minute));
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
|
||||
public void testParser_SingleMinute_Number_Invalid() throws Exception {
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field("minute", invalidMinute())
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
new HourlySchedule.Parser().parse(parser);
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParser_SingleMinute_String() throws Exception {
|
||||
int minute = validMinute();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field("minute", String.valueOf(minute))
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
HourlySchedule schedule = new HourlySchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.minutes().length, is(1));
|
||||
assertThat(schedule.minutes()[0], is(minute));
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
|
||||
public void testParser_SingleMinute_String_Invalid() throws Exception {
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field("minute", String.valueOf(invalidMinute()))
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
new HourlySchedule.Parser().parse(parser);
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParser_MultipleMinutes_Numbers() throws Exception {
|
||||
int[] minutes = validMinutes();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field("minute", Ints.asList(minutes))
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
HourlySchedule schedule = new HourlySchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.minutes().length, is(minutes.length));
|
||||
for (int i = 0; i < minutes.length; i++) {
|
||||
assertThat(Ints.contains(schedule.minutes(), minutes[i]), is(true));
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
|
||||
public void testParser_MultipleMinutes_Numbers_Invalid() throws Exception {
|
||||
int[] minutes = invalidMinutes();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field("minute", Ints.asList(minutes))
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
new HourlySchedule.Parser().parse(parser);
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParser_MultipleMinutes_Strings() throws Exception {
|
||||
int[] minutes = validMinutes();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field("minute", Collections2.transform(Ints.asList(minutes), Ints.stringConverter().reverse()))
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
HourlySchedule schedule = new HourlySchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.minutes().length, is(minutes.length));
|
||||
for (int i = 0; i < minutes.length; i++) {
|
||||
assertThat(Ints.contains(schedule.minutes(), minutes[i]), is(true));
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
|
||||
public void testParser_MultipleMinutes_Strings_Invalid() throws Exception {
|
||||
int[] minutes = invalidMinutes();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field("minute", Collections2.transform(Ints.asList(minutes), Ints.stringConverter().reverse()))
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
new HourlySchedule.Parser().parse(parser);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule;
|
||||
|
||||
import org.elasticsearch.alerts.AlertsSettingsException;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.IntervalSchedule.Interval.Unit;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.common.xcontent.json.JsonXContent;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class IntervalScheduleTests extends ElasticsearchTestCase {
|
||||
|
||||
@Test
|
||||
public void testParse_Number() throws Exception {
|
||||
long value = (long) randomInt();
|
||||
XContentBuilder builder = jsonBuilder().value(value);
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
IntervalSchedule schedule = new IntervalSchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.interval().seconds(), is(value));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParse_String() throws Exception {
|
||||
IntervalSchedule.Interval value = randomTimeValue();
|
||||
XContentBuilder builder = jsonBuilder().value(value);
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
IntervalSchedule schedule = new IntervalSchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.interval(), is(value));
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class)
|
||||
public void testParse_Invalid_String() throws Exception {
|
||||
XContentBuilder builder = jsonBuilder().value("43S");
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
new IntervalSchedule.Parser().parse(parser);
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class)
|
||||
public void testParse_Invalid_Object() throws Exception {
|
||||
XContentBuilder builder = jsonBuilder().startObject().endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
new IntervalSchedule.Parser().parse(parser);
|
||||
}
|
||||
|
||||
private static IntervalSchedule.Interval randomTimeValue() {
|
||||
Unit unit = Unit.values()[randomIntBetween(0, Unit.values().length - 1)];
|
||||
return new IntervalSchedule.Interval(randomIntBetween(1, 100), unit);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule;
|
||||
|
||||
import com.carrotsearch.randomizedtesting.annotations.Repeat;
|
||||
import org.elasticsearch.alerts.AlertsSettingsException;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.support.DayTimes;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.support.MonthTimes;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.common.primitives.Ints;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.common.xcontent.json.JsonXContent;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class MonthlyScheduleTests extends ScheduleTestCase {
|
||||
|
||||
@Test
|
||||
public void test_Default() throws Exception {
|
||||
MonthlySchedule schedule = new MonthlySchedule();
|
||||
String[] crons = schedule.crons();
|
||||
assertThat(crons, arrayWithSize(1));
|
||||
assertThat(crons, arrayContaining("0 0 0 1 * ?"));
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void test_SingleTime() throws Exception {
|
||||
MonthTimes time = validMonthTime();
|
||||
MonthlySchedule schedule = new MonthlySchedule(time);
|
||||
String[] crons = schedule.crons();
|
||||
assertThat(crons, arrayWithSize(time.times().length));
|
||||
for (DayTimes dayTimes : time.times()) {
|
||||
String minStr = Ints.join(",", dayTimes.minute());
|
||||
String hrStr = Ints.join(",", dayTimes.hour());
|
||||
String dayStr = Ints.join(",", time.days());
|
||||
dayStr = dayStr.replace("32", "L");
|
||||
assertThat(crons, hasItemInArray("0 " + minStr + " " + hrStr + " " + dayStr + " * ?"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void test_MultipleTimes() throws Exception {
|
||||
MonthTimes[] times = validMonthTimes();
|
||||
MonthlySchedule schedule = new MonthlySchedule(times);
|
||||
String[] crons = schedule.crons();
|
||||
int count = 0;
|
||||
for (int i = 0; i < times.length; i++) {
|
||||
count += times[i].times().length;
|
||||
}
|
||||
assertThat(crons, arrayWithSize(count));
|
||||
for (MonthTimes monthTimes : times) {
|
||||
for (DayTimes dayTimes : monthTimes.times()) {
|
||||
String minStr = Ints.join(",", dayTimes.minute());
|
||||
String hrStr = Ints.join(",", dayTimes.hour());
|
||||
String dayStr = Ints.join(",", monthTimes.days());
|
||||
dayStr = dayStr.replace("32", "L");
|
||||
assertThat(crons, hasItemInArray("0 " + minStr + " " + hrStr + " " + dayStr + " * ?"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParser_Empty() throws Exception {
|
||||
XContentBuilder builder = jsonBuilder().startObject().endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
MonthlySchedule schedule = new MonthlySchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.times().length, is(1));
|
||||
assertThat(schedule.times()[0], is(new MonthTimes()));
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParser_SingleTime() throws Exception {
|
||||
DayTimes time = validDayTime();
|
||||
Object day = randomDayOfMonth();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field("on", day)
|
||||
.startObject("at")
|
||||
.field("hour", time.hour())
|
||||
.field("minute", time.minute())
|
||||
.endObject()
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
MonthlySchedule schedule = new MonthlySchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.times().length, is(1));
|
||||
assertThat(schedule.times()[0].days().length, is(1));
|
||||
assertThat(schedule.times()[0].days()[0], is(dayOfMonthToInt(day)));
|
||||
assertThat(schedule.times()[0].times(), arrayWithSize(1));
|
||||
assertThat(schedule.times()[0].times(), hasItemInArray(time));
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
|
||||
public void testParser_SingleTime_Invalid() throws Exception {
|
||||
HourAndMinute time = invalidDayTime();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field("on", randomBoolean() ? invalidDayOfMonth() : randomDayOfMonth())
|
||||
.startObject("at")
|
||||
.field("hour", time.hour)
|
||||
.field("minute", time.minute)
|
||||
.endObject()
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
new MonthlySchedule.Parser().parse(parser);
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParser_MultipleTimes() throws Exception {
|
||||
MonthTimes[] times = validMonthTimes();
|
||||
XContentBuilder builder = jsonBuilder().value(times);
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
MonthlySchedule schedule = new MonthlySchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.times().length, is(times.length));
|
||||
for (MonthTimes time : times) {
|
||||
assertThat(schedule.times(), hasItemInArray(time));
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
|
||||
public void testParser_MultipleTimes_Invalid() throws Exception {
|
||||
HourAndMinute[] times = invalidDayTimes();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field("on", randomDayOfMonth())
|
||||
.array("at", (Object[]) times)
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
new MonthlySchedule.Parser().parse(parser);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule;
|
||||
|
||||
import com.carrotsearch.randomizedtesting.annotations.Repeat;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.common.xcontent.json.JsonXContent;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.elasticsearch.alerts.scheduler.schedule.Schedules.cron;
|
||||
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class ScheduleRegistryTests extends ScheduleTestCase {
|
||||
|
||||
private ScheduleRegistry registry;
|
||||
|
||||
@Before
|
||||
public void init() throws Exception {
|
||||
Map<String, Schedule.Parser> parsers = new HashMap<>();
|
||||
parsers.put(IntervalSchedule.TYPE, new IntervalSchedule.Parser());
|
||||
parsers.put(CronSchedule.TYPE, new CronSchedule.Parser());
|
||||
parsers.put(HourlySchedule.TYPE, new HourlySchedule.Parser());
|
||||
parsers.put(DailySchedule.TYPE, new DailySchedule.Parser());
|
||||
parsers.put(WeeklySchedule.TYPE, new WeeklySchedule.Parser());
|
||||
parsers.put(MonthlySchedule.TYPE, new MonthlySchedule.Parser());
|
||||
registry = new ScheduleRegistry(parsers);
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParser_Interval() throws Exception {
|
||||
IntervalSchedule interval = randomIntervalSchedule();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field(IntervalSchedule.TYPE, interval)
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken();
|
||||
Schedule schedule = registry.parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule, instanceOf(IntervalSchedule.class));
|
||||
assertThat((IntervalSchedule) schedule, is(interval));
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParse_Cron() throws Exception {
|
||||
Object cron = randomBoolean() ?
|
||||
cron("* 0/5 * * * ?") :
|
||||
cron("* 0/2 * * * ?", "* 0/3 * * * ?", "* 0/5 * * * ?");
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field(CronSchedule.TYPE, cron)
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken();
|
||||
Schedule schedule = registry.parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule, instanceOf(CronSchedule.class));
|
||||
assertThat(schedule, is(cron));
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParse_Hourly() throws Exception {
|
||||
HourlySchedule hourly = randomHourlySchedule();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field(HourlySchedule.TYPE, hourly)
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken();
|
||||
Schedule schedule = registry.parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule, instanceOf(HourlySchedule.class));
|
||||
assertThat((HourlySchedule) schedule, equalTo(hourly));
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParse_Daily() throws Exception {
|
||||
DailySchedule daily = randomDailySchedule();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field(DailySchedule.TYPE, daily)
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
System.out.println(bytes.toUtf8());
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken();
|
||||
Schedule schedule = registry.parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule, instanceOf(DailySchedule.class));
|
||||
assertThat((DailySchedule) schedule, equalTo(daily));
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParse_Weekly() throws Exception {
|
||||
WeeklySchedule weekly = randomWeeklySchedule();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field(WeeklySchedule.TYPE, weekly)
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
System.out.println(bytes.toUtf8());
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken();
|
||||
Schedule schedule = registry.parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule, instanceOf(WeeklySchedule.class));
|
||||
assertThat((WeeklySchedule) schedule, equalTo(weekly));
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParse_Monthly() throws Exception {
|
||||
MonthlySchedule monthly = randomMonthlySchedule();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field(MonthlySchedule.TYPE, monthly)
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
System.out.println(bytes.toUtf8());
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken();
|
||||
Schedule schedule = registry.parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule, instanceOf(MonthlySchedule.class));
|
||||
assertThat((MonthlySchedule) schedule, equalTo(monthly));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,370 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.IntervalSchedule.Interval.Unit;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.support.*;
|
||||
import org.elasticsearch.common.xcontent.ToXContent;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.elasticsearch.alerts.scheduler.schedule.Schedules.*;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public abstract class ScheduleTestCase extends ElasticsearchTestCase {
|
||||
|
||||
protected static MonthlySchedule randomMonthlySchedule() {
|
||||
switch (randomIntBetween(1, 4)) {
|
||||
case 1: return monthly().build();
|
||||
case 2: return monthly().time(MonthTimes.builder().atMidnight()).build();
|
||||
case 3: return monthly().time(MonthTimes.builder().on(randomIntBetween(1, 31)).atMidnight()).build();
|
||||
default: return new MonthlySchedule(validMonthTimes());
|
||||
}
|
||||
}
|
||||
|
||||
protected static WeeklySchedule randomWeeklySchedule() {
|
||||
switch (randomIntBetween(1, 4)) {
|
||||
case 1: return weekly().build();
|
||||
case 2: return weekly().time(WeekTimes.builder().atMidnight()).build();
|
||||
case 3: return weekly().time(WeekTimes.builder().on(DayOfWeek.THURSDAY).atMidnight()).build();
|
||||
default: return new WeeklySchedule(validWeekTimes());
|
||||
}
|
||||
}
|
||||
|
||||
protected static DailySchedule randomDailySchedule() {
|
||||
switch (randomIntBetween(1, 4)) {
|
||||
case 1: return daily().build();
|
||||
case 2: return daily().atMidnight().build();
|
||||
case 3: return daily().atNoon().build();
|
||||
default: return new DailySchedule(validDayTimes());
|
||||
}
|
||||
}
|
||||
|
||||
protected static HourlySchedule randomHourlySchedule() {
|
||||
switch (randomIntBetween(1, 4)) {
|
||||
case 1: return hourly().build();
|
||||
case 2: return hourly().minutes(randomIntBetween(0, 59)).build();
|
||||
case 3: return hourly(randomIntBetween(0, 59));
|
||||
default: return hourly().minutes(validMinutes()).build();
|
||||
}
|
||||
}
|
||||
|
||||
protected static IntervalSchedule randomIntervalSchedule() {
|
||||
switch (randomIntBetween(1, 3)) {
|
||||
case 1: return interval(randomInterval().toString());
|
||||
case 2: return interval(randomIntBetween(1, 100), randomIntervalUnit());
|
||||
default: return new IntervalSchedule(randomInterval());
|
||||
}
|
||||
}
|
||||
|
||||
protected static IntervalSchedule.Interval randomInterval() {
|
||||
return new IntervalSchedule.Interval(randomIntBetween(1, 100), randomIntervalUnit());
|
||||
}
|
||||
|
||||
protected static Unit randomIntervalUnit() {
|
||||
return Unit.values()[randomIntBetween(0, Unit.values().length - 1)];
|
||||
}
|
||||
|
||||
protected static YearTimes validYearTime() {
|
||||
return new YearTimes(randomMonths(), randomDaysOfMonth(), validDayTimes());
|
||||
}
|
||||
|
||||
protected static YearTimes[] validYearTimes() {
|
||||
int count = randomIntBetween(2, 5);
|
||||
Set<YearTimes> times = new HashSet<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
times.add(validYearTime());
|
||||
}
|
||||
return times.toArray(new YearTimes[times.size()]);
|
||||
}
|
||||
|
||||
protected static MonthTimes validMonthTime() {
|
||||
return new MonthTimes(randomDaysOfMonth(), validDayTimes());
|
||||
}
|
||||
|
||||
protected static MonthTimes[] validMonthTimes() {
|
||||
int count = randomIntBetween(2, 5);
|
||||
Set<MonthTimes> times = new HashSet<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
times.add(validMonthTime());
|
||||
}
|
||||
return times.toArray(new MonthTimes[times.size()]);
|
||||
}
|
||||
|
||||
protected static WeekTimes validWeekTime() {
|
||||
return new WeekTimes(randomDaysOfWeek(), validDayTimes());
|
||||
}
|
||||
|
||||
protected static WeekTimes[] validWeekTimes() {
|
||||
int count = randomIntBetween(2, 5);
|
||||
Set<WeekTimes> times = new HashSet<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
times.add(validWeekTime());
|
||||
}
|
||||
return times.toArray(new WeekTimes[times.size()]);
|
||||
}
|
||||
|
||||
protected static EnumSet<DayOfWeek> randomDaysOfWeek() {
|
||||
int count = randomIntBetween(1, DayOfWeek.values().length-1);
|
||||
Set<DayOfWeek> days = new HashSet<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
days.add(DayOfWeek.values()[randomIntBetween(0, count)]);
|
||||
}
|
||||
return EnumSet.copyOf(days);
|
||||
}
|
||||
|
||||
protected static EnumSet<Month> randomMonths() {
|
||||
int count = randomIntBetween(1, 11);
|
||||
Set<Month> months = new HashSet<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
months.add(Month.values()[randomIntBetween(0, 11)]);
|
||||
}
|
||||
return EnumSet.copyOf(months);
|
||||
}
|
||||
|
||||
protected static Object randomMonth() {
|
||||
int m = randomIntBetween(1, 14);
|
||||
switch (m) {
|
||||
case 13:
|
||||
return "first";
|
||||
case 14:
|
||||
return "last";
|
||||
default:
|
||||
return Month.resolve(m);
|
||||
}
|
||||
}
|
||||
|
||||
protected static int[] randomDaysOfMonth() {
|
||||
int count = randomIntBetween(1, 5);
|
||||
Set<Integer> days = new HashSet<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
days.add(randomIntBetween(1, 32));
|
||||
}
|
||||
return Ints.toArray(days);
|
||||
}
|
||||
|
||||
protected static Object randomDayOfMonth() {
|
||||
int day = randomIntBetween(1, 32);
|
||||
if (day == 32) {
|
||||
return "last_day";
|
||||
}
|
||||
if (day == 1) {
|
||||
return randomBoolean() ? "first_day" : 1;
|
||||
}
|
||||
return day;
|
||||
}
|
||||
|
||||
protected static int dayOfMonthToInt(Object dom) {
|
||||
if (dom instanceof Integer) {
|
||||
return (Integer) dom;
|
||||
}
|
||||
if ("last_day".equals(dom)) {
|
||||
return 32;
|
||||
}
|
||||
if ("first_day".equals(dom)) {
|
||||
return 1;
|
||||
}
|
||||
throw new IllegalStateException("cannot convert given day-of-month [" + dom + "] to int");
|
||||
}
|
||||
|
||||
protected static Object invalidDayOfMonth() {
|
||||
return randomBoolean() ?
|
||||
randomAsciiOfLength(5) :
|
||||
randomBoolean() ? randomIntBetween(-30, -1) : randomIntBetween(33, 45);
|
||||
}
|
||||
|
||||
protected static DayTimes validDayTime() {
|
||||
return randomBoolean() ? DayTimes.parse(validDayTimeStr()) : new DayTimes(validHours(), validMinutes());
|
||||
}
|
||||
|
||||
protected static String validDayTimeStr() {
|
||||
int hour = validHour();
|
||||
int min = validMinute();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (hour < 10 && randomBoolean()) {
|
||||
sb.append("0");
|
||||
}
|
||||
sb.append(hour).append(":");
|
||||
if (min < 10) {
|
||||
sb.append("0");
|
||||
}
|
||||
return sb.append(min).toString();
|
||||
}
|
||||
|
||||
protected static HourAndMinute invalidDayTime() {
|
||||
return randomBoolean() ?
|
||||
new HourAndMinute(invalidHour(), invalidMinute()) :
|
||||
randomBoolean() ?
|
||||
new HourAndMinute(validHour(), invalidMinute()) :
|
||||
new HourAndMinute(invalidHour(), validMinute());
|
||||
}
|
||||
|
||||
protected static String invalidDayTimeStr() {
|
||||
int hour;
|
||||
int min;
|
||||
switch (randomIntBetween(1, 3)) {
|
||||
case 1:
|
||||
hour = invalidHour();
|
||||
min = validMinute();
|
||||
break;
|
||||
case 2:
|
||||
hour = validHour();
|
||||
min = invalidMinute();
|
||||
break;
|
||||
default:
|
||||
hour = invalidHour();
|
||||
min = invalidMinute();
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (hour < 10 && randomBoolean()) {
|
||||
sb.append("0");
|
||||
}
|
||||
sb.append(hour).append(":");
|
||||
if (min < 10) {
|
||||
sb.append("0");
|
||||
}
|
||||
return sb.append(min).toString();
|
||||
}
|
||||
|
||||
protected static DayTimes[] validDayTimes() {
|
||||
int count = randomIntBetween(2, 5);
|
||||
Set<DayTimes> times = new HashSet<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
times.add(validDayTime());
|
||||
}
|
||||
return times.toArray(new DayTimes[times.size()]);
|
||||
}
|
||||
|
||||
protected static DayTimes[] validDayTimesFromNumbers() {
|
||||
int count = randomIntBetween(2, 5);
|
||||
Set<DayTimes> times = new HashSet<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
times.add(new DayTimes(validHours(), validMinutes()));
|
||||
}
|
||||
return times.toArray(new DayTimes[times.size()]);
|
||||
}
|
||||
|
||||
protected static DayTimes[] validDayTimesFromStrings() {
|
||||
int count = randomIntBetween(2, 5);
|
||||
Set<DayTimes> times = new HashSet<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
times.add(DayTimes.parse(validDayTimeStr()));
|
||||
}
|
||||
return times.toArray(new DayTimes[times.size()]);
|
||||
}
|
||||
|
||||
protected static HourAndMinute[] invalidDayTimes() {
|
||||
int count = randomIntBetween(2, 5);
|
||||
Set<HourAndMinute> times = new HashSet<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
times.add(invalidDayTime());
|
||||
}
|
||||
return times.toArray(new HourAndMinute[times.size()]);
|
||||
}
|
||||
|
||||
protected static String[] invalidDayTimesAsStrings() {
|
||||
int count = randomIntBetween(2, 5);
|
||||
Set<String> times = new HashSet<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
times.add(invalidDayTimeStr());
|
||||
}
|
||||
return times.toArray(new String[times.size()]);
|
||||
}
|
||||
|
||||
protected static int validMinute() {
|
||||
return randomIntBetween(0, 59);
|
||||
}
|
||||
|
||||
protected static int[] validMinutes() {
|
||||
int count = randomIntBetween(2, 6);
|
||||
int inc = 59 / count;
|
||||
int[] minutes = new int[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
minutes[i] = randomIntBetween(i * inc, (i + 1) * inc);
|
||||
}
|
||||
return minutes;
|
||||
}
|
||||
|
||||
protected static int invalidMinute() {
|
||||
return randomBoolean() ? randomIntBetween(60, 100) : randomIntBetween(-60, -1);
|
||||
}
|
||||
|
||||
protected static int[] invalidMinutes() {
|
||||
int count = randomIntBetween(2, 6);
|
||||
int[] minutes = new int[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
minutes[i] = invalidMinute();
|
||||
}
|
||||
return minutes;
|
||||
}
|
||||
|
||||
protected static int validHour() {
|
||||
return randomIntBetween(0, 23);
|
||||
}
|
||||
|
||||
protected static int[] validHours() {
|
||||
int count = randomIntBetween(2, 6);
|
||||
int inc = 23 / count;
|
||||
int[] hours = new int[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
hours[i] = randomIntBetween(i * inc, (i + 1) * inc);
|
||||
}
|
||||
return hours;
|
||||
}
|
||||
|
||||
protected static int invalidHour() {
|
||||
return randomBoolean() ? randomIntBetween(24, 40) : randomIntBetween(-60, -1);
|
||||
}
|
||||
|
||||
static class HourAndMinute implements ToXContent {
|
||||
|
||||
int hour;
|
||||
int minute;
|
||||
|
||||
public HourAndMinute(int hour, int minute) {
|
||||
this.hour = hour;
|
||||
this.minute = minute;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||
return builder.startObject()
|
||||
.field(DayTimes.HOUR_FIELD.getPreferredName(), hour)
|
||||
.field(DayTimes.MINUTE_FIELD.getPreferredName(), minute)
|
||||
.endObject();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
HourAndMinute that = (HourAndMinute) o;
|
||||
|
||||
if (hour != that.hour) return false;
|
||||
if (minute != that.minute) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = hour;
|
||||
result = 31 * result + minute;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule;
|
||||
|
||||
import com.carrotsearch.randomizedtesting.annotations.Repeat;
|
||||
import org.elasticsearch.alerts.AlertsSettingsException;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.support.DayTimes;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.support.DayOfWeek;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.support.WeekTimes;
|
||||
import org.elasticsearch.common.base.Joiner;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.common.primitives.Ints;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.common.xcontent.json.JsonXContent;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class WeeklyScheduleTests extends ScheduleTestCase {
|
||||
|
||||
@Test
|
||||
public void test_Default() throws Exception {
|
||||
WeeklySchedule schedule = new WeeklySchedule();
|
||||
String[] crons = schedule.crons();
|
||||
assertThat(crons, arrayWithSize(1));
|
||||
assertThat(crons, arrayContaining("0 0 0 ? * MON"));
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void test_SingleTime() throws Exception {
|
||||
WeekTimes time = validWeekTime();
|
||||
WeeklySchedule schedule = new WeeklySchedule(time);
|
||||
String[] crons = schedule.crons();
|
||||
assertThat(crons, arrayWithSize(time.times().length));
|
||||
for (DayTimes dayTimes : time.times()) {
|
||||
assertThat(crons, hasItemInArray("0 " + Ints.join(",", dayTimes.minute()) + " " + Ints.join(",", dayTimes.hour()) + " ? * " + Joiner.on(",").join(time.days())));
|
||||
}
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void test_MultipleTimes() throws Exception {
|
||||
WeekTimes[] times = validWeekTimes();
|
||||
WeeklySchedule schedule = new WeeklySchedule(times);
|
||||
String[] crons = schedule.crons();
|
||||
int count = 0;
|
||||
for (int i = 0; i < times.length; i++) {
|
||||
count += times[i].times().length;
|
||||
}
|
||||
assertThat(crons, arrayWithSize(count));
|
||||
for (WeekTimes weekTimes : times) {
|
||||
for (DayTimes dayTimes : weekTimes.times()) {
|
||||
assertThat(crons, hasItemInArray("0 " + Ints.join(",", dayTimes.minute()) + " " + Ints.join(",", dayTimes.hour()) + " ? * " + Joiner.on(",").join(weekTimes.days())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParser_Empty() throws Exception {
|
||||
XContentBuilder builder = jsonBuilder().startObject().endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
WeeklySchedule schedule = new WeeklySchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.times().length, is(1));
|
||||
assertThat(schedule.times()[0], is(new WeekTimes(DayOfWeek.MONDAY, new DayTimes())));
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParser_SingleTime() throws Exception {
|
||||
DayTimes time = validDayTime();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field("on", "mon")
|
||||
.startObject("at")
|
||||
.field("hour", time.hour())
|
||||
.field("minute", time.minute())
|
||||
.endObject()
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
WeeklySchedule schedule = new WeeklySchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.times().length, is(1));
|
||||
assertThat(schedule.times()[0].days(), hasSize(1));
|
||||
assertThat(schedule.times()[0].days(), contains(DayOfWeek.MONDAY));
|
||||
assertThat(schedule.times()[0].times(), arrayWithSize(1));
|
||||
assertThat(schedule.times()[0].times(), hasItemInArray(time));
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
|
||||
public void testParser_SingleTime_Invalid() throws Exception {
|
||||
HourAndMinute time = invalidDayTime();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field("on", "mon")
|
||||
.startObject("at")
|
||||
.field("hour", time.hour)
|
||||
.field("minute", time.minute)
|
||||
.endObject()
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
new WeeklySchedule.Parser().parse(parser);
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParser_MultipleTimes() throws Exception {
|
||||
WeekTimes[] times = validWeekTimes();
|
||||
XContentBuilder builder = jsonBuilder().value(times);
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
WeeklySchedule schedule = new WeeklySchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.times().length, is(times.length));
|
||||
for (int i = 0; i < times.length; i++) {
|
||||
assertThat(schedule.times(), hasItemInArray(times[i]));
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
|
||||
public void testParser_MultipleTimes_Objects_Invalid() throws Exception {
|
||||
HourAndMinute[] times = invalidDayTimes();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field("on", randomDaysOfWeek())
|
||||
.array("at", (Object[]) times)
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
new WeeklySchedule.Parser().parse(parser);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.alerts.scheduler.schedule;
|
||||
|
||||
import com.carrotsearch.randomizedtesting.annotations.Repeat;
|
||||
import org.elasticsearch.alerts.AlertsSettingsException;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.support.DayTimes;
|
||||
import org.elasticsearch.alerts.scheduler.schedule.support.YearTimes;
|
||||
import org.elasticsearch.common.base.Joiner;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.common.primitives.Ints;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.common.xcontent.json.JsonXContent;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class YearlyScheduleTests extends ScheduleTestCase {
|
||||
|
||||
@Test
|
||||
public void test_Default() throws Exception {
|
||||
YearlySchedule schedule = new YearlySchedule();
|
||||
String[] crons = schedule.crons();
|
||||
assertThat(crons, arrayWithSize(1));
|
||||
assertThat(crons, arrayContaining("0 0 0 1 JAN ?"));
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void test_SingleTime() throws Exception {
|
||||
YearTimes time = validYearTime();
|
||||
YearlySchedule schedule = new YearlySchedule(time);
|
||||
String[] crons = schedule.crons();
|
||||
assertThat(crons, arrayWithSize(time.times().length));
|
||||
for (DayTimes dayTimes : time.times()) {
|
||||
String minStr = Ints.join(",", dayTimes.minute());
|
||||
String hrStr = Ints.join(",", dayTimes.hour());
|
||||
String dayStr = Ints.join(",", time.days());
|
||||
dayStr = dayStr.replace("32", "L");
|
||||
String monthStr = Joiner.on(",").join(time.months());
|
||||
assertThat(crons, hasItemInArray("0 " + minStr + " " + hrStr + " " + dayStr + " " + monthStr + " ?"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void test_MultipleTimes() throws Exception {
|
||||
YearTimes[] times = validYearTimes();
|
||||
YearlySchedule schedule = new YearlySchedule(times);
|
||||
String[] crons = schedule.crons();
|
||||
int count = 0;
|
||||
for (int i = 0; i < times.length; i++) {
|
||||
count += times[i].times().length;
|
||||
}
|
||||
assertThat(crons, arrayWithSize(count));
|
||||
for (YearTimes yearTimes : times) {
|
||||
for (DayTimes dayTimes : yearTimes.times()) {
|
||||
String minStr = Ints.join(",", dayTimes.minute());
|
||||
String hrStr = Ints.join(",", dayTimes.hour());
|
||||
String dayStr = Ints.join(",", yearTimes.days());
|
||||
dayStr = dayStr.replace("32", "L");
|
||||
String monthStr = Joiner.on(",").join(yearTimes.months());
|
||||
assertThat(crons, hasItemInArray("0 " + minStr + " " + hrStr + " " + dayStr + " " + monthStr + " ?"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParser_Empty() throws Exception {
|
||||
XContentBuilder builder = jsonBuilder().startObject().endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
YearlySchedule schedule = new YearlySchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.times().length, is(1));
|
||||
assertThat(schedule.times()[0], is(new YearTimes()));
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParser_SingleTime() throws Exception {
|
||||
DayTimes time = validDayTime();
|
||||
Object day = randomDayOfMonth();
|
||||
Object month = randomMonth();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field("in", month)
|
||||
.field("on", day)
|
||||
.startObject("at")
|
||||
.field("hour", time.hour())
|
||||
.field("minute", time.minute())
|
||||
.endObject()
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
YearlySchedule schedule = new YearlySchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.times().length, is(1));
|
||||
assertThat(schedule.times()[0].days().length, is(1));
|
||||
assertThat(schedule.times()[0].days()[0], is(dayOfMonthToInt(day)));
|
||||
assertThat(schedule.times()[0].times(), arrayWithSize(1));
|
||||
assertThat(schedule.times()[0].times(), hasItemInArray(time));
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
|
||||
public void testParser_SingleTime_Invalid() throws Exception {
|
||||
HourAndMinute time = invalidDayTime();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field("in", randomMonth())
|
||||
.field("on", randomBoolean() ? invalidDayOfMonth() : randomDayOfMonth())
|
||||
.startObject("at")
|
||||
.field("hour", time.hour)
|
||||
.field("minute", time.minute)
|
||||
.endObject()
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
new MonthlySchedule.Parser().parse(parser);
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 20)
|
||||
public void testParser_MultipleTimes() throws Exception {
|
||||
YearTimes[] times = validYearTimes();
|
||||
XContentBuilder builder = jsonBuilder().value(times);
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
YearlySchedule schedule = new YearlySchedule.Parser().parse(parser);
|
||||
assertThat(schedule, notNullValue());
|
||||
assertThat(schedule.times().length, is(times.length));
|
||||
for (YearTimes time : times) {
|
||||
assertThat(schedule.times(), hasItemInArray(time));
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
|
||||
public void testParser_MultipleTimes_Invalid() throws Exception {
|
||||
HourAndMinute[] times = invalidDayTimes();
|
||||
XContentBuilder builder = jsonBuilder()
|
||||
.startObject()
|
||||
.field("in", randomMonth())
|
||||
.field("on", randomDayOfMonth())
|
||||
.array("at", (Object[]) times)
|
||||
.endObject();
|
||||
BytesReference bytes = builder.bytes();
|
||||
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
|
||||
parser.nextToken(); // advancing to the start object
|
||||
new YearlySchedule.Parser().parse(parser);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue