/*
* Copyright © 2014 Cask Data, Inc.
*
* Licensed 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 co.cask.tigon.internal.app.runtime;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import org.apache.twill.api.RunId;
import org.apache.twill.common.Cancellable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nonnull;
/**
*
*/
public abstract class AbstractProgramController implements ProgramController {
private static final Logger LOG = LoggerFactory.getLogger(ProgramController.class);
private final AtomicReference<State> state;
private final String programName;
private final RunId runId;
private final ConcurrentMap<ListenerCaller, Cancellable> listeners;
private final Listener caller;
protected AbstractProgramController(String programName, RunId runId) {
this.state = new AtomicReference<State>(State.STARTING);
this.programName = programName;
this.runId = runId;
this.listeners = Maps.newConcurrentMap();
this.caller = new MultiListenerCaller();
}
@Override
public RunId getRunId() {
return runId;
}
@Override
public final ListenableFuture<ProgramController> suspend() {
if (!state.compareAndSet(State.ALIVE, State.SUSPENDING)) {
return Futures.immediateFailedFuture(new IllegalStateException("Suspension not allowed").fillInStackTrace());
}
final SettableFuture<ProgramController> result = SettableFuture.create();
executor(State.SUSPENDING).execute(new Runnable() {
@Override
public void run() {
try {
caller.suspending();
doSuspend();
state.set(State.SUSPENDED);
result.set(AbstractProgramController.this);
caller.suspended();
} catch (Throwable t) {
error(t, result);
}
}
});
return result;
}
@Override
public final ListenableFuture<ProgramController> resume() {
if (!state.compareAndSet(State.SUSPENDED, State.RESUMING)) {
return Futures.immediateFailedFuture(new IllegalStateException("Resumption not allowed").fillInStackTrace());
}
final SettableFuture<ProgramController> result = SettableFuture.create();
executor(State.RESUMING).execute(new Runnable() {
@Override
public void run() {
try {
caller.resuming();
doResume();
state.set(State.ALIVE);
result.set(AbstractProgramController.this);
caller.alive();
} catch (Throwable t) {
error(t, result);
}
}
});
return result;
}
@Override
public final ListenableFuture<ProgramController> stop() {
if (!state.compareAndSet(State.STARTING, State.STOPPING)
&& !state.compareAndSet(State.ALIVE, State.STOPPING)
&& !state.compareAndSet(State.SUSPENDED, State.STOPPING)) {
return Futures.immediateFailedFuture(new IllegalStateException("Resumption not allowed").fillInStackTrace());
}
final SettableFuture<ProgramController> result = SettableFuture.create();
executor(State.STOPPING).execute(new Runnable() {
@Override
public void run() {
try {
caller.stopping();
doStop();
state.set(State.STOPPED);
result.set(AbstractProgramController.this);
caller.stopped();
} catch (Throwable t) {
error(t, result);
}
}
});
return result;
}
@Override
public final Cancellable addListener(Listener listener, Executor executor) {
Preconditions.checkNotNull(listener, "Listener shouldn't be null.");
Preconditions.checkNotNull(executor, "Executor shouldn't be null.");
final ListenerCaller caller = new ListenerCaller(listener, executor);
Cancellable cancellable = new Cancellable() {
@Override
public void cancel() {
listeners.remove(caller);
}
};
Cancellable result = listeners.putIfAbsent(caller, cancellable);
if (result != null) {
return result;
}
caller.init(state.get());
return cancellable;
}
@Override
public final ListenableFuture<ProgramController> command(final String name, final Object value) {
final SettableFuture<ProgramController> result = SettableFuture.create();
executor("command").execute(new Runnable() {
@Override
public void run() {
try {
doCommand(name, value);
result.set(AbstractProgramController.this);
} catch (Throwable t) {
error(t, result);
}
}
});
return result;
}
@Override
public final State getState() {
return state.get();
}
protected final void error(Throwable t) {
error(t, null);
}
/**
* Force this controller into error state.
* @param t The
*/
protected final <V> void error(Throwable t, SettableFuture<V> future) {
state.set(State.ERROR);
if (future != null) {
future.setException(t);
}
caller.error(t);
}
/**
* Children call this method to signal the program is started.
*/
protected final void started() {
if (!state.compareAndSet(State.STARTING, State.ALIVE)) {
LOG.info("Program already started {} {}", programName, runId);
return;
}
LOG.info("Program started: {} {}", programName, runId);
executor(State.ALIVE).execute(new Runnable() {
@Override
public void run() {
state.set(State.ALIVE);
caller.alive();
}
});
}
/**
* Creates a new executor that execute using new thread everytime.
*/
protected Executor executor(final String name) {
return new Executor() {
@Override
public void execute(@Nonnull Runnable command) {
Thread t = new Thread(command, programName + "-" + state);
t.setDaemon(true);
t.start();
}
};
}
protected abstract void doSuspend() throws Exception;
protected abstract void doResume() throws Exception;
protected abstract void doStop() throws Exception;
protected abstract void doCommand(String name, Object value) throws Exception;
private Executor executor(State state) {
return executor(state.name());
}
private final class MultiListenerCaller implements Listener {
@Override
public void init(State currentState) {
for (ListenerCaller caller : listeners.keySet()) {
caller.init(currentState);
}
}
@Override
public void suspending() {
for (ListenerCaller caller : listeners.keySet()) {
caller.suspending();
}
}
@Override
public void suspended() {
for (ListenerCaller caller : listeners.keySet()) {
caller.suspended();
}
}
@Override
public void resuming() {
for (ListenerCaller caller : listeners.keySet()) {
caller.resuming();
}
}
@Override
public void alive() {
for (ListenerCaller caller : listeners.keySet()) {
caller.alive();
}
}
@Override
public void stopping() {
for (ListenerCaller caller : listeners.keySet()) {
caller.stopping();
}
}
@Override
public void stopped() {
for (ListenerCaller caller : listeners.keySet()) {
caller.stopped();
}
}
@Override
public void error(Throwable cause) {
for (ListenerCaller caller : listeners.keySet()) {
caller.error(cause);
}
}
}
private static final class ListenerCaller implements Listener {
private final Listener listener;
private final Executor executor;
private ListenerCaller(Listener listener, Executor executor) {
this.listener = listener;
this.executor = executor;
}
@Override
public void init(final State currentState) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
listener.init(currentState);
} catch (Throwable t) {
LOG.info(t.getMessage(), t);
}
}
});
}
@Override
public void suspending() {
executor.execute(new Runnable() {
@Override
public void run() {
try {
listener.suspending();
} catch (Throwable t) {
LOG.info(t.getMessage(), t);
}
}
});
}
@Override
public void suspended() {
executor.execute(new Runnable() {
@Override
public void run() {
try {
listener.suspended();
} catch (Throwable t) {
LOG.info(t.getMessage(), t);
}
}
});
}
@Override
public void resuming() {
executor.execute(new Runnable() {
@Override
public void run() {
try {
listener.resuming();
} catch (Throwable t) {
LOG.info(t.getMessage(), t);
}
}
});
}
@Override
public void alive() {
executor.execute(new Runnable() {
@Override
public void run() {
try {
listener.alive();
} catch (Throwable t) {
LOG.info(t.getMessage(), t);
}
}
});
}
@Override
public void stopping() {
executor.execute(new Runnable() {
@Override
public void run() {
try {
listener.stopping();
} catch (Throwable t) {
LOG.info(t.getMessage(), t);
}
}
});
}
@Override
public void stopped() {
executor.execute(new Runnable() {
@Override
public void run() {
try {
listener.stopped();
} catch (Throwable t) {
LOG.info(t.getMessage(), t);
}
}
});
}
@Override
public void error(final Throwable cause) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
listener.error(cause);
} catch (Throwable t) {
LOG.info(t.getMessage(), t);
}
}
});
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
// Only compare with the listener
ListenerCaller other = (ListenerCaller) o;
return Objects.equal(listener, other.listener);
}
@Override
public int hashCode() {
return Objects.hashCode(listener);
}
}
}