gobblin.metrics.kafka.KafkaSchemaRegistry.java Source code

Java tutorial

Introduction

Here is the source code for gobblin.metrics.kafka.KafkaSchemaRegistry.java

Source

/*
 * 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.metrics.kafka;

import java.lang.reflect.InvocationTargetException;
import java.util.Properties;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.lang3.reflect.ConstructorUtils;

import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Maps;

import lombok.extern.slf4j.Slf4j;

/**
 * A abstract schema registry class for Kafka, which supports fetching schema by key, fetching the latest schema
 * of a topic, and registering a schema.
 *
 * @param <K> key type
 * @param <S> schema type
 *
 * @author Ziyang Liu
 */
@Slf4j
public abstract class KafkaSchemaRegistry<K, S> {

    public static final String KAFKA_SCHEMA_REGISTRY_CLASS = "kafka.schema.registry.class";
    public static final String KAFKA_SCHEMA_REGISTRY_URL = "kafka.schema.registry.url";

    public static final int GET_SCHEMA_BY_ID_MAX_TIRES = 3;
    public static final int GET_SCHEMA_BY_ID_MIN_INTERVAL_SECONDS = 1;
    public static final String KAFKA_SCHEMA_REGISTRY_MAX_CACHE_SIZE = "kafka.schema.registry.max.cache.size";
    public static final String DEFAULT_KAFKA_SCHEMA_REGISTRY_MAX_CACHE_SIZE = "1000";
    public static final String KAFKA_SCHEMA_REGISTRY_CACHE_EXPIRE_AFTER_WRITE_MIN = "kafka.schema.registry.cache.expire.after.write.min";
    public static final String DEFAULT_KAFKA_SCHEMA_REGISTRY_CACHE_EXPIRE_AFTER_WRITE_MIN = "10";

    protected final Properties props;

    // Cache that stores schemas by keys.
    protected final LoadingCache<K, S> cachedSchemasByKeys;

    protected KafkaSchemaRegistry(Properties props) {
        this.props = props;
        int maxCacheSize = Integer.parseInt(props.getProperty(KAFKA_SCHEMA_REGISTRY_MAX_CACHE_SIZE,
                DEFAULT_KAFKA_SCHEMA_REGISTRY_MAX_CACHE_SIZE));
        int expireAfterWriteMin = Integer
                .parseInt(props.getProperty(KAFKA_SCHEMA_REGISTRY_CACHE_EXPIRE_AFTER_WRITE_MIN,
                        DEFAULT_KAFKA_SCHEMA_REGISTRY_CACHE_EXPIRE_AFTER_WRITE_MIN));
        this.cachedSchemasByKeys = CacheBuilder.newBuilder().maximumSize(maxCacheSize)
                .expireAfterWrite(expireAfterWriteMin, TimeUnit.MINUTES).build(new KafkaSchemaCacheLoader());
    }

    @SuppressWarnings("unchecked")
    public static <K, S> KafkaSchemaRegistry<K, S> get(Properties props) {
        Preconditions.checkArgument(props.containsKey(KAFKA_SCHEMA_REGISTRY_CLASS),
                "Missing required property " + KAFKA_SCHEMA_REGISTRY_CLASS);
        Class<? extends KafkaSchemaRegistry<?, ?>> clazz;
        try {
            clazz = (Class<? extends KafkaSchemaRegistry<?, ?>>) Class
                    .forName(props.getProperty(KAFKA_SCHEMA_REGISTRY_CLASS));
            return (KafkaSchemaRegistry<K, S>) ConstructorUtils.invokeConstructor(clazz, props);
        } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException
                | InstantiationException e) {
            log.error("Failed to instantiate " + KafkaSchemaRegistry.class, e);
            throw Throwables.propagate(e);
        }
    }

    /**
     * Get schema from schema registry by key.
     * @throws SchemaRegistryException if failed to get schema by key.
     */
    public S getSchemaByKey(K key) throws SchemaRegistryException {
        try {
            return cachedSchemasByKeys.get(key);
        } catch (ExecutionException e) {
            throw new SchemaRegistryException(String.format("Schema with key %s cannot be retrieved", key), e);
        }
    }

    /**
     * Fetch schema by key.
     *
     * This method is called in {@link #getSchemaByKey(K)} to fetch the schema if the given key does not exist
     * in the cache.
     * @throws SchemaRegistryException if failed to fetch schema by key.
     */
    protected abstract S fetchSchemaByKey(K key) throws SchemaRegistryException;

    /**
     * Get the latest schema of a topic.
     * @throws SchemaRegistryException if failed to get schema by topic.
     */
    public abstract S getLatestSchemaByTopic(String topic) throws SchemaRegistryException;

    /**
     * Register a schema to the schema registry
     * @return the key of the registered schema.
     * @throws SchemaRegistryException if registration failed.
     */
    public abstract K register(S schema) throws SchemaRegistryException;

    /**
     * Register a schema to the schema registry under the given name
     * @return the key of the registered schema.
     * @throws SchemaRegistryException if registration failed.
     */
    public abstract K register(S schema, String name) throws SchemaRegistryException;

    private class KafkaSchemaCacheLoader extends CacheLoader<K, S> {

        private final ConcurrentMap<K, FailedFetchHistory> failedFetchHistories;

        private KafkaSchemaCacheLoader() {
            super();
            this.failedFetchHistories = Maps.newConcurrentMap();
        }

        @Override
        public S load(K key) throws Exception {
            if (shouldFetchFromSchemaRegistry(key)) {
                try {
                    return KafkaSchemaRegistry.this.fetchSchemaByKey(key);
                } catch (SchemaRegistryException e) {
                    addFetchToFailureHistory(key);
                    throw e;
                }
            }

            // Throw exception if we've just tried to fetch this id, or if we've tried too many times for this id.
            throw new SchemaRegistryException(String.format("Schema with key %s cannot be retrieved", key));
        }

        private void addFetchToFailureHistory(K key) {
            this.failedFetchHistories.putIfAbsent(key, new FailedFetchHistory(System.nanoTime()));
            this.failedFetchHistories.get(key).incrementNumOfAttempts();
            this.failedFetchHistories.get(key).setPreviousAttemptTime(System.nanoTime());
        }

        private boolean shouldFetchFromSchemaRegistry(K key) {
            if (!this.failedFetchHistories.containsKey(key)) {
                return true;
            }
            FailedFetchHistory failedFetchHistory = this.failedFetchHistories.get(key);
            boolean maxTriesNotExceeded = failedFetchHistory.getNumOfAttempts() < GET_SCHEMA_BY_ID_MAX_TIRES;
            boolean minRetryIntervalSatisfied = System.nanoTime()
                    - failedFetchHistory.getPreviousAttemptTime() >= TimeUnit.SECONDS
                            .toNanos(GET_SCHEMA_BY_ID_MIN_INTERVAL_SECONDS);
            return maxTriesNotExceeded && minRetryIntervalSatisfied;
        }

        private class FailedFetchHistory {

            private final AtomicInteger numOfAttempts;
            private long previousAttemptTime;

            private FailedFetchHistory(long previousAttemptTime) {
                this.numOfAttempts = new AtomicInteger();
                this.previousAttemptTime = previousAttemptTime;
            }

            private int getNumOfAttempts() {
                return numOfAttempts.get();
            }

            private long getPreviousAttemptTime() {
                return previousAttemptTime;
            }

            private void setPreviousAttemptTime(long previousAttemptTime) {
                this.previousAttemptTime = previousAttemptTime;
            }

            private void incrementNumOfAttempts() {
                this.numOfAttempts.incrementAndGet();
            }
        }
    }

}