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