/* * 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(); } } } }