Merge branch 'develop' into development/Release_2_12_Branch_JAL-4102_21126_merge
[jalview.git] / src / jalview / workers / AlignCalcManager2.java
1 package jalview.workers;
2
3 import java.util.HashMap;
4 import java.util.List;
5 import java.util.Map;
6 import java.util.NoSuchElementException;
7 import java.util.Objects;
8 import java.util.WeakHashMap;
9 import java.util.concurrent.CopyOnWriteArrayList;
10 import java.util.concurrent.Executors;
11 import java.util.concurrent.Future;
12 import java.util.concurrent.ScheduledExecutorService;
13 import java.util.concurrent.TimeUnit;
14
15 import static java.lang.String.format;
16 import static java.util.Collections.synchronizedMap;
17 import static java.util.Collections.unmodifiableList;
18
19 import java.util.ArrayList;
20
21 import jalview.api.AlignCalcListener;
22 import jalview.api.AlignCalcManagerI2;
23 import jalview.api.AlignCalcWorkerI;
24 import jalview.api.PollableAlignCalcWorkerI;
25 import jalview.bin.Cache;
26 import jalview.bin.Console;
27 import jalview.datamodel.AlignmentAnnotation;
28
29 public class AlignCalcManager2 implements AlignCalcManagerI2
30 {
31   private abstract class WorkerManager
32   {
33     protected volatile boolean enabled = true;
34
35     protected AlignCalcWorkerI worker;
36
37     WorkerManager(AlignCalcWorkerI worker)
38     {
39       this.worker = worker;
40     }
41
42     protected AlignCalcWorkerI getWorker()
43     {
44       return worker;
45     }
46
47     boolean isEnabled()
48     {
49       return enabled;
50     }
51
52     void setEnabled(boolean enabled)
53     {
54       this.enabled = enabled;
55     }
56
57     synchronized void restart()
58     {
59       if (!isEnabled())
60       {
61         return;
62       }
63       if (!isRegistered())
64       {
65         setEnabled(false);
66       }
67       if (isWorking())
68       {
69         cancel();
70       }
71       submit();
72     }
73
74     protected boolean isRegistered()
75     {
76       return registered.containsKey(getWorker());
77     }
78
79     abstract boolean isWorking();
80
81     protected abstract void submit();
82
83     abstract void cancel();
84   }
85
86   private class SimpleWorkerManager extends WorkerManager
87   {
88     private Future<?> task = null;
89
90     SimpleWorkerManager(AlignCalcWorkerI worker)
91     {
92       super(worker);
93     }
94
95     @Override
96     boolean isWorking()
97     {
98       return task != null && !task.isDone();
99     }
100
101     @Override
102     protected void submit()
103     {
104       if (task != null && !(task.isDone() || task.isCancelled()))
105       {
106         throw new IllegalStateException(
107                 "Cannot submit new task if the prevoius one is still running");
108       }
109       Console.debug(
110               format("Worker %s queued", getWorker().getClass().getName()));
111       task = executor.submit(() -> {
112         try
113         {
114           Console.debug(format("Worker %s started",
115                   getWorker().getClass().getName()));
116           getWorker().run();
117           Console.debug(format("Worker %s finished",
118                   getWorker().getClass().getName()));
119         } catch (InterruptedException e)
120         {
121           Console.debug(format("Worker %s interrupted",
122                   getWorker().getClass().getName()));
123         } catch (Throwable th)
124         {
125           Console.debug(format("Worker %s failed",
126                   getWorker().getClass().getName()), th);
127         } finally
128         {
129           if (!isRegistered())
130           {
131             // delete worker reference so garbage collector can remove it
132             worker = null;
133           }
134         }
135       });
136     }
137
138     @Override
139     synchronized void cancel()
140     {
141       if (!isWorking())
142       {
143         return;
144       }
145       Console.debug(format("Cancelling worker %s",
146               getWorker().getClass().getName()));
147       task.cancel(true);
148     }
149   }
150
151   private class PollableWorkerManager extends WorkerManager
152   {
153     private Future<?> task = null;
154
155     PollableWorkerManager(PollableAlignCalcWorkerI worker)
156     {
157       super(worker);
158     }
159
160     @Override
161     protected PollableAlignCalcWorkerI getWorker()
162     {
163       return (PollableAlignCalcWorkerI) super.getWorker();
164     }
165
166     @Override
167     boolean isWorking()
168     {
169       return task != null && !task.isDone();
170     }
171
172     protected void submit()
173     {
174       if (task != null && !(task.isDone() || task.isCancelled()))
175       {
176         throw new IllegalStateException(
177                 "Cannot submit new task if the prevoius one is still running");
178       }
179       Console.debug(
180               format("Worker %s queued", getWorker().getClass().getName()));
181       final var runnable = new Runnable()
182       {
183         private boolean started = false;
184
185         private boolean completed = false;
186
187         Future<?> future = null;
188
189         @Override
190         public void run()
191         {
192           try
193           {
194             if (!started)
195             {
196               Console.debug(format("Worker %s started",
197                       getWorker().getClass().getName()));
198               getWorker().startUp();
199               started = true;
200             }
201             else if (!completed)
202             {
203               Console.debug(format("Polling worker %s",
204                       getWorker().getClass().getName()));
205               if (getWorker().poll())
206               {
207                 Console.debug(format("Worker %s finished",
208                         getWorker().getClass().getName()));
209                 completed = true;
210               }
211             }
212           } catch (Throwable th)
213           {
214             Console.debug(format("Worker %s failed",
215                     getWorker().getClass().getName()), th);
216             completed = true;
217           }
218           if (completed)
219           {
220             final var worker = getWorker();
221             if (!isRegistered())
222               PollableWorkerManager.super.worker = null;
223             Console.debug(format("Finalizing completed worker %s",
224                     worker.getClass().getName()));
225             worker.done();
226             // almost impossible, but the future may be null at this point
227             // let it throw NPE to cancel forcefully
228             future.cancel(false);
229           }
230         }
231       };
232       runnable.future = task = executor.scheduleWithFixedDelay(runnable, 10,
233               1000, TimeUnit.MILLISECONDS);
234     }
235
236     synchronized protected void cancel()
237     {
238       if (!isWorking())
239       {
240         return;
241       }
242       Console.debug(format("Cancelling worker %s",
243               getWorker().getClass().getName()));
244       task.cancel(false);
245       executor.submit(() -> {
246         final var worker = getWorker();
247         if (!isRegistered())
248           PollableWorkerManager.super.worker = null;
249         if (worker != null)
250         {
251           worker.cancel();
252           Console.debug(format("Finalizing cancelled worker %s",
253                   worker.getClass().getName()));
254           worker.done();
255         }
256       });
257     }
258   }
259
260   private final ScheduledExecutorService executor = Executors
261           .newSingleThreadScheduledExecutor();
262
263   private final Map<AlignCalcWorkerI, WorkerManager> registered = synchronizedMap(
264           new HashMap<>());
265
266   private final Map<AlignCalcWorkerI, WorkerManager> oneshot = synchronizedMap(
267           new WeakHashMap<>());
268
269   private final List<AlignCalcListener> listeners = new CopyOnWriteArrayList<>();
270
271   private WorkerManager createManager(AlignCalcWorkerI worker)
272   {
273     if (worker instanceof PollableAlignCalcWorkerI)
274     {
275       return new PollableWorkerManager((PollableAlignCalcWorkerI) worker);
276     }
277     else
278     {
279       return new SimpleWorkerManager(worker);
280     }
281   }
282
283   @Override
284   public void registerWorker(AlignCalcWorkerI worker)
285   {
286     Objects.requireNonNull(worker);
287     WorkerManager manager = createManager(worker);
288     registered.putIfAbsent(worker, manager);
289     startWorker(worker);
290   }
291
292   @Override
293   public List<AlignCalcWorkerI> getWorkers()
294   {
295     List<AlignCalcWorkerI> result = new ArrayList<>(registered.size());
296     result.addAll(registered.keySet());
297     return result;
298   }
299
300   @Override
301   public List<AlignCalcWorkerI> getWorkersOfClass(
302           Class<? extends AlignCalcWorkerI> cls)
303   {
304     List<AlignCalcWorkerI> collected = new ArrayList<>();
305     for (var worker : getWorkers())
306     {
307       if (worker.getClass().equals(cls))
308       {
309         collected.add(worker);
310       }
311     }
312     return unmodifiableList(collected);
313   }
314
315   @Override
316   public void removeWorker(AlignCalcWorkerI worker)
317   {
318     if (worker.isDeletable())
319     {
320       registered.remove(worker);
321     }
322   }
323
324   @Override
325   public void removeWorkerForAnnotation(AlignmentAnnotation annot)
326   {
327     synchronized (registered)
328     {
329       for (var worker : getWorkers())
330       {
331         if (worker.involves(annot))
332         {
333           removeWorker(worker);
334         }
335       }
336     }
337   }
338
339   @Override
340   public void removeWorkersOfClass(Class<? extends AlignCalcWorkerI> cls)
341   {
342     synchronized (registered)
343     {
344       for (var worker : getWorkers())
345       {
346         if (worker.getClass().equals(cls))
347         {
348           removeWorker(worker);
349         }
350       }
351     }
352   }
353
354   @Override
355   public void disableWorker(AlignCalcWorkerI worker)
356   {
357     // Null pointer check might be needed
358     registered.get(worker).setEnabled(false);
359   }
360
361   @Override
362   public void enableWorker(AlignCalcWorkerI worker)
363   {
364     // Null pointer check might be needed
365     registered.get(worker).setEnabled(true);
366   }
367
368   @Override
369   public boolean isDisabled(AlignCalcWorkerI worker)
370   {
371     if (registered.containsKey(worker))
372     {
373       return !registered.get(worker).isEnabled();
374     }
375     else
376     {
377       return false;
378     }
379   }
380
381   @Override
382   public boolean isWorking(AlignCalcWorkerI worker)
383   {
384     var manager = registered.get(worker);
385     if (manager == null)
386       manager = oneshot.get(worker);
387     if (manager == null)
388       return false;
389     else
390       return manager.isWorking();
391   }
392
393   @Override
394   public boolean isWorkingWithAnnotation(AlignmentAnnotation annot)
395   {
396     synchronized (registered)
397     {
398       for (var entry : registered.entrySet())
399         if (entry.getKey().involves(annot) && entry.getValue().isWorking())
400           return true;
401     }
402     synchronized (oneshot)
403     {
404       for (var entry : registered.entrySet())
405         if (entry.getKey().involves(annot) && entry.getValue().isWorking())
406           return true;
407     }
408     return false;
409   }
410
411   @Override
412   public boolean isWorking()
413   {
414     synchronized (registered)
415     {
416       for (var manager : registered.values())
417         if (manager.isWorking())
418           return true;
419     }
420     synchronized (oneshot)
421     {
422       for (var manager : oneshot.values())
423         if (manager.isWorking())
424           return true;
425     }
426     return false;
427   }
428
429   @Override
430   public void startWorker(AlignCalcWorkerI worker)
431   {
432     Objects.requireNonNull(worker);
433     var manager = registered.get(worker);
434     if (manager == null)
435     {
436       Console.warn("Starting unregistered worker " + worker);
437       manager = createManager(worker);
438       oneshot.put(worker, manager);
439     }
440     manager.restart();
441   }
442
443   @Override
444   public void restartWorkers()
445   {
446     synchronized (registered)
447     {
448       for (var manager : registered.values())
449       {
450         manager.restart();
451       }
452     }
453   }
454
455   @Override
456   public void cancelWorker(AlignCalcWorkerI worker)
457   {
458     Objects.requireNonNull(worker);
459     var manager = registered.get(worker);
460     if (manager == null)
461       manager = oneshot.get(worker);
462     if (manager == null)
463     {
464       throw new NoSuchElementException();
465     }
466     manager.cancel();
467   }
468
469   private void notifyQueued(AlignCalcWorkerI worker)
470   {
471     for (AlignCalcListener listener : listeners)
472     {
473       listener.workerQueued(worker);
474     }
475   }
476
477   private void notifyStarted(AlignCalcWorkerI worker)
478   {
479     for (AlignCalcListener listener : listeners)
480     {
481       listener.workerStarted(worker);
482     }
483   }
484
485   private void notifyCompleted(AlignCalcWorkerI worker)
486   {
487     for (AlignCalcListener listener : listeners)
488     {
489       try
490       {
491         listener.workerCompleted(worker);
492       } catch (RuntimeException e)
493       {
494         e.printStackTrace();
495       }
496     }
497   }
498
499   private void notifyCancelled(AlignCalcWorkerI worker)
500   {
501     for (AlignCalcListener listener : listeners)
502     {
503       try
504       {
505         listener.workerCancelled(worker);
506       } catch (RuntimeException e)
507       {
508         e.printStackTrace();
509       }
510     }
511   }
512
513   private void notifyExceptional(AlignCalcWorkerI worker,
514           Throwable throwable)
515   {
516     for (AlignCalcListener listener : listeners)
517     {
518       try
519       {
520         listener.workerExceptional(worker, throwable);
521       } catch (RuntimeException e)
522       {
523         e.printStackTrace();
524       }
525     }
526   }
527
528   @Override
529   public void addAlignCalcListener(AlignCalcListener listener)
530   {
531     listeners.add(listener);
532   }
533
534   @Override
535   public void removeAlignCalcListener(AlignCalcListener listener)
536   {
537     listeners.remove(listener);
538   }
539
540   @Override
541   public void shutdown()
542   {
543     executor.shutdownNow();
544     listeners.clear();
545     registered.clear();
546   }
547
548 }