Make sure that we can cancel worker pools if something goes wrong and an exception is thrown

This commit is contained in:
Reinhard Pointner 2016-04-08 22:59:41 +00:00
parent e3be1e1bad
commit 1a4c66d977
5 changed files with 99 additions and 74 deletions

View File

@ -107,7 +107,6 @@ public final class WebServices {
} }
public static final ExecutorService requestThreadPool = Executors.newCachedThreadPool(); public static final ExecutorService requestThreadPool = Executors.newCachedThreadPool();
public static final ExecutorService workerThreadPool = Executors.newWorkStealingPool(getPreferredThreadPoolSize());
public static class TheTVDBClientWithLocalSearch extends TheTVDBClient { public static class TheTVDBClientWithLocalSearch extends TheTVDBClient {

View File

@ -27,6 +27,8 @@ import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -120,13 +122,18 @@ public class AutoDetection {
Map<Group, Set<File>> groups = new TreeMap<Group, Set<File>>(); Map<Group, Set<File>> groups = new TreeMap<Group, Set<File>>();
// can't use parallel stream because default fork/join pool doesn't play well with the security manager // can't use parallel stream because default fork/join pool doesn't play well with the security manager
stream(files).collect(toMap(f -> f, f -> workerThreadPool.submit(() -> detectGroup(f)))).forEach((file, group) -> { ExecutorService workerThreadPool = Executors.newWorkStealingPool();
try { try {
groups.computeIfAbsent(group.get(), k -> new TreeSet<File>()).add(file); stream(files).collect(toMap(f -> f, f -> workerThreadPool.submit(() -> detectGroup(f)))).forEach((file, group) -> {
} catch (Exception e) { try {
debug.log(Level.SEVERE, e.getMessage(), e); groups.computeIfAbsent(group.get(), k -> new TreeSet<File>()).add(file);
} } catch (Exception e) {
}); debug.log(Level.SEVERE, e.getMessage(), e);
}
});
} finally {
workerThreadPool.shutdownNow();
}
return groups; return groups;
} }

View File

@ -15,6 +15,8 @@ import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -37,19 +39,24 @@ class AutoDetectMatcher implements AutoCompleteMatcher {
Map<Group, Set<File>> groups = new AutoDetection(files, false, locale).group(); Map<Group, Set<File>> groups = new AutoDetection(files, false, locale).group();
// can't use parallel stream because default fork/join pool doesn't play well with the security manager // can't use parallel stream because default fork/join pool doesn't play well with the security manager
Map<Group, Future<List<Match<File, ?>>>> matches = groups.entrySet().stream().collect(toMap(Entry::getKey, it -> { ExecutorService workerThreadPool = Executors.newWorkStealingPool();
return workerThreadPool.submit(() -> match(it.getKey(), it.getValue(), strict, order, locale, autodetection, parent)); try {
})); Map<Group, Future<List<Match<File, ?>>>> matches = groups.entrySet().stream().collect(toMap(Entry::getKey, it -> {
return workerThreadPool.submit(() -> match(it.getKey(), it.getValue(), strict, order, locale, autodetection, parent));
}));
// collect results // collect results
return matches.entrySet().stream().flatMap(it -> { return matches.entrySet().stream().flatMap(it -> {
try { try {
return it.getValue().get().stream(); return it.getValue().get().stream();
} catch (Exception e) { } catch (Exception e) {
log.log(Level.WARNING, "Failed to process group: %s" + it.getKey(), e); log.log(Level.WARNING, "Failed to process group: %s" + it.getKey(), e);
} }
return Stream.empty(); return Stream.empty();
}).collect(toList()); }).collect(toList());
} finally {
workerThreadPool.shutdownNow();
}
} }
private List<Match<File, ?>> match(Group group, Collection<File> files, boolean strict, SortOrder order, Locale locale, boolean autodetection, Component parent) throws Exception { private List<Match<File, ?>> match(Group group, Collection<File> files, boolean strict, SortOrder order, Locale locale, boolean autodetection, Component parent) throws Exception {

View File

@ -24,6 +24,8 @@ import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.FutureTask; import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture; import java.util.concurrent.RunnableFuture;
@ -51,7 +53,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
private boolean anime; private boolean anime;
// only allow one fetch session at a time so later requests can make use of cached results // only allow one fetch session at a time so later requests can make use of cached results
private final Object providerLock = new Object(); private Object providerLock = new Object();
public EpisodeListMatcher(EpisodeListProvider provider, boolean anime) { public EpisodeListMatcher(EpisodeListProvider provider, boolean anime) {
this.provider = provider; this.provider = provider;
@ -141,7 +143,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
return provider.getEpisodeList(selectedSearchResult, sortOrder, locale); return provider.getEpisodeList(selectedSearchResult, sortOrder, locale);
} }
} }
return new ArrayList<Episode>(); return (List<Episode>) EMPTY_LIST;
}); });
}).collect(toList()); }).collect(toList());
@ -169,38 +171,44 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
Map<String, SearchResult> selectionMemory = new TreeMap<String, SearchResult>(CommonSequenceMatcher.getLenientCollator(Locale.ENGLISH)); Map<String, SearchResult> selectionMemory = new TreeMap<String, SearchResult>(CommonSequenceMatcher.getLenientCollator(Locale.ENGLISH));
Map<String, List<String>> inputMemory = new TreeMap<String, List<String>>(CommonSequenceMatcher.getLenientCollator(Locale.ENGLISH)); Map<String, List<String>> inputMemory = new TreeMap<String, List<String>>(CommonSequenceMatcher.getLenientCollator(Locale.ENGLISH));
// detect series names and create episode list fetch tasks
List<Future<List<Match<File, ?>>>> tasks = new ArrayList<Future<List<Match<File, ?>>>>();
if (strict) {
// in strict mode simply process file-by-file (ignoring all files that don't contain clear SxE patterns)
mediaFiles.stream().filter(f -> isEpisode(f, false)).map(f -> {
return workerThreadPool.submit(() -> {
return matchEpisodeSet(singletonList(f), detectSeriesNames(singleton(f), anime, locale), sortOrder, strict, locale, autodetection, selectionMemory, inputMemory, parent);
});
}).forEach(tasks::add);
} else {
// in non-strict mode use the complicated (more powerful but also more error prone) match-batch-by-batch logic
mapSeriesNamesByFiles(mediaFiles, locale, anime).forEach((f, n) -> {
// 1. handle series name batch set all at once -> only 1 batch set
// 2. files don't seem to belong to any series -> handle folder per folder -> multiple batch sets
Collection<List<File>> batches = n != null && n.size() > 0 ? singleton(new ArrayList<File>(f)) : mapByFolder(f).values();
batches.stream().map(b -> {
return workerThreadPool.submit(() -> {
return matchEpisodeSet(b, n, sortOrder, strict, locale, autodetection, selectionMemory, inputMemory, parent);
});
}).forEach(tasks::add);
});
}
// merge episode matches // merge episode matches
List<Match<File, ?>> matches = new ArrayList<Match<File, ?>>(); List<Match<File, ?>> matches = new ArrayList<Match<File, ?>>();
for (Future<List<Match<File, ?>>> future : tasks) {
// make sure each episode has unique object data ExecutorService workerThreadPool = Executors.newWorkStealingPool();
for (Match<File, ?> it : future.get()) { try {
matches.add(new Match<File, Episode>(it.getValue(), ((Episode) it.getCandidate()).clone())); // detect series names and create episode list fetch tasks
List<Future<List<Match<File, ?>>>> tasks = new ArrayList<Future<List<Match<File, ?>>>>();
if (strict) {
// in strict mode simply process file-by-file (ignoring all files that don't contain clear SxE patterns)
mediaFiles.stream().filter(f -> isEpisode(f, false)).map(f -> {
return workerThreadPool.submit(() -> {
return matchEpisodeSet(singletonList(f), detectSeriesNames(singleton(f), anime, locale), sortOrder, strict, locale, autodetection, selectionMemory, inputMemory, parent);
});
}).forEach(tasks::add);
} else {
// in non-strict mode use the complicated (more powerful but also more error prone) match-batch-by-batch logic
mapSeriesNamesByFiles(mediaFiles, locale, anime).forEach((f, n) -> {
// 1. handle series name batch set all at once -> only 1 batch set
// 2. files don't seem to belong to any series -> handle folder per folder -> multiple batch sets
Collection<List<File>> batches = n != null && n.size() > 0 ? singleton(new ArrayList<File>(f)) : mapByFolder(f).values();
batches.stream().map(b -> {
return workerThreadPool.submit(() -> {
return matchEpisodeSet(b, n, sortOrder, strict, locale, autodetection, selectionMemory, inputMemory, parent);
});
}).forEach(tasks::add);
});
} }
for (Future<List<Match<File, ?>>> future : tasks) {
// make sure each episode has unique object data
for (Match<File, ?> it : future.get()) {
matches.add(new Match<File, Episode>(it.getValue(), ((Episode) it.getCandidate()).clone()));
}
}
} finally {
workerThreadPool.shutdownNow();
} }
// handle derived files // handle derived files

