All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.bazaarvoice.emodb.databus.db.generic.CachingSubscriptionDAO Maven / Gradle / Ivy

The newest version!
package com.bazaarvoice.emodb.databus.db.generic;

import com.bazaarvoice.emodb.cachemgr.api.CacheHandle;
import com.bazaarvoice.emodb.cachemgr.api.CacheRegistry;
import com.bazaarvoice.emodb.cachemgr.api.InvalidationScope;
import com.bazaarvoice.emodb.common.dropwizard.time.ClockTicker;
import com.bazaarvoice.emodb.databus.db.SubscriptionDAO;
import com.bazaarvoice.emodb.databus.model.DefaultOwnedSubscription;
import com.bazaarvoice.emodb.databus.model.OwnedSubscription;
import com.bazaarvoice.emodb.sor.condition.Condition;
import com.bazaarvoice.emodb.sor.condition.Conditions;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.google.common.base.Ticker;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.ForwardingLoadingCache;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalListener;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.inject.Inject;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.time.Clock;
import java.time.Duration;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

import static java.util.Objects.requireNonNull;

/**
 * Wraps a {@link SubscriptionDAO} with a cache that makes it fast and efficient to lookup subscription metadata.  The
 * downside is that servers must globally coordinate changes to subscriptions because the consequences of using
 * out-of-date cached subscription metadata are pretty severe.
 * 

* There have been two implementations for how cache invalidation is managed using the cache registry: * *

    *
  • * The current implementation registers a cache named "subscriptionsByName" which maps subscription names * to OwnedSubscriptions. When a subscription is changed only the corresponding subscription is invalidated. *
  • *
  • * The legacy implementation registered a cache named "subscriptions" which mapped a single key, also named * "subscriptions", to a complete map of subscription names to OwnedSubscriptions. When a subscription was * changed the single value in the cache was invalidated, causing all subscriptions to be reloaded and * cached as a map. *
  • *
*

* Over time, as the number of total subscriptions, single-subscription lookups and cache invalidations scaled up the * legacy caching became a bottleneck and was replaced with the current one. However, since the two cache invalidation * systems are not compatible it is not possible to just upgrade from one to another on an in-flight system without * either incurring downtime or potentially losing invalidation messages. Therefore to support in-flight upgrades * there is a {@link CachingMode} parameter which controls how cache invalidations are managed. * *

    *
  1. * In legacy mode the DAO listens for invalidation messages and posts them to both the legacy cache and * current cache. *
  2. *
  3. * In normal mode the DAO exclusively uses current cache invalidation. *
  4. *
*

* A safe upgrade requires upgrading all servers from legacy to normal mode. Only once all active servers are in one * mode is it safe to move to the next. */ public class CachingSubscriptionDAO implements SubscriptionDAO { public enum CachingMode { legacy, normal } private static final String SUBSCRIPTIONS = "subscriptions"; // Loading cache cannot have null values, so use a single dummy value as a stand-in when a subscription does not exist. private static final OwnedSubscription NULL_SUBSCRIPTION = new DefaultOwnedSubscription("__null", Conditions.alwaysFalse(), new Date(0), Duration.ZERO, "__null"); private final SubscriptionDAO _delegate; private final LoadingCache _subscriptionCache; private final LoadingCache> _allSubscriptionsCache; private final CacheHandle _subscriptionCacheHandle; private final ListeningExecutorService _refreshService; private final LoadingCache> _legacyCache; private final CacheHandle _legacyCacheHandle; private final Meter _invalidationEventMeter; private final Meter _subscriptionFromCacheMeter; private final Meter _subscriptionFromDelegateMeter; private final Timer _reloadAllSubscriptionsTimer; private final Meter _subscriptionsReloaded; private final CachingMode _cachingMode; @Inject public CachingSubscriptionDAO(@CachingSubscriptionDAODelegate SubscriptionDAO delegate, @CachingSubscriptionDAORegistry CacheRegistry cacheRegistry, @CachingSubscriptionDAOExecutorService ListeningExecutorService refreshService, MetricRegistry metricRegistry, Clock clock, CachingMode cachingMode) { _delegate = requireNonNull(delegate, "delegate"); _refreshService = requireNonNull(refreshService, "refreshService"); _cachingMode = requireNonNull(cachingMode, "cachingMode"); Ticker ticker = ClockTicker.getTicker(clock); _reloadAllSubscriptionsTimer = metricRegistry.timer(MetricRegistry.name("bv.emodb.databus", "CachingSubscriptionDAO", "reload-all-subscriptions")); _subscriptionsReloaded = metricRegistry.meter(MetricRegistry.name("bv.emodb.databus", "CachingSubscriptionDAO", "subscriptions-reloaded")); // The all subscription cache is only used to track the set of all subscriptions and only has a single value. _allSubscriptionsCache = CacheBuilder.newBuilder() .refreshAfterWrite(10, TimeUnit.MINUTES) .ticker(ticker) .recordStats() .build(new CacheLoader>() { @Override public List load(String key) throws Exception { try (Timer.Context ignore = _reloadAllSubscriptionsTimer.time()) { assert SUBSCRIPTIONS.equals(key) : "All subscriptions cache should only be accessed by a single key"; Iterable subscriptionNames = _delegate.getAllSubscriptionNames(); ImmutableList.Builder subscriptionsBuilder = ImmutableList.builder(); for (String name : subscriptionNames) { // As much as possible take advantage of already cached subscriptions OwnedSubscription subscription = getSubscription(name); if (subscription != null) { subscriptionsBuilder.add(subscription); } } List subscriptions = subscriptionsBuilder.build(); _subscriptionsReloaded.mark(subscriptions.size()); return subscriptions; } } }); final LoadingCache rawSubscriptionCache = CacheBuilder.newBuilder() .refreshAfterWrite(10, TimeUnit.MINUTES) .ticker(ticker) .recordStats() .build(new CacheLoader() { @Override public OwnedSubscription load(String subscription) throws Exception { OwnedSubscription ownedSubscription = _delegate.getSubscription(subscription); // Can't cache null, use special null value if the subscription does not exist return Optional.ofNullable(ownedSubscription).orElse(NULL_SUBSCRIPTION); } /** * When the cached value is reloaded due to {@link CacheBuilder#refreshAfterWrite(long, TimeUnit)} * having expired reload the value asynchronously. */ @Override public ListenableFuture reload(String key, OwnedSubscription oldValue) throws Exception { return _refreshService.submit(() -> load(key)); } }); // Add a forwarding layer to the raw subscription cache to handle several custom responses. _subscriptionCache = new InvalidationListeningForwardingCache(rawSubscriptionCache) { @Override protected void valueInvalidated() { // If any subscription is invalidated also invalidate the full list of subscriptions to force a refresh. _allSubscriptionsCache.invalidate(SUBSCRIPTIONS); } // The only get methods called by the DAO are getIfPresent() and getUnchecked(). Override both // to invalidate non-existent subscriptions immediately so we don't leak memory if there is a flood of // requests for non-existent subscriptions. @Nullable @Override public OwnedSubscription getIfPresent(Object key) { return getWithNullInvalidation(key, super.getIfPresent(key)); } @Override public OwnedSubscription getUnchecked(String key) { return getWithNullInvalidation(key, super.getUnchecked(key)); } private OwnedSubscription getWithNullInvalidation(Object key, @Nullable OwnedSubscription subscription) { if (subscription == NULL_SUBSCRIPTION) { delegate().invalidate(key); } return subscription; } }; _subscriptionCacheHandle = cacheRegistry.register("subscriptionsByName", _subscriptionCache, true); if (cachingMode == CachingMode.legacy) { LoggerFactory.getLogger(getClass()).info("Subscription caching mode is {}", cachingMode); _legacyCache = new InvalidationListeningForwardingCache>( CacheBuilder.newBuilder().build(new CacheLoader>() { @Override public Map load(String key) throws Exception { // The actual cached object doesn't matter since this cache is only used for receiving // invalidation messages. Just need to provide a non-null value. return ImmutableMap.of(); } }) ) { @Override protected void valueInvalidated() { // The legacy system sends a single notification when any subscription changes. Without knowing // which subscription changed the only safe action is to invalidate them all. This is inefficient // but is only necessary during an in-flight upgrade. _subscriptionCache.invalidateAll(); } }; _legacyCacheHandle = cacheRegistry.register("subscriptions", _legacyCache, true); } else { _legacyCache = null; _legacyCacheHandle = null; } _invalidationEventMeter = metricRegistry.meter( MetricRegistry.name("bv.emodb.databus", "CachingSubscriptionDAO", "invalidation-events")); _subscriptionFromCacheMeter = metricRegistry.meter( MetricRegistry.name("bv.emodb.databus", "CachingSubscriptionDAO", "get-subscription-from-cache")); _subscriptionFromDelegateMeter = metricRegistry.meter( MetricRegistry.name("bv.emodb.databus", "CachingSubscriptionDAO", "get-subscription-from-delegate")); } @Override public void insertSubscription(String ownerId, String subscription, Condition tableFilter, Duration subscriptionTtl, Duration eventTtl) { _delegate.insertSubscription(ownerId, subscription, tableFilter, subscriptionTtl, eventTtl); // Invalidate this subscription. No need to invalidate the list of all subscriptions since this will happen // naturally in _subscriptionCache's invalidation handler. invalidateSubscription(subscription); } @Override public void deleteSubscription(String subscription) { _delegate.deleteSubscription(subscription); // Synchronously tell every other server in the cluster to forget what it has cached about the subscription. invalidateSubscription(subscription); } private void invalidateSubscription(String subscription) { _invalidationEventMeter.mark(); _subscriptionCacheHandle.invalidate(InvalidationScope.DATA_CENTER, subscription); // If in legacy mode also invalidate the legacy cache if (_cachingMode == CachingMode.legacy) { _legacyCacheHandle.invalidate(InvalidationScope.DATA_CENTER, SUBSCRIPTIONS); } } @Override public OwnedSubscription getSubscription(String subscription) { // Try to load the subscription without forcing a synchronous reload. This will return null only if it is the // first time the subscription has been loaded or if it has been invalidated. The returned subscription may // be dirty but the cache registry will have invalidated the cached value if it were changed -- the reload is // only used as a failsafe. If the value is dirty the cache will asynchronously reload it in the background. OwnedSubscription ownedSubscription = _subscriptionCache.getIfPresent(subscription); if (ownedSubscription != null) { _subscriptionFromCacheMeter.mark(); } else { // This time call get() to force the value to load, possibly synchronously. This will also cause the value // to be cached. ownedSubscription = _subscriptionCache.getUnchecked(subscription); _subscriptionFromDelegateMeter.mark(); } // If the subscription did not exist return null if (ownedSubscription == NULL_SUBSCRIPTION) { ownedSubscription = null; } return ownedSubscription; } @Override public Iterable getAllSubscriptions() { return _allSubscriptionsCache.getUnchecked(SUBSCRIPTIONS); } @Override public Iterable getAllSubscriptionNames() { return Iterables.transform(getAllSubscriptions(), OwnedSubscription::getName); } /** * Custom forwarding cache used by this class. Any time a subscription is invalidated the single value in * {@link #_allSubscriptionsCache} also needs to be invalidated. Normally this could be accomplished by providing * a listener to {@link CacheBuilder#removalListener(RemovalListener)}. However, that listener only receives * notification if a cached value is actually removed. If a brand new subscription is created remotely then * there will be no locally cached entry and therefore no notification. Therefore, the solution instead is to * wrap the cache with a forwarding implementation which calls {@link InvalidationListeningForwardingCache#valueInvalidated()} * whenever any value is removed since the action taken on any value invalidation is the same. */ private abstract static class InvalidationListeningForwardingCache extends ForwardingLoadingCache.SimpleForwardingLoadingCache { InvalidationListeningForwardingCache(LoadingCache delegate) { super(delegate); } protected abstract void valueInvalidated(); @Override public void invalidate(Object key) { super.invalidate(key); valueInvalidated(); } @Override public void invalidateAll(Iterable keys) { super.invalidateAll(keys); valueInvalidated(); } @Override public void invalidateAll() { super.invalidateAll(); valueInvalidated(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy