/*
* 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
*
* 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.apache.streams.local.builders;
import org.apache.streams.config.StreamsConfiguration;
import org.apache.streams.config.StreamsConfigurator;
import org.apache.streams.core.DatumStatusCountable;
import org.apache.streams.core.StreamBuilder;
import org.apache.streams.core.StreamsPersistWriter;
import org.apache.streams.core.StreamsProcessor;
import org.apache.streams.core.StreamsProvider;
import org.apache.streams.jackson.StreamsJacksonMapper;
import org.apache.streams.local.LocalRuntimeConfiguration;
import org.apache.streams.local.counters.StreamsTaskCounter;
import org.apache.streams.local.executors.ShutdownStreamOnUnhandleThrowableThreadPoolExecutor;
import org.apache.streams.local.monitoring.MonitoringConfiguration;
import org.apache.streams.local.queues.ThroughputQueue;
import org.apache.streams.local.tasks.BaseStreamsTask;
import org.apache.streams.local.tasks.LocalStreamProcessMonitorThread;
import org.apache.streams.local.tasks.StatusCounterMonitorThread;
import org.apache.streams.local.tasks.StreamsProviderTask;
import org.apache.streams.local.tasks.StreamsTask;
import org.apache.streams.monitoring.tasks.BroadcastMonitorThread;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.Uninterruptibles;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import java.math.BigInteger;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* {@link org.apache.streams.local.builders.LocalStreamBuilder} implementation to run a data processing stream in a single
* JVM across many threads. Depending on your data stream, the JVM heap may need to be set to a high value. Default
* implementation uses unbound {@link java.util.concurrent.ConcurrentLinkedQueue} to connect stream components.
*/
public class LocalStreamBuilder implements StreamBuilder {
private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger(LocalStreamBuilder.class);
private static final int DEFAULT_QUEUE_SIZE = 500;
public static final String TIMEOUT_KEY = "TIMEOUT";
public static final String BROADCAST_KEY = "broadcastURI";
public static final String STREAM_IDENTIFIER_KEY = "streamsID";
public static final String BROADCAST_INTERVAL_KEY = "monitoring_broadcast_interval_ms";
public static final String DEFAULT_STREAM_IDENTIFIER = "Unknown_Stream";
public static final String DEFAULT_STARTED_AT_KEY = "startedAt";
private Map<String, StreamComponent> providers;
private Map<String, StreamComponent> components;
private LocalRuntimeConfiguration streamConfig;
private Map<StreamsTask, Future> futures;
private ExecutorService executor;
private ExecutorService monitor;
private int totalTasks;
private int monitorTasks;
private LocalStreamProcessMonitorThread monitorThread;
private Map<String, List<StreamsTask>> tasks;
private Thread shutdownHook;
private BroadcastMonitorThread broadcastMonitor;
private int maxQueueCapacity;
private String streamIdentifier = DEFAULT_STREAM_IDENTIFIER;
private DateTime startedAt = new DateTime();
private boolean useDeprecatedMonitors;
/**
* Creates a local stream builder with all configuration resolved by typesafe
*/
public LocalStreamBuilder() {
this(new ObjectMapper().convertValue(StreamsConfigurator.detectConfiguration(), LocalRuntimeConfiguration.class));
}
/**
* Creates a local stream builder with a config object and default maximum internal queue size of 500
* @param streamConfig
* @deprecated use LocalRuntimeConfiguration constructor instead
*/
@Deprecated
public LocalStreamBuilder(Map<String, Object> streamConfig) {
this(DEFAULT_QUEUE_SIZE, streamConfig);
}
/**
* Creates a local stream builder with no config object. If maxQueueCapacity is less than 1 the queue is
* unbounded.
* @param maxQueueCapacity
*
* @deprecated use LocalRuntimeConfiguration constructor instead
*/
@Deprecated
public LocalStreamBuilder(int maxQueueCapacity) {
this(maxQueueCapacity, null);
}
/**
* Creates a local stream builder with a config object. If maxQueueCapacity is less than 1 the queue is
* unbounded.
*
* @param maxQueueCapacity
* @param streamConfig
*
* @deprecated use LocalRuntimeConfiguration constructor instead
*/
@Deprecated
public LocalStreamBuilder(int maxQueueCapacity, Map<String, Object> streamConfig) {
this(new LocalRuntimeConfiguration());
this.streamConfig.setQueueSize((long) maxQueueCapacity);
if( streamConfig != null && streamConfig.get(LocalStreamBuilder.TIMEOUT_KEY) != null )
this.streamConfig.setProviderTimeoutMs(new Long((Integer) (streamConfig.get(LocalStreamBuilder.TIMEOUT_KEY))));
if( streamConfig != null && streamConfig.get(LocalStreamBuilder.STREAM_IDENTIFIER_KEY) != null )
this.streamConfig.setIdentifier((String)streamConfig.get(LocalStreamBuilder.STREAM_IDENTIFIER_KEY));
if( streamConfig != null && streamConfig.get(LocalStreamBuilder.BROADCAST_KEY) != null ) {
MonitoringConfiguration monitoringConfiguration = new MonitoringConfiguration();
monitoringConfiguration.setBroadcastURI((String)streamConfig.get(LocalStreamBuilder.BROADCAST_KEY));
if(streamConfig.get(LocalStreamBuilder.BROADCAST_INTERVAL_KEY) != null)
monitoringConfiguration.setMonitoringBroadcastIntervalMs(Long.parseLong((String)streamConfig.get(LocalStreamBuilder.BROADCAST_INTERVAL_KEY)));
this.streamConfig.setMonitoring(monitoringConfiguration);
}
}
public LocalStreamBuilder(LocalRuntimeConfiguration streamConfig) {
this.streamConfig = streamConfig;
this.providers = new HashMap<>();
this.components = new HashMap<>();
this.totalTasks = 0;
this.monitorTasks = 0;
this.futures = new HashMap<>();
}
public void prepare() {
this.streamIdentifier = streamConfig.getIdentifier();
this.streamConfig.setStartedAt(startedAt.getMillis());
final LocalStreamBuilder self = this;
this.shutdownHook = new Thread(() -> {
LOGGER.debug("Shutdown hook received. Beginning shutdown");
self.stopInternal(true);
});
this.useDeprecatedMonitors = false;
this.broadcastMonitor = new BroadcastMonitorThread(this.streamConfig.getMonitoring());
}
public void setUseDeprecatedMonitors(boolean useDeprecatedMonitors) {
this.useDeprecatedMonitors = useDeprecatedMonitors;
}
@Override
public StreamBuilder newPerpetualStream(String id, StreamsProvider provider) {
validateId(id);
this.providers.put(id, new StreamComponent(id, provider, true, streamConfig));
++this.totalTasks;
if(this.useDeprecatedMonitors && provider instanceof DatumStatusCountable )
++this.monitorTasks;
return this;
}
@Override
public StreamBuilder newReadCurrentStream(String id, StreamsProvider provider) {
validateId(id);
this.providers.put(id, new StreamComponent(id, provider, false, streamConfig));
++this.totalTasks;
if(this.useDeprecatedMonitors && provider instanceof DatumStatusCountable )
++this.monitorTasks;
return this;
}
@Override
public StreamBuilder newReadNewStream(String id, StreamsProvider provider, BigInteger sequence) {
validateId(id);
this.providers.put(id, new StreamComponent(id, provider, sequence, streamConfig));
++this.totalTasks;
if(this.useDeprecatedMonitors && provider instanceof DatumStatusCountable )
++this.monitorTasks;
return this;
}
@Override
public StreamBuilder newReadRangeStream(String id, StreamsProvider provider, DateTime start, DateTime end) {
validateId(id);
this.providers.put(id, new StreamComponent(id, provider, start, end, streamConfig));
++this.totalTasks;
if(this.useDeprecatedMonitors && provider instanceof DatumStatusCountable )
++this.monitorTasks;
return this;
}
@Override
public StreamBuilder setStreamsConfiguration(StreamsConfiguration configuration) {
streamConfig = StreamsJacksonMapper.getInstance().convertValue(configuration, LocalRuntimeConfiguration.class);
return this;
}
@Override
public StreamsConfiguration getStreamsConfiguration() {
return StreamsJacksonMapper.getInstance().convertValue(streamConfig, StreamsConfiguration.class);
}
@Override
public StreamBuilder addStreamsProcessor(String id, StreamsProcessor processor, int numTasks, String... inBoundIds) {
validateId(id);
StreamComponent comp = new StreamComponent(id, processor, new ThroughputQueue<>(this.maxQueueCapacity, id, streamIdentifier, startedAt.getMillis()), numTasks, streamConfig);
this.components.put(id, comp);
connectToOtherComponents(inBoundIds, comp);
this.totalTasks += numTasks;
if(this.useDeprecatedMonitors && processor instanceof DatumStatusCountable )
++this.monitorTasks;
return this;
}
@Override
public StreamBuilder addStreamsPersistWriter(String id, StreamsPersistWriter writer, int numTasks, String... inBoundIds) {
validateId(id);
StreamComponent comp = new StreamComponent(id, writer, new ThroughputQueue<>(this.maxQueueCapacity, id, streamIdentifier, startedAt.getMillis()), numTasks, streamConfig);
this.components.put(id, comp);
connectToOtherComponents(inBoundIds, comp);
this.totalTasks += numTasks;
if(this.useDeprecatedMonitors && writer instanceof DatumStatusCountable )
++this.monitorTasks;
return this;
}
/**
* Runs the data stream in the this JVM and blocks till completion.
*/
@Override
public void start() {
prepare();
attachShutdownHandler();
boolean isRunning = true;
this.executor = new ShutdownStreamOnUnhandleThrowableThreadPoolExecutor(this.totalTasks, this);
this.monitor = Executors.newCachedThreadPool();
Map<String, StreamsProviderTask> provTasks = new HashMap<>();
tasks = new HashMap<>();
boolean forcedShutDown = false;
try {
if (this.useDeprecatedMonitors) {
monitorThread = new LocalStreamProcessMonitorThread(executor, 10);
this.monitor.submit(monitorThread);
}
setupComponentTasks(tasks);
setupProviderTasks(provTasks);
LOGGER.info("Started stream with {} components", tasks.size());
while(isRunning) {
Uninterruptibles.sleepUninterruptibly(streamConfig.getShutdownCheckDelay(), TimeUnit.MILLISECONDS);
isRunning = false;
for(StreamsProviderTask task : provTasks.values()) {
isRunning = isRunning || task.isRunning();
}
for(StreamComponent task: components.values()) {
boolean tasksRunning = false;
for(StreamsTask t : task.getStreamsTasks()) {
if(t instanceof BaseStreamsTask) {
tasksRunning = tasksRunning || t.isRunning();
}
}
isRunning = isRunning || (tasksRunning && task.getInBoundQueue().size() > 0);
}
if(isRunning) {
Uninterruptibles.sleepUninterruptibly(streamConfig.getShutdownCheckInterval(), TimeUnit.MILLISECONDS);
}
}
LOGGER.info("Components are no longer running or timed out");
} catch (Exception e){
LOGGER.warn("Runtime exception. Beginning shutdown");
forcedShutDown = true;
} finally{
LOGGER.info("Stream has completed, pausing @ {}", System.currentTimeMillis());
Uninterruptibles.sleepUninterruptibly(streamConfig.getShutdownPauseMs(), TimeUnit.MILLISECONDS);
LOGGER.info("Stream has completed, shutting down @ {}", System.currentTimeMillis());
stopInternal(forcedShutDown);
}
}
private void attachShutdownHandler() {
LOGGER.debug("Attaching shutdown handler");
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
private void detachShutdownHandler() {
LOGGER.debug("Detaching shutdown handler");
Runtime.getRuntime().removeShutdownHook(shutdownHook);
}
protected void forceShutdown(Map<String, List<StreamsTask>> streamsTasks) {
LOGGER.debug("Shutdown failed. Forcing shutdown");
for(List<StreamsTask> tasks : streamsTasks.values()) {
for(StreamsTask task : tasks) {
task.stopTask();
if(task.isWaiting()) {
this.futures.get(task).cancel(true);
}
}
}
this.executor.shutdown();
this.monitor.shutdown();
try {
if(!this.executor.awaitTermination(streamConfig.getExecutorShutdownPauseMs(), TimeUnit.MILLISECONDS)){
this.executor.shutdownNow();
}
if(!this.monitor.awaitTermination(streamConfig.getMonitorShutdownPauseMs(), TimeUnit.MILLISECONDS)){
this.monitor.shutdownNow();
}
}catch (InterruptedException ie) {
this.executor.shutdownNow();
this.monitor.shutdownNow();
throw new RuntimeException(ie);
}
}
protected void shutdown(Map<String, List<StreamsTask>> streamsTasks) throws InterruptedException {
LOGGER.info("Attempting to shutdown tasks");
if (this.monitorThread != null) {
this.monitorThread.shutdown();
}
this.executor.shutdown();
//complete stream shut down gracfully
for(StreamComponent prov : this.providers.values()) {
shutDownTask(prov, streamsTasks);
}
//need to make this configurable
if(!this.executor.awaitTermination(streamConfig.getExecutorShutdownWaitMs(), TimeUnit.MILLISECONDS)) { // all threads should have terminated already.
this.executor.shutdownNow();
this.executor.awaitTermination(streamConfig.getExecutorShutdownWaitMs(), TimeUnit.MILLISECONDS);
}
if(!this.monitor.awaitTermination(streamConfig.getMonitorShutdownWaitMs(), TimeUnit.MILLISECONDS)) { // all threads should have terminated already.
this.monitor.shutdownNow();
this.monitor.awaitTermination(streamConfig.getMonitorShutdownWaitMs(), TimeUnit.MILLISECONDS);
}
}
protected void setupProviderTasks(Map<String, StreamsProviderTask> provTasks) {
for(StreamComponent prov : this.providers.values()) {
StreamsTask task = prov.createConnectedTask(getTimeout());
task.setStreamConfig(this.streamConfig);
StreamsTaskCounter counter = new StreamsTaskCounter(prov.getId(), streamIdentifier, startedAt.getMillis());
task.setStreamsTaskCounter(counter);
this.executor.submit(task);
provTasks.put(prov.getId(), (StreamsProviderTask) task);
if(this.useDeprecatedMonitors && prov.isOperationCountable() ) {
this.monitor.submit(new StatusCounterMonitorThread((DatumStatusCountable) prov.getOperation(), 10));
this.monitor.submit(new StatusCounterMonitorThread((DatumStatusCountable) task, 10));
}
}
}
protected void setupComponentTasks(Map<String, List<StreamsTask>> streamsTasks) {
for(StreamComponent comp : this.components.values()) {
int tasks = comp.getNumTasks();
List<StreamsTask> compTasks = new LinkedList<>();
StreamsTaskCounter counter = new StreamsTaskCounter(comp.getId(), streamIdentifier, startedAt.getMillis());
for(int i=0; i < tasks; ++i) {
StreamsTask task = comp.createConnectedTask(getTimeout());
task.setStreamsTaskCounter(counter);
task.setStreamConfig(this.streamConfig);
this.futures.put(task, this.executor.submit(task));
compTasks.add(task);
if(this.useDeprecatedMonitors && comp.isOperationCountable() ) {
this.monitor.submit(new StatusCounterMonitorThread((DatumStatusCountable) comp.getOperation(), 10));
this.monitor.submit(new StatusCounterMonitorThread((DatumStatusCountable) task, 10));
}
this.monitor.submit(broadcastMonitor);
}
streamsTasks.put(comp.getId(), compTasks);
}
}
/**
* Shutsdown the running tasks in sudo depth first search kind of way. Checks that the upstream components have
* finished running before shutting down. Waits till inbound queue is empty to shutdown.
* @param comp StreamComponent to shut down.
* @param streamTasks the list of non-StreamsProvider tasks for this stream.
* @throws InterruptedException
*/
private void shutDownTask(StreamComponent comp, Map<String, List<StreamsTask>> streamTasks) throws InterruptedException {
List<StreamsTask> tasks = streamTasks.get(comp.getId());
if(tasks != null) { //not a StreamProvider
boolean parentsShutDown = true;
for(StreamComponent parent : comp.getUpStreamComponents()) {
List<StreamsTask> parentTasks = streamTasks.get(parent.getId());
//if parentTask == null, its a provider and is not running anymore
if(parentTasks != null) {
for(StreamsTask task : parentTasks) {
parentsShutDown = parentsShutDown && !task.isRunning();
}
}
}
if(parentsShutDown) {
for(StreamsTask task : tasks) {
task.stopTask();
if(task.isWaiting()) {
this.futures.get(task).cancel(true); // no data to process, interrupt block queue
}
}
for(StreamsTask task : tasks) {
int count = 0;
while(count < streamConfig.getTaskTimeoutMs() / 1000 && task.isRunning()) {
Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
count++;
}
if(task.isRunning()) {
LOGGER.warn("Task {} failed to terminate in allotted timeframe", task.toString());
}
}
}
}
Collection<StreamComponent> children = comp.getDownStreamComponents();
if(children != null) {
for(StreamComponent child : comp.getDownStreamComponents()) {
shutDownTask(child, streamTasks);
}
}
}
/**
* NOT IMPLEMENTED.
*/
@Override
public void stop() {
stopInternal(false);
}
protected void stopInternal(boolean systemExiting) {
try {
shutdown(tasks);
} catch (Exception e) {
LOGGER.error("Exception while trying to shutdown Stream: {}", e);
forceShutdown(tasks);
} finally {
try {
if(!systemExiting) {
detachShutdownHandler();
}
} catch( Throwable e3 ) {
LOGGER.error("StopInternal caught Throwable: {}", e3);
System.exit(1);
}
}
}
private void connectToOtherComponents(String[] conntectToIds, StreamComponent toBeConnected) {
for(String id : conntectToIds) {
StreamComponent upStream;
if(this.providers.containsKey(id)) {
upStream = this.providers.get(id);
}
else if(this.components.containsKey(id)) {
upStream = this.components.get(id);
}
else {
throw new InvalidStreamException("Cannot connect to id, "+id+", because id does not exist.");
}
upStream.addOutBoundQueue(toBeConnected, toBeConnected.getInBoundQueue());
toBeConnected.addInboundQueue(upStream);
}
}
private void validateId(String id) {
if(this.providers.containsKey(id) || this.components.containsKey(id)) {
throw new InvalidStreamException("Duplicate id. "+id+" is already assigned to another component");
} else if(id.contains(":")) {
throw new InvalidStreamException("Invalid character, ':', in component id : "+id);
}
}
protected int getTimeout() {
//Set the timeout of it is configured, otherwise signal downstream components to use their default
return streamConfig.getProviderTimeoutMs().intValue();
}
private LocalRuntimeConfiguration convertConfiguration(Map<String, Object> streamConfig) {
LocalRuntimeConfiguration config = new LocalRuntimeConfiguration();
if( streamConfig != null ) {
for( Map.Entry<String, Object> item : streamConfig.entrySet() ) {
config.setAdditionalProperty(item.getKey(), item.getValue());
}
}
return config;
}
}