/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.jooby.exec;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import org.jooby.Env;
import org.jooby.Jooby.Module;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.inject.Binder;
import com.google.inject.Key;
import com.google.inject.name.Names;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigObject;
import com.typesafe.config.ConfigValue;
import com.typesafe.config.ConfigValueFactory;
import com.typesafe.config.ConfigValueType;
import javaslang.Function4;
import javaslang.control.Try;
/**
* <h1>executor</h1>
* <p>
* Manage the life cycle of {@link ExecutorService} and build async apps, schedule tasks, etc...
* </p>
*
* <h2>usage</h2>
*
* <pre>{@code
* ...
* import org.jooby.exec.Exec;
* ...
*
* {
* use(new Exec());
*
* get("/", req -> {
* ExecutorService executor = req.require(ExecutorService.class);
* // work with executor
* });
* }
* }</pre>
*
* <p>
* The default executor is a {@link Executors#newFixedThreadPool(int)} with threads defined by
* {@link Runtime#availableProcessors()}
* </p>
*
* <h2>explicit creation</h2>
* <p>
* The default {@link ExecutorService} is nice and give you something that just works out of the
* box. But, what if you need to control the number of threads?
* </p>
* <p>
* Explicit control is provided via <code>executors</code> which allow the following syntax:
* </p>
*
* <pre>
* type (= int)? (, daemon (= boolean)? )? (, priority (= int)? )?
* </pre>
*
* <p>
* Let's see some examples:
* </p>
*
* <pre>
* # fixed thread pool with a max number of threads equals to the available runtime processors
* executors = "fixed"
* </pre>
*
* <pre>
* # fixed thread pool with a max number of 10 threads
* executors = "fixed = 10"
* </pre>
*
* <pre>
* # fixed thread pool with a max number of 10 threads
* executors = "fixed = 10"
* </pre>
*
* <pre>
* # scheduled thread pool with a max number of 10 threads
* executors = "scheduled = 10"
* </pre>
*
* <pre>
* # cached thread pool with daemon threads and max priority
* executors = "cached, daemon = true, priority = 10"
* </pre>
*
* <pre>
* # forkjoin thread pool with asyncMode
* executors = "forkjoin, asyncMode = true"
* </pre>
*
* <h2>multiple executors</h2>
* <p>
* Multiple executors are provided by expanding the <code>executors</code> properties, like:
* </p>
*
* <pre>
* executors {
* pool1: fixed
* jobs: forkjoin
* }
* </pre>
*
* <p>
* Later, you can request your executor like:
* </p>
* <pre>{@code
* {
* get("/", req -> {
* ExecutorService pool1 = req.require("pool1", ExecutorService.class);
* ExecutorService jobs = req.require("jobs", ExecutorService.class);
* });
* }
* }</pre>
*
* <h2>shutdown</h2>
* <p>
* Any {@link ExecutorService} created by this module will automatically shutdown on application
* shutdown time.
* </p>
*
* @author edgar
* @since 0.16.0
*/
public class Exec implements Module {
private static final BiConsumer<String, Executor> NOOP = (n, e) -> {
};
/** The logging system. */
private final Logger log = LoggerFactory.getLogger(getClass());
private boolean daemon = true;
private int priority = Thread.NORM_PRIORITY;
private Map<String, Function4<String, Integer, Supplier<ThreadFactory>, Map<String, Object>, ExecutorService>> f =
/** executor factory. */
ImmutableMap
.of(
"cached", (name, n, tf, opts) -> Executors.newCachedThreadPool(tf.get()),
"fixed", (name, n, tf, opts) -> Executors.newFixedThreadPool(n, tf.get()),
"scheduled", (name, n, tf, opts) -> Executors.newScheduledThreadPool(n, tf.get()),
"forkjoin", (name, n, tf, opts) -> {
boolean asyncMode = Boolean.parseBoolean(opts.getOrDefault("asyncMode", "false")
.toString());
return new ForkJoinPool(n, fjwtf(name), null, asyncMode);
});
private String namespace;
protected Exec(final String namespace) {
this.namespace = namespace;
}
public Exec() {
this("executors");
}
/**
* Defined the default value for daemon. This value is used when a executor spec doesn't define a
* value for daemon. Default is: <code>true</code>
*
* @param daemon True for default daemon.
* @return This module.
*/
public Exec daemon(final boolean daemon) {
this.daemon = daemon;
return this;
}
/**
* Defined the default value for priority. This value is used when a executor spec doesn't define
* a value for priority. Default is: {@link Thread#NORM_PRIORITY}.
*
* @param priority One of {@link Thread#MIN_PRIORITY}, {@link Thread#NORM_PRIORITY} or
* {@link Thread#MAX_PRIORITY}.
* @return This module.
*/
public Exec priority(final int priority) {
this.priority = priority;
return this;
}
@Override
public Config config() {
return ConfigFactory.empty("exec.conf").withValue(namespace,
ConfigValueFactory.fromAnyRef("fixed"));
}
@Override
public void configure(final Env env, final Config conf, final Binder binder) {
configure(env, conf, binder, NOOP);
}
protected void configure(final Env env, final Config conf, final Binder binder,
final BiConsumer<String, Executor> callback) {
List<Map<String, Object>> executors = conf.hasPath(namespace)
? executors(conf.getValue(namespace), daemon, priority,
Runtime.getRuntime().availableProcessors())
: Collections.emptyList();
List<Entry<String, ExecutorService>> services = new ArrayList<>(executors.size());
for (Map<String, Object> options : executors) {
// thread factory options
String name = (String) options.remove("name");
log.debug("found executor: {}{}", name, options);
Boolean daemon = (Boolean) options.remove("daemon");
Integer priority = (Integer) options.remove("priority");
String type = String.valueOf(options.remove("type"));
// number of processors
Integer n = (Integer) options.remove(type);
// create executor
Function4<String, Integer, Supplier<ThreadFactory>, Map<String, Object>, ExecutorService> factory = f
.get(type);
if (factory == null) {
throw new IllegalArgumentException(
"Unknown executor: " + type + " must be one of " + f.keySet());
}
ExecutorService executor = factory.apply(type, n, () -> factory(name, daemon, priority),
options);
bind(binder, name, executor);
callback.accept(name, executor);
services.add(Maps.immutableEntry(name, executor));
}
services.stream()
.filter(it -> it.getKey().equals("default"))
.findFirst()
.ifPresent(e -> {
bind(binder, null, e.getValue());
});
env.onStop(() -> {
services.forEach(exec -> Try.run(() -> exec.getValue().shutdown()).onFailure(cause -> {
log.error("shutdown of {} resulted in error", exec.getKey(), cause);
}));
services.clear();
});
}
@SuppressWarnings({"rawtypes", "unchecked" })
private static void bind(final Binder binder, final String name, final ExecutorService executor) {
Class klass = executor.getClass();
Set<Class> types = collector(klass);
for (Class type : types) {
Key key = name == null ? Key.get(type) : Key.get(type, Names.named(name));
binder.bind(key).toInstance(executor);
}
}
@SuppressWarnings("rawtypes")
private static Set<Class> collector(final Class type) {
if (type != null && Executor.class.isAssignableFrom(type)) {
Set<Class> types = new HashSet<>();
if (type.isInterface() || !Modifier.isAbstract(type.getModifiers())) {
types.add(type);
}
types.addAll(collector(type.getSuperclass()));
Arrays.asList(type.getInterfaces()).forEach(it -> types.addAll(collector(it)));
return types;
}
return Collections.emptySet();
}
private static ThreadFactory factory(final String name, final boolean daemon,
final int priority) {
AtomicLong id = new AtomicLong(0);
return r -> {
Thread thread = new Thread(r, name + "-" + id.incrementAndGet());
thread.setDaemon(daemon);
thread.setPriority(priority);
return thread;
};
}
private static ForkJoinWorkerThreadFactory fjwtf(final String name) {
AtomicLong id = new AtomicLong();
return pool -> {
ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);
thread.setName(name + "-" + id.incrementAndGet());
return thread;
};
}
private static List<Map<String, Object>> executors(final ConfigValue candidate,
final boolean daemon, final int priority, final int n) {
if (candidate.valueType() == ConfigValueType.STRING) {
Map<String, Object> options = executor("default", daemon, priority, n,
candidate.unwrapped());
return ImmutableList.of(options);
}
ConfigObject conf = (ConfigObject) candidate;
List<Map<String, Object>> result = new ArrayList<>();
for (Entry<String, ConfigValue> executor : conf.entrySet()) {
String name = executor.getKey();
Object value = executor.getValue().unwrapped();
Map<String, Object> options = new HashMap<>();
options.putAll(executor(name, daemon, priority, n, value));
result.add(options);
}
return result;
}
@SuppressWarnings("unchecked")
private static Map<String, Object> executor(final String name, final boolean daemon,
final int priority,
final int n,
final Object value) {
Map<String, Object> options = new HashMap<>();
options.put("name", name);
options.put("daemon", daemon);
options.put("priority", priority);
if (value instanceof Map) {
Map<String, Object> config = (Map<String, Object>) value;
Object rawType = config.get("type");
if (rawType == null) {
throw new IllegalArgumentException("Missing executor type");
}
String type = rawType.toString();
options.put("type", type);
options.put(type, config.containsKey("size") ?
Integer.parseInt(config.get("size").toString()) : n);
options.put("daemon", config.containsKey("daemon") ?
Boolean.parseBoolean(config.get("daemon").toString()) : daemon);
options.put("asyncMode", config.containsKey("asyncMode") ?
Boolean.parseBoolean(config.get("asyncMode").toString()) : false);
options.put("priority", config.containsKey("priority") ?
Integer.parseInt(config.get("priority").toString()) : priority);
} else {
Iterable<String> spec = Splitter.on(",").trimResults().omitEmptyStrings()
.split(value.toString());
for (String option : spec) {
String[] opt = option.split("=");
String optname = opt[0].trim();
Object optvalue;
if (optname.equals("daemon")) {
optvalue = opt.length > 1 ? Boolean.parseBoolean(opt[1].trim()) : daemon;
} else if (optname.equals("asyncMode")) {
optvalue = opt.length > 1 ? Boolean.parseBoolean(opt[1].trim()) : false;
} else if (optname.equals("priority")) {
optvalue = opt.length > 1 ? Integer.parseInt(opt[1].trim()) : priority;
} else {
optvalue = opt.length > 1 ? Integer.parseInt(opt[1].trim()) : n;
options.put("type", optname);
}
options.put(optname, optvalue);
}
}
return options;
}
}