BackupScheduler.java
/*
* Copyright (c) 2007-2024 MetaSolutions AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.entrystore.repository.backup;
import lombok.Getter;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.entrystore.config.Config;
import org.entrystore.repository.RepositoryManager;
import org.entrystore.repository.config.Settings;
import org.entrystore.repository.util.MetadataUtil;
import org.quartz.CronTrigger;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.ParseException;
import java.util.LinkedList;
import java.util.OptionalInt;
import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Supports continuously backing up the repository.
*
* @author Hannes Ebner
*/
public class BackupScheduler {
private static final Logger log = LoggerFactory.getLogger(BackupScheduler.class);
Scheduler scheduler;
JobDetail job;
RepositoryManager rm;
boolean gzip;
@Getter
String cronExpression;
boolean maintenance;
int upperLimit;
int lowerLimit;
int expiresAfterDays;
boolean simple;
boolean deleteAfter;
boolean includeFiles;
RDFFormat format;
public static BackupScheduler instance;
private BackupScheduler(RepositoryManager rm, String cronExp, boolean gzip, boolean deleteAfter, boolean includeFiles, boolean maintenance, int upperLimit, int lowerLimit, int expiresAfterDays, RDFFormat format) {
try {
scheduler = StdSchedulerFactory.getDefaultScheduler();
} catch (SchedulerException e) {
log.error(e.getMessage());
}
this.rm = rm;
this.gzip = gzip;
this.deleteAfter = deleteAfter;
this.cronExpression = cronExp;
this.includeFiles = includeFiles;
this.maintenance = maintenance;
this.upperLimit = upperLimit;
this.lowerLimit = lowerLimit;
this.expiresAfterDays = expiresAfterDays;
this.format = format;
if (upperLimit < 2 && lowerLimit < 2 && expiresAfterDays < 2) {
log.info("Switching to simple backup strategy with one folder without date and time in name");
this.simple = true;
this.maintenance = false;
}
log.info("Created backup scheduler");
}
public static synchronized BackupScheduler getInstance(RepositoryManager rm) {
if (instance == null) {
log.info("Loading backup configuration");
Config config = rm.getConfiguration();
String cronExp = config.getString(Settings.BACKUP_CRONEXP, config.getString(Settings.BACKUP_TIMEREGEXP_DEPRECATED));
if (cronExp == null) {
return null;
}
boolean gzip = config.getBoolean(Settings.BACKUP_GZIP, false);
boolean maintenance = config.getBoolean(Settings.BACKUP_MAINTENANCE, false);
boolean deleteAfter = config.getBoolean(Settings.BACKUP_DELETE_AFTER, false);
boolean includeFiles = config.getBoolean(Settings.BACKUP_INCLUDE_FILES, true);
int upperLimit = config.getInt(Settings.BACKUP_MAINTENANCE_UPPER_LIMIT, -1);
int lowerLimit = config.getInt(Settings.BACKUP_MAINTENANCE_LOWER_LIMIT, -1);
int expiresAfterDays = config.getInt(Settings.BACKUP_MAINTENANCE_EXPIRES_AFTER_DAYS, -1);
RDFFormat format = MetadataUtil.getRDFFormat(config.getString(Settings.BACKUP_FORMAT, RDFFormat.TRIX.getName()));
if (format == null) {
log.warn("Invalid backup format {}, falling back to TriX", config.getString(Settings.BACKUP_FORMAT));
format = RDFFormat.TRIX;
}
if (cronExp.toLowerCase().contains("rnd")) {
cronExp = randomizeCronString(cronExp);
}
log.info("Cron expression: {}", cronExp);
log.info("GZIP: {}", gzip);
log.info("Include files: {}", includeFiles);
log.info("Delete previous backup after new backup: {}", deleteAfter);
log.info("Maintenance: {}", maintenance);
log.info("Maintenance upper limit: {}", upperLimit);
log.info("Maintenance lower limit: {}", lowerLimit);
log.info("Maintenance expires after days: {}", expiresAfterDays);
instance = new BackupScheduler(rm, cronExp, gzip, deleteAfter, includeFiles, maintenance, upperLimit, lowerLimit, expiresAfterDays, format);
}
return instance;
}
private static String randomizeCronString(String cronExp) {
String[] parts = cronExp.split("\\s+");
if (parts.length < 6) {
log.warn("Cron expression seems to be incorrect and cannot be parsed correctly: {}", cronExp);
return cronExp;
}
LinkedList<String> result = new LinkedList<>();
Pattern pattern = Pattern.compile("rnd\\(([\\*0-9]+)[\\-]?([0-9]*)\\)", Pattern.CASE_INSENSITIVE);
for (int i = 0; i < parts.length; i++) {
String p = parts[i];
Matcher matcher = pattern.matcher(p);
if (!matcher.matches()) {
result.add(p);
} else {
String first = matcher.group(1);
String second = matcher.group(2);
if ("*".equals(first)) {
if (i == 0 || i == 1) {
// second or minute
OptionalInt findFirst = ThreadLocalRandom.current().ints(0, 60).limit(1).findFirst();
if (findFirst.isPresent()) {
result.add(Integer.toString(findFirst.getAsInt()));
}
} else if (i == 2) {
// hour
OptionalInt findFirst = ThreadLocalRandom.current().ints(0, 24).limit(1).findFirst();
if (findFirst.isPresent()) {
result.add(Integer.toString(findFirst.getAsInt()));
}
} else {
result.add(first);
}
} else if (isInt(first) && isInt(second)) {
int i1 = Integer.parseInt(first);
int i2 = Integer.parseInt(second);
if (i == 0 || i == 1) {
// second or minute
OptionalInt findFirst = ThreadLocalRandom.current().ints(i1, i2 + 1).limit(1).findFirst();
if (findFirst.isPresent()) {
result.add(Integer.toString(findFirst.getAsInt()));
}
} else if (i == 2) {
// hour
OptionalInt findFirst = ThreadLocalRandom.current().ints(i1, i2 + 1).limit(1).findFirst();
if (findFirst.isPresent()) {
result.add(Integer.toString(findFirst.getAsInt()));
}
} else {
result.add(first);
}
} else {
result.add(p);
}
}
}
return String.join(" ", result);
}
private static boolean isInt(String s) {
try {
Integer.parseInt(s);
} catch (NumberFormatException nfe) {
return false;
}
return true;
}
public void run() {
if (!rm.getConfiguration().getBoolean(Settings.BACKUP_SCHEDULER, false)) {
log.warn("Backup is disabled in configuration");
return;
}
try {
String[] names = scheduler.getJobNames("backupGroup");
int index = 1;
if (names.length > 0) {
// this only works for up to 10 jobs in this group
index = Integer.parseInt(names[names.length - 1]);
index++;
}
String jobIndex = String.valueOf(index);
job = new JobDetail(jobIndex, "backupGroup", BackupJob.class);
job.getJobDataMap().put("rm", this.rm);
job.getJobDataMap().put("gzip", this.gzip);
job.getJobDataMap().put("includeFiles", this.includeFiles);
job.getJobDataMap().put("deleteAfter", this.deleteAfter);
job.getJobDataMap().put("maintenance", this.maintenance);
job.getJobDataMap().put("upperLimit", this.upperLimit);
job.getJobDataMap().put("lowerLimit", this.lowerLimit);
job.getJobDataMap().put("expiresAfterDays", this.expiresAfterDays);
job.getJobDataMap().put("format", this.format);
job.getJobDataMap().put("simple", this.simple);
CronTrigger trigger = new CronTrigger("trigger" + jobIndex, "backupGroup", jobIndex, "backupGroup", this.cronExpression);
scheduler.addJob(job, true);
scheduler.scheduleJob(trigger);
scheduler.start();
} catch (ParseException | SchedulerException e) {
log.error(e.getMessage());
}
}
// public void stop() {
// try {
// scheduler.standby();
// } catch (SchedulerException e) {
// log.error(e.getMessage());
// e.printStackTrace();
// }
// }
//
// public void start() {
// try {
// scheduler.start();
// } catch (SchedulerException e) {
// log.error(e.getMessage());
// }
// }
public boolean delete() {
try {
if (job != null) {
log.info("Deleting backup job");
scheduler.deleteJob(job.getName(), job.getGroup());
job = null;
}
} catch (SchedulerException e) {
log.error(e.getMessage());
return false;
}
return true;
}
}