/* ************************************************************************
#
# 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.hub;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import org.joda.time.DateTimeUtils;
import org.joda.time.DateTimeZone;
import org.joda.time.ReadableInstant;
import divconq.lang.op.OperationContext;
import divconq.lang.op.OperationResult;
import divconq.log.Logger;
import divconq.util.BasicSettingsObfuscator;
import divconq.util.ISettingsObfuscator;
import divconq.util.StringUtil;
import divconq.util.TimeUtil;
import divconq.xml.XElement;
/**
* Clock has three uses:
*
* 1) Control the date time settings - DivConq's DateTime are based on Joda's library.
* With Joda it is possible to set the effective timezone and the current date time
* itself. Through configuration the Clock can be made to enforce a fixed time,
* a sped up time (clock moving faster than 1 second per system second), or a regular
* time. Also the time zone can be set.
* 2) There are some methods for scheduling work on another thread. However,
* typically one should use {@link divconq.scheduler.Scheduler} for task scheduling.
* 3) DivConq supports obfuscated (encrypted) settings in the config files. This class
* provides access to the feature for encrypting and decrypting config settings.
*
* @author Andy
*
*/
public class Clock {
class ClockThreadFactory implements ThreadFactory {
protected String name = null;
protected boolean daemon = true;
public ClockThreadFactory(String name, boolean daemon) {
this.name = name;
this.daemon = daemon;
}
public Thread newThread(Runnable r) {
Thread t = new Thread(r, this.name);
t.setDaemon(this.daemon);
// TODO consider this option
//t.setUncaughtExceptionHandler(eh);
return t;
}
}
// periodic internal intervals, not for use with scheduling real tasks like dcTasks
// does not preserve user context, uses system clock not divconq clock
// for the fast clock which run only tasks that are very quick to execute, < 10 ms total per entire run
protected ScheduledExecutorService fastscheduler = Executors.newSingleThreadScheduledExecutor(
new ClockThreadFactory("FastScheduler", false) // we need something to keep us alive in Service mode - fast is not a daemon
);
protected ScheduledFuture<?> fastclock = null;
protected List<ISystemWork> fastsyswork = new CopyOnWriteArrayList<>();
protected int fastsysworkcycle = 1;
protected SysReporter fastreporter = new SysReporter();
// for the clock itself
// how many seconds to travel per system second - only enabled if using CoreDate
protected long speed = 0;
protected XElement config = null;
protected ISettingsObfuscator obfus = null;
protected DateTimeZone serverTimeZone = null;
// for the slow clock - which is still fairly fast - execute system tasks that take less < 100ms total per entire run.
// in a bad case, if it took a little longer not a problem...but generally runs fastish
protected ScheduledExecutorService slowscheduler = Executors.newSingleThreadScheduledExecutor(
new ClockThreadFactory("SlowScheduler", true)
);
protected ScheduledFuture<?> slowclock = null;
protected List<ISystemWork> slowsyswork = new CopyOnWriteArrayList<>();
protected int slowsysworkcycle = 1;
protected SysReporter slowreporter = new SysReporter();
public SysReporter getFastSysReporter() {
return this.fastreporter;
}
public SysReporter getSlowSysReporter() {
return this.slowreporter;
}
public DateTimeZone getServerTimeZone() {
return this.serverTimeZone;
}
public Clock() {
this.serverTimeZone = DateTimeZone.getDefault();
}
/**
* Called from Hub.start this method configures the clock features.
*
* @param or logger for the initialize
* @param config xml holding the configuration
*/
public void init(OperationResult or, XElement config) {
this.config = config;
if (config != null) {
String timeZone = config.getAttribute("TimeZone", "UTC");
if (StringUtil.isNotEmpty(timeZone))
DateTimeZone.setDefault(DateTimeZone.forID(timeZone));
String coreDate = config.getAttribute("CoreDate");
if (StringUtil.isNotEmpty(coreDate)) {
this.setAppClock(TimeUtil.parseDateTime(coreDate));
}
this.speed = StringUtil.parseInt(config.getAttribute("Speed"), 0);
String obclass = config.getAttribute("TimerClass");
if (StringUtil.isNotEmpty(obclass)) {
try {
Class<?> obc = this.getClass().getClassLoader().loadClass(obclass);
this.obfus = (ISettingsObfuscator) obc.newInstance();
}
catch (Exception x) {
Logger.error("Unable to load custom Settings Obfuscator class: " + obclass, "Code", "207");
}
}
}
else {
DateTimeZone.setDefault(DateTimeZone.UTC);
}
if (this.obfus == null)
this.obfus = new BasicSettingsObfuscator();
this.obfus.init(config);
}
/**
* If the framework time is configured to use an accelerated clock, this method gets
* that accelerated clock going.
*
* @param or logger for the start up
*/
public void start(OperationResult or) {
// we check our app schedule ever 1 (or so) system seconds
this.slowclock = this.slowscheduler.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
OperationContext.useHubContext();
int cycle = Clock.this.slowsysworkcycle;
Clock.this.slowreporter.setStatus("Slow Sys starting cycle: " + cycle);
for (ISystemWork work : Clock.this.slowsyswork) {
try {
int period = work.period();
if (period > 300)
period = 300; // max allowed
if (cycle % period == 0) {
Clock.this.slowreporter.setStatus("before sys work: " + work.getClass());
work.run(Clock.this.slowreporter);
Clock.this.slowreporter.setStatus("after sys work: " + work.getClass());
}
}
catch(Exception x) {
System.out.println("sys scheduler error: " + x);
}
}
Clock.this.slowreporter.setStatus("Slow Sys finished cycle: " + cycle);
cycle++;
if (cycle > 300)
cycle = 1;
Clock.this.slowsysworkcycle = cycle;
}
}, 1, 1, TimeUnit.SECONDS);
// we check our app schedule ever 1 (or so) system seconds
this.fastclock = this.fastscheduler.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
OperationContext.useHubContext();
int cycle = Clock.this.fastsysworkcycle;
Clock.this.fastreporter.setStatus("Fast Sys starting cycle: " + cycle);
for (ISystemWork work : Clock.this.fastsyswork) {
try {
int period = work.period();
if (period > 300)
period = 300; // max allowed
if (cycle % period == 0) {
Clock.this.fastreporter.setStatus("before sys work: " + work.getClass());
work.run(Clock.this.fastreporter);
Clock.this.fastreporter.setStatus("after sys work: " + work.getClass());
}
}
catch(Exception x) {
System.out.println("sys scheduler error: " + x);
}
}
Clock.this.fastreporter.setStatus("Fast Sys finished cycle: " + cycle);
cycle++;
if (cycle > 300)
cycle = 1;
Clock.this.fastsysworkcycle = cycle;
}
}, 1, 1, TimeUnit.SECONDS);
if (this.speed != 0)
this.addFastSystemWorker(new ISystemWork() {
@Override
public int period() {
return 1;
}
@Override
public void run(SysReporter reporter) {
reporter.setStatus("Before set date time");
DateTimeUtils.setCurrentMillisFixed(DateTimeUtils.currentTimeMillis() + (Clock.this.speed * 1000));
reporter.setStatus("After set date time");
}
});
}
/**
* Terminate tasks being run on the Clock's scheduler. Called by
* Hub.stop during the final steps of shutdown.
*
* @param or logger for the shutdown
*/
public void stop(OperationResult or) {
if (this.slowclock != null)
this.slowclock.cancel(false);
if (this.fastclock != null)
this.fastclock.cancel(false);
this.slowscheduler.shutdown();
this.fastscheduler.shutdown();
try {
this.slowscheduler.awaitTermination(60, TimeUnit.SECONDS);
this.fastscheduler.awaitTermination(60, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
// TODO
}
this.slowscheduler.shutdownNow();
this.fastscheduler.shutdownNow();
}
// your period must be 300 or less or it will never occur
// you must be very careful with this, problems in your sysworker will lock down the whole hub process
public void addSlowSystemWorker(ISystemWork v) {
if (v == null)
return;
this.slowsyswork.add(v);
}
/**
* Typically use {@link divconq.scheduler.Scheduler} for task scheduling.
* However, when a task should run on the system clock rather than the
* app clock, these methods may be used. Use sparingly as these are run on
* a single threaded pool. These methods also do not preserve the TaskContext.
*
* @param v new work for the fast thread
*/
// your period must be 300 or less or it will never occur
// you must be very careful with this, problems in your sysworker will lock down the whole hub process
public void addFastSystemWorker(ISystemWork v) {
if (v == null)
return;
this.fastsyswork.add(v);
}
/**
* set framework's time to use the system time
*/
public void resetAppClock() {
DateTimeUtils.setCurrentMillisSystem();
}
/**
* set framework's time to use a fixed time
*
* @param time fixed time to use
*/
public void setAppClock(ReadableInstant time) {
DateTimeUtils.setCurrentMillisFixed(time.getMillis());
}
/**
* Typically use {@link divconq.scheduler.Scheduler} for task scheduling.
* However, when a task should run on the system clock rather than the
* app clock, these methods may be used. Use sparingly as these are run on
* a single threaded pool. These methods also do not preserve the TaskContext.
*
* @param command the code for the task to run
* @param delaySecs how many seconds until the task is run
* @return the token for canceling the task
*/
public ScheduledFuture<?> scheduleOnceInternal(Runnable command, long delaySecs) {
return this.slowscheduler.schedule(command, delaySecs, TimeUnit.SECONDS);
}
/**
* Typically use {@link divconq.scheduler.Scheduler} for task scheduling.
* However, when a task should run on the system clock rather than the
* app clock, these methods may be used. Use sparingly as these are run on
* a single threaded pool. These methods also do not preserve the TaskContext.
*
* @param command the code for the task to run
* @param delay how many "units" until the task is run
* @param unit time unit used with delay
* @return the token for canceling the task
*/
public ScheduledFuture<?> scheduleOnceInternal(Runnable command, long delay, TimeUnit unit) {
return this.slowscheduler.schedule(command, delay, unit);
}
/**
* Typically use {@link divconq.scheduler.Scheduler} for task scheduling.
* However, when a task should run on the system clock rather than the
* app clock, these methods may be used. Use sparingly as these are run on
* a single threaded pool. These methods also do not preserve the TaskContext.
*
* @param command the code for the task to run
* @param periodSecs how many seconds until the task is run first time, and then how long between runs
* @return the token for canceling the task
*/
public ScheduledFuture<?> schedulePeriodicInternal(Runnable command, long periodSecs) {
return this.slowscheduler.scheduleAtFixedRate(command, periodSecs, periodSecs, TimeUnit.SECONDS);
}
/**
* Typically use {@link divconq.scheduler.Scheduler} for task scheduling.
* However, when a task should run on the system clock rather than the
* app clock, these methods may be used. Use sparingly as these are run on
* a single threaded pool. These methods also do not preserve the TaskContext.
*
* @param command the code for the task to run
* @param initialDelay how many "units" until the task is first run
* @param period how many "units" between task runs, after first run
* @param unit time unit used with delay and period
* @return the token for canceling the task
*/
public ScheduledFuture<?> schedulePeriodicInternal(Runnable command, long initialDelay, long period, TimeUnit unit) {
return this.slowscheduler.scheduleAtFixedRate(command, initialDelay, period, unit);
}
/**
* Config file settings containing sensitive info may be obscured (encypted)
* to make it hard for a hacker to get anything useful from just a copy of the
* config file. How the settings are obscured is based on the Clock's settings.
*
* For more details on how this works @see divconq.util.ISettingsObfuscator
*
* @return the obfuscator used with this settings file
*/
public ISettingsObfuscator getObfuscator() {
return this.obfus;
}
}