/* ************************************************************************ # # 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.work; import java.util.Collection; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; import divconq.hub.Hub; import divconq.hub.ISystemWork; import divconq.hub.SysReporter; import divconq.lang.op.IOperationObserver; import divconq.lang.op.OperationResult; import divconq.log.Logger; import divconq.struct.ListStruct; import divconq.struct.RecordStruct; import divconq.xml.XAttribute; import divconq.xml.XElement; public class WorkPool implements ExecutorService { protected LinkedBlockingQueue<TaskRun> queue = new LinkedBlockingQueue<>(); protected Worker[] slots = null; protected ConcurrentHashMap<String, WorkBucket> buckets = new ConcurrentHashMap<>(); // when set, the pool will work only on N number of tasks until one of those tasks completes // where upon a new task from the general queue "queue" can be accepted // in other words, when fullsize == inprogress.size() we are full and do no additional processing // when this is null, we pull tasks off the general queue ANY time a thread has spare cycles protected AtomicLong totalThreadsCreated = new AtomicLong(); protected AtomicLong totalThreadsHung = new AtomicLong(); // based on timeout protected boolean shutdown = false; // TODO replace with state - starting, running, stopping, stopped protected long scheduleFreq = 150; protected boolean poolTrace = false; public void init(OperationResult or, XElement config) { int size = 16; if (config != null) { size = Integer.parseInt(config.getAttribute("Threads", "16")); this.scheduleFreq = Integer.parseInt(config.getAttribute("TimeoutChecker", "150")); this.poolTrace = "True".equals(config.getAttribute("Trace", "False")); } this.slots = new Worker[size]; // place the default bucket in - it might be overridden in config WorkBucket defbucket = new WorkBucket(); defbucket.init(or, new XElement("Bucket", new XAttribute("Name", "Default")), size); if (or.hasErrors()) return; this.buckets.put(defbucket.getName(), defbucket); if (config != null) { for (XElement bucketel : config.selectAll("Bucket")) { WorkBucket bucket = new WorkBucket(); bucket.init(or, bucketel, size); if (or.hasErrors()) return; this.buckets.put(bucket.getName(), bucket); } } Hub.instance.getCountManager().allocateSetNumberCounter("dcWorkPool_Buckets", this.buckets.size()); Hub.instance.getCountManager().allocateSetNumberCounter("dcWorkPool_Threads", size); } public void addBucket(WorkBucket bucket) { this.buckets.put(bucket.getName(), bucket); } public void removeBucket(String name) { this.buckets.remove(name); } public int threadCount() { return this.slots.length; } public long threadsCreated() { return this.totalThreadsCreated.get(); } public void incThreadsCreated() { long n = this.totalThreadsCreated.incrementAndGet(); Hub.instance.getCountManager().allocateSetNumberCounter("dcWorkPool_ThreadsCreated", n); } public long threadsHung() { return this.totalThreadsHung.get(); } public void incThreadsHung() { long n = this.totalThreadsHung.incrementAndGet(); Hub.instance.getCountManager().allocateSetNumberCounter("dcWorkPool_ThreadsHung", n); } public Collection<WorkBucket> getBuckets() { return this.buckets.values(); } // to work as an Executor @Override public void execute(Runnable command) { if (command instanceof TaskRun) this.submit((TaskRun)command); // useful for resume else { Task builder = new Task() .withSubContext() .withWork(command); this.submit(builder); } } public TaskRun submit(IWork work) { Task task = new Task() .withSubContext() .withWork(work); return this.submit(task); } public TaskRun submit(Task task) { TaskRun run = new TaskRun(task); this.submit(run); return run; } public TaskRun submit(IWork work, IOperationObserver observer) { Task task = new Task() .withSubContext() .withWork(work); return this.submit(task, observer); } public TaskRun submit(Task task, IOperationObserver observer) { TaskRun run = new TaskRun(task); if (observer != null) task.withObserver(observer); this.submit(run); return run; } // this might accept "resubmits" or "new" - either way we should run "complete" if it fails public void submit(TaskRun run) { if (run == null) return; // don't run if shut down if (this.shutdown) { run.errorTr(197, run); run.complete(); return; } // make sure context and logging, etc are ready run.prep(); // this will also catch if run was resubmitted but killed if ((run.hasErrors() && !run.hasStarted()) || run.isKilled()) { run.errorTr(216, run); // TODO different error messages if resume run.complete(); return; } // after prep, prep will setup context if (run.getContext() == null) { run.errorTr(198, run); run.complete(); return; } // if resume then see if we are a currently running thread, if so just reuse the thread (throttling allowing) if (run.hasStarted()) { Worker w = this.slots[run.slot]; if ((w != null) && w.resume(run)) return; } // find the work bucket WorkBucket bucket = this.getBucketOrDefault(run); // see if the bucket advises a submit, if not the bucket will hold onto the run // in a wait queue. if true then we put right on the active work queue if (bucket.canSubmit(run)) this.queue.add(run); } public TaskRun take() throws InterruptedException { TaskRun run = this.queue.take(); // find the work bucket WorkBucket bucket = this.getBucketOrDefault(run); // let the bucket know this run is in progress bucket.took(run); return run; } public void complete(TaskRun run) { // find the work bucket WorkBucket bucket = this.getBucketOrDefault(run); // tell the bucket to complete run TaskRun newrun = bucket.complete(run); // see if the bucket advises a submit if (newrun != null) { Logger.traceTr(199, newrun); this.queue.add(newrun); } } public WorkBucket getBucketOrDefault(String name) { WorkBucket bucket = this.buckets.get(name); if (bucket != null) return bucket; return this.buckets.get("Default"); } public WorkBucket getBucketOrDefault(TaskRun run) { WorkBucket bucket = this.buckets.get(run.getTask().getBucket()); if (bucket != null) return bucket; return this.buckets.get("Default"); } public void start(OperationResult or) { for (int i = 0; i < this.slots.length; i++) this.initSlot(i); // the task defines a timeout, not the pool. tasks with no timeout set // will simply not timeout and the pool will be burdened - so set timeouts // on tasks if there is any possibility that they might Hub.instance.getClock().addSlowSystemWorker(new ISystemWork() { @Override public void run(SysReporter reporter) { reporter.setStatus("Reviewing hung buckets"); // even when stopping we still want to clear hung tasks for (int i = 0; i < WorkPool.this.slots.length; i++) { Worker w = WorkPool.this.slots[i]; if (w != null) w.checkIfHung(); } for (WorkBucket b : WorkPool.this.buckets.values()) b.checkIfHung(); reporter.setStatus("After reviewing hung buckets"); } @Override public int period() { return 5; // TODO remove/advise -- this.scheduleFreq); } }); } protected void initSlot(int slot) { if (!this.shutdown) { Worker work = new Worker(); this.slots[slot] = work; work.start(slot); } else this.slots[slot] = null; //Logger.trace("Thread Pool slot " + slot + " changed, now have " + this.slots.size() + " threads"); } public void stop(OperationResult or) { or.trace(0, "Work Pool Stopping"); this.shutdown = true; // quickly let everyone know it is time to stop or.trace(0, "Work Pool Stopping Nice"); for (int i = 0; i < WorkPool.this.slots.length; i++) { Worker w = WorkPool.this.slots[i]; if (w != null) w.stopNice(); } or.trace(0, "Work Pool Waiting"); int remaincnt = 0; // wait a minute for things to finish up. -- TODO config for (int i2 = 0; i2 < 60; i2++) { remaincnt = 0; for (int i = 0; i < WorkPool.this.slots.length; i++) { Worker w = WorkPool.this.slots[i]; if (w != null) remaincnt++; } if (remaincnt == 0) break; try { Thread.sleep(1000); } catch (Exception x) { } } or.trace(0, "Work Pool Size: " + remaincnt); or.trace(0, "Work Pool Interrupt Remaining Workers"); for (int i = 0; i < WorkPool.this.slots.length; i++) { Worker w = WorkPool.this.slots[i]; if (w != null) w.stop(); } or.trace(0, "Work Pool Cleaning Buckets"); for (WorkBucket bucket : this.buckets.values()) bucket.stop(); or.trace(0, "Work Pool Stopped"); } public int queued() { return this.queue.size(); } public RecordStruct toStatusReport() { RecordStruct rec = new RecordStruct(); rec.setField("Queued", this.queued()); rec.setField("Threads", this.threadCount()); rec.setField("ThreadsCreated", this.threadsCreated()); rec.setField("ThreadsHung", this.threadsHung()); ListStruct buckets = new ListStruct(); for (WorkBucket bucket : this.buckets.values()) buckets.addItem(bucket.toStatusReport()); rec.setField("Buckets", buckets); return rec; } // for a task by identity alone public RecordStruct status(String taskid) { /* TODO */ return null; } // for a task by identity plus workid (slightly more secure) public RecordStruct status(String taskid, String workid) { for (WorkBucket bucket : this.buckets.values()) { TaskRun run = bucket.findTask(taskid); if (run != null) return run.status(); } return null; } // TODO start - someday make this a full working executor service @Override public boolean awaitTermination(long arg0, TimeUnit arg1) throws InterruptedException { // TODO Auto-generated method stub return false; } @Override public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> arg0) throws InterruptedException { // TODO Auto-generated method stub return null; } @Override public <T> List<Future<T>> invokeAll( Collection<? extends Callable<T>> arg0, long arg1, TimeUnit arg2) throws InterruptedException { // TODO Auto-generated method stub return null; } @Override public <T> T invokeAny(Collection<? extends Callable<T>> arg0) throws InterruptedException, ExecutionException { // TODO Auto-generated method stub return null; } @Override public <T> T invokeAny(Collection<? extends Callable<T>> arg0, long arg1, TimeUnit arg2) throws InterruptedException, ExecutionException, TimeoutException { // TODO Auto-generated method stub return null; } @Override public boolean isShutdown() { // TODO Auto-generated method stub return false; } @Override public boolean isTerminated() { // TODO Auto-generated method stub return false; } @Override public void shutdown() { // TODO Auto-generated method stub } @Override public List<Runnable> shutdownNow() { // TODO Auto-generated method stub return null; } @Override public <T> Future<T> submit(Callable<T> arg0) { // TODO Auto-generated method stub return null; } @Override public Future<?> submit(Runnable arg0) { // TODO Auto-generated method stub return null; } @Override public <T> Future<T> submit(Runnable arg0, T arg1) { // TODO Auto-generated method stub return null; } public int inprogress() { int cnt = 0; for (WorkBucket bucket : this.buckets.values()) cnt += bucket.inprogress(); return cnt; } // TODO end - someday make this a full working executor service }