package com.linkedin.camus.schemaregistry; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class CachedSchemaRegistry<S> implements SchemaRegistry<S> { private static final String GET_SCHEMA_BY_ID_MAX_RETIRES = "get.schema.by.id.max.retries"; private static final String GET_SCHEMA_BY_ID_MAX_RETIRES_DEFAULT = "3"; private static final String GET_SCHEMA_BY_ID_MIN_INTERVAL_SECONDS = "get.schema.by.id.min.interval.seconds"; private static final String GET_SCHEMA_BY_ID_MIN_INTERVAL_SECONDS_DEFAULT = "1"; private final SchemaRegistry<S> registry; private final ConcurrentHashMap<CachedSchemaTuple, S> cachedById; private final ConcurrentHashMap<String, SchemaDetails<S>> cachedLatest; private int getByIdMaxRetries; private int getByIdMinIntervalSeconds; private Map<String, FailedFetchHistory> failedFetchHistories; private class FailedFetchHistory { private int numOfAttempts; private long previousAttemptTime; private FailedFetchHistory(int numOfnumOfAttempts, long previousAttemptTime) { this.numOfAttempts = numOfnumOfAttempts; this.previousAttemptTime = previousAttemptTime; } } public void init(Properties props) { this.getByIdMaxRetries = Integer.parseInt(props.getProperty(GET_SCHEMA_BY_ID_MAX_RETIRES, GET_SCHEMA_BY_ID_MAX_RETIRES_DEFAULT)); this.getByIdMinIntervalSeconds = Integer.parseInt(props.getProperty(GET_SCHEMA_BY_ID_MIN_INTERVAL_SECONDS, GET_SCHEMA_BY_ID_MIN_INTERVAL_SECONDS_DEFAULT)); } public CachedSchemaRegistry(SchemaRegistry<S> registry, Properties props) { this.registry = registry; this.cachedById = new ConcurrentHashMap<CachedSchemaTuple, S>(); this.cachedLatest = new ConcurrentHashMap<String, SchemaDetails<S>>(); this.failedFetchHistories = new HashMap<String, FailedFetchHistory>(); this.init(props); } public String register(String topic, S schema) { return registry.register(topic, schema); } public S getSchemaByID(String topic, String id) { CachedSchemaTuple cacheKey = new CachedSchemaTuple(topic, id); S schema = cachedById.get(cacheKey); if (schema == null) { synchronized (this) { if (shouldFetchFromSchemaRegistry(id)) { schema = fetchFromSchemaRegistry(topic, id); cachedById.putIfAbsent(new CachedSchemaTuple(topic, id), schema); } else { throw new SchemaNotFoundException(); } } } return schema; } private synchronized boolean shouldFetchFromSchemaRegistry(String id) { if (!failedFetchHistories.containsKey(id)) { return true; } FailedFetchHistory failedFetchHistory = failedFetchHistories.get(id); boolean maxRetriesNotExceeded = failedFetchHistory.numOfAttempts < getByIdMaxRetries; boolean minRetryIntervalSatisfied = System.nanoTime() - failedFetchHistory.previousAttemptTime >= TimeUnit.SECONDS .toNanos(getByIdMinIntervalSeconds); return maxRetriesNotExceeded && minRetryIntervalSatisfied; } private synchronized S fetchFromSchemaRegistry(String topic, String id) { try { S schema = registry.getSchemaByID(topic, id); return schema; } catch (SchemaNotFoundException e) { addFetchToFailureHistory(id); throw e; } } private void addFetchToFailureHistory(String id) { if (!failedFetchHistories.containsKey(id)) { failedFetchHistories.put(id, new FailedFetchHistory(1, System.nanoTime())); } else { failedFetchHistories.get(id).numOfAttempts++; failedFetchHistories.get(id).previousAttemptTime = System.nanoTime(); } } public SchemaDetails<S> getLatestSchemaByTopic(String topicName) { SchemaDetails<S> schema = cachedLatest.get(topicName); if (schema == null) { schema = registry.getLatestSchemaByTopic(topicName); cachedLatest.putIfAbsent(topicName, schema); } return schema; } public static class CachedSchemaTuple { private final String topic; private final String id; public CachedSchemaTuple(String topic, String id) { this.topic = topic; this.id = id; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((id == null) ? 0 : id.hashCode()); result = prime * result + ((topic == null) ? 0 : topic.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; CachedSchemaTuple other = (CachedSchemaTuple) obj; if (id == null) { if (other.id != null) return false; } else if (!id.equals(other.id)) return false; if (topic == null) { if (other.topic != null) return false; } else if (!topic.equals(other.topic)) return false; return true; } @Override public String toString() { return "CachedSchemaTuple [topic=" + topic + ", id=" + id + "]"; } } }