/*
* 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 gobblin.util.callbacks;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import gobblin.util.ExecutorsUtils;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* A helper to dispatch callbacks to a set of listeners. The CallbacksDispatcher is responsible for
* managing the list if listeners which implement a common interface L. Invocation happens through
* the {@link #execCallbacks(CallbackFactory) method.
*
* @param L the listener type; it is strongly advised that the class implements toString() to
* provide useful logging
*/
public class CallbacksDispatcher<L> implements Closeable {
private final Logger _log;
private final List<L> _listeners = new ArrayList<>();
private final WeakHashMap<L, Void> _autoListeners = new WeakHashMap<>();
private final ExecutorService _execService;
/**
* Constructor
* @param execService optional executor services for the callbacks; if none is specified, a
* single-thread executor will be used
* @param log optional logger; if none is specified, a default one will be created
*/
public CallbacksDispatcher(Optional<ExecutorService> execService, Optional<Logger> log) {
Preconditions.checkNotNull(execService);
Preconditions.checkNotNull(log);
_log = log.isPresent() ? log.get() : LoggerFactory.getLogger(getClass());
_execService = execService.isPresent() ? execService.get() : getDefaultExecutor(_log);
}
public static ExecutorService getDefaultExecutor(Logger log) {
return Executors.newSingleThreadExecutor(
ExecutorsUtils.newThreadFactory(Optional.of(log), Optional.of(log.getName() + "-%d")));
}
public CallbacksDispatcher() {
this(Optional.<ExecutorService>absent(), Optional.<Logger>absent());
}
public CallbacksDispatcher(Logger log) {
this(Optional.<ExecutorService>absent(), Optional.of(log));
}
public CallbacksDispatcher(ExecutorService execService, Logger log) {
this(Optional.of(execService), Optional.of(log));
}
@Override
public void close()
throws IOException {
ExecutorsUtils.shutdownExecutorService(_execService, Optional.of(_log), 5, TimeUnit.SECONDS);
}
public synchronized List<L> getListeners() {
// Clone to protect against adding/removing listeners while running callbacks
ArrayList<L> res = new ArrayList<>(_listeners);
// Scan any auto listeners
for (Map.Entry<L, Void> entry: _autoListeners.entrySet()) {
res.add(entry.getKey());
}
return res;
}
public synchronized void addListener(L listener) {
Preconditions.checkNotNull(listener);
_log.info("Adding listener:" + listener);
_listeners.add(listener);
}
/**
* Only weak references are stored for weak listeners. They will be removed from the dispatcher
* automatically, once the listener objects are GCed. Note that weak listeners cannot be removed
* explicitly. */
public synchronized void addWeakListener(L listener) {
Preconditions.checkNotNull(listener);
_log.info("Adding a weak listener " + listener);
_autoListeners.put(listener, null);
}
public synchronized void removeListener(L listener) {
Preconditions.checkNotNull(listener);
_log.info("Removing listener:" + listener);
_listeners.remove(listener);
}
public Logger getLog() {
return _log;
}
public <R> CallbackResults<L, R> execCallbacks(Function<? super L, R> callback, L listener)
throws InterruptedException {
Preconditions.checkNotNull(listener);
List<L> listenerList = new ArrayList<>(1);
listenerList.add(listener);
return execCallbacks(callback, listenerList);
}
public <R> CallbackResults<L, R> execCallbacks(Function<? super L, R> callback)
throws InterruptedException {
Preconditions.checkNotNull(callback);
List<L> listeners = getListeners();
return execCallbacks(callback, listeners);
}
private <R> CallbackResults<L, R> execCallbacks(Function<? super L, R> callback,
List<L> listeners)
throws InterruptedException {
CallbackResults<L, R> res = new CallbackResults<L, R>();
if (0 == listeners.size()) {
return res;
}
List<Callable<R>> callbacks = new ArrayList<>(listeners.size());
for (L listener: listeners) {
callbacks.add(new CallbackCallable<>(callback, listener));
}
List<Future<R>> futures = _execService.invokeAll(callbacks);
for (int i = 0; i < listeners.size(); ++i) {
CallbackResult<R> cr = CallbackResult.createFromFuture(futures.get(i));
L listener = listeners.get(i);
if (cr.isCanceled()) {
_log.warn("Callback cancelled: " + callbacks.get(i) + " on " + listener);
res.cancellations.put(listener, cr);
}
else if (cr.hasFailed()) {
_log.error("Callback error: " + callbacks.get(i) + " on " + listener + ":" + cr.getError());
res.failures.put(listener, cr);
}
else {
res.successes.put(listener, cr);
}
}
return res;
}
@AllArgsConstructor
public class CallbackCallable<R> implements Callable<R> {
final Function<? super L, R> _callback;
final L _listener;
@Override public R call() throws Exception {
_log.info("Calling " + _callback + " on " + _listener);
return _callback.apply(_listener);
}
}
@Data
public static class CallbackResults<L, R> {
private final Map<L, CallbackResult<R>> successes = new IdentityHashMap<>();
private final Map<L, CallbackResult<R>> failures = new IdentityHashMap<>();
private final Map<L, CallbackResult<R>> cancellations = new IdentityHashMap<>();
}
}