Considered that one or more instances of a DOTS tasklet could be running at the same time, writing solid code might represent a challenge. Some points of attention:
- Each tasklet instantiates its own configuration (escheduled tasks with different time intervals included), in other words no global scope out of the box.
- DOTS doesn’t keep track of when any of a tasklet scheduled methods has run and whether it has ever run since DOTS task was started. It just guarantees a tasklet scheduled method will run at the given interval with starting date set at the time the Domino DOTS task was loaded. That is a problem. If we have a tasklet scheduled every 24 hours and DOTS task is restarted, the 24 hour mark will not be triggered when we expect it.
- Once the tasklet has run the instance is discarded and so are all the objects initialized before the actual execution. If the tasklet initialization is expensive this could constitute an inefficiency. However, scheduled tasks stay in memory for as long as the DOTS task runs.
- Recycling
lotus.domino
objects that are set and used by multiple instances at the same time will lead to pitfalls;lotus.domino
package wasn’t exactly developed having concurrency in mind.
For the above mentioned problems here some possible approaches:
The Preferences enum
The first challenge is to provide a single entry point for our tasklet configuration. All the various instances will then read from that very same instantiated configuration without the need of continuous reinitializations.
Storing the configuration in an enum
is a good solution. An enum
is implicitly a singleton. This means that one, and only one, instance of that configuration will exist at any time.
We can also provide the enum
with some flexibility when it comes to defining properties. Instead of a simple POJO, we could do something like this:
public enum Preferences {
INSTANCE;
public enum Property {
WATCHMAIL_FILE_PATH,
LOG_FILE_PATH,
LAST_RUN_TIMESTAMP(Long.class, 0L);
private final Class<?> type;
private final Object defaultValue;
private Property() {
this(String.class);
}
private Property(Class<?> c) {
this(c, "");
}
private Property(Class<?> c, Object v) {
this.type = c;
this.defaultValue = v;
}
public Class<?> getType() {
return type;
}
public Object getDefaultValue() {
return defaultValue;
}
}
private final Map<Property, Object> cache = new HashMap<Property, Object>();
private boolean runnable;
private boolean running;
public <T> T get(Property p, Class<T> cls) {
return cls.cast(cache.get(p));
}
public String getString(Property p) {
return get(p, String.class);
}
public Long getLong(Property p) {
return get(p, Long.class);
}
public boolean isRunnable() {
return runnable;
}
public boolean isRunning() {
return running;
}
public synchronized boolean start() {
if (!isRunning()) {
long now = System.currentTimeMillis();
long previous = getLong(Property.LAST_RUN_TIMESTAMP);
// Run only every 5 minutes and never sooner
if ((now - previous) > 1000 * 60 * 5) {
running = true;
return true;
}
}
return false;
}
public synchronized void stop() {
try {
long now = System.currentTimeMillis();
cache.put(Property.LAST_RUN_TIMESTAMP, (Long) now);
IEclipsePreferences preferences = Platform.getPreferences(Activator.PLUGIN_ID);
preferences.put(Property.LAST_RUN_TIMESTAMP.toString(), String.valueOf(now));
preferences.flush();
} catch (BackingStoreException e) {
e.printStackTrace();
}
running = false;
}
...
}
Each Property
is meant to be mapped to a configuration document field (eg. WATCHMAIL_FILE_PATH
to doc.pref_WATCHMAIL_FILE_PATH
). To read its value we would just need to call Preferences.INSTANCE.getString
, Preferences.INSTANCE.getLong
or the more generic Preferences.INSTANCE.get
.
start
and stop
methods make for the choice of toning down on concurrency. Yes, DOTS tasklet can run concurrently but that requires careful code design. In this case we want to avoid messing up with potential concurrency problems. Also we need to save a timestamp of last execution to deal with the lack of it in DOTS. In this example, the choice for storing that information falls onto the DOTS configuration document.
Preferences.Property
approach we can make it smarter:
public class Initializer extends AbstractConfigurationInitializer {
@Override
protected void initializeDefaultConfigurationParameters(Document paramDocument) throws NotesException {
for (Property p : Preferences.Property.values()) {
// com.ibm.dots.internal.preferences.IPrefConstants.FIELD_PREF_PREFIX
String fieldName = "pref_" + p.toString();
if (!paramDocument.hasItem(fieldName)) {
paramDocument.replaceItemValue(fieldName, p.getDefaultValue());
}
}
}
}
Caching the configuration document
I haven’t published the code to set the preferences yet. Keeping in mind that it’s always best to avoid working with lotus.domino
backend objects until it’s really necessary what we can do is to cache the configuration document in the Preferences
enum. It will be read only once and, advantageously enough, any modifications made to the underlying configuration document will not be reflected until we decide to refresh the tasklet cached configuration.
The method to access and load the configuration from the document:
public enum Preferences {
...
public synchronized void load() throws BackingStoreException {
IEclipsePreferences preferences = Platform.getPreferences(Activator.PLUGIN_ID);
/*
* Forcing syncying... sometimes, preferences don't get
* loaded... weird...
*/
preferences.sync();
for (Property p : Property.values()) {
String s = preferences.get(p.toString(), null);
switch (p) {
case LAST_RUN_TIMESTAMP:
try {
cache.put(p, Long.valueOf(s));
} catch (NumberFormatException e) {
cache.put(p, p.getDefaultValue());
}
break;
default:
cache.put(p, s);
break;
}
}
runnable = true;
}
...
}
The code references Activator.PLUGIN_ID
. That property is set as follows:
public class Activator implements BundleActivator {
public static final String PLUGIN_ID = Activator.class.getPackage().getName();
...
}
The result
With everything now in place, from our tasklet main class we can simply declare:
public class WatchmailTask extends AbstractServerTask {
...
@RunOnStart
public void runOnStart(IProgressMonitor monitor) {
try {
Preferences.INSTANCE.load();
} catch (Exception e) {
logException(e);
}
}
@Override
public void run(RunWhen runWhen, String[] args, IProgressMonitor progressMonitor) throws NotesException {
if (!Preferences.INSTANCE.isRunnable()) {
logMessage(this.getClass().getName() + " isn't runnable!");
return;
}
// Making sure no other instances are
// running at the moment
if (Preferences.INSTANCE.start()) {
logMessage(Preferences.INSTANCE.getString(Property.WATCHMAIL_FILE_PATH));
Preferences.INSTANCE.stop();
}
}
...
}
Preferences.INSTANCE.isRunnable
is a method whose bound property is evaluated to true
at the end of the load
method, granted it executes without errors, which you can trigger for, say, determining potential invalid configuration. Were that to be the case the tasklet would be prevented from running.