The problem
Required is a service, initialized with a pre-set time span (for example 3 seconds), that receives work (objects) over time and each time 3 seconds have passed with no new work, it releases all the accumulated work as a collection.
Each time a collection of items is released, that collection should be processed by a user specified processor (see ElementsProcessor below).
Example
Assume the pre-set time span is 3000ms and the received work is:
1 -- 1000ms -- 2 -- 1000ms -- 3 -- 1000ms -- 4 -- 3500ms -- 5
The result should be items 1,2,3,4 released together 3 seconds after item 4 arrival, and item 5 will be released later, once 3 seconds with no new work have passed.
My temporary solution
I attempted to implement this using ScheduledFuture but encountered a problem because it seems one can tell from it whether a task completed or not but not whether it started or not. Same issue with FutureTask. And one of the constraints I have is that work, once started, shouldn't be interrupted.
So I had to resort to some ugly hack looking at the delay of the ScheduledFuture. So I believe I might be doing something wrong and I'd love to hear about better ideas.
For reference I put my code (2 files) here:
import java.util.Collection;
/**
* @param <E> element type.
* @param <V> the result type of method {@link #process(Collection)}.
*/
public interface ElementsProcessor<E, V>
{
public V process(Collection<E> elements);
}
and
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Deque;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
public class WorkAggregator<E, V>
{
private final ElementsProcessor<E, V> elementsProcessor;
private final long waitPeriod;
private Deque<E> elements = new ArrayDeque<>();
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private ScheduledFuture<V> scheduledFuture;
public WorkAggregator(ElementsProcessor<E, V> elementsProcessor, long waitPeriod)
{
this.elementsProcessor = elementsProcessor;
this.waitPeriod = waitPeriod;
}
public synchronized void add(E element)
{
attemptToCancelScheduledWork();
elements.add(element);
print("In add(), added element " + element + ", elements=" + elements);
Handler<E, V> handler = new Handler<E, V>(elementsProcessor, elements);
scheduledFuture = scheduler.schedule(handler, waitPeriod, MILLISECONDS);
}
private void attemptToCancelScheduledWork()
{
if (scheduledFuture == null)
return;
long delay = scheduledFuture.getDelay(MILLISECONDS);
print("In attemptToCancelScheduledWork(), scheduledFuture delay=" + delay);
if (delay < 50 || !scheduledFuture.cancel(false)) // delay < 50 - ugly hack!
elements = new ArrayDeque<>(); // if we weren't able to cancel work we need to remove it from future processing
}
public synchronized void flush()
{
attemptToCancelScheduledWork();
if (!elements.isEmpty())
elementsProcessor.process(elements);
}
public synchronized void close() throws InterruptedException
{
print("In close() elements=" + elements);
flush();
print("In close() scheduler=" + scheduler);
scheduler.shutdown();
scheduler.awaitTermination(1000, MILLISECONDS);
}
private static void print(String s)
{
System.out.format("%tT %-16s %s\n", Calendar.getInstance(), Thread.currentThread().getName(), s);
}
public static void main(String... args) throws InterruptedException
{
long[] sleepTimes = { 1000, 1000, 1000, 3500, 3500, 1000, 3500, 1000, 1000 }; // each value is the sleep AFTER the element is added
ElementsProcessor<String, Boolean> elementsProcessor = new ElementsProcessor<String, Boolean>()
{
@Override
public Boolean process(Collection<String> elements)
{
print("Starting processing " + elements);
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
print("Finished processing " + elements);
return null;
}
};
WorkAggregator<String, Boolean> workAggregator = new WorkAggregator<>(elementsProcessor, 3000);
print("Start, sleep-times=" + Arrays.toString(sleepTimes));
for (int i = 1; i <= sleepTimes.length; i++)
{
workAggregator.add(String.valueOf(i));
try { Thread.sleep(sleepTimes[i - 1]); } catch (InterruptedException e) { e.printStackTrace(); }
}
workAggregator.close();
print("The end");
}
}
class Handler<E, V> implements Callable<V>
{
private final ElementsProcessor<E, V> elementsProcessor;
private final Deque<E> elements;
Handler(ElementsProcessor<E, V> elementsProcessor, Deque<E> elements)
{
this.elementsProcessor = elementsProcessor;
this.elements = elements;
}
@Override
public V call() throws Exception
{
return elementsProcessor.process(elements);
}
}
The output from main() is:
19:58:01 main Start, sleep-times=[1000, 1000, 1000, 3500, 3500, 1000, 3500, 1000, 1000]
19:58:01 main In add(), added element 1, elements=[1]
19:58:02 main In attemptToCancelScheduledWork(), scheduledFuture delay=1999
19:58:02 main In add(), added element 2, elements=[1, 2]
19:58:03 main In attemptToCancelScheduledWork(), scheduledFuture delay=1999
19:58:03 main In add(), added element 3, elements=[1, 2, 3]
19:58:04 main In attemptToCancelScheduledWork(), scheduledFuture delay=1999
19:58:04 main In add(), added element 4, elements=[1, 2, 3, 4]
19:58:07 pool-1-thread-1 Starting processing [1, 2, 3, 4]
19:58:08 main In attemptToCancelScheduledWork(), scheduledFuture delay=-500
19:58:08 main In add(), added element 5, elements=[5]
19:58:08 pool-1-thread-1 Finished processing [1, 2, 3, 4]
19:58:11 pool-1-thread-1 Starting processing [5]
19:58:11 main In attemptToCancelScheduledWork(), scheduledFuture delay=-500
19:58:11 main In add(), added element 6, elements=[6]
19:58:12 pool-1-thread-1 Finished processing [5]
19:58:12 main In attemptToCancelScheduledWork(), scheduledFuture delay=1999
19:58:12 main In add(), added element 7, elements=[6, 7]
19:58:15 pool-1-thread-1 Starting processing [6, 7]
19:58:16 main In attemptToCancelScheduledWork(), scheduledFuture delay=-500
19:58:16 main In add(), added element 8, elements=[8]
19:58:16 pool-1-thread-1 Finished processing [6, 7]
19:58:17 main In attemptToCancelScheduledWork(), scheduledFuture delay=1999
19:58:17 main In add(), added element 9, elements=[8, 9]
19:58:18 main In close() elements=[8, 9]
19:58:18 main In attemptToCancelScheduledWork(), scheduledFuture delay=1999
19:58:18 main Starting processing [8, 9]
19:58:19 main Finished processing [8, 9]
19:58:19 main In close() scheduler=java.util.concurrent.ScheduledThreadPoolExecutor@3d4eac69[Running, pool size = 1, active threads = 0, queued tasks = 1, completed tasks = 8]
19:58:19 main The end