package wowodc.background.tasks;
import java.text.DecimalFormat;
import java.text.Format;
import java.util.Iterator;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import wowodc.background.utilities.Utilities;
import wowodc.eof.ResultItem;
import wowodc.eof.TaskInfo;
import com.webobjects.eocontrol.EOEditingContext;
import com.webobjects.eocontrol.EOGlobalID;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSTimestamp;
import er.extensions.concurrency.ERXExecutorService;
import er.extensions.concurrency.ERXTask;
import er.extensions.concurrency.IERXPercentComplete;
import er.extensions.concurrency.IERXStoppable;
import er.extensions.eof.ERXEOControlUtilities;
import er.extensions.foundation.IERXStatus;
/**
* A task that <em>returns</em> an EOGlobalID result.
*
* What does this demonstration task do?
*
* A "TaskInfo" entity is created for every run of this task.
* Its attributes include
* <ul>
* <li>startNumber
* <li>endNumber
* <li>startTime
* <li>endTime
* </ul>
*
* For a random amount of time between 5 and 15 seconds, and
* starting at a random number, this task begins looking for prime numbers
* for every
*
* @author kieran
*/
public class T05MultiThreadedEOFTask extends ERXTask<EOGlobalID> implements Callable<EOGlobalID>, IERXStatus , IERXPercentComplete, IERXStoppable {
private static final Logger log = LoggerFactory.getLogger(T05MultiThreadedEOFTask.class);
// Duration of the example task in milliseconds
// Random between 5 and 15 seconds
private final long _taskDuration;
// Task elapsed time in milliseconds
private long _elapsedTime = 0l;
// Value between 0.0 and 1.0 indicating the task's percentage complete
private double _percentComplete = 0.0d;
// A message indicating current status
private String _status = "Starting...";
private final long _startNumber = 0;
private long _endNumber;
// Base quantity - we use this plus a random amount up to the same quantity
// just to ensure slightly varied processing times per task thread.
// Otherwise all threads are starting and stopping at the same time
// and it looks too fake :-)
private final int _childBatchBaseQuantity = 2000;
// Volatile since it is being updated from child threads
private volatile long _count = 0;
// We assign an ID to each child just for demo purposes so that users can
// see that different child tasks are being started and finished in the App Monitor page.
// This value is passed to child task in constructor and used in toString.
private int _nextChildIDValue = 1;
private volatile boolean _isStopped = false;
// Just so we can have this for toString for the AppMonitor display
private int _parentTaskPrimaryKey = -1;
// Lazy initialization of static variable.
// This prevents more than N threads in this pool even if multiple instances of
// this task is run, which may or may not be what you want.
// If you want a pool per task, then use a lazy initialized instance variable
private static class ChildTaskPool {
final static ExecutorService EXECUTOR_SERVICE = ERXExecutorService.newFiniteThreadPool(4);
}
public T05MultiThreadedEOFTask() {
_taskDuration = 15000;
}
/**
* Use a demo duration parameter rather than default random demo duration.
*
* @param demoTaskDuration duration in milliseconds
*/
public T05MultiThreadedEOFTask(long demoTaskDuration) {
_taskDuration = demoTaskDuration;
}
private EOGlobalID _resultGid;
@Override
public EOGlobalID _call() throws Exception {
// Start at zero to gauge performance rate with different numbers of threads and OSCs
//_startNumber = Utilities.newStartNumber();
_elapsedTime = 0;
Format wholeNumberFormatter = new DecimalFormat("#,##0");
long startTime = System.currentTimeMillis();
EOEditingContext ec = newEditingContext();
ec.lock();
try {
// Array for monitoring completed tasks to ensure normal completion
NSMutableArray<Future<?>> childFutures = new NSMutableArray<Future<?>>();
// Create the new TaskInfo
TaskInfo taskInfo = ERXEOControlUtilities.createAndInsertObject(ec, TaskInfo.class);
// Task start time
taskInfo.setStartTime(new NSTimestamp(startTime));
taskInfo.setStartNumber(_startNumber);
taskInfo.setDuration(_taskDuration);
ec.saveChanges();
_resultGid = ec.globalIDForObject(taskInfo);
_parentTaskPrimaryKey = (Integer) taskInfo.rawPrimaryKey();
// Initialize loop variables
long childTaskStartNumber = _startNumber;
int incrementQuantity = _childBatchBaseQuantity + Utilities.sharedRandom().nextInt(_childBatchBaseQuantity);
long childTaskEndNumber = childTaskStartNumber + incrementQuantity;
// Loop for a period of time
while (_elapsedTime < _taskDuration && !_isStopped) {
ChildPrimeTask childTask = new ChildPrimeTask(_nextChildIDValue, _resultGid, childTaskStartNumber, childTaskEndNumber);
_nextChildIDValue++;
boolean isRejected = true;
while (isRejected && !ChildTaskPool.EXECUTOR_SERVICE.isShutdown() && !_isStopped) {
try {
Future<?> future = ChildTaskPool.EXECUTOR_SERVICE.submit(childTask);
log.info("Submitted task corresponding to {}", future);
isRejected = false;
childFutures.add(future);
// For the sake of demo, we assume all child tasks complete their work.
_endNumber = childTaskEndNumber;
} catch (RejectedExecutionException e) {
try {
Thread.sleep(2000);
removeCompletedFutures(childFutures);
} catch (InterruptedException e1) {
stop();
}
}
}
childTaskStartNumber = childTaskEndNumber + 1;
incrementQuantity = _childBatchBaseQuantity + Utilities.sharedRandom().nextInt(_childBatchBaseQuantity * 2);
childTaskEndNumber = childTaskStartNumber + incrementQuantity;
_elapsedTime = System.currentTimeMillis() - startTime;
// Update progress variables
_percentComplete = (double)(_elapsedTime) / (double)_taskDuration;
_status = wholeNumberFormatter.format(_count) + " numbers checked for prime qualification";
}
if (_isStopped) {
_status = "Stopped";
}
// Wait for all child tasks to finish
while (childFutures.count() > 0) {
removeCompletedFutures(childFutures);
Thread.sleep(1000);
}
// Complete the stats
// Refresh it since the object has been already updated (its relationship) and saved on ChildThreads
ERXEOControlUtilities.refreshObject(taskInfo);
taskInfo.setEndNumber(_endNumber);
taskInfo.setEndTime(new NSTimestamp());
taskInfo.setWorkflowState(TaskInfo.WORKFLOW_PRIME_CHECKED);
ec.saveChanges();
_resultGid = ec.globalIDForObject(taskInfo);
} finally {
ec.unlock();
}
return _resultGid;
}
/**
* Removes completed futures from the futures array.
*
* @param futures array of futures
*/
public void removeCompletedFutures(NSMutableArray<Future<?>> futures) {
Iterator<Future<?>> iterator = futures.iterator();
while (iterator.hasNext()) {
Future<?> future = iterator.next();
if (future.isDone()) {
// Before removal, we can take this opportunity to check for errors in the child tasks
try {
Object result = future.get();
} catch (Exception e) {
// An exception here means the task did not complete normally
throw new RuntimeException("Unexpected exception occurred in ChildTask", e);
}
iterator.remove();
}
}
}
/* (non-Javadoc)
* @see er.extensions.concurrency.IERXPercentComplete#percentComplete()
*/
public Double percentComplete() {
return _percentComplete;
}
/* (non-Javadoc)
* @see er.extensions.foundation.IERXStatus#status()
*/
public String status() {
return _status;
}
/* (non-Javadoc)
* @see er.extensions.concurrency.IERXStoppable#stop()
*/
public void stop() {
log.info("The task was stopped by the user.");
_isStopped = true;
_status = "Stopping";
}
@Override
public String toString() {
StringBuilder b = new StringBuilder();
b.append(this.getClass().getSimpleName());
if (_parentTaskPrimaryKey > 0) {
b.append(": #").append(_parentTaskPrimaryKey);
}
return b.toString();
}
/**
* A child task that will be used to process a batch of numbers in its own thread.
*
* Note we declare this as a non-static inner class so that the child thread can update the parent thread _count (for demo of volatile)
*
* @author kieran
*/
private class ChildPrimeTask extends ERXTask implements Runnable, IERXStatus, IERXPercentComplete {
private final int _childID;
private EOGlobalID _childTaskInfoGID = null;
private final long _childFromNumber;
private final long _childToNumber;
private long _childCurrentNumber;
public ChildPrimeTask(int childID, EOGlobalID taskInfoGID, long fromNumber, long toNumber) {
_childID = childID;
_childTaskInfoGID = taskInfoGID;
_childFromNumber = fromNumber;
_childToNumber = toNumber;
// Starting value
_childCurrentNumber = fromNumber;
}
public Double percentComplete() {
return Double.valueOf((_childCurrentNumber - _childFromNumber + 1) / (_childToNumber - _childFromNumber + 1));
}
public String status() {
return "Checking " + _childCurrentNumber + " in range " + _childFromNumber + " - " + _childToNumber;
}
@Override
public void _run() {
EOEditingContext ec = newEditingContext();
ec.lock();
try {
log.info("Started child in {} with OSC {}", Thread.currentThread().getName(), ec.parentObjectStore());
TaskInfo taskInfo = (TaskInfo) ec.faultForGlobalID(_childTaskInfoGID, ec);
while (_childCurrentNumber <= _childToNumber) {
ResultItem resultItem = ERXEOControlUtilities.createAndInsertObject(ec, ResultItem.class);
resultItem.setTaskInfo(taskInfo);
resultItem.setNumberToCheck(_childCurrentNumber);
if (Utilities.isPrime(_childCurrentNumber)) {
log.info("==>> {} is a PRIME number.", _childCurrentNumber);
resultItem.setIsPrime(Boolean.TRUE);
} else {
log.debug("{} is not a prime number but is a COMPOSITE number.", _childCurrentNumber);
resultItem.setIsPrime(Boolean.FALSE);
}
// We could save changes once per child task, but let's do this to keep EOF busy for the demo.
ec.saveChanges();
// Update our number to check
_childCurrentNumber++;
// Update parent task count statistic
_count++;
}
} finally {
ec.unlock();
}
}
//
private String _toString = null;
@Override
public String toString() {
if (_toString == null) {
// We cache it since it will not change.
StringBuilder b = new StringBuilder();
b.append("ChildTask: #");
b.append(_childID);
b.append(", Parent ID=" + _parentTaskPrimaryKey);
b.append(", From=" + _childFromNumber);
b.append(", To=" + _childToNumber);
_toString = b.toString();
}
return _toString;
}
}
}