Smoothing out DOTS configuration and sharing it across instances

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.

in the article Providing configuration document for DOTS tasklets I explained how to hook up a configuration document. In that very same article I also explained how the field initialization occurs. By using the 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());
			}
		}
	}

}
granted, it may look overkill, even convoluted. But take it for what it is, just a possible approach.

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.

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.