/*
* 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.runtime.job_monitor;
import java.io.IOException;
import java.net.URI;
import java.util.Collection;
import com.codahale.metrics.Counter;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.typesafe.config.Config;
import gobblin.runtime.api.JobSpec;
import gobblin.runtime.api.JobSpecMonitor;
import gobblin.runtime.api.MutableJobCatalog;
import gobblin.runtime.kafka.HighLevelConsumer;
import gobblin.runtime.metrics.RuntimeMetrics;
import gobblin.util.ConfigUtils;
import gobblin.util.Either;
import kafka.message.MessageAndMetadata;
import lombok.extern.slf4j.Slf4j;
/**
* Abstract {@link JobSpecMonitor} that reads {@link JobSpec}s from a Kafka stream. Subclasses should implement
* {@link KafkaJobMonitor#parseJobSpec(byte[])} to transform the message into one or multiple {@link JobSpec}s.
*/
@Slf4j
public abstract class KafkaJobMonitor extends HighLevelConsumer<byte[], byte[]> implements JobSpecMonitor {
public static final String KAFKA_JOB_MONITOR_PREFIX = "jobSpecMonitor.kafka";
public static final String KAFKA_AUTO_OFFSET_RESET_KEY = KAFKA_JOB_MONITOR_PREFIX + ".auto.offset.reset";
public static final String KAFKA_AUTO_OFFSET_RESET_SMALLEST = "smallest";
public static final String KAFKA_AUTO_OFFSET_RESET_LARGEST = "largest";
private final MutableJobCatalog jobCatalog;
private Counter newSpecs;
private Counter remmovedSpecs;
/**
* @return A collection of either {@link JobSpec}s to add/update or {@link URI}s to remove from the catalog,
* parsed from the Kafka message.
* @throws IOException
*/
public abstract Collection<Either<JobSpec, URI>> parseJobSpec(byte[] message) throws IOException;
public KafkaJobMonitor(String topic, MutableJobCatalog catalog, Config config) {
super(topic, ConfigUtils.getConfigOrEmpty(config, KAFKA_JOB_MONITOR_PREFIX), 1);
this.jobCatalog = catalog;
}
@Override
protected void createMetrics() {
super.createMetrics();
this.newSpecs = this.getMetricContext().counter(RuntimeMetrics.GOBBLIN_JOB_MONITOR_KAFKA_NEW_SPECS);
this.remmovedSpecs = this.getMetricContext().counter(RuntimeMetrics.GOBBLIN_JOB_MONITOR_KAFKA_REMOVED_SPECS);
}
@VisibleForTesting
@Override
protected void buildMetricsContextAndMetrics() {
super.buildMetricsContextAndMetrics();
}
@VisibleForTesting
@Override
protected void shutdownMetrics()
throws IOException {
super.shutdownMetrics();
}
@Override
protected void processMessage(MessageAndMetadata<byte[], byte[]> message) {
try {
Collection<Either<JobSpec, URI>> parsedCollection = parseJobSpec(message.message());
for (Either<JobSpec, URI> parsedMessage : parsedCollection) {
if (parsedMessage instanceof Either.Left) {
this.newSpecs.inc();
this.jobCatalog.put(((Either.Left<JobSpec, URI>) parsedMessage).getLeft());
} else if (parsedMessage instanceof Either.Right) {
this.remmovedSpecs.inc();
this.jobCatalog.remove(((Either.Right<JobSpec, URI>) parsedMessage).getRight());
}
}
} catch (IOException ioe) {
String messageStr = new String(message.message(), Charsets.UTF_8);
log.error(String.format("Failed to parse kafka message with offset %d: %s.", message.offset(), messageStr), ioe);
}
}
}