/* ************************************************************************
#
# DivConq
#
# http://divconq.com/
#
# Copyright:
# Copyright 2014 eTimeline, LLC. All rights reserved.
#
# License:
# See the license.txt file in the project's top-level directory for details.
#
# Authors:
# * Andy White
#
************************************************************************ */
package divconq.scheduler;
import java.util.HashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.locks.ReentrantLock;
import org.joda.time.DateTimeUtils;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.joda.time.LocalTime;
import org.joda.time.Period;
import org.joda.time.ReadableInstant;
import divconq.hub.Hub;
import divconq.hub.ISystemWork;
import divconq.hub.SysReporter;
import divconq.lang.op.FuncResult;
import divconq.lang.op.OperationContext;
import divconq.lang.op.OperationResult;
import divconq.log.Logger;
import divconq.scheduler.common.CommonSchedule;
import divconq.scheduler.limit.LimitHelper;
import divconq.struct.ListStruct;
import divconq.util.StringUtil;
import divconq.work.Task;
import divconq.work.WorkPool;
import divconq.xml.XElement;
/**
* Handles scheduling application tasks.
*
* @author Andy White
*
*/
// TODO add tracing settings
public class Scheduler {
// the first node in the list of scheduled nodes - the head of the list is moved
// forward as the items on the list get scheduled. List is single linked list.
protected SchedulerNode first = null;
// how many are currently in the linked list?
protected long nodeCnt = 0;
// lock during adding and removing of scheduled work. all add and remove operations
// are thread safe
protected ReentrantLock lock = new ReentrantLock();
protected ScheduledFuture<?> clock = null;
protected HashMap<String,LimitHelper> batches = new HashMap<String,LimitHelper>();
protected ISchedulerDriver driver = null;
public LimitHelper getBatch(String name) {
return this.batches.get(name);
}
public void init(OperationResult or, XElement config) {
// TODO
if (config != null) {
for (XElement el : config.selectAll("Batch")) {
XElement bel = el.find("Limits");
String name = el.getAttribute("Name");
if (StringUtil.isNotEmpty(name) && (bel != null)) {
LimitHelper h = new LimitHelper();
h.init(bel);
this.batches.put(name, h);
}
}
// setup the provider of the work queue
String classname = config.getAttribute("InterfaceClass");
if (StringUtil.isEmpty(classname))
classname = "divconq.scheduler.LocalSchedulerDriver";
if (StringUtil.isNotEmpty(classname)) {
Object impl = Hub.instance.getInstance(classname);
if ((impl == null) || !(impl instanceof ISchedulerDriver)) {
or.errorTr(227, classname);
return;
}
this.driver = (ISchedulerDriver)impl;
this.driver.init(or, config);
}
}
}
public void start(OperationResult or) {
Hub.instance.getClock().addFastSystemWorker(new ISystemWork() {
@Override
public void run(SysReporter reporter) {
Scheduler.this.execute();
}
@Override
public int period() {
return 1;
}
});
Hub.instance.getClock().addSlowSystemWorker(new ISystemWork() {
@Override
public void run(SysReporter reporter) {
reporter.setStatus("before schedule update");
// TODO check for updates to the schedule
reporter.setStatus("after schedule update");
}
@Override
public int period() {
return 5;
}
});
if (this.driver != null) {
FuncResult<ListStruct> loadres = this.driver.loadSchedule();
if (loadres.isNotEmptyResult()) {
loadres.getResult().recordStream().forEach(rec -> {
XElement schedule = rec.getFieldAsXml("Schedule");
ISchedule sched = "CommonSchedule".equals(schedule.getName()) ? new CommonSchedule() : new SimpleSchedule();
sched.init(schedule);
sched.setTask(new Task()
.withId(Task.nextTaskId("ScheduleLoader"))
.withTitle("Scheduled Task Loader: " + rec.getFieldAsString("Title"))
.withRootContext()
.withWork(trun -> {
FuncResult<ScheduleEntry> loadres2 = Scheduler.this.driver.loadEntry(rec.getFieldAsString("Id"));
if (loadres2.isNotEmptyResult()) {
ScheduleEntry entry = loadres2.getResult();
entry.setSchedule(sched);
entry.submit(trun);
}
// we are done, no need to wait
trun.complete();
})
);
Scheduler.this.addNode(sched);
});
}
}
}
public void stop(OperationResult or) {
// TODO
if (this.clock != null)
this.clock.cancel(false);
}
public long size() {
return this.nodeCnt;
}
// the scheduler runs on its own thread, this is the code that starts and runs the scheduler
private void execute() {
OperationContext.useHubContext();
long loadcnt = this.nodeCnt;
this.lock.lock();
try {
SchedulerNode curr = this.first;
long now = DateTimeUtils.currentTimeMillis();
WorkPool p = Hub.instance.getWorkPool();
//System.out.println(new DateTime() + " - scheduler - " + new DateTime(now) + " > " + new DateTime(curr.when));
while ((curr != null) && (curr.when <= now)) {
//System.out.println("Scheduled node: " + curr.scheduler.isCanceled());
if (!curr.scheduler.isCanceled())
p.submit(curr.task, curr.scheduler);
SchedulerNode old = curr;
curr = old.next;
this.first = curr;
this.nodeCnt--;
// reduce references for better GC
old.next = null;
old.scheduler = null;
old.task = null;
}
loadcnt = this.nodeCnt;
}
catch(Exception x) {
// TODO trace/log
}
finally {
this.lock.unlock();
}
// it is possible due to race conditions to get a mis-ordered value in the counter
// a) it doesn't matter 99.99999999% of the time, b) we cannot afford to do this in the lock
Hub.instance.getCountManager().allocateSetNumberCounter("dcSchedulerLoad", loadcnt);
}
// add a work unit to run (almost) immediately - much the same as directly adding to the thread pool
// any work unit submitted to the scheduler (or to any thread pool) will become owned by the
// scheduler (or thread pool).
public ISchedule runNow(Task work) {
return this.addNode(new SimpleSchedule(work, DateTimeUtils.currentTimeMillis(), 0));
}
// run the work unit once in Sec seconds from now
public ISchedule runIn(Task work, int secs) {
return this.addNode(new SimpleSchedule(work, DateTimeUtils.currentTimeMillis() + (1000 * secs), 0));
}
// run the work unit once at the specified time. If less than now then submits immediately
// if in the distant future, it may not be run if the process is terminated. adding working
// to the schedule is no guarantee work will be run.
public ISchedule runAt(Task work, ReadableInstant time) {
return this.addNode(new SimpleSchedule(work, time.getMillis(), 0));
}
public ISchedule runAt(Task work, LocalDate date, Period period) {
LocalDateTime ldt = date.toLocalDateTime(new LocalTime(0, 0).plus(period));
return this.runAt(work, ldt.toDateTime());
}
// run the work unit repeatedly, every Secs seconds - note scheduler will not own work, you have to keep track of it
public ISchedule runEvery(Task work, int secs) {
return this.addNode(new SimpleSchedule(work, DateTimeUtils.currentTimeMillis() + (1000 * secs), secs));
}
public ISchedule addNode(ISchedule schedule) {
long when = schedule.when();
if (when < 0)
return null;
if ((schedule.task() == null) || (schedule.task().getContext() == null)) {
Logger.warn("Schedule missing task or context: " + schedule.task());
return null;
}
long loadcnt = this.nodeCnt;
this.lock.lock();
try {
SchedulerNode snode = new SchedulerNode();
snode.task = schedule.task();
snode.when = when;
snode.scheduler = schedule;
SchedulerNode curr = this.first;
SchedulerNode last = null;
this.nodeCnt++;
// loop through the scheduling linked list and find the right place to insert
// the new TScheduleNode
while (curr != null) {
if (snode.when < curr.when) {
snode.next = curr;
if (last == null)
this.first = snode;
else
last.next = snode;
return schedule;
}
last = curr;
curr = curr.next;
}
// none found then add to end
if (last == null)
this.first = snode;
else
last.next = snode;
loadcnt = this.nodeCnt;
}
catch(Exception x) {
// TODO
return null;
}
finally {
this.lock.unlock();
}
// it is possible, due to race conditions, to get a mis-ordered value in the counter
// a) it doesn't matter 99.99999999% of the time, b) we cannot afford to do this in the lock
Hub.instance.getCountManager().allocateSetNumberCounter("dcSchedulerLoad", loadcnt);
return schedule;
}
public class SchedulerNode {
protected SchedulerNode next = null;
protected long when = 0;
protected Task task = null;
protected ISchedule scheduler = null;
}
public void dump() {
SchedulerNode curr = this.first;
int cnt = 0;
while (curr != null) {
//Logger.info(" + " + curr.task.getTitle());
cnt++;
curr = curr.next;
}
Logger.info("Outstanding schedule nodes: " + cnt);
}
}