View File

@ -5,7 +5,6 @@ import static java.util.Comparator.*;
import static java.util.stream.Collectors.*; import static java.util.stream.Collectors.*;
import static net.filebot.Logging.*; import static net.filebot.Logging.*;
import static net.filebot.MediaTypes.*; import static net.filebot.MediaTypes.*;
import static net.filebot.WebServices.*;
import static net.filebot.media.MediaDetection.*; import static net.filebot.media.MediaDetection.*;
import static net.filebot.similarity.CommonSequenceMatcher.*; import static net.filebot.similarity.CommonSequenceMatcher.*;
import static net.filebot.similarity.Normalization.*; import static net.filebot.similarity.Normalization.*;
@ -28,6 +27,8 @@ import java.util.SortedSet;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.FutureTask; import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture; import java.util.concurrent.RunnableFuture;
@ -145,31 +146,32 @@ class MovieMatcher implements AutoCompleteMatcher {
movieMatchFiles.addAll(filter(orphanedFiles, SUBTITLE_FILES)); // run movie detection only on orphaned subtitle files movieMatchFiles.addAll(filter(orphanedFiles, SUBTITLE_FILES)); // run movie detection only on orphaned subtitle files
// match remaining movies file by file in parallel // match remaining movies file by file in parallel
List<Future<Map<File, List<Movie>>>> tasks = movieMatchFiles.stream().filter(f -> movieByFile.get(f) == null).map(f -> { ExecutorService workerThreadPool = Executors.newWorkStealingPool();
return workerThreadPool.submit(() -> { try {
if (strict) { List<Future<Map<File, List<Movie>>>> tasks = movieMatchFiles.stream().filter(f -> movieByFile.get(f) == null).map(f -> {
// in strict mode, only process movies that follow the name (year) pattern return workerThreadPool.submit(() -> {
List<Integer> year = parseMovieYear(getRelativePathTail(f, 3).getPath()); if (strict) {
if (year.isEmpty() || isEpisode(f, true)) { // in strict mode, only process movies that follow the name (year) pattern
return null; List<Integer> year = parseMovieYear(getRelativePathTail(f, 3).getPath());
if (year.isEmpty() || isEpisode(f, true)) {
return (Map<File, List<Movie>>) EMPTY_MAP;
}
// allow only movie matches where the the movie year matches the year pattern in the filename
return singletonMap(f, detectMovie(f, service, locale, strict).stream().filter(m -> year.contains(m.getYear())).collect(toList()));
} else {
// in non-strict mode just allow all options
return singletonMap(f, detectMovie(f, service, locale, strict));
} }
});
}).collect(toList());
// allow only movie matches where the the movie year matches the year pattern in the filename // remember user decisions and only bother user once
return singletonMap(f, detectMovie(f, service, locale, strict).stream().filter(m -> year.contains(m.getYear())).collect(toList())); Map<String, Object> memory = new HashMap<String, Object>();
} else { memory.put(MEMORY_INPUT, new TreeMap<String, String>(getLenientCollator(locale)));
// in non-strict mode just allow all options memory.put(MEMORY_SELECTION, new TreeMap<String, String>(getLenientCollator(locale)));
return singletonMap(f, detectMovie(f, service, locale, strict));
}
});
}).collect(toList());
// remember user decisions and only bother user once for (Future<Map<File, List<Movie>>> future : tasks) {
Map<String, Object> memory = new HashMap<String, Object>();
memory.put(MEMORY_INPUT, new TreeMap<String, String>(getLenientCollator(locale)));
memory.put(MEMORY_SELECTION, new TreeMap<String, String>(getLenientCollator(locale)));
for (Future<Map<File, List<Movie>>> future : tasks) {
if (future.get() != null) {
for (Entry<File, List<Movie>> it : future.get().entrySet()) { for (Entry<File, List<Movie>> it : future.get().entrySet()) {
// auto-select movie or ask user // auto-select movie or ask user
Movie movie = grabMovieName(it.getKey(), it.getValue(), strict, locale, autodetect, memory, parent); Movie movie = grabMovieName(it.getKey(), it.getValue(), strict, locale, autodetect, memory, parent);
@ -180,6 +182,8 @@ class MovieMatcher implements AutoCompleteMatcher {
} }
} }
} }
} finally {
workerThreadPool.shutdownNow();
} }
// map movies to (possibly multiple) files (in natural order) // map movies to (possibly multiple) files (in natural order)