[WIFI-10819] parse cron into valid quartz cron (#89)

Signed-off-by: Jun Woo Shin <jwoos@fb.com>
This commit is contained in:
Jun Woo Shin
2022-09-29 14:37:18 -04:00
committed by GitHub
parent df21d07ec9
commit 0ac189f493
5 changed files with 299 additions and 127 deletions

View File

@@ -478,8 +478,10 @@ components:
RRMSchedule:
type: object
properties:
cron:
type: string
crons:
type: array
items:
type: string
algorithms:
type: array
items:

View File

@@ -19,9 +19,9 @@ public class RRMSchedule {
*
* This field expects a cron-like format as defined by the Quartz Job
* Scheduler (CronTrigger):
* https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html
* https://www.quartz-scheduler.org/documentation/quartz-2.4.0/tutorials/crontrigger.html
*/
public String cron;
public List<String> crons;
/**
* The list of RRM algorithms to run.

View File

@@ -8,6 +8,7 @@
package com.facebook.openwifirrm.modules;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@@ -159,12 +160,21 @@ public class ProvMonitor implements Runnable {
return null;
}
RRMSchedule schedule = new RRMSchedule();
schedule.cron = RRMScheduler
String[] crons = RRMScheduler
.parseIntoQuartzCron(details.rrm.schedule);
if (schedule.cron == null || schedule.cron.isEmpty()) {
if (crons == null || crons.length == 0) {
return null;
}
// if ANY crons are invalid throw it out since it doesn't make sense to
// schedule partial jobs
for (String cron : crons) {
if (cron == null || cron.isEmpty()) {
return null;
}
}
RRMSchedule schedule = new RRMSchedule();
schedule.crons = Arrays.asList(crons);
if (details.rrm.algorithms != null) {
schedule.algorithms =
@@ -175,6 +185,7 @@ public class ProvMonitor implements Runnable {
)
.collect(Collectors.toList());
}
return schedule;
}

View File

@@ -35,6 +35,7 @@ import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.DeviceConfig;
import com.facebook.openwifirrm.DeviceDataManager;
import com.facebook.openwifirrm.RRMAlgorithm;
import com.facebook.openwifirrm.RRMSchedule;
import com.facebook.openwifirrm.RRMConfig.ModuleConfig.RRMSchedulerParams;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@@ -74,15 +75,21 @@ public class RRMScheduler {
/** The scheduler instance. */
private Scheduler scheduler;
/** The zones with active triggers scheduled. */
private Set<String> scheduledZones;
/**
* The job keys with active triggers scheduled. Job keys take the format of
* {@code <zone>:<index>}
*
* @see #parseIntoQuartzCron(String)
* */
private Set<String> scheduledJobKeys;
/** RRM job. */
public static class RRMJob implements Job {
@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
String zone = context.getTrigger().getKey().getName();
String jobKey = context.getTrigger().getKey().getName();
String zone = jobKey.split(":")[0];
logger.debug("Executing job for zone: {}", zone);
try {
SchedulerContext schedulerContext =
@@ -107,13 +114,14 @@ public class RRMScheduler {
* @param linuxCron Linux cron with seconds
* (seconds minutes hours day_of_month month day_of_week [year])
*
* @throws IllegalArgumentException when a linux cron cannot be parsed
* into a valid Quartz spec
* @return String a Quartz supported cron
* @throws IllegalArgumentException when a linux cron cannot be parsed into a
* valid Quartz spec
* @return String[] an array of length 1 or 2 of Quartz supported cron that's
* equivalent to the original linux cron
*/
public static String parseIntoQuartzCron(String linuxCron) {
public static String[] parseIntoQuartzCron(String linuxCron) {
if (CronExpression.isValidExpression(linuxCron)) {
return linuxCron;
return new String[] { linuxCron };
}
String[] split = linuxCron.split(" ");
@@ -144,15 +152,36 @@ public class RRMScheduler {
// if first case failed and only day of week is *, set to ?
split[DAY_OF_WEEK_INDEX] = "?";
} else {
// Quartz does not support both values being set, so return null
return null;
// Quartz does not support both values being set but the standard says that
// if both are specified then it becomes OR of the two fields. Which means
// that we can split it into two separate crons and have it work the same way
split[DAY_OF_MONTH_INDEX] = "?";
String dayOfWeekCron = String.join(" ", split);
split[DAY_OF_MONTH_INDEX] = dayOfMonth;
split[DAY_OF_WEEK_INDEX] = "?";
String dayOfMonthCron = String.join(" ", split);
if (
!CronExpression.isValidExpression(dayOfWeekCron) ||
!CronExpression.isValidExpression(dayOfMonthCron)
) {
logger.error(
"Unable to parse cron {} into valid crons",
linuxCron
);
return null;
}
return new String[] { dayOfWeekCron, dayOfMonthCron };
}
String quartzCron = String.join(" ", split);
if (!CronExpression.isValidExpression(quartzCron)) {
return null;
}
return quartzCron;
return new String[] { quartzCron };
}
/** Constructor. */
@@ -194,7 +223,7 @@ public class RRMScheduler {
// Schedule job and triggers
scheduler.addJob(job, false);
syncTriggers();
logger.info("Scheduled {} RRM trigger(s)", scheduledZones.size());
logger.info("Scheduled {} RRM trigger(s)", scheduledJobKeys.size());
// Start scheduler
scheduler.start();
@@ -218,85 +247,98 @@ public class RRMScheduler {
/**
* Synchronize triggers to the current topology, adding/updating/deleting
* them as necessary. This updates {@link #scheduledZones}.
* them as necessary. This updates {@link #scheduledJobKeys}.
*/
public void syncTriggers() {
Set<String> scheduled = ConcurrentHashMap.newKeySet();
Set<String> prevScheduled = new HashSet<>();
if (scheduledZones != null) {
prevScheduled.addAll(scheduledZones);
if (scheduledJobKeys != null) {
prevScheduled.addAll(scheduledJobKeys);
}
// Add new triggers
for (String zone : deviceDataManager.getZones()) {
DeviceConfig config = deviceDataManager.getZoneConfig(zone);
RRMSchedule schedule = config.schedule;
if (
config.schedule == null ||
config.schedule.cron == null ||
config.schedule.cron.isEmpty()
schedule == null || schedule.crons == null ||
schedule.crons.isEmpty()
) {
continue; // RRM not scheduled
}
try {
CronExpression.validateExpression(config.schedule.cron);
} catch (ParseException e) {
logger.error(
String.format(
"Invalid cron expression (%s) for zone %s",
config.schedule.cron,
zone
),
e
for (int i = 0; i < schedule.crons.size(); i++) {
String cron = schedule.crons.get(i);
// if even one schedule has invalid cron, the whole thing is probably wrong
if (cron == null || cron.isEmpty()) {
logger.error("There was an invalid cron in the schedule");
break;
}
try {
CronExpression.validateExpression(cron);
} catch (ParseException e) {
logger.error(
String.format(
"Invalid cron expression (%s) for zone %s",
cron,
zone
),
e
);
continue;
}
// Create trigger
String jobKey = String.format("%s:%d", zone, i);
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(jobKey)
.forJob(job)
.withSchedule(
CronScheduleBuilder.cronSchedule(cron)
)
.build();
try {
if (!prevScheduled.contains(jobKey)) {
scheduler.scheduleJob(trigger);
} else {
scheduler.rescheduleJob(trigger.getKey(), trigger);
}
} catch (SchedulerException e) {
logger.error(
"Failed to schedule RRM trigger for job key: " + jobKey,
e
);
continue;
}
scheduled.add(jobKey);
logger.debug(
"Scheduled/updated RRM for job key '{}' at: < {} >",
jobKey,
cron
);
continue;
}
// Create trigger
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(zone)
.forJob(job)
.withSchedule(
CronScheduleBuilder.cronSchedule(config.schedule.cron)
)
.build();
try {
if (!prevScheduled.contains(zone)) {
scheduler.scheduleJob(trigger);
} else {
scheduler.rescheduleJob(trigger.getKey(), trigger);
}
} catch (SchedulerException e) {
logger.error(
"Failed to schedule RRM trigger for zone: " + zone,
e
);
continue;
}
scheduled.add(zone);
logger.debug(
"Scheduled/updated RRM for zone '{}' at: < {} >",
zone,
config.schedule.cron
);
}
// Remove old triggers
prevScheduled.removeAll(scheduled);
for (String zone : prevScheduled) {
for (String jobKey : prevScheduled) {
try {
scheduler.unscheduleJob(TriggerKey.triggerKey(zone));
scheduler.unscheduleJob(TriggerKey.triggerKey(jobKey));
} catch (SchedulerException e) {
logger.error(
"Failed to remove RRM trigger for zone: " + zone,
"Failed to remove RRM trigger for jobKey: " + jobKey,
e
);
continue;
}
logger.debug("Removed RRM trigger for zone '{}'", zone);
logger.debug("Removed RRM trigger for jobKey '{}'", jobKey);
}
this.scheduledZones = scheduled;
this.scheduledJobKeys = scheduled;
}
/** Run RRM algorithms for the given zone. */
@@ -305,16 +347,19 @@ public class RRMScheduler {
// Get algorithms from zone config
DeviceConfig config = deviceDataManager.getZoneConfig(zone);
if (config.schedule == null) {
RRMSchedule schedule = config.schedule;
if (schedule == null) {
logger.error("RRM schedule missing for zone '{}', aborting!", zone);
return;
}
if (
config.schedule.algorithms == null ||
config.schedule.algorithms.isEmpty()
schedule.algorithms == null ||
schedule.algorithms.isEmpty()
) {
logger.debug("Using default RRM algorithms for zone '{}'", zone);
config.schedule.algorithms = Arrays.asList(
logger
.debug("Using default RRM algorithms for zone '{}'", zone);
schedule.algorithms = Arrays.asList(
new RRMAlgorithm(
RRMAlgorithm.AlgorithmType.OptimizeChannel.name()
),
@@ -325,7 +370,7 @@ public class RRMScheduler {
}
// Execute algorithms
for (RRMAlgorithm algo : config.schedule.algorithms) {
for (RRMAlgorithm algo : schedule.algorithms) {
RRMAlgorithm.AlgorithmResult result = algo.run(
deviceDataManager,
configManager,

View File

@@ -9,7 +9,7 @@
package com.facebook.openwifirrm.modules;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import org.junit.jupiter.api.Test;
@@ -23,48 +23,48 @@ public class RRMSchedulerTest {
assertNull(RRMScheduler.parseIntoQuartzCron("* * * * * * * *"));
// correct (6 fields)
assertEquals(
"* * * ? * *",
assertArrayEquals(
new String[] { "* * * ? * *" },
RRMScheduler.parseIntoQuartzCron("* * * * * *")
);
// correct (7 fields)
assertEquals(
"* * * ? * * *",
assertArrayEquals(
new String[] { "* * * ? * * *" },
RRMScheduler.parseIntoQuartzCron("* * * * * * *")
);
// correct value other than * for day of month
assertEquals(
"* * * 1 * ?",
assertArrayEquals(
new String[] { "* * * 1 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1 * *")
);
assertEquals(
"* * * 1 * ? *",
assertArrayEquals(
new String[] { "* * * 1 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1 * * *")
);
assertEquals(
"* * * 1/2 * ?",
assertArrayEquals(
new String[] { "* * * 1/2 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1/2 * *")
);
assertEquals(
"* * * 1/2 * ? *",
assertArrayEquals(
new String[] { "* * * 1/2 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1/2 * * *")
);
assertEquals(
"* * * 1-2 * ?",
assertArrayEquals(
new String[] { "* * * 1-2 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1-2 * *")
);
assertEquals(
"* * * 1-2 * ? *",
assertArrayEquals(
new String[] { "* * * 1-2 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1-2 * * *")
);
assertEquals(
"* * * 1,2 * ?",
assertArrayEquals(
new String[] { "* * * 1,2 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1,2 * *")
);
assertEquals(
"* * * 1,2 * ? *",
assertArrayEquals(
new String[] { "* * * 1,2 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1,2 * * *")
);
@@ -79,70 +79,70 @@ public class RRMSchedulerTest {
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * * *"));
// correct value other than * for day of month
assertEquals(
"* * * ? * 1",
assertArrayEquals(
new String[] { "* * * ? * 1" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1")
);
assertEquals(
"* * * ? * 1 *",
assertArrayEquals(
new String[] { "* * * ? * 1 *" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1 *")
);
assertEquals(
"* * * ? * 1/3",
assertArrayEquals(
new String[] { "* * * ? * 1/3" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1/3")
);
assertEquals(
"* * * ? * 1/3 *",
assertArrayEquals(
new String[] { "* * * ? * 1/3 *" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1/3 *")
);
assertEquals(
"* * * ? * 1-3",
assertArrayEquals(
new String[] { "* * * ? * 1-3" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1-3")
);
assertEquals(
"* * * ? * 1-3 *",
assertArrayEquals(
new String[] { "* * * ? * 1-3 *" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1-3 *")
);
assertEquals(
"* * * ? * 1,3",
assertArrayEquals(
new String[] { "* * * ? * 1,3" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1,3")
);
assertEquals(
"* * * ? * 1,3 *",
assertArrayEquals(
new String[] { "* * * ? * 1,3 *" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1,3 *")
);
// correct value other than * for day of month, make sure 0 turns into 7
assertEquals(
"* * * ? * 7",
assertArrayEquals(
new String[] { "* * * ? * 7" },
RRMScheduler.parseIntoQuartzCron("* * * * * 0")
);
assertEquals(
"* * * ? * 7 *",
assertArrayEquals(
new String[] { "* * * ? * 7 *" },
RRMScheduler.parseIntoQuartzCron("* * * * * 0 *")
);
assertEquals(
"* * * ? * 1/7",
assertArrayEquals(
new String[] { "* * * ? * 1/7" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1/0")
);
assertEquals(
"* * * ? * 1/7 *",
assertArrayEquals(
new String[] { "* * * ? * 1/7 *" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1/0 *")
);
assertEquals(
"* * * ? * 1-7",
assertArrayEquals(
new String[] { "* * * ? * 1-7" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1-0")
);
assertEquals(
"* * * ? * 1-7 *",
assertArrayEquals(
new String[] { "* * * ? * 1-7 *" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1-0 *")
);
assertEquals(
"* * * ? * 1,7",
assertArrayEquals(
new String[] { "* * * ? * 1,7" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1,0")
);
assertEquals(
"* * * ? * 1,7 *",
assertArrayEquals(
new String[] { "* * * ? * 1,7 *" },
RRMScheduler.parseIntoQuartzCron("* * * * * 1,0 *")
);
@@ -155,5 +155,119 @@ public class RRMSchedulerTest {
assertNull(RRMScheduler.parseIntoQuartzCron("* * * * * 7-8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * * * 7,8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * * * 7,8 *"));
// correct value for both day of week and day of month
assertArrayEquals(
new String[] { "* * * ? * 7", "* * * 1 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1 * 0")
);
assertArrayEquals(
new String[] { "* * * ? * 7 *", "* * * 1 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1 * 0 *")
);
assertArrayEquals(
new String[] { "* * * ? * 1/7", "* * * 1 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1 * 1/0")
);
assertArrayEquals(
new String[] { "* * * ? * 1", "* * * 1/2 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1/2 * 1")
);
assertArrayEquals(
new String[] { "* * * ? * 1/7", "* * * 1/2 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1/2 * 1/0")
);
assertArrayEquals(
new String[] { "* * * ? * 1/7 *", "* * * 1 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1 * 1/0 *")
);
assertArrayEquals(
new String[] { "* * * ? * 1 *", "* * * 1/2 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1/2 * 1 *")
);
assertArrayEquals(
new String[] { "* * * ? * 1/7 *", "* * * 1/2 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1/2 * 1/0 *")
);
assertArrayEquals(
new String[] { "* * * ? * 1-7", "* * * 1 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1 * 1-0")
);
assertArrayEquals(
new String[] { "* * * ? * 1", "* * * 1-3 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1-3 * 1")
);
assertArrayEquals(
new String[] { "* * * ? * 1-7", "* * * 1-3 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1-3 * 1-0")
);
assertArrayEquals(
new String[] { "* * * ? * 1-7 *", "* * * 1 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1 * 1-0 *")
);
assertArrayEquals(
new String[] { "* * * ? * 1 *", "* * * 1-3 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1-3 * 1 *")
);
assertArrayEquals(
new String[] { "* * * ? * 1-7 *", "* * * 1-3 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1-3 * 1-0 *")
);
assertArrayEquals(
new String[] { "* * * ? * 1-7", "* * * 1/3 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1/3 * 1-0")
);
assertArrayEquals(
new String[] { "* * * ? * 1/7", "* * * 1-3 * ?" },
RRMScheduler.parseIntoQuartzCron("* * * 1-3 * 1/0")
);
assertArrayEquals(
new String[] { "* * * ? * 1-7 *", "* * * 1/3 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1/3 * 1-0 *")
);
assertArrayEquals(
new String[] { "* * * ? * 1/7 *", "* * * 1-3 * ? *" },
RRMScheduler.parseIntoQuartzCron("* * * 1-3 * 1/0 *")
);
// wrong value for either day of week or day of month
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 7/8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 7/8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 7-8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 7-8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 7,8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0 * 7,8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 7/8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 7/8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 7-8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 7-8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 7,8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0/1 * 7,8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 7/8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 7/8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 7-8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 7-8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 7,8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0-1 * 7,8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 7/8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 7/8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 7-8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 7-8 *"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 7,8"));
assertNull(RRMScheduler.parseIntoQuartzCron("* * * 0,1 * 7,8 *"));
}
}