killbill-memoizeit

entitlement: harden ordering code in ProxyBlockingStateDao Signed-off-by:

12/3/2013 3:12:49 PM

Details

diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/dao/ProxyBlockingStateDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/dao/ProxyBlockingStateDao.java
index 87ffd36..bef2260 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/dao/ProxyBlockingStateDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/dao/ProxyBlockingStateDao.java
@@ -18,6 +18,7 @@ package com.ning.billing.entitlement.dao;
 
 import java.util.Collection;
 import java.util.Comparator;
+import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -41,12 +42,12 @@ import com.ning.billing.entitlement.EventsStream;
 import com.ning.billing.entitlement.api.BlockingState;
 import com.ning.billing.entitlement.api.BlockingStateType;
 import com.ning.billing.entitlement.api.DefaultEntitlementApi;
-import com.ning.billing.entitlement.api.Entitlement.EntitlementState;
 import com.ning.billing.entitlement.api.EntitlementApiException;
 import com.ning.billing.entitlement.engine.core.EventsStreamBuilder;
 import com.ning.billing.subscription.api.SubscriptionBase;
 import com.ning.billing.subscription.api.SubscriptionBaseInternalApi;
 import com.ning.billing.util.cache.CacheControllerDispatcher;
+import com.ning.billing.util.customfield.ShouldntHappenException;
 import com.ning.billing.util.dao.NonEntityDao;
 import com.ning.billing.util.entity.Pagination;
 
