killbill-memoizeit

Implement catalog caching for catalog served through catalog

3/22/2017 9:18:18 PM

Details

diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithCatalogPlugin.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithCatalogPlugin.java
index b2f1779..a67a191 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithCatalogPlugin.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithCatalogPlugin.java
@@ -35,6 +35,8 @@ import org.killbill.billing.catalog.StandaloneCatalog;
 import org.killbill.billing.catalog.StandaloneCatalogWithPriceOverride;
 import org.killbill.billing.catalog.VersionedCatalog;
 import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.CatalogUserApi;
 import org.killbill.billing.catalog.api.Plan;
 import org.killbill.billing.catalog.api.PriceList;
 import org.killbill.billing.catalog.api.Product;
@@ -53,7 +55,9 @@ import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.billing.util.callcontext.TenantContext;
 import org.killbill.xmlloader.XMLLoader;
+import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import com.google.common.base.Function;
@@ -72,6 +76,9 @@ public class TestWithCatalogPlugin extends TestIntegrationBase {
     @Inject
     private InternalCallContextFactory internalCallContextFactory;
 
+    @Inject
+    private CatalogUserApi catalogUserApi;
+
     private TestCatalogPluginApi testCatalogPluginApi;
 
     @BeforeClass(groups = "slow")
@@ -97,8 +104,17 @@ public class TestWithCatalogPlugin extends TestIntegrationBase {
         }, testCatalogPluginApi);
     }
 
