/* * Copyright © 2014-2016 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.cdap.internal.app.runtime.flow; import co.cask.cdap.api.annotation.Batch; import co.cask.cdap.api.annotation.HashPartition; import co.cask.cdap.api.annotation.ProcessInput; import co.cask.cdap.api.annotation.RoundRobin; import co.cask.cdap.api.data.schema.Schema; import co.cask.cdap.api.flow.FlowSpecification; import co.cask.cdap.api.flow.FlowletDefinition; import co.cask.cdap.api.metrics.MetricDeleteQuery; import co.cask.cdap.api.metrics.MetricStore; import co.cask.cdap.app.program.Program; import co.cask.cdap.app.queue.QueueSpecification; import co.cask.cdap.app.queue.QueueSpecificationGenerator; import co.cask.cdap.common.conf.Constants; import co.cask.cdap.common.queue.QueueName; import co.cask.cdap.data2.queue.ConsumerGroupConfig; import co.cask.cdap.data2.queue.DequeueStrategy; import co.cask.cdap.data2.transaction.ForwardingTransactionAware; import co.cask.cdap.data2.transaction.Transactions; import co.cask.cdap.data2.transaction.queue.QueueAdmin; import co.cask.cdap.data2.transaction.queue.QueueConfigurer; import co.cask.cdap.data2.transaction.stream.StreamAdmin; import co.cask.cdap.internal.app.queue.SimpleQueueSpecificationGenerator; import co.cask.cdap.internal.io.ReflectionSchemaGenerator; import co.cask.cdap.internal.io.SchemaGenerator; import co.cask.cdap.internal.lang.MethodVisitor; import co.cask.cdap.internal.lang.Reflections; import co.cask.cdap.internal.specification.FlowletMethod; import co.cask.cdap.proto.Id; import co.cask.tephra.TransactionAware; import co.cask.tephra.TransactionExecutor; import co.cask.tephra.TransactionExecutorFactory; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.common.collect.Table; import com.google.common.hash.Hashing; import com.google.common.io.Closeables; import com.google.common.reflect.TypeToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Closeable; import java.io.IOException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; /** * Set of static helper methods used by flow system. */ public final class FlowUtils { private static final Logger LOG = LoggerFactory.getLogger(FlowUtils.class); /** * Generates a queue consumer groupId for the given flowlet in the given program. */ public static long generateConsumerGroupId(Program program, String flowletId) { return generateConsumerGroupId(program.getId(), flowletId); } /** * Generates a queue consumer groupId for the given flowlet in the given program id. */ public static long generateConsumerGroupId(Id.Program program, String flowletId) { // Use 'developer' in place of a program's namespace for programs in the 'default' namespace // to support backwards compatibility for queues and streams. String namespace = program.getNamespaceId(); String backwardsCompatibleNamespace = Id.Namespace.DEFAULT.getId().equals(namespace) ? Constants.DEVELOPER_ACCOUNT : namespace; return Hashing.md5().newHasher() .putString(backwardsCompatibleNamespace) .putString(program.getApplicationId()) .putString(program.getId()) .putString(flowletId).hash().asLong(); } /** * Creates a {@link ConsumerGroupConfig} by inspecting the given process method. */ public static ConsumerGroupConfig createConsumerGroupConfig(long groupId, int groupSize, Method processMethod) { // Determine input queue partition type HashPartition hashPartition = processMethod.getAnnotation(HashPartition.class); RoundRobin roundRobin = processMethod.getAnnotation(RoundRobin.class); DequeueStrategy strategy = DequeueStrategy.FIFO; String hashKey = null; Preconditions.checkArgument(!(hashPartition != null && roundRobin != null), "Only one strategy allowed for process() method: %s", processMethod.getName()); if (hashPartition != null) { strategy = DequeueStrategy.HASH; hashKey = hashPartition.value(); Preconditions.checkArgument(!hashKey.isEmpty(), "Partition key cannot be empty: %s", processMethod.getName()); } else if (roundRobin != null) { strategy = DequeueStrategy.ROUND_ROBIN; } return new ConsumerGroupConfig(groupId, groupSize, strategy, hashKey); } /** * Configures all queues being used in a flow. * * @return A Multimap from flowletId to QueueName where the flowlet is a consumer of. */ public static Multimap<String, QueueName> configureQueue(Program program, FlowSpecification flowSpec, StreamAdmin streamAdmin, QueueAdmin queueAdmin, TransactionExecutorFactory txExecutorFactory) { // Generate all queues specifications Id.Application appId = Id.Application.from(program.getNamespaceId(), program.getApplicationId()); Table<QueueSpecificationGenerator.Node, String, Set<QueueSpecification>> queueSpecs = new SimpleQueueSpecificationGenerator(appId).create(flowSpec); // For each queue in the flow, gather all consumer groups information Multimap<QueueName, ConsumerGroupConfig> queueConfigs = HashMultimap.create(); // Loop through each flowlet and generate the map from consumer flowlet id to queue ImmutableSetMultimap.Builder<String, QueueName> resultBuilder = ImmutableSetMultimap.builder(); for (Map.Entry<String, FlowletDefinition> entry : flowSpec.getFlowlets().entrySet()) { String flowletId = entry.getKey(); for (QueueSpecification queueSpec : Iterables.concat(queueSpecs.column(flowletId).values())) { resultBuilder.put(flowletId, queueSpec.getQueueName()); } } // For each queue, gather all consumer groups. for (QueueSpecification queueSpec : Iterables.concat(queueSpecs.values())) { QueueName queueName = queueSpec.getQueueName(); queueConfigs.putAll(queueName, getAllConsumerGroups(program, flowSpec, queueName, queueSpecs)); } try { // Configure each stream consumer in the Flow. Also collects all queue configurers. final List<ConsumerGroupConfigurer> groupConfigurers = Lists.newArrayList(); for (Map.Entry<QueueName, Collection<ConsumerGroupConfig>> entry : queueConfigs.asMap().entrySet()) { LOG.info("Queue config for {} : {}", entry.getKey(), entry.getValue()); if (entry.getKey().isStream()) { Map<Long, Integer> configs = Maps.newHashMap(); for (ConsumerGroupConfig config : entry.getValue()) { configs.put(config.getGroupId(), config.getGroupSize()); } streamAdmin.configureGroups(entry.getKey().toStreamId(), configs); } else { groupConfigurers.add(new ConsumerGroupConfigurer(queueAdmin.getQueueConfigurer(entry.getKey()), entry.getValue())); } } // Configure queue transactionally try { Transactions.createTransactionExecutor(txExecutorFactory, groupConfigurers) .execute(new TransactionExecutor.Subroutine() { @Override public void apply() throws Exception { for (ConsumerGroupConfigurer configurer : groupConfigurers) { configurer.configure(); } } }); } finally { for (ConsumerGroupConfigurer configurer : groupConfigurers) { Closeables.closeQuietly(configurer); } } return resultBuilder.build(); } catch (Exception e) { LOG.error("Failed to configure queues", e); throw Throwables.propagate(e); } } /** * Reconfigures stream / queue consumer due to instances change. * * @param consumerQueues all queues that need to reconfigure * @param groupId consumer group id * @param instances consumer instance count */ public static void reconfigure(Iterable<QueueName> consumerQueues, final long groupId, final int instances, StreamAdmin streamAdmin, QueueAdmin queueAdmin, TransactionExecutorFactory txExecutorFactory) throws Exception { // Reconfigure stream and collects all queue configurers final List<QueueConfigurer> queueConfigurers = Lists.newArrayList(); for (QueueName queueName : consumerQueues) { if (queueName.isStream()) { streamAdmin.configureInstances(queueName.toStreamId(), groupId, instances); } else { queueConfigurers.add(queueAdmin.getQueueConfigurer(queueName)); } } // Reconfigure queue transactionally try { Transactions.createTransactionExecutor(txExecutorFactory, queueConfigurers) .execute(new TransactionExecutor.Subroutine() { @Override public void apply() throws Exception { for (QueueConfigurer queueConfigurer : queueConfigurers) { queueConfigurer.configureInstances(groupId, instances); } } }); } finally { for (QueueConfigurer configurer : queueConfigurers) { Closeables.closeQuietly(configurer); } } } /** * Gets all consumer group configurations for the given queue. */ private static Set<ConsumerGroupConfig> getAllConsumerGroups( Program program, FlowSpecification flowSpec, QueueName queueName, Table<QueueSpecificationGenerator.Node, String, Set<QueueSpecification>> queueSpecs) { Set<ConsumerGroupConfig> groupConfigs = Sets.newHashSet(); SchemaGenerator schemaGenerator = new ReflectionSchemaGenerator(); // Get all the consumers of this queue. for (Map.Entry<String, FlowletDefinition> entry : flowSpec.getFlowlets().entrySet()) { String flowletId = entry.getKey(); for (QueueSpecification queueSpec : Iterables.concat(queueSpecs.column(flowletId).values())) { if (!queueSpec.getQueueName().equals(queueName)) { continue; } try { // Inspect the flowlet consumer FlowletDefinition flowletDefinition = entry.getValue(); Class<?> flowletClass = program.getClassLoader().loadClass(flowletDefinition.getFlowletSpec().getClassName()); long groupId = generateConsumerGroupId(program, flowletId); addConsumerGroup(queueSpec, flowletClass, groupId, flowletDefinition.getInstances(), schemaGenerator, groupConfigs); } catch (ClassNotFoundException e) { // There is no way for not able to load a Flowlet class as it should be verified during deployment. throw Throwables.propagate(e); } } } return groupConfigs; } /** * Finds all consumer group for the given queue from the given flowlet. */ private static void addConsumerGroup(final QueueSpecification queueSpec, final Type flowletType, final long groupId, final int groupSize, final SchemaGenerator schemaGenerator, final Collection<ConsumerGroupConfig> groupConfigs) { final Set<FlowletMethod> seenMethods = Sets.newHashSet(); Reflections.visit(null, flowletType, new MethodVisitor() { @Override public void visit(Object instance, Type inspectType, Type declareType, Method method) throws Exception { if (!seenMethods.add(FlowletMethod.create(method, inspectType))) { // The method is already seen. It can only happen if a children class override a parent class method and // is visiting the parent method, since the method visiting order is always from the leaf class walking // up the class hierarchy. return; } ProcessInput processInputAnnotation = method.getAnnotation(ProcessInput.class); if (processInputAnnotation == null) { // Consumer has to be process method return; } Set<String> inputNames = Sets.newHashSet(processInputAnnotation.value()); if (inputNames.isEmpty()) { // If there is no input name, it would be ANY_INPUT inputNames.add(FlowletDefinition.ANY_INPUT); } TypeToken<?> inspectTypeToken = TypeToken.of(inspectType); TypeToken<?> dataType = inspectTypeToken.resolveType(method.getGenericParameterTypes()[0]); // For batch mode and if the parameter is Iterator, need to get the actual data type from the Iterator. if (method.isAnnotationPresent(Batch.class) && Iterator.class.equals(dataType.getRawType())) { Preconditions.checkArgument(dataType.getType() instanceof ParameterizedType, "Only ParameterizedType is supported for batch Iterator."); dataType = inspectTypeToken.resolveType(((ParameterizedType) dataType.getType()).getActualTypeArguments()[0]); } Schema schema = schemaGenerator.generate(dataType.getType()); if (queueSpec.getInputSchema().equals(schema) && (inputNames.contains(queueSpec.getQueueName().getSimpleName()) || inputNames.contains(FlowletDefinition.ANY_INPUT))) { groupConfigs.add(createConsumerGroupConfig(groupId, groupSize, method)); } } }); } /** * Delete the "system.queue.pending" metrics for a flow or for all flows in an app or a namespace. * * @param namespace the namespace id; may only be null if the appId and flowId are null * @param appId the application id; may only be null if the flowId is null */ public static void deleteFlowPendingMetrics(MetricStore metricStore, @Nullable String namespace, @Nullable String appId, @Nullable String flowId) throws Exception { Preconditions.checkArgument(namespace != null || appId == null, "Namespace may only be null if AppId is null"); Preconditions.checkArgument(appId != null || flowId == null, "AppId may only be null if FlowId is null"); Collection<String> names = Collections.singleton("system.queue.pending"); Map<String, String> tags = Maps.newHashMap(); if (namespace != null) { tags.put(Constants.Metrics.Tag.NAMESPACE, namespace); if (appId != null) { tags.put(Constants.Metrics.Tag.APP, appId); if (flowId != null) { tags.put(Constants.Metrics.Tag.FLOW, flowId); } } } LOG.info("Deleting 'system.queue.pending' metric for context {}", tags); // we must delete up to the current time - let's round up to the next second. long nextSecond = System.currentTimeMillis() / 1000 + 1; metricStore.delete(new MetricDeleteQuery(0L, nextSecond, names, tags)); } /** * Helper class for configuring queue with new consumer groups information. */ private static final class ConsumerGroupConfigurer extends ForwardingTransactionAware implements Closeable { private final QueueConfigurer queueConfigurer; private final List<ConsumerGroupConfig> groupConfigs; private ConsumerGroupConfigurer(QueueConfigurer queueConfigurer, Iterable<? extends ConsumerGroupConfig> groupConfigs) { this.queueConfigurer = queueConfigurer; this.groupConfigs = ImmutableList.copyOf(groupConfigs); } private void configure() throws Exception { queueConfigurer.configureGroups(groupConfigs); } @Override public void close() throws IOException { queueConfigurer.close(); } @Override protected TransactionAware delegate() { return queueConfigurer; } } private FlowUtils() { } }