@@ -60,7 +61,101 @@ public class ProxyBlockingStateDao implements BlockingStateDao {
     private static final Logger log = LoggerFactory.getLogger(ProxyBlockingStateDao.class);
 
     // Ordering is critical here, especially for Junction
-    public static final Ordering<BlockingState> BLOCKING_STATE_ORDERING = Ordering.<BlockingState>from(new Comparator<BlockingState>() {
+    public static List<BlockingState> sortedCopy(final Iterable<BlockingState> blockingStates) {
+        final List<BlockingState> blockingStatesSomewhatSorted = BLOCKING_STATE_ORDERING_WITH_TIES_UNHANDLED.immutableSortedCopy(blockingStates);
+
+        final List<BlockingState> result = new LinkedList<BlockingState>();
+
+        // Take care of the ties
+        final Iterator<BlockingState> iterator = blockingStatesSomewhatSorted.iterator();
+        BlockingState prev = null;
+        while (iterator.hasNext()) {
+            final BlockingState current = iterator.next();
+            if (iterator.hasNext()) {
+                final BlockingState next = iterator.next();
+                if (prev != null && current.getEffectiveDate().equals(next.getEffectiveDate()) && current.getBlockedId().equals(next.getBlockedId())) {
+                    // Same date, same blockable id
+
+                    // Make sure block billing transitions are respected first
+                    BlockingState prevCandidate = insertTiedBlockingStatesInTheRightOrder(result, current, next, prev.isBlockBilling(), current.isBlockBilling(), next.isBlockBilling());
+                    if (prevCandidate == null) {
+                        // Then respect block entitlement transitions
+                        prevCandidate = insertTiedBlockingStatesInTheRightOrder(result, current, next, prev.isBlockEntitlement(), current.isBlockEntitlement(), next.isBlockEntitlement());
+                        if (prevCandidate == null) {
+                            // And finally block changes transitions
+                            prevCandidate = insertTiedBlockingStatesInTheRightOrder(result, current, next, prev.isBlockChange(), current.isBlockChange(), next.isBlockChange());
+                            if (prevCandidate == null) {
+                                // Trust the creation date (see BLOCKING_STATE_ORDERING_WITH_TIES_UNHANDLED below)
+                                result.add(current);
+                                result.add(next);
+                                prev = next;
+                            } else {
+                                prev = prevCandidate;
+                            }
+                        } else {
+                            prev = prevCandidate;
+                        }
+                    } else {
+                        prev = prevCandidate;
+                    }
+                } else {
+                    result.add(current);
+                    result.add(next);
+                    prev = next;
+                }
+            } else {
+                // End of the list
+                result.add(current);
+            }
+        }
+
+        return result;
+    }
+
+    private static BlockingState insertTiedBlockingStatesInTheRightOrder(final Collection<BlockingState> result,
+                                                                         final BlockingState current,
+                                                                         final BlockingState next,
+                                                                         final boolean prevBlocked,
+                                                                         final boolean currentBlocked,
+                                                                         final boolean nextBlocked) {
+        final BlockingState prev;
+
+        if (prevBlocked && currentBlocked && nextBlocked) {
+            // Tricky use case, bail
+            return null;
+        } else if (prevBlocked && currentBlocked && !nextBlocked) {
+            result.add(next);
+            result.add(current);
+            prev = current;
+        } else if (prevBlocked && !currentBlocked && nextBlocked) {
+            result.add(current);
+            result.add(next);
+            prev = next;
+        } else if (prevBlocked && !currentBlocked && !nextBlocked) {
+            // Tricky use case, bail
+            return null;
+        } else if (!prevBlocked && currentBlocked && nextBlocked) {
+            // Tricky use case, bail
+            return null;
+        } else if (!prevBlocked && currentBlocked && !nextBlocked) {
+            result.add(current);
+            result.add(next);
+            prev = next;
+        } else if (!prevBlocked && !currentBlocked && nextBlocked) {
+            result.add(next);
+            result.add(current);
+            prev = current;
+        } else if (!prevBlocked && !currentBlocked && !nextBlocked) {
+            // Tricky use case, bail
+            return null;
+        } else {
+            throw new ShouldntHappenException("Marker exception for code clarity");
+        }
+
+        return prev;
+    }
+
+    private static final Ordering<BlockingState> BLOCKING_STATE_ORDERING_WITH_TIES_UNHANDLED = Ordering.<BlockingState>from(new Comparator<BlockingState>() {
         @Override
         public int compare(final BlockingState o1, final BlockingState o2) {
             // effective_date column NOT NULL
@@ -72,23 +167,7 @@ public class ProxyBlockingStateDao implements BlockingStateDao {
                 if (blockableIdComparison != 0) {
                     return blockableIdComparison;
                 } else {
-                    // Same date, same blockable id - make sure billing transitions are respected first (assume block -> clear transitions)
-                    if (!o1.isBlockBilling() && o2.isBlockBilling()) {
-                        return 1;
-                    } else if (o1.isBlockBilling() && !o2.isBlockBilling()) {
-                        return -1;
-                    }
-
-                    // Then respect other blocking states
-                    if ((!o1.isBlockChange() && o2.isBlockChange()) ||
-                        (!o1.isBlockEntitlement() && o2.isBlockEntitlement())) {
-                        return 1;
-                    } else if ((o1.isBlockChange() && !o2.isBlockChange()) ||
-                               (o1.isBlockEntitlement() && !o2.isBlockEntitlement())) {
-                        return -1;
-                    }
-
-                    // Otherwise, just respect the created date
+                    // Same date, same blockable id, just respect the created date for now (see sortedCopyOf method above)
                     return o1.getCreatedDate().compareTo(o2.getCreatedDate());
                 }
             }
@@ -254,7 +333,7 @@ public class ProxyBlockingStateDao implements BlockingStateDao {
         }
 
         // Return the sorted list
-        return BLOCKING_STATE_ORDERING.immutableSortedCopy(blockingStatesOnDiskCopy);
+        return sortedCopy(blockingStatesOnDiskCopy);
     }
 
     private BlockingState findEntitlementCancellationBlockingState(@Nullable final UUID blockedId, final Iterable<BlockingState> blockingStatesOnDisk) {
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EventsStreamBuilder.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EventsStreamBuilder.java
index a051f1a..1dcf932 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EventsStreamBuilder.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EventsStreamBuilder.java
@@ -46,6 +46,7 @@ import com.ning.billing.entitlement.api.svcs.DefaultAccountEventsStreams;
 import com.ning.billing.entitlement.block.BlockingChecker;
 import com.ning.billing.entitlement.dao.DefaultBlockingStateDao;
 import com.ning.billing.entitlement.dao.OptimizedProxyBlockingStateDao;
+import com.ning.billing.entitlement.dao.ProxyBlockingStateDao;
 import com.ning.billing.subscription.api.SubscriptionBase;
 import com.ning.billing.subscription.api.SubscriptionBaseInternalApi;
 import com.ning.billing.subscription.api.user.SubscriptionBaseApiException;
@@ -60,8 +61,6 @@ import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 
-import static com.ning.billing.entitlement.dao.ProxyBlockingStateDao.BLOCKING_STATE_ORDERING;
-
 @Singleton
 public class EventsStreamBuilder {
 
@@ -207,7 +206,7 @@ public class EventsStreamBuilder {
                 final Collection<BlockingState> blockingStateSet = new LinkedHashSet<BlockingState>(accountBlockingStates);
                 blockingStateSet.addAll(bundleBlockingStates);
                 blockingStateSet.addAll(subscriptionBlockingStates);
-                final List<BlockingState> blockingStates = BLOCKING_STATE_ORDERING.immutableSortedCopy(blockingStateSet);
+                final List<BlockingState> blockingStates = ProxyBlockingStateDao.sortedCopy(blockingStateSet);
 
                 final EventsStream eventStream = buildForEntitlement(account, bundle, baseSubscription, subscription, allSubscriptionsForBundle, blockingStates, internalTenantContext);
                 entitlementsPerBundle.get(bundleId).add(eventStream);
@@ -315,7 +314,7 @@ public class EventsStreamBuilder {
         final Collection<BlockingState> blockingStateSet = new LinkedHashSet<BlockingState>(accountBlockingStates);
         blockingStateSet.addAll(bundleBlockingStates);
         blockingStateSet.addAll(subscriptionBlockingStates);
-        final List<BlockingState> blockingStates = BLOCKING_STATE_ORDERING.immutableSortedCopy(blockingStateSet);
+        final List<BlockingState> blockingStates = ProxyBlockingStateDao.sortedCopy(blockingStateSet);
 
         return buildForEntitlement(account, bundle, baseSubscription, subscription, allSubscriptionsForBundle, blockingStates, internalTenantContext);
     }
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/TestDefaultSubscriptionApi.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/TestDefaultSubscriptionApi.java
index 0aed9fc..2117962 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/TestDefaultSubscriptionApi.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/TestDefaultSubscriptionApi.java
@@ -46,14 +46,16 @@ public class TestDefaultSubscriptionApi extends EntitlementTestSuiteWithEmbedded
         final Account account = accountApi.createAccount(getAccountData(7), callContext);
         final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
         testListener.pushExpectedEvents(NextEvent.CREATE, NextEvent.CREATE, NextEvent.BLOCK);
-        final Entitlement entitlement1 = entitlementApi.createBaseEntitlement(account.getId(), spec, UUID.fromString("d87c78b4-c6de-4387-8a3b-4a5850dc29fc").toString(), initialDate, callContext);
-        final Entitlement entitlement2 = entitlementApi.createBaseEntitlement(account.getId(), spec, UUID.fromString("c56245e7-11a5-4a41-8854-3d31e24bcdcc").toString(), initialDate, callContext);
+        // Hardcode the UUIDs to have a predictable ordering
+        final Entitlement entitlement1 = entitlementApi.createBaseEntitlement(account.getId(), spec, UUID.fromString("a87c78b4-c6de-4387-8a3b-4a5850dc29fc").toString(), initialDate, callContext);
+        final Entitlement entitlement2 = entitlementApi.createBaseEntitlement(account.getId(), spec, UUID.fromString("b56245e7-11a5-4a41-8854-3d31e24bcdcc").toString(), initialDate, callContext);
         entitlementUtils.setBlockingStateAndPostBlockingTransitionEvent(new DefaultBlockingState(account.getId(), BlockingStateType.ACCOUNT, "stateName", "service", false, false, false, clock.getUTCNow()),
                                                                         internalCallContextFactory.createInternalCallContext(account.getId(), callContext));
         assertListenerStatus();
 
         final List<SubscriptionBundle> bundles = subscriptionApi.getSubscriptionBundlesForAccountId(account.getId(), callContext);
         Assert.assertEquals(bundles.size(), 2);
+
         // This will test the ordering as well
         subscriptionBundleChecker(bundles, initialDate, entitlement1, 0);
         subscriptionBundleChecker(bundles, initialDate, entitlement2, 1);
diff --git a/junction/src/test/java/com/ning/billing/junction/plumbing/billing/TestBlockingCalculator.java b/junction/src/test/java/com/ning/billing/junction/plumbing/billing/TestBlockingCalculator.java
index dbf0294..71a5b22 100644
--- a/junction/src/test/java/com/ning/billing/junction/plumbing/billing/TestBlockingCalculator.java
+++ b/junction/src/test/java/com/ning/billing/junction/plumbing/billing/TestBlockingCalculator.java
@@ -711,7 +711,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
         assertEquals(pairs.get(0).getEnd(), now.plusDays(4));
 
         // Verify ordering at the same effective date doesn't matter. This is to work around nondeterministic ordering
-        // behavior in ProxyBlockingStateDao#BLOCKING_STATE_ORDERING. See also TestDefaultInternalBillingApi.
+        // behavior in ProxyBlockingStateDao#BLOCKING_STATE_ORDERING_WITH_TIES_UNHANDLED. See also TestDefaultInternalBillingApi.
         blockingEvents = new ArrayList<BlockingState>();
         blockingEvents.add(new DefaultBlockingState(ovdId, BlockingStateType.SUBSCRIPTION_BUNDLE, DISABLED_BUNDLE, "test", true, true, true, now.plusDays(1)));
         blockingEvents.add(new DefaultBlockingState(ovdId, BlockingStateType.SUBSCRIPTION_BUNDLE, CLEAR_BUNDLE, "test", false, false, false, now.plusDays(2)));
diff --git a/junction/src/test/java/com/ning/billing/junction/plumbing/billing/TestDefaultInternalBillingApi.java b/junction/src/test/java/com/ning/billing/junction/plumbing/billing/TestDefaultInternalBillingApi.java
index 6aac83a..e99269a 100644
--- a/junction/src/test/java/com/ning/billing/junction/plumbing/billing/TestDefaultInternalBillingApi.java
+++ b/junction/src/test/java/com/ning/billing/junction/plumbing/billing/TestDefaultInternalBillingApi.java
@@ -48,7 +48,7 @@ public class TestDefaultInternalBillingApi extends JunctionTestSuiteWithEmbedded
     //
     // Pierre, why do we have invocationCount > 0 here?
     //
-    // This test will exercise ProxyBlockingStateDao#BLOCKING_STATE_ORDERING - unfortunately, for some reason,
+    // This test will exercise ProxyBlockingStateDao#BLOCKING_STATE_ORDERING_WITH_TIES_UNHANDLED - unfortunately, for some reason,
     // the ordering doesn't seem deterministic. In some scenarii,
     // BlockingState(idA, effectiveDate1, BLOCK), BlockingState(idA, effectiveDate2, CLEAR), BlockingState(idB, effectiveDate2, BLOCK), BlockingState(idB, effectiveDate3, CLEAR)
     // is ordered
@@ -200,4 +200,70 @@ public class TestDefaultInternalBillingApi extends JunctionTestSuiteWithEmbedded
         Assert.assertEquals(events.get(6).getTransitionType(), SubscriptionBaseTransitionType.END_BILLING_DISABLED);
         Assert.assertEquals(events.get(6).getEffectiveDate(), block5Date);
     }
+
+    // See https://github.com/killbill/killbill/commit/92042843e38a67f75495b207385e4c1f9ca60990#commitcomment-4749967
+    @Test(groups = "slow", description = "Check unblock then block states with same effective date are correctly handled", invocationCount = 10)
+    public void testUnblockThenBlockBlockingStatesWithSameEffectiveDate() throws Exception {
+        final LocalDate initialDate = new LocalDate(2013, 8, 7);
+        clock.setDay(initialDate);
+
+        final Account account = accountApi.createAccount(getAccountData(7), callContext);
+        final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
+
+        testListener.pushExpectedEvent(NextEvent.CREATE);
+        final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        final Entitlement entitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), initialDate, callContext);
+        final SubscriptionBase subscription = subscriptionInternalApi.getSubscriptionFromId(entitlement.getId(), internalCallContext);
+        assertListenerStatus();
+
+        final DateTime block1Date = clock.getUTCNow();
+        testListener.pushExpectedEvents(NextEvent.BLOCK);
+        final DefaultBlockingState state1 = new DefaultBlockingState(account.getId(),
+                                                                     BlockingStateType.ACCOUNT,
+                                                                     DefaultEntitlementApi.ENT_STATE_BLOCKED,
+                                                                     EntitlementService.ENTITLEMENT_SERVICE_NAME,
+                                                                     true,
+                                                                     true,
+                                                                     true,
+                                                                     block1Date);
+        blockingInternalApi.setBlockingState(state1, internalCallContext);
+
+        clock.addDays(1);
+
+        final DateTime block2Date = clock.getUTCNow();
+        testListener.pushExpectedEvents(NextEvent.BLOCK, NextEvent.BLOCK);
+        final DefaultBlockingState state2 = new DefaultBlockingState(account.getId(),
+                                                                     BlockingStateType.ACCOUNT,
+                                                                     DefaultEntitlementApi.ENT_STATE_CLEAR,
+                                                                     EntitlementService.ENTITLEMENT_SERVICE_NAME,
+                                                                     false,
+                                                                     false,
+                                                                     false,
+                                                                     block2Date);
+        blockingInternalApi.setBlockingState(state2, internalCallContext);
+        // Same date
+        final DefaultBlockingState state3 = new DefaultBlockingState(account.getId(),
+                                                                     BlockingStateType.ACCOUNT,
+                                                                     DefaultEntitlementApi.ENT_STATE_BLOCKED,
+                                                                     EntitlementService.ENTITLEMENT_SERVICE_NAME,
+                                                                     true,
+                                                                     true,
+                                                                     true,
+                                                                     block2Date);
+        blockingInternalApi.setBlockingState(state3, internalCallContext);
+        assertListenerStatus();
+
+        // Nothing should happen
+        clock.addDays(3);
+        assertListenerStatus();
+
+        // Expected blocking duration:
+        // * 2013-08-07 to now [2013-08-07 to 2013-08-08 then 2013-08-08 to now]
+        final List<BillingEvent> events = ImmutableList.<BillingEvent>copyOf(billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), internalCallContext));
+        Assert.assertEquals(events.size(), 2);
+        Assert.assertEquals(events.get(0).getTransitionType(), SubscriptionBaseTransitionType.CREATE);
+        Assert.assertEquals(events.get(0).getEffectiveDate(), subscription.getStartDate());
+        Assert.assertEquals(events.get(1).getTransitionType(), SubscriptionBaseTransitionType.START_BILLING_DISABLED);
+        Assert.assertEquals(events.get(1).getEffectiveDate(), block1Date);
+    }
 }