killbill-uncached

subscription: code review for #897 Signed-off-by: Pierre-Alexandre

3/14/2018 9:49:37 AM

Details

diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java
index 75513df..2a6dfff 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java
@@ -671,7 +671,28 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
 
         this.events = inputEvents;
 
-        filterOutDuplicateCancelEvents(events);
+        Collections.sort(inputEvents, new Comparator<SubscriptionBaseEvent>() {
+            @Override
+            public int compare(final SubscriptionBaseEvent o1, final SubscriptionBaseEvent o2) {
+                final int res = o1.getEffectiveDate().compareTo(o2.getEffectiveDate());
+                if (res != 0) {
+                    return res;
+                }
+
+                // In-memory events have a total order of 0, make sure they are after on disk event
+                if (o1.getTotalOrdering() == 0 && o2.getTotalOrdering() > 0) {
+                    return 1;
+                } else if (o1.getTotalOrdering() > 0 && o2.getTotalOrdering() == 0) {
+                    return -1;
+                } else if (o1.getTotalOrdering() == o2.getTotalOrdering()) {
+                    return 0;
+                } else {
+                    return o1.getTotalOrdering() < (o2.getTotalOrdering()) ? -1 : 1;
+                }
+            }
+        });
+
+        removeEverythingPastCancelEvent(events);
 
         UUID nextUserToken = null;
 
@@ -692,30 +713,11 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
 
         transitions = new LinkedList<SubscriptionBaseTransition>();
 
-        // DefaultSubscriptionDao#buildBundleSubscriptions may have added an out-of-order cancellation event (https://github.com/killbill/killbill/issues/897)
-        final SubscriptionBaseEvent cancellationEvent = Iterables.tryFind(inputEvents,
-                                                                          new Predicate<SubscriptionBaseEvent>() {
-                                                                              @Override
-                                                                              public boolean apply(final SubscriptionBaseEvent input) {
-                                                                                  return input.getType() == EventType.API_USER && ((ApiEvent) input).getApiEventType() == ApiEventType.CANCEL;
-                                                                              }
-                                                                          }).orNull();
         for (final SubscriptionBaseEvent cur : inputEvents) {
             if (!cur.isActive()) {
                 continue;
             }
 
-            if (cancellationEvent != null) {
-                if (cur.getId().compareTo(cancellationEvent.getId()) == 0) {
-                    // Keep the cancellation event
-                } else if (cur.getType() == EventType.API_USER && (((ApiEvent) cur).getApiEventType() == ApiEventType.TRANSFER || ((ApiEvent) cur).getApiEventType() == ApiEventType.CREATE)) {
-                    // Keep the initial event (SOT use-case)
-                } else if (cur.getEffectiveDate().compareTo(cancellationEvent.getEffectiveDate()) >= 0) {
-                    // Event to ignore past cancellation date
-                    continue;
-                }
-            }
-
             ApiEventType apiEventType = null;
             boolean isFromDisk = true;
 
@@ -811,73 +813,41 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
         }
     }
 
+    // Skip any event after a CANCEL event:
     //
-    // Hardening against data integrity issues where we have multiple active CANCEL (See #619):
-    // We skip any cancel events after the first one (subscription cannot be cancelled multiple times).
-    // The code should prevent such cases from happening but because of #619, some invalid data could be there so to be safe we added this code
-    //
-    // Also we remove !onDisk cancel events when there is an onDisk cancel event (can happen during the path where we process the base plan cancel notification, and are
-    // in the process of adding the new cancel events for the AO)
+    //  * DefaultSubscriptionDao#buildBundleSubscriptions may have added an out-of-order cancellation event (https://github.com/killbill/killbill/issues/897)
+    //  * Hardening against data integrity issues where we have multiple active CANCEL (https://github.com/killbill/killbill/issues/619)
     //
-    private void filterOutDuplicateCancelEvents(final List<SubscriptionBaseEvent> inputEvents) {
-
-        Collections.sort(inputEvents, new Comparator<SubscriptionBaseEvent>() {
-            @Override
-            public int compare(final SubscriptionBaseEvent o1, final SubscriptionBaseEvent o2) {
-                int res = o1.getEffectiveDate().compareTo(o2.getEffectiveDate());
-                if (res == 0) {
-                    // In-memory events have a total order of 0, make sure they are after on disk event
-                    if (o1.getTotalOrdering() == 0 && o2.getTotalOrdering() > 0) {
-                        return 1;
-                    } else if (o1.getTotalOrdering() > 0 && o2.getTotalOrdering() == 0) {
-                        return -1;
-                    } else {
-                        res = o1.getTotalOrdering() < (o2.getTotalOrdering()) ? -1 : 1;
-                    }
-                }
-                return res;
-            }
-        });
-
-        final boolean isCancelled = Iterables.any(inputEvents, new Predicate<SubscriptionBaseEvent>() {
-            @Override
-            public boolean apply(final SubscriptionBaseEvent input) {
-                if (input.isActive() && input.getType() == EventType.API_USER) {
-                    final ApiEvent userEV = (ApiEvent) input;
-                    if (userEV.getApiEventType() == ApiEventType.CANCEL && userEV.isFromDisk()) {
-                        return true;
-                    }
-                }
-                return false;
-            }
-        });
-
-        if (!isCancelled) {
+    private void removeEverythingPastCancelEvent(final List<SubscriptionBaseEvent> inputEvents) {
+        final SubscriptionBaseEvent cancellationEvent = Iterables.tryFind(inputEvents,
+                                                                          new Predicate<SubscriptionBaseEvent>() {
+                                                                              @Override
+                                                                              public boolean apply(final SubscriptionBaseEvent input) {
+                                                                                  return input.getType() == EventType.API_USER && ((ApiEvent) input).getApiEventType() == ApiEventType.CANCEL;
+                                                                              }
+                                                                          }).orNull();
+        if (cancellationEvent == null) {
             return;
         }
 
-
-        boolean foundFirstOnDiskCancel = false;
-        final Iterator<SubscriptionBaseEvent> it =  inputEvents.iterator();
-        while(it.hasNext()) {
+        final Iterator<SubscriptionBaseEvent> it = inputEvents.iterator();
+        while (it.hasNext()) {
             final SubscriptionBaseEvent input = it.next();
             if (!input.isActive()) {
                 continue;
             }
 
-            if (input.getType() == EventType.API_USER) {
-                final ApiEvent userEV = (ApiEvent) input;
-                if (userEV.getApiEventType() == ApiEventType.CANCEL) {
-                    if (userEV.isFromDisk()) {
-                        if (!foundFirstOnDiskCancel) {
-                            foundFirstOnDiskCancel = true;
-                        } else {
-                            it.remove();
-                        }
-                    } else {
-                        it.remove();
-                    }
-                }
+            if (input.getId().compareTo(cancellationEvent.getId()) == 0) {
+                // Keep the cancellation event
+            } else if (input.getType() == EventType.API_USER && (((ApiEvent) input).getApiEventType() == ApiEventType.TRANSFER || ((ApiEvent) input).getApiEventType() == ApiEventType.CREATE)) {
+                // Keep the initial event (SOT use-case)
+            } else if (input.getType() == EventType.API_USER && (((ApiEvent) input).getApiEventType() == ApiEventType.CANCEL) && !((ApiEvent) input).isFromDisk()) {
+                // Also we remove !onDisk cancel events when there is an onDisk cancel event (can happen during the path where we process the base plan cancel notification, and are
+                // in the process of adding the new cancel events for the AO)
+                it.remove();
+            } else if (input.getEffectiveDate().compareTo(cancellationEvent.getEffectiveDate()) >= 0) {
+                // Event to ignore past cancellation date
+                it.remove();
             }
         }
     }
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
index cada68b..54f4493 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
@@ -809,13 +809,14 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
         final UUID subscriptionId = subscription.getId();
         cancelFutureEventsFromTransaction(subscriptionId, cancelEvent.getEffectiveDate(), entitySqlDaoWrapperFactory, true, context);
         final SubscriptionEventSqlDao subscriptionEventSqlDao = entitySqlDaoWrapperFactory.become(SubscriptionEventSqlDao.class);
-        createAndRefresh(subscriptionEventSqlDao, new SubscriptionEventModelDao(cancelEvent), context);
+        final SubscriptionEventModelDao cancelEventWithUpdatedTotalOrdering = createAndRefresh(subscriptionEventSqlDao, new SubscriptionEventModelDao(cancelEvent), context);
 
-        final boolean isBusEvent = cancelEvent.getEffectiveDate().compareTo(clock.getUTCNow()) <= 0;
-        recordBusOrFutureNotificationFromTransaction(subscription, cancelEvent, entitySqlDaoWrapperFactory, isBusEvent, seqId, catalog, context);
+        final SubscriptionBaseEvent refreshedSubscriptionEvent = SubscriptionEventModelDao.toSubscriptionEvent(cancelEventWithUpdatedTotalOrdering);
+        final boolean isBusEvent = refreshedSubscriptionEvent.getEffectiveDate().compareTo(clock.getUTCNow()) <= 0;
+        recordBusOrFutureNotificationFromTransaction(subscription, refreshedSubscriptionEvent, entitySqlDaoWrapperFactory, isBusEvent, seqId, catalog, context);
 
         // Notify the Bus of the requested change
-        notifyBusOfRequestedChange(entitySqlDaoWrapperFactory, subscription, cancelEvent, SubscriptionBaseTransitionType.CANCEL, context);
+        notifyBusOfRequestedChange(entitySqlDaoWrapperFactory, subscription, refreshedSubscriptionEvent, SubscriptionBaseTransitionType.CANCEL, context);
     }
 
     private void cancelNextPhaseEventFromTransaction(final UUID subscriptionId, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalCallContext context) {