+    @BeforeMethod(groups = "slow")
+    public void beforeMethod() throws Exception {
+        super.beforeMethod();
+        testCatalogPluginApi.reset();
+    }
+
     @Test(groups = "slow")
     public void testCreateSubscriptionWithCatalogPlugin() throws Exception {
+
+        testCatalogPluginApi.addCatalogVersion("WeaponsHire.xml");
+
         // We take april as it has 30 days (easier to play with BCD)
         // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
         clock.setDay(new LocalDate(2012, 4, 1));
@@ -111,26 +127,120 @@ public class TestWithCatalogPlugin extends TestIntegrationBase {
         final DefaultEntitlement bpSubscription = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
         invoiceChecker.checkInvoice(account.getId(), 1, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
         subscriptionChecker.checkSubscriptionCreated(bpSubscription.getId(), internalCallContext);
+
+        // Code went to retrieve catalog more than one time
+        Assert.assertTrue(testCatalogPluginApi.getNbLatestCatalogVersionApiCalls() > 1);
+
+        // Code only retrieved catalog from plugin once (caching works!)
+        Assert.assertEquals(testCatalogPluginApi.getNbVersionedPluginCatalogApiCalls(), 1);
+    }
+
+
+    @Test(groups = "slow")
+    public void testWithMultipleVersions() throws Exception {
+
+        testCatalogPluginApi.addCatalogVersion("versionedCatalog/WeaponsHireSmall-1.xml");
+
+        final VersionedCatalog catalog1 = (VersionedCatalog) catalogUserApi.getCatalog("whatever", callContext);
+        Assert.assertEquals(testCatalogPluginApi.getNbLatestCatalogVersionApiCalls(), 1);
+        Assert.assertEquals(testCatalogPluginApi.getNbVersionedPluginCatalogApiCalls(), 1);
+        Assert.assertEquals(catalog1.getEffectiveDate().compareTo(testCatalogPluginApi.getLatestCatalogUpdate().toDate()), 0);
+
+        // Retrieve 3 more times
+        catalogUserApi.getCatalog("whatever", callContext);
+        catalogUserApi.getCatalog("whatever", callContext);
+        catalogUserApi.getCatalog("whatever", callContext);
+        Assert.assertEquals(testCatalogPluginApi.getNbLatestCatalogVersionApiCalls(), 4);
+        Assert.assertEquals(testCatalogPluginApi.getNbVersionedPluginCatalogApiCalls(), 1);
+
+        testCatalogPluginApi.addCatalogVersion("versionedCatalog/WeaponsHireSmall-2.xml");
+
+        final VersionedCatalog catalog2 = (VersionedCatalog) catalogUserApi.getCatalog("whatever", callContext);
+        Assert.assertEquals(testCatalogPluginApi.getNbLatestCatalogVersionApiCalls(), 5);
+        Assert.assertEquals(testCatalogPluginApi.getNbVersionedPluginCatalogApiCalls(), 2);
+        Assert.assertEquals(catalog2.getEffectiveDate().compareTo(testCatalogPluginApi.getLatestCatalogUpdate().toDate()), 0);
+
+        testCatalogPluginApi.addCatalogVersion("versionedCatalog/WeaponsHireSmall-3.xml");
+
+        final VersionedCatalog catalog3 = (VersionedCatalog) catalogUserApi.getCatalog("whatever", callContext);
+        Assert.assertEquals(testCatalogPluginApi.getNbLatestCatalogVersionApiCalls(), 6);
+        Assert.assertEquals(testCatalogPluginApi.getNbVersionedPluginCatalogApiCalls(), 3);
+        Assert.assertEquals(catalog3.getEffectiveDate().compareTo(testCatalogPluginApi.getLatestCatalogUpdate().toDate()), 0);
+
+
+        // Retrieve 4 more times
+        catalogUserApi.getCatalog("whatever", callContext);
+        catalogUserApi.getCatalog("whatever", callContext);
+        catalogUserApi.getCatalog("whatever", callContext);
+        catalogUserApi.getCatalog("whatever", callContext);
+        Assert.assertEquals(testCatalogPluginApi.getNbLatestCatalogVersionApiCalls(), 10);
+        Assert.assertEquals(testCatalogPluginApi.getNbVersionedPluginCatalogApiCalls(), 3);
+
     }
 
+
     public static class TestCatalogPluginApi implements CatalogPluginApi {
 
-        private final VersionedCatalog versionedCatalog;
+        final PriceOverride priceOverride;
+        final InternalTenantContext internalTenantContext;
+        final InternalCallContextFactory internalCallContextFactory;
+
+        private VersionedCatalog versionedCatalog;
+        private DateTime latestCatalogUpdate;
+        private int nbLatestCatalogVersionApiCalls;
+        private int nbVersionedPluginCatalogApiCalls;
 
         public TestCatalogPluginApi(final PriceOverride priceOverride, final InternalTenantContext internalTenantContext, final InternalCallContextFactory internalCallContextFactory) throws Exception {
-            final StandaloneCatalog inputCatalog = XMLLoader.getObjectFromString(Resources.getResource("WeaponsHire.xml").toExternalForm(), StandaloneCatalog.class);
-            final List<StandaloneCatalog> versions = new ArrayList<StandaloneCatalog>();
-            final StandaloneCatalogWithPriceOverride standaloneCatalogWithPriceOverride = new StandaloneCatalogWithPriceOverride(inputCatalog, priceOverride, internalTenantContext.getTenantRecordId(), internalCallContextFactory);
-            versions.add(standaloneCatalogWithPriceOverride);
-            versionedCatalog = new VersionedCatalog(getClock());
-            versionedCatalog.addAll(versions);
+            this.priceOverride = priceOverride;
+            this.internalTenantContext = internalTenantContext;
+            this.internalCallContextFactory = internalCallContextFactory;
+            reset();
+        }
+
+        @Override
+        public DateTime getLatestCatalogVersion(final Iterable<PluginProperty> iterable, final TenantContext tenantContext) {
+            nbLatestCatalogVersionApiCalls++;
+            return latestCatalogUpdate;
         }
 
         @Override
         public VersionedPluginCatalog getVersionedPluginCatalog(final Iterable<PluginProperty> properties, final TenantContext tenantContext) {
+            nbVersionedPluginCatalogApiCalls++;
+            Assert.assertNotNull(versionedCatalog, "test did not initialize plugin catalog");
             return new TestModelVersionedPluginCatalog(versionedCatalog.getCatalogName(), versionedCatalog.getRecurringBillingMode(), toStandalonePluginCatalogs(versionedCatalog.getVersions()));
         }
 
+        public void addCatalogVersion(final String catalogResource) throws Exception {
+
+            final StandaloneCatalog inputCatalogVersion = XMLLoader.getObjectFromString(Resources.getResource(catalogResource).toExternalForm(), StandaloneCatalog.class);
+            final StandaloneCatalogWithPriceOverride inputCatalogVersionWithOverride = new StandaloneCatalogWithPriceOverride(inputCatalogVersion, priceOverride, internalTenantContext.getTenantRecordId(), internalCallContextFactory);
+
+            this.latestCatalogUpdate = new DateTime(inputCatalogVersion.getEffectiveDate());
+            if (versionedCatalog == null) {
+                versionedCatalog = new VersionedCatalog(getClock());
+            }
+            versionedCatalog.add(inputCatalogVersionWithOverride);
+        }
+
+        public void reset() {
+            this.versionedCatalog = null;
+            this.latestCatalogUpdate = null;
+            this.nbLatestCatalogVersionApiCalls = 0;
+            this.nbVersionedPluginCatalogApiCalls = 0;
+        }
+
+        public int getNbLatestCatalogVersionApiCalls() {
+            return nbLatestCatalogVersionApiCalls;
+        }
+
+        public int getNbVersionedPluginCatalogApiCalls() {
+            return nbVersionedPluginCatalogApiCalls;
+        }
+
+        public DateTime getLatestCatalogUpdate() {
+            return latestCatalogUpdate;
+        }
+
         private Iterable<StandalonePluginCatalog> toStandalonePluginCatalogs(final List<StandaloneCatalog> input) {
             return Iterables.transform(input, new Function<StandaloneCatalog, StandalonePluginCatalog>() {
                 @Override
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheCatalogCache.java b/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheCatalogCache.java
index ee21422..6a9c625 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheCatalogCache.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheCatalogCache.java
@@ -21,6 +21,7 @@ import java.util.List;
 
 import javax.inject.Inject;
 
+import org.joda.time.DateTime;
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.ObjectType;
 import org.killbill.billing.callcontext.InternalTenantContext;
@@ -91,7 +92,6 @@ public class EhCacheCatalogCache implements CatalogCache {
     @Override
     public VersionedCatalog getCatalog(final boolean useDefaultCatalog, final boolean filterTemplateCatalog, final InternalTenantContext tenantContext) throws CatalogApiException {
 
-        // STEPH TODO what are the possibilities for caching here ?
         final VersionedCatalog pluginVersionedCatalog = getCatalogFromPlugins(tenantContext);
         if (pluginVersionedCatalog != null) {
             return pluginVersionedCatalog;
@@ -132,11 +132,35 @@ public class EhCacheCatalogCache implements CatalogCache {
         final TenantContext tenantContext = internalCallContextFactory.createTenantContext(internalTenantContext);
         for (final String service : pluginRegistry.getAllServices()) {
             final CatalogPluginApi plugin = pluginRegistry.getServiceForName(service);
+
+            //
+            // Beware plugin implementors:  latestCatalogUpdatedDate returned by the plugin should also match the effectiveDate of the VersionedCatalog.
+            //
+            // However, this is the plugin choice to return one, or many catalog versions (StandaloneCatalog), Kill Bill catalog module does not care.
+            // As a guideline, if plugin keeps seeing new Plans/Products, this can all fit into the same version; however if there is a true versioning
+            // (e.g deleted Plans...), then multiple versions must be returned.
+            //
+            final DateTime latestCatalogUpdatedDate = plugin.getLatestCatalogVersion(ImmutableList.<PluginProperty>of(), tenantContext);
+            // A null latestCatalogUpdatedDate by passing caching, by fetching full catalog from plugin below (compatibility mode with 0.18.x or non optimized plugin api mode)
+            //
+            if (latestCatalogUpdatedDate != null) {
+                final VersionedCatalog tenantCatalog = (VersionedCatalog) cacheController.get(internalTenantContext.getTenantRecordId());
+                if (tenantCatalog != null) {
+                    if (tenantCatalog.getEffectiveDate().compareTo(latestCatalogUpdatedDate.toDate()) == 0) {
+                        // Current cached version matches the one from the plugin
+                        return tenantCatalog;
+                    }
+                }
+            }
+
             final VersionedPluginCatalog pluginCatalog = plugin.getVersionedPluginCatalog(ImmutableList.<PluginProperty>of(), tenantContext);
             // First plugin that gets something (for that tenant) returns it
             if (pluginCatalog != null) {
                 logger.info("Returning catalog from plugin {} on tenant {} ", service, internalTenantContext.getTenantRecordId());
-                return versionedCatalogMapper.toVersionedCatalog(pluginCatalog, internalTenantContext);
+                final VersionedCatalog resolvedPluginCatalog = versionedCatalogMapper.toVersionedCatalog(pluginCatalog, internalTenantContext);
+                cacheController.remove(internalTenantContext.getTenantRecordId());
+                cacheController.add(internalTenantContext.getTenantRecordId(), resolvedPluginCatalog);
+                return resolvedPluginCatalog;
             }
         }
         return null;