org.killbill.billing.tenant.api.TenantCacheInvalidation Maven / Gradle / Ivy
/*
* Copyright 2014-2016 Groupon, Inc
* Copyright 2014-2016 The Billing Project, LLC
*
* The Billing Project 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 org.killbill.billing.tenant.api;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import javax.inject.Inject;
import javax.inject.Named;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.events.BusInternalEvent;
import org.killbill.billing.tenant.api.TenantInternalApi.CacheInvalidationCallback;
import org.killbill.billing.tenant.api.TenantKV.TenantKey;
import org.killbill.billing.tenant.api.user.DefaultTenantConfigChangeInternalEvent;
import org.killbill.billing.tenant.api.user.DefaultTenantConfigDeletionInternalEvent;
import org.killbill.billing.tenant.dao.TenantBroadcastDao;
import org.killbill.billing.tenant.dao.TenantBroadcastModelDao;
import org.killbill.billing.tenant.dao.TenantDao;
import org.killbill.billing.tenant.dao.TenantKVModelDao;
import org.killbill.billing.tenant.glue.DefaultTenantModule;
import org.killbill.billing.util.config.definition.TenantConfig;
import org.killbill.bus.api.PersistentBus;
import org.killbill.bus.api.PersistentBus.EventBusException;
import org.killbill.commons.concurrent.Executors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Predicate;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
/**
* This class manages the callbacks that have been registered when per tenant objects have been inserted into the
* tenant_kvs store; the flow is the following (for e.g catalog):
* 1. CatalogUserApi is invoked to retrieve per tenant catalog
* 2. If cache is empty, TenantCacheLoader is invoked and uses TenantInternalApi is load the data; at that time, the invalidation callback
* is registered
*
* When this class initializes, it reads the current entry in the tenant_broadcasts table and from then on, keeps polling for new entries; when new
* entries are found, it invokes the callback to invalidate the current caching and force the TenantCacheLoader to be invoked again.
*/
public class TenantCacheInvalidation {
private final static int TERMINATION_TIMEOUT_SEC = 5;
private static final Logger logger = LoggerFactory.getLogger(TenantCacheInvalidation.class);
private final Multimap cache;
private final TenantBroadcastDao broadcastDao;
private final TenantConfig tenantConfig;
private final PersistentBus eventBus;
private final TenantDao tenantDao;
private AtomicLong latestRecordIdProcessed;
private volatile boolean isStopped;
private ScheduledExecutorService tenantExecutor;
@Inject
public TenantCacheInvalidation(@Named(DefaultTenantModule.NO_CACHING_TENANT) final TenantBroadcastDao broadcastDao,
@Named(DefaultTenantModule.NO_CACHING_TENANT) final TenantDao tenantDao,
final PersistentBus eventBus,
final TenantConfig tenantConfig) {
this.cache = HashMultimap.create();
this.broadcastDao = broadcastDao;
this.tenantConfig = tenantConfig;
this.tenantDao = tenantDao;
this.eventBus = eventBus;
this.isStopped = false;
}
public void initialize() {
final TenantBroadcastModelDao entry = broadcastDao.getLatestEntry();
this.latestRecordIdProcessed = entry != null ? new AtomicLong(entry.getRecordId()) : new AtomicLong(0L);
this.tenantExecutor = Executors.newSingleThreadScheduledExecutor("TenantExecutor");
this.isStopped = false;
}
public void start() {
final TimeUnit pendingRateUnit = tenantConfig.getTenantBroadcastServiceRunningRate().getUnit();
final long pendingPeriod = tenantConfig.getTenantBroadcastServiceRunningRate().getPeriod();
tenantExecutor.scheduleAtFixedRate(new TenantCacheInvalidationRunnable(this, broadcastDao, tenantDao), pendingPeriod, pendingPeriod, pendingRateUnit);
}
public void stop() {
if (isStopped) {
logger.warn("TenantExecutor is already in a stopped state");
return;
}
try {
tenantExecutor.shutdown();
boolean success = tenantExecutor.awaitTermination(TERMINATION_TIMEOUT_SEC, TimeUnit.SECONDS);
if (!success) {
logger.warn("TenantExecutor failed to complete termination within " + TERMINATION_TIMEOUT_SEC + "sec");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.warn("TenantExecutor stop sequence got interrupted");
} finally {
isStopped = true;
}
}
public void registerCallback(final TenantKey key, final CacheInvalidationCallback value) {
cache.put(key, value);
}
public Collection getCacheInvalidations(final TenantKey key) {
return cache.get(key);
}
public AtomicLong getLatestRecordIdProcessed() {
return latestRecordIdProcessed;
}
public boolean isStopped() {
return isStopped;
}
public void setLatestRecordIdProcessed(final Long newProcessedRecordId) {
this.latestRecordIdProcessed.set(newProcessedRecordId);
}
public PersistentBus getEventBus() {
return eventBus;
}
public static class TenantCacheInvalidationRunnable implements Runnable {
private final TenantCacheInvalidation parent;
private final TenantBroadcastDao broadcastDao;
private final TenantDao tenantDao;
public TenantCacheInvalidationRunnable(final TenantCacheInvalidation parent,
final TenantBroadcastDao broadcastDao,
final TenantDao tenantDao) {
this.parent = parent;
this.broadcastDao = broadcastDao;
this.tenantDao = tenantDao;
}
@Override
public void run() {
if (parent.isStopped) {
return;
}
final List entries = broadcastDao.getLatestEntriesFrom(parent.getLatestRecordIdProcessed().get());
for (TenantBroadcastModelDao cur : entries) {
if (parent.isStopped()) {
return;
}
try {
final TenantKeyAndCookie tenantKeyAndCookie = extractTenantKeyAndCookie(cur.getType());
if (tenantKeyAndCookie != null) {
final Collection callbacks = parent.getCacheInvalidations(tenantKeyAndCookie.getTenantKey());
if (!callbacks.isEmpty()) {
final InternalTenantContext tenantContext = new InternalTenantContext(cur.getTenantRecordId());
for (final CacheInvalidationCallback callback : callbacks) {
callback.invalidateCache(tenantKeyAndCookie.getTenantKey(), tenantKeyAndCookie.getCookie(), tenantContext);
}
final Long tenantKvsTargetRecordId = cur.getTargetRecordId();
final BusInternalEvent event;
if (tenantKvsTargetRecordId != null) {
final TenantKVModelDao tenantModelDao = tenantDao.getKeyByRecordId(tenantKvsTargetRecordId, tenantContext);
if (tenantModelDao == null) {
// Probably inactive entry
continue;
}
event = new DefaultTenantConfigChangeInternalEvent(tenantModelDao.getId(), cur.getType(),
null, tenantContext.getTenantRecordId(), cur.getUserToken());
} else {
event = new DefaultTenantConfigDeletionInternalEvent(cur.getType(),
null, tenantContext.getTenantRecordId(), cur.getUserToken());
}
try {
parent.getEventBus().post(event);
} catch (final EventBusException e) {
logger.warn("Failed to post event {}", event, e);
}
}
} else {
logger.warn("Failed to find CacheInvalidationCallback for " + cur.getType());
}
} finally {
parent.setLatestRecordIdProcessed(cur.getRecordId());
}
}
}
private TenantKeyAndCookie extractTenantKeyAndCookie(final String key) {
final TenantKey tenantKey = Iterables.tryFind(ImmutableList.copyOf(TenantKey.values()), new Predicate() {
@Override
public boolean apply(final TenantKey input) {
return key.startsWith(input.toString());
}
}).orNull();
if (tenantKey == null) {
return null;
}
final String cookie = !key.equals(tenantKey.toString()) ?
key.substring(tenantKey.toString().length()) :
null;
return new TenantKeyAndCookie(tenantKey, cookie);
}
}
private static final class TenantKeyAndCookie {
private final TenantKey tenantKey;
private final Object cookie;
public TenantKeyAndCookie(final TenantKey tenantKey, final Object cookie) {
this.tenantKey = tenantKey;
this.cookie = cookie;
}
public TenantKey getTenantKey() {
return tenantKey;
}
public Object getCookie() {
return cookie;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy