killbill-memoizeit

Merge branch 'junction-rework' of github.com:killbill/killbill

4/29/2017 8:00:55 PM

Details

diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BlockingCalculator.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BlockingCalculator.java
index ab0c806..864d7f9 100644
--- a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BlockingCalculator.java
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BlockingCalculator.java
@@ -33,7 +33,6 @@ import java.util.concurrent.atomic.AtomicLong;
 import javax.annotation.Nullable;
 
 import org.joda.time.DateTime;
-import org.joda.time.Days;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.Catalog;
@@ -66,74 +65,6 @@ public class BlockingCalculator {
     private final BlockingInternalApi blockingApi;
     private final CatalogService catalogService;
 
-    protected static class DisabledDuration implements Comparable<DisabledDuration> {
-
-        private final DateTime start;
-        private DateTime end;
-
-        public DisabledDuration(final DateTime start, final DateTime end) {
-            this.start = start;
-            this.end = end;
-        }
-
-        public DateTime getStart() {
-            return start;
-        }
-
-        public DateTime getEnd() {
-            return end;
-        }
-
-        public void setEnd(final DateTime end) {
-            this.end = end;
-        }
-
-
-        // Order by start date first and then end date
-        @Override
-        public int compareTo(final DisabledDuration o) {
-            int result = start.compareTo(o.getStart());
-            if (result == 0) {
-                result = end.compareTo(o.getEnd());
-            }
-            return result;
-        }
-
-        //
-        //
-        //  Assumptions (based on ordering):
-        //   * this.start <= o.start
-        //   * this.end <= o.end when this.start == o.start
-        //
-        // Case 1: this contained into o => false
-        // |---------|       this
-        // |--------------|  o
-        //
-        // Case 2: this overlaps with o => false
-        // |---------|            this
-        //      |--------------|  o
-        //
-        // Case 3: o contains into this => false
-        // |---------| this
-        //      |---|  o
-        //
-        // Case 4: this and o are adjacent => false
-        // |---------| this
-        //           |---|  o
-        // Case 5: this and o are disjoint => true
-        // |---------| this
-        //             |---|  o
-        public boolean isDisjoint(final DisabledDuration o) {
-            return end.compareTo(o.getStart()) < 0;
-        }
-
-        public static DisabledDuration mergeDuration(DisabledDuration d1, DisabledDuration d2) {
-            final DateTime endDate = d1.getEnd().compareTo(d2.getEnd()) < 0 ? d2.getEnd() : d1.getEnd();
-            return new DisabledDuration(d1.getStart(), endDate);
-        }
-
-    }
-
     @Inject
     public BlockingCalculator(final BlockingInternalApi blockingApi, final CatalogService catalogService) {
         this.blockingApi = blockingApi;
@@ -368,7 +299,7 @@ public class BlockingCalculator {
     // In ascending order
     protected List<DisabledDuration> createBlockingDurations(final Iterable<BlockingState> inputBundleEvents) {
 
-        final List<DisabledDuration> result = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> result = new ArrayList<DisabledDuration>();
 
         final Set<String> services = ImmutableSet.copyOf(Iterables.transform(inputBundleEvents, new Function<BlockingState, String>() {
             @Override
@@ -383,43 +314,14 @@ public class BlockingCalculator {
         }
 
         for (final BlockingState e : inputBundleEvents) {
-
             final BlockingStateNesting svcBlockingStateNesting = svcBlockedNesting.get(e.getService());
-            int blockedNesting = svcBlockingStateNesting.getBlockedNesting();
-            BlockingState first = svcBlockingStateNesting.getFirst();
-
-            if (e.isBlockBilling() && blockedNesting == 0) {
-                // First blocking event of contiguous series of blocking events
-                first = e;
-                blockedNesting++;
-            } else if (e.isBlockBilling() && blockedNesting > 0) {
-                // Nest blocking states
-                blockedNesting++;
-            } else if (!e.isBlockBilling() && blockedNesting > 0) {
-                blockedNesting--;
-                if (blockedNesting == 0) {
-                    // End of the interval
-                    svcBlockingStateNesting.addDisabledDuration(first, e);
-                    first = null;
-                }
-            }
-
-            svcBlockingStateNesting.setFirst(first);
-            svcBlockingStateNesting.setBlockedNesting(blockedNesting);
-            svcBlockingStateNesting.setLastOne(e);
-        }
-
-
-        for (final BlockingStateNesting svc : svcBlockedNesting.values()) {
-            if (svc.getFirst() != null) {
-                svc.addDisabledDuration(svc.getFirst(), svc.getLastOne().isBlockBilling() ? null : svc.getLastOne());
-            }
+            svcBlockingStateNesting.addBlockingState(e);
         }
 
         Iterable<DisabledDuration> unorderedDisabledDuration = Iterables.concat(Iterables.transform(svcBlockedNesting.values(), new Function<BlockingStateNesting, List<DisabledDuration>>() {
             @Override
             public List<DisabledDuration> apply(final BlockingStateNesting input) {
-                return input.getResult();
+                return input.build();
             }
         }));
 
@@ -446,69 +348,6 @@ public class BlockingCalculator {
         return result;
     }
 
-
-    private static class BlockingStateNesting {
-
-        final List<DisabledDuration> result;
-
-        private int blockedNesting;
-        private BlockingState first;
-
-        private BlockingState lastOne;
-
-        public BlockingStateNesting() {
-            this.blockedNesting = 0;
-            this.first = null;
-            this.lastOne = null;
-            this.result = new ArrayList<DisabledDuration>();
-        }
-
-        public int getBlockedNesting() {
-            return blockedNesting;
-        }
-
-        public BlockingState getFirst() {
-            return first;
-        }
-
-        public List<DisabledDuration> getResult() {
-            return result;
-        }
-
-        public BlockingState getLastOne() {
-            return lastOne;
-        }
-
-        public void setLastOne(final BlockingState lastOne) {
-            this.lastOne = lastOne;
-        }
-
-        public void setBlockedNesting(final int blockedNesting) {
-            this.blockedNesting = blockedNesting;
-        }
-
-        public void setFirst(final BlockingState input) {
-            first = input;
-        }
-
-        private void addDisabledDuration(final BlockingState firstBlocking, @Nullable final BlockingState firstNonBlocking) {
-
-            final DisabledDuration lastOne = result.isEmpty() ? null : result.get(result.size() - 1);
-
-            final DateTime startDate = firstBlocking.getEffectiveDate();
-            final DateTime endDate = firstNonBlocking == null ? null : firstNonBlocking.getEffectiveDate();
-
-
-            if (lastOne != null && lastOne.getEnd().compareTo(startDate) >= 0) {
-                lastOne.setEnd(endDate);
-            } else if (endDate == null || Days.daysBetween(startDate, endDate).getDays() >= 1) {
-                // Don't disable for periods less than a day (see https://github.com/killbill/killbill/issues/267)
-                result.add(new DisabledDuration(startDate, endDate));
-            }
-        }
-    }
-
-
     @VisibleForTesting
     static AtomicLong getGlobalTotalOrder() {
         return globaltotalOrder;
diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BlockingStateNesting.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BlockingStateNesting.java
new file mode 100644
index 0000000..1e5c730
--- /dev/null
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BlockingStateNesting.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 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.junction.plumbing.billing;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.Days;
+import org.killbill.billing.entitlement.api.BlockingState;
+
+public class BlockingStateNesting {
+
+    private int nestingLevel;
+    private BlockingState first;
+
+    final List<DisabledDuration> result;
+
+    public BlockingStateNesting() {
+        this.nestingLevel = 0;
+        this.first = null;
+        this.result = new ArrayList<DisabledDuration>();
+    }
+
+
+    public List<DisabledDuration> build() {
+        if (first != null) {
+            addDisabledDuration(null);
+        }
+        return result;
+    }
+
+    public void addBlockingState(final BlockingState currentBlockingState) {
+
+        if (currentBlockingState.isBlockBilling()) {
+            if (nestingLevel == 0) {
+                first = currentBlockingState;
+            }
+            nestingLevel++;
+        }
+
+        if (!currentBlockingState.isBlockBilling() && nestingLevel > 0) {
+            nestingLevel--;
+            if (nestingLevel == 0) {
+                addDisabledDuration(currentBlockingState.getEffectiveDate());
+                first = null;
+            }
+        }
+    }
+
+    private void addDisabledDuration(@Nullable final DateTime disableDurationEndDate) {
+
+        if (disableDurationEndDate == null || Days.daysBetween(first.getEffectiveDate(), disableDurationEndDate).getDays() >= 1) {
+            // Don't disable for periods less than a day (see https://github.com/killbill/killbill/issues/267)
+            result.add(new DisabledDuration(first.getEffectiveDate(), disableDurationEndDate));
+        }
+    }
+}
diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DisabledDuration.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DisabledDuration.java
new file mode 100644
index 0000000..78633b9
--- /dev/null
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DisabledDuration.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 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.junction.plumbing.billing;
+
+import org.joda.time.DateTime;
+
+class DisabledDuration implements Comparable<DisabledDuration> {
+
+    private final DateTime start;
+    private DateTime end;
+
+    public DisabledDuration(final DateTime start, final DateTime end) {
+        this.start = start;
+        this.end = end;
+    }
+
+    public DateTime getStart() {
+        return start;
+    }
+
+    public DateTime getEnd() {
+        return end;
+    }
+
+    public void setEnd(final DateTime end) {
+        this.end = end;
+    }
+
+    // Order by start date first and then end date
+    @Override
+    public int compareTo(final DisabledDuration o) {
+        int result = start.compareTo(o.getStart());
+        if (result == 0) {
+            if (end == null && o.getEnd() == null) {
+                result = 0;
+            } else if (end != null && o.getEnd() != null) {
+                result = end.compareTo(o.getEnd());
+            } else if (o.getEnd() == null) {
+                return -1;
+            } else {
+                return 1;
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof DisabledDuration)) {
+            return false;
+        }
+
+        final DisabledDuration that = (DisabledDuration) o;
+
+        return compareTo(that) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = start != null ? start.hashCode() : 0;
+        result = 31 * result + (end != null ? end.hashCode() : 0);
+        return result;
+    }
+
+    //
+    //
+    //  Assumptions (based on ordering):
+    //   * this.start <= o.start
+    //   * this.end <= o.end when this.start == o.start
+    //
+    // Case 1: this contained into o => false
+    // |---------|       this
+    // |--------------|  o
+    //
+    // Case 2: this overlaps with o => false
+    // |---------|            this
+    //      |--------------|  o
+    //
+    // Case 3: o contains into this => false
+    // |---------| this
+    //      |---|  o
+    //
+    // Case 4: this and o are adjacent => false
+    // |---------| this
+    //           |---|  o
+    // Case 5: this and o are disjoint => true
+    // |---------| this
+    //             |---|  o
+    public boolean isDisjoint(final DisabledDuration o) {
+        return end.compareTo(o.getStart()) < 0;
+    }
+
+    public static DisabledDuration mergeDuration(DisabledDuration d1, DisabledDuration d2) {
+        final DateTime endDate = d1.getEnd().compareTo(d2.getEnd()) < 0 ? d2.getEnd() : d1.getEnd();
+        return new DisabledDuration(d1.getStart(), endDate);
+    }
+
+}
diff --git a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingCalculator.java b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingCalculator.java
index 32a7bb7..0affa91 100644
--- a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingCalculator.java
+++ b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingCalculator.java
@@ -47,7 +47,6 @@ import org.killbill.billing.entitlement.dao.MockBlockingStateDao;
 import org.killbill.billing.junction.BillingEvent;
 import org.killbill.billing.junction.DefaultBlockingState;
 import org.killbill.billing.junction.JunctionTestSuiteNoDB;
-import org.killbill.billing.junction.plumbing.billing.BlockingCalculator.DisabledDuration;
 import org.killbill.billing.subscription.api.SubscriptionBase;
 import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
 import org.mockito.Mockito;
@@ -154,7 +153,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testEventsToRemoveOpenPrev() {
         final DateTime now = clock.getUTCNow();
-        final List<DisabledDuration> disabledDuration = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> disabledDuration = new ArrayList<DisabledDuration>();
         final SortedSet<BillingEvent> billingEvents = new TreeSet<BillingEvent>();
 
         disabledDuration.add(new DisabledDuration(now, null));
@@ -170,7 +169,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testEventsToRemoveOpenPrevFollow() {
         final DateTime now = clock.getUTCNow();
-        final List<DisabledDuration> disabledDuration = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> disabledDuration = new ArrayList<DisabledDuration>();
         final SortedSet<BillingEvent> billingEvents = new TreeSet<BillingEvent>();
 
         disabledDuration.add(new DisabledDuration(now, null));
@@ -190,7 +189,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testEventsToRemoveOpenFollow() {
         final DateTime now = clock.getUTCNow();
-        final List<DisabledDuration> disabledDuration = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> disabledDuration = new ArrayList<DisabledDuration>();
         final SortedSet<BillingEvent> billingEvents = new TreeSet<BillingEvent>();
 
         disabledDuration.add(new DisabledDuration(now, null));
@@ -208,7 +207,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testEventsToRemoveOpenSameTime() {
         final DateTime now = clock.getUTCNow();
-        final List<DisabledDuration> disabledDuration = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> disabledDuration = new ArrayList<DisabledDuration>();
         final SortedSet<BillingEvent> billingEvents = new TreeSet<BillingEvent>();
 
         disabledDuration.add(new DisabledDuration(now, null));
@@ -226,7 +225,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testEventsToRemoveClosedPrev() {
         final DateTime now = clock.getUTCNow();
-        final List<DisabledDuration> disabledDuration = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> disabledDuration = new ArrayList<DisabledDuration>();
         final SortedSet<BillingEvent> billingEvents = new TreeSet<BillingEvent>();
 
         disabledDuration.add(new DisabledDuration(now, now.plusDays(2)));
@@ -243,7 +242,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testEventsToRemoveClosedPrevBetw() {
         final DateTime now = clock.getUTCNow();
-        final List<DisabledDuration> disabledDuration = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> disabledDuration = new ArrayList<DisabledDuration>();
         final SortedSet<BillingEvent> billingEvents = new TreeSet<BillingEvent>();
 
         disabledDuration.add(new DisabledDuration(now, now.plusDays(2)));
@@ -263,7 +262,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testEventsToRemoveClosedPrevBetwNext() {
         final DateTime now = clock.getUTCNow();
-        final List<DisabledDuration> disabledDuration = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> disabledDuration = new ArrayList<DisabledDuration>();
         final SortedSet<BillingEvent> billingEvents = new TreeSet<BillingEvent>();
 
         disabledDuration.add(new DisabledDuration(now, now.plusDays(2)));
@@ -285,7 +284,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testEventsToRemoveClosedBetwn() {
         final DateTime now = clock.getUTCNow();
-        final List<DisabledDuration> disabledDuration = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> disabledDuration = new ArrayList<DisabledDuration>();
         final SortedSet<BillingEvent> billingEvents = new TreeSet<BillingEvent>();
 
         disabledDuration.add(new DisabledDuration(now, now.plusDays(2)));
@@ -303,7 +302,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testEventsToRemoveClosedBetweenFollow() {
         final DateTime now = clock.getUTCNow();
-        final List<DisabledDuration> disabledDuration = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> disabledDuration = new ArrayList<DisabledDuration>();
         final SortedSet<BillingEvent> billingEvents = new TreeSet<BillingEvent>();
 
         disabledDuration.add(new DisabledDuration(now, now.plusDays(2)));
@@ -324,7 +323,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testEventsToRemoveClosedFollow() {
         final DateTime now = clock.getUTCNow();
-        final List<DisabledDuration> disabledDuration = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> disabledDuration = new ArrayList<DisabledDuration>();
         final SortedSet<BillingEvent> billingEvents = new TreeSet<BillingEvent>();
 
         disabledDuration.add(new DisabledDuration(now, now.plusDays(2)));
@@ -343,7 +342,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testCreateNewEventsOpenPrev() throws CatalogApiException {
         final DateTime now = clock.getUTCNow();
-        final List<DisabledDuration> disabledDuration = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> disabledDuration = new ArrayList<DisabledDuration>();
         final SortedSet<BillingEvent> billingEvents = new TreeSet<BillingEvent>();
 
         disabledDuration.add(new DisabledDuration(now, null));
@@ -364,7 +363,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testCreateNewEventsOpenPrevFollow() throws CatalogApiException {
         final DateTime now = clock.getUTCNow();
-        final List<DisabledDuration> disabledDuration = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> disabledDuration = new ArrayList<DisabledDuration>();
         final SortedSet<BillingEvent> billingEvents = new TreeSet<BillingEvent>();
 
         disabledDuration.add(new DisabledDuration(now, null));
@@ -386,7 +385,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testCreateNewEventsOpenFollow() throws CatalogApiException {
         final DateTime now = clock.getUTCNow();
-        final List<DisabledDuration> disabledDuration = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> disabledDuration = new ArrayList<DisabledDuration>();
         final SortedSet<BillingEvent> billingEvents = new TreeSet<BillingEvent>();
 
         disabledDuration.add(new DisabledDuration(now, null));
@@ -402,7 +401,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testCreateNewEventsOpenSameTime() throws CatalogApiException {
         final DateTime now = clock.getUTCNow();
-        final List<DisabledDuration> disabledDuration = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> disabledDuration = new ArrayList<DisabledDuration>();
         final SortedSet<BillingEvent> billingEvents = new TreeSet<BillingEvent>();
 
         disabledDuration.add(new DisabledDuration(now, null));
@@ -418,7 +417,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testCreateNewEventsClosedPrev() throws CatalogApiException {
         final DateTime now = clock.getUTCNow();
-        final List<DisabledDuration> disabledDuration = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> disabledDuration = new ArrayList<DisabledDuration>();
         final SortedSet<BillingEvent> billingEvents = new TreeSet<BillingEvent>();
 
         disabledDuration.add(new DisabledDuration(now, now.plusDays(2)));
@@ -443,7 +442,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testCreateNewEventsClosedPrevBetw() throws CatalogApiException {
         final DateTime now = clock.getUTCNow();
-        final List<DisabledDuration> disabledDuration = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> disabledDuration = new ArrayList<DisabledDuration>();
         final SortedSet<BillingEvent> billingEvents = new TreeSet<BillingEvent>();
 
         disabledDuration.add(new DisabledDuration(now, now.plusDays(2)));
@@ -468,7 +467,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testCreateNewEventsClosedPrevBetwNext() throws CatalogApiException {
         final DateTime now = clock.getUTCNow();
-        final List<DisabledDuration> disabledDuration = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> disabledDuration = new ArrayList<DisabledDuration>();
         final SortedSet<BillingEvent> billingEvents = new TreeSet<BillingEvent>();
 
         disabledDuration.add(new DisabledDuration(now, now.plusDays(2)));
@@ -494,7 +493,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testCreateNewEventsClosedBetwn() throws CatalogApiException {
         final DateTime now = clock.getUTCNow();
-        final List<DisabledDuration> disabledDuration = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> disabledDuration = new ArrayList<DisabledDuration>();
         final SortedSet<BillingEvent> billingEvents = new TreeSet<BillingEvent>();
 
         disabledDuration.add(new DisabledDuration(now, now.plusDays(2)));
@@ -513,7 +512,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testCreateNewEventsClosedBetweenFollow() throws CatalogApiException {
         final DateTime now = clock.getUTCNow();
-        final List<DisabledDuration> disabledDuration = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> disabledDuration = new ArrayList<DisabledDuration>();
         final SortedSet<BillingEvent> billingEvents = new TreeSet<BillingEvent>();
 
         disabledDuration.add(new DisabledDuration(now, now.plusDays(2)));
@@ -532,7 +531,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testCreateNewEventsClosedFollow() throws CatalogApiException {
         final DateTime now = clock.getUTCNow();
-        final List<DisabledDuration> disabledDuration = new ArrayList<BlockingCalculator.DisabledDuration>();
+        final List<DisabledDuration> disabledDuration = new ArrayList<DisabledDuration>();
         final SortedSet<BillingEvent> billingEvents = new TreeSet<BillingEvent>();
 
         disabledDuration.add(new DisabledDuration(now, now.plusDays(2)));
diff --git a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingStateNesting.java b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingStateNesting.java
new file mode 100644
index 0000000..b4e0806
--- /dev/null
+++ b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingStateNesting.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 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.junction.plumbing.billing;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.billing.junction.DefaultBlockingState;
+import org.killbill.billing.junction.JunctionTestSuiteNoDB;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+
+import static org.testng.Assert.assertEquals;
+
+public class TestBlockingStateNesting extends JunctionTestSuiteNoDB {
+
+
+    //
+    // In all tests:
+    // * events are B(locked) or U(nblocked)
+    // * Types does not matter as we only care about nesting level (A(ccount), B(undle), S(ubscription))
+
+    //             B    B     U     U
+    //             |----|-----|-----|
+    //             A    B     A     B
+    //
+    //  Expected:  B----------------U
+    //
+    @Test(groups = "fast")
+    public void testZeroNestingLevel() throws Exception {
+
+        final List<BlockingState> input = new ArrayList<BlockingState>();
+
+        final DateTimeZone tz = DateTimeZone.forID("America/Los_Angeles");
+        final DateTime testInit = new DateTime(2017, 04, 29, 14, 15, 53, tz);
+        clock.setTime(testInit);
+        input.add(createBillingBlockingState(BlockingStateType.ACCOUNT, "SVC", true, testInit));
+        input.add(createBillingBlockingState(BlockingStateType.SUBSCRIPTION_BUNDLE, "SVC", true, testInit.plusDays(1)));
+        input.add(createBillingBlockingState(BlockingStateType.ACCOUNT, "SVC", false, testInit.plusDays(2)));
+        input.add(createBillingBlockingState(BlockingStateType.SUBSCRIPTION_BUNDLE, "SVC", false, testInit.plusDays(3)));
+
+        final BlockingStateNesting test = new BlockingStateNesting();
+        for (BlockingState cur : input) {
+            test.addBlockingState(cur);
+        }
+        final List<DisabledDuration> result = test.build();
+
+        final List<DisabledDuration> expected = ImmutableList.of(new DisabledDuration(testInit, testInit.plusDays(3)));
+
+        verify(result, expected);
+    }
+
+
+    //               B    B     U
+    //               |----|-----|-----
+    //               A    B     A
+    //
+    // Expected:     B-------------------
+    //
+    @Test(groups = "fast")
+    public void testPositiveNestingLevel() throws Exception {
+
+        final List<BlockingState> input = new ArrayList<BlockingState>();
+
+        final DateTimeZone tz = DateTimeZone.forID("America/Los_Angeles");
+        final DateTime testInit = new DateTime(2017, 04, 29, 14, 15, 53, tz);
+        clock.setTime(testInit);
+        input.add(createBillingBlockingState(BlockingStateType.ACCOUNT, "SVC", true, testInit));
+        input.add(createBillingBlockingState(BlockingStateType.SUBSCRIPTION_BUNDLE, "SVC", true, testInit.plusDays(1)));
+        input.add(createBillingBlockingState(BlockingStateType.ACCOUNT, "SVC", false, testInit.plusDays(2)));
+
+        final BlockingStateNesting test = new BlockingStateNesting();
+        for (BlockingState cur : input) {
+            test.addBlockingState(cur);
+        }
+        final List<DisabledDuration> result = test.build();
+
+        final List<DisabledDuration> expected = ImmutableList.of(new DisabledDuration(testInit, null));
+
+        verify(result, expected);
+    }
+
+    //             B    U     B     U
+    //             |----|-----|-----|
+    //             A    A     A     A
+    //
+    //  Expected:  B----------------U
+    //
+    @Test(groups = "fast")
+    public void testMultipleDisabledDurations() throws Exception {
+
+        final List<BlockingState> input = new ArrayList<BlockingState>();
+
+        final DateTimeZone tz = DateTimeZone.forID("America/Los_Angeles");
+        final DateTime testInit = new DateTime(2017, 04, 29, 14, 15, 53, tz);
+        clock.setTime(testInit);
+        input.add(createBillingBlockingState(BlockingStateType.ACCOUNT, "SVC", true, testInit));
+        input.add(createBillingBlockingState(BlockingStateType.ACCOUNT, "SVC", false, testInit.plusDays(1)));
+        input.add(createBillingBlockingState(BlockingStateType.ACCOUNT, "SVC", true, testInit.plusDays(2)));
+        input.add(createBillingBlockingState(BlockingStateType.ACCOUNT, "SVC", false, testInit.plusDays(3)));
+
+        final BlockingStateNesting test = new BlockingStateNesting();
+        for (BlockingState cur : input) {
+            test.addBlockingState(cur);
+        }
+        final List<DisabledDuration> result = test.build();
+
+        final List<DisabledDuration> expected = ImmutableList.of(new DisabledDuration(testInit, testInit.plusDays(1)),
+                                                                 new DisabledDuration(testInit.plusDays(2), testInit.plusDays(3)));
+
+        verify(result, expected);
+    }
+
+
+    //             B    U     U
+    //             |----|-----|
+    //             AB   B     A
+    //
+    //  Expected:  B----------U
+    //
+    @Test(groups = "fast")
+    public void testSameBlockingDates() throws Exception {
+
+        final List<BlockingState> input = new ArrayList<BlockingState>();
+
+        final DateTimeZone tz = DateTimeZone.forID("America/Los_Angeles");
+        final DateTime testInit = new DateTime(2017, 04, 29, 14, 15, 53, tz);
+        clock.setTime(testInit);
+        input.add(createBillingBlockingState(BlockingStateType.ACCOUNT, "SVC", true, testInit));
+        input.add(createBillingBlockingState(BlockingStateType.SUBSCRIPTION_BUNDLE, "SVC", true, testInit));
+        input.add(createBillingBlockingState(BlockingStateType.SUBSCRIPTION_BUNDLE, "SVC", false, testInit.plusDays(1)));
+        input.add(createBillingBlockingState(BlockingStateType.ACCOUNT, "SVC", false, testInit.plusDays(2)));
+
+        final BlockingStateNesting test = new BlockingStateNesting();
+        for (BlockingState cur : input) {
+            test.addBlockingState(cur);
+        }
+        final List<DisabledDuration> result = test.build();
+
+        final List<DisabledDuration> expected = ImmutableList.of(new DisabledDuration(testInit, testInit.plusDays(2)));
+
+        verify(result, expected);
+    }
+
+
+    //             BU
+    //             |
+    //             AA
+    //
+    //  Expected:  None
+    //
+    @Test(groups = "fast")
+    public void testSameBlockingUnblockingDates() throws Exception {
+
+        final List<BlockingState> input = new ArrayList<BlockingState>();
+
+        final DateTimeZone tz = DateTimeZone.forID("America/Los_Angeles");
+        final DateTime testInit = new DateTime(2017, 04, 29, 14, 15, 53, tz);
+        clock.setTime(testInit);
+        input.add(createBillingBlockingState(BlockingStateType.ACCOUNT, "SVC", true, testInit));
+        input.add(createBillingBlockingState(BlockingStateType.ACCOUNT, "SVC", false, testInit));
+
+        final BlockingStateNesting test = new BlockingStateNesting();
+        for (BlockingState cur : input) {
+            test.addBlockingState(cur);
+        }
+        final List<DisabledDuration> result = test.build();
+
+        final List<DisabledDuration> expected = ImmutableList.of();
+
+        verify(result, expected);
+    }
+
+
+    //             B U
+    //             |-|
+    //             A A
+    //
+    //  Expected:  None
+    //
+    @Test(groups = "fast")
+    public void testBlockingUnblockingDatesLessThanADay() throws Exception {
+
+        final List<BlockingState> input = new ArrayList<BlockingState>();
+
+        final DateTimeZone tz = DateTimeZone.forID("America/Los_Angeles");
+        final DateTime testInit = new DateTime(2017, 04, 29, 14, 15, 53, tz);
+        clock.setTime(testInit);
+        input.add(createBillingBlockingState(BlockingStateType.ACCOUNT, "SVC", true, testInit));
+        input.add(createBillingBlockingState(BlockingStateType.ACCOUNT, "SVC", false, testInit.plusHours(10)));
+
+        final BlockingStateNesting test = new BlockingStateNesting();
+        for (BlockingState cur : input) {
+            test.addBlockingState(cur);
+        }
+        final List<DisabledDuration> result = test.build();
+
+        final List<DisabledDuration> expected = ImmutableList.of();
+
+        verify(result, expected);
+    }
+
+
+
+    private void verify(final List<DisabledDuration> actual, final List<DisabledDuration> expected) {
+        assertEquals(expected.size(), actual.size());
+        for (int i = 0; i < actual.size(); i++) {
+            assertEquals(actual.get(i), expected.get(i));
+        }
+    }
+
+    private BlockingState createBillingBlockingState(final BlockingStateType type, final String service, final boolean blockBilling, final DateTime effectiveDate) {
+        return new DefaultBlockingState(UUID.randomUUID(), type, UUID.randomUUID().toString(), service, false, false, blockBilling, effectiveDate);
+    }
+
+}
\ No newline at end of file