killbill-memoizeit

Details

diff --git a/beatrix/src/test/resources/beatrix.properties b/beatrix/src/test/resources/beatrix.properties
index acc60b4..c0f1c1a 100644
--- a/beatrix/src/test/resources/beatrix.properties
+++ b/beatrix/src/test/resources/beatrix.properties
@@ -1,3 +1,4 @@
 org.killbill.catalog.uri=catalogTest.xml
+org.killbill.invoice.maxDailyNumberOfItemsSafetyBound=30
 org.killbill.payment.retry.days=8,8,8,8,8,8,8,8
 org.killbill.osgi.bundle.install.dir=/var/tmp/beatrix-bundles
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/plugin/StandaloneCatalogMapper.java b/catalog/src/main/java/org/killbill/billing/catalog/plugin/StandaloneCatalogMapper.java
index 6f52dc1..37184b7 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/plugin/StandaloneCatalogMapper.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/plugin/StandaloneCatalogMapper.java
@@ -76,6 +76,7 @@ import org.killbill.billing.catalog.rules.DefaultPlanRules;
 import com.google.common.base.Function;
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 
 public class StandaloneCatalogMapper {
@@ -83,8 +84,8 @@ public class StandaloneCatalogMapper {
     private final String catalogName;
     private final BillingMode recurringBillingMode;
 
-    private Iterable<Product> tmpDefaultProducts;
-    private Iterable<Plan> tmpDefaultPlans;
+    private Map<String, Product> tmpDefaultProducts;
+    private Map<String, Plan> tmpDefaultPlans;
     private DefaultPriceListSet tmpDefaultPriceListSet;
     private Map<String, DefaultPriceList> tmpDefaultPriceListMap;
 
@@ -110,19 +111,16 @@ public class StandaloneCatalogMapper {
         result.setUnits(toDefaultUnits(pluginCatalog.getUnits()));
         result.setPlanRules(toDefaultPlanRules(pluginCatalog.getPlanRules()));
         for (final Product cur : pluginCatalog.getProducts()) {
-            for (Product target :  result.getCurrentProducts()) {
-                if (target.getName().equals(cur.getName())) {
-                    ((DefaultProduct) target).setAvailable(toFilteredDefaultProduct(cur.getAvailable()));
-                    ((DefaultProduct) target).setIncluded(toFilteredDefaultProduct(cur.getIncluded()));
-                    break;
-                }
+            final Product target = result.getCatalogEntityCollectionProduct().findByName(cur.getName());
+            if (target != null) {
+                ((DefaultProduct) target).setAvailable(toFilteredDefaultProduct(cur.getAvailable()));
+                ((DefaultProduct) target).setIncluded(toFilteredDefaultProduct(cur.getIncluded()));
             }
         }
         result.initialize(result, catalogURI);
         return result;
     }
 
-
     private DefaultPlanRules toDefaultPlanRules(final PlanRules input) {
         final DefaultPlanRules result = new DefaultPlanRules();
         result.setBillingAlignmentCase(toDefaultCaseBillingAlignments(input.getCaseBillingAlignment()));
@@ -257,15 +255,11 @@ public class StandaloneCatalogMapper {
 
     private Iterable<Product> toDefaultProducts(final Iterable<Product> input) {
         if (tmpDefaultProducts == null) {
-            final Function<Product, Product> productTransformer = new Function<Product, Product>() {
-                @Override
-                public Product apply(final Product input) {
-                    return toDefaultProduct(input);
-                }
-            };
-            tmpDefaultProducts = ImmutableList.copyOf(Iterables.transform(input, productTransformer));
+            final Map<String, Product> map = new HashMap<String, Product>();
+            for (final Product product : input) map.put(product.getName(), toDefaultProduct(product));
+            tmpDefaultProducts = map;
         }
-        return tmpDefaultProducts;
+        return tmpDefaultProducts.values();
     }
 
     private Collection<Product> toFilteredDefaultProduct(final Collection<Product> input) {
@@ -280,12 +274,8 @@ public class StandaloneCatalogMapper {
         });
         final Collection<Product> filteredAndOrdered = new ArrayList<Product>(input.size());
         for (final String cur : inputProductNames) {
-            final Product found = findOrIllegalState(tmpDefaultProducts, new Predicate<Product>() {
-                @Override
-                public boolean apply(final Product inputPredicate) {
-                    return inputPredicate.getName().equals(cur);
-                }
-            }, "Failed to find product " + cur);
+            final Product found = tmpDefaultProducts.get(cur);
+            if (found == null) throw new IllegalStateException("Failed to find product " + cur);
             filteredAndOrdered.add(found);
         }
         return filteredAndOrdered;
@@ -293,22 +283,18 @@ public class StandaloneCatalogMapper {
 
     private Iterable<Plan> toDefaultPlans(final Iterable<Plan> input) {
         if (tmpDefaultPlans == null) {
-            final Function<Plan, Plan> planTransformer = new Function<Plan, Plan>() {
-                @Override
-                public Plan apply(final Plan input) {
-                    return toDefaultPlan(input);
-                }
-            };
-            tmpDefaultPlans = ImmutableList.copyOf(Iterables.transform(input, planTransformer));
+            final Map<String, Plan> map = new HashMap<String, Plan>();
+            for (final Plan plan : input) map.put(plan.getName(), toDefaultPlan(plan));
+            tmpDefaultPlans = map;
         }
-        return tmpDefaultPlans;
+        return tmpDefaultPlans.values();
     }
 
     private Iterable<Plan> toFilterDefaultPlans(final String priceListName) {
         if (tmpDefaultPlans == null) {
             throw new IllegalStateException("Cannot filter on uninitialized plans");
         }
-        return Iterables.filter(tmpDefaultPlans, new Predicate<Plan>() {
+        return Iterables.filter(tmpDefaultPlans.values(), new Predicate<Plan>() {
             @Override
             public boolean apply(final Plan input) {
                 return input.getPriceListName().equals(priceListName);
@@ -390,12 +376,8 @@ public class StandaloneCatalogMapper {
             return null;
         }
         if (tmpDefaultProducts != null) {
-            final Product existingProduct = findOrIllegalState(tmpDefaultProducts, new Predicate<Product>() {
-                @Override
-                public boolean apply(final Product predicateInput) {
-                    return predicateInput.getName().equals(input.getName());
-                }
-            }, "Unknown product " + input.getName());
+            final Product existingProduct = tmpDefaultProducts.get(input.getName());
+            if (existingProduct == null) throw new IllegalStateException("Unknown product " + input.getName());
             return existingProduct;
         }
         final DefaultProduct result = new DefaultProduct();
@@ -407,12 +389,8 @@ public class StandaloneCatalogMapper {
 
     private Plan toDefaultPlan(final Plan input) {
         if (tmpDefaultPlans != null) {
-            final Plan existingPlan = findOrIllegalState(tmpDefaultPlans, new Predicate<Plan>() {
-                @Override
-                public boolean apply(final Plan predicateInput) {
-                    return predicateInput.getName().equals(input.getName());
-                }
-            }, "Unknown plan " + input.getName());
+            final Plan existingPlan = tmpDefaultPlans.get(input.getName());
+            if (existingPlan == null) throw new IllegalStateException("Unknown plan " + input.getName());
             return existingPlan;
         }
         final DefaultPlan result = new DefaultPlan();
@@ -497,11 +475,4 @@ public class StandaloneCatalogMapper {
         return ImmutableList.<C>copyOf(input).toArray(foo);
     }
 
-    private <T> T findOrIllegalState(final Iterable<T> input, final Predicate<T> predicate, final String msg) {
-        T result = Iterables.<T> tryFind(input, predicate).orNull();
-        if (result == null) {
-            throw new IllegalStateException(msg);
-        }
-        return result;
-    }
 }
\ No newline at end of file
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java b/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java
index 79a0a45..acb4dd0 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/config/MultiTenantInvoiceConfig.java
@@ -53,6 +53,20 @@ public class MultiTenantInvoiceConfig extends MultiTenantConfigBase implements I
     }
 
     @Override
+    public int getMaxDailyNumberOfItemsSafetyBound() {
+        return staticConfig.getMaxDailyNumberOfItemsSafetyBound();
+    }
+
+    @Override
+    public int getMaxDailyNumberOfItemsSafetyBound(final InternalTenantContext tenantContext) {
+        final String result = getStringTenantConfig("getMaxDailyNumberOfItemsSafetyBound", tenantContext);
+        if (result != null) {
+            return Integer.parseInt(result);
+        }
+        return getMaxDailyNumberOfItemsSafetyBound();
+    }
+
+    @Override
     public TimeSpan getDryRunNotificationSchedule() {
         return staticConfig.getDryRunNotificationSchedule();
     }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
index 55106c2..0f22e05 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
@@ -19,6 +19,7 @@ package org.killbill.billing.invoice.generator;
 
 import java.math.BigDecimal;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -30,6 +31,7 @@ import org.joda.time.LocalDate;
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.account.api.ImmutableAccountData;
 import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.BillingMode;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.CatalogApiException;
@@ -47,11 +49,15 @@ import org.killbill.billing.invoice.model.RecurringInvoiceItemDataWithNextBillin
 import org.killbill.billing.invoice.tree.AccountItemTree;
 import org.killbill.billing.junction.BillingEvent;
 import org.killbill.billing.junction.BillingEventSet;
+import org.killbill.billing.util.config.definition.InvoiceConfig;
 import org.killbill.billing.util.currency.KillBillMoney;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.inject.Inject;
 
 import static org.killbill.billing.invoice.generator.InvoiceDateUtils.calculateNumberOfWholeBillingPeriods;
 import static org.killbill.billing.invoice.generator.InvoiceDateUtils.calculateProRationAfterLastBillingCycleDate;
@@ -61,10 +67,19 @@ public class FixedAndRecurringInvoiceItemGenerator extends InvoiceItemGenerator 
 
     private static final Logger log = LoggerFactory.getLogger(FixedAndRecurringInvoiceItemGenerator.class);
 
+    private final InvoiceConfig config;
+
+    @Inject
+    public FixedAndRecurringInvoiceItemGenerator(final InvoiceConfig config) {
+        this.config = config;
+    }
+
     public List<InvoiceItem> generateItems(final ImmutableAccountData account, final UUID invoiceId, final BillingEventSet eventSet,
                                            @Nullable final List<Invoice> existingInvoices, final LocalDate targetDate,
                                            final Currency targetCurrency, final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDate,
                                            final InternalCallContext internalCallContext) throws InvoiceApiException {
+        final Multimap<UUID, LocalDate> createdItemsPerDayPerSubscription = LinkedListMultimap.<UUID, LocalDate>create();
+
         final AccountItemTree accountItemTree = new AccountItemTree(account.getId(), invoiceId);
         if (existingInvoices != null) {
             for (final Invoice invoice : existingInvoices) {
@@ -73,6 +88,8 @@ public class FixedAndRecurringInvoiceItemGenerator extends InvoiceItemGenerator 
                         !eventSet.getSubscriptionIdsWithAutoInvoiceOff()
                                  .contains(item.getSubscriptionId())) { //don't add items with auto_invoice_off tag
                         accountItemTree.addExistingItem(item);
+
+                        trackInvoiceItemCreatedDay(item, createdItemsPerDayPerSubscription, internalCallContext);
                     }
                 }
             }
@@ -83,7 +100,11 @@ public class FixedAndRecurringInvoiceItemGenerator extends InvoiceItemGenerator 
         processRecurringBillingEvents(invoiceId, account.getId(), eventSet, targetDate, targetCurrency, proposedItems, perSubscriptionFutureNotificationDate, existingInvoices, internalCallContext);
         processFixedBillingEvents(invoiceId, account.getId(), eventSet, targetDate, targetCurrency, proposedItems, internalCallContext);
         accountItemTree.mergeWithProposedItems(proposedItems);
-        return accountItemTree.getResultingItemList();
+
+        final List<InvoiceItem> resultingItems = accountItemTree.getResultingItemList();
+        safetyBound(resultingItems, createdItemsPerDayPerSubscription, internalCallContext);
+
+        return resultingItems;
     }
 
     private void processRecurringBillingEvents(final UUID invoiceId, final UUID accountId, final BillingEventSet events,
@@ -376,4 +397,42 @@ public class FixedAndRecurringInvoiceItemGenerator extends InvoiceItemGenerator 
             }
         }
     }
+
+    // Trigger an exception if we create too many subscriptions for a subscription on a given day
+    private void safetyBound(final Iterable<InvoiceItem> resultingItems, final Multimap<UUID, LocalDate> createdItemsPerDayPerSubscription, final InternalTenantContext internalCallContext) throws InvoiceApiException {
+        if (config.getMaxDailyNumberOfItemsSafetyBound(internalCallContext) == -1) {
+            // Safety bound disabled
+            return;
+        }
+
+        for (final InvoiceItem invoiceItem : resultingItems) {
+            if (invoiceItem.getSubscriptionId() != null) {
+                final LocalDate resultingItemCreationDay = trackInvoiceItemCreatedDay(invoiceItem, createdItemsPerDayPerSubscription, internalCallContext);
+
+                final Collection<LocalDate> creationDaysForSubscription = createdItemsPerDayPerSubscription.get(invoiceItem.getSubscriptionId());
+                int i = 0;
+                for (final LocalDate creationDayForSubscription : creationDaysForSubscription) {
+                    if (creationDayForSubscription.compareTo(resultingItemCreationDay) == 0) {
+                        i++;
+                        if (i > config.getMaxDailyNumberOfItemsSafetyBound(internalCallContext)) {
+                            // Proposed items have already been logged
+                            throw new InvoiceApiException(ErrorCode.UNEXPECTED_ERROR, String.format("SAFETY BOUND TRIGGERED subscriptionId='%s', resultingItem=%s", invoiceItem.getSubscriptionId(), invoiceItem));
+                        }
+
+                    }
+                }
+            }
+        }
+    }
+
+    private LocalDate trackInvoiceItemCreatedDay(final InvoiceItem invoiceItem, final Multimap<UUID, LocalDate> createdItemsPerDayPerSubscription, final InternalTenantContext internalCallContext) {
+        final UUID subscriptionId = invoiceItem.getSubscriptionId();
+        if (subscriptionId == null) {
+            return null;
+        }
+
+        final LocalDate createdDay = internalCallContext.toLocalDate(invoiceItem.getCreatedDate());
+        createdItemsPerDayPerSubscription.put(subscriptionId, createdDay);
+        return createdDay;
+    }
 }
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
index 8c9c8c5..f28bca2 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * 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
@@ -122,6 +122,16 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
             }
 
             @Override
+            public int getMaxDailyNumberOfItemsSafetyBound() {
+                return 10;
+            }
+
+            @Override
+            public int getMaxDailyNumberOfItemsSafetyBound(final InternalTenantContext tenantContext) {
+                return 10;
+            }
+
+            @Override
             public boolean isEmailNotificationsEnabled() {
                 return false;
             }
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
index 3cd5432..5fbfc79 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
@@ -19,11 +19,14 @@ package org.killbill.billing.invoice.generator;
 
 import java.math.BigDecimal;
 import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
+import org.killbill.billing.ErrorCode;
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.catalog.DefaultPrice;
 import org.killbill.billing.catalog.MockInternationalPrice;
@@ -37,21 +40,25 @@ import org.killbill.billing.catalog.api.Plan;
 import org.killbill.billing.catalog.api.PlanPhase;
 import org.killbill.billing.invoice.InvoiceTestSuiteNoDB;
 import org.killbill.billing.invoice.MockBillingEventSet;
+import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.invoice.api.InvoiceApiException;
 import org.killbill.billing.invoice.api.InvoiceItem;
 import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
+import org.killbill.billing.invoice.model.DefaultInvoice;
 import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
+import org.killbill.billing.invoice.model.RecurringInvoiceItem;
 import org.killbill.billing.junction.BillingEvent;
 import org.killbill.billing.junction.BillingEventSet;
 import org.killbill.billing.subscription.api.SubscriptionBase;
 import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
-import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
 
 public class TestFixedAndRecurringInvoiceItemGenerator extends InvoiceTestSuiteNoDB {
 
@@ -67,7 +74,7 @@ public class TestFixedAndRecurringInvoiceItemGenerator extends InvoiceTestSuiteN
             account = invoiceUtil.createAccount(callContext);
             subscription = invoiceUtil.createSubscription();
         } catch (final Exception e) {
-            Assert.fail(e.getMessage());
+            fail(e.getMessage());
         }
     }
 
@@ -180,7 +187,6 @@ public class TestFixedAndRecurringInvoiceItemGenerator extends InvoiceTestSuiteN
                                                                        SubscriptionBaseTransitionType.CREATE);
         events.add(event1);
 
-
         final BillingEvent event2 = invoiceUtil.createMockBillingEvent(account, subscription, new DateTime("2016-01-08"),
                                                                        plan, phase,
                                                                        null, null, Currency.USD, BillingPeriod.NO_BILLING_PERIOD, 1,
@@ -213,7 +219,6 @@ public class TestFixedAndRecurringInvoiceItemGenerator extends InvoiceTestSuiteN
                                                                        SubscriptionBaseTransitionType.CREATE);
         events.add(event1);
 
-
         final BillingEvent event2 = invoiceUtil.createMockBillingEvent(account, subscription, new DateTime("2016-01-09"),
                                                                        plan, phase,
                                                                        null, null, Currency.USD, BillingPeriod.NO_BILLING_PERIOD, 1,
@@ -228,7 +233,6 @@ public class TestFixedAndRecurringInvoiceItemGenerator extends InvoiceTestSuiteN
         assertEquals(proposedItems.get(0).getAmount().compareTo(fixedPriceAmount), 0);
     }
 
-
     @Test(groups = "fast")
     public void testProcessFixedBillingEventsWithMultipleChangeOnSameDay() throws InvoiceApiException {
 
@@ -249,8 +253,6 @@ public class TestFixedAndRecurringInvoiceItemGenerator extends InvoiceTestSuiteN
                                                                        SubscriptionBaseTransitionType.CREATE);
         events.add(event1);
 
-
-
         final BigDecimal fixedPriceAmount2 = null;
         final MockInternationalPrice fixedPrice2 = new MockInternationalPrice(new DefaultPrice(fixedPriceAmount2, Currency.USD));
         final Plan plan2 = new MockPlan("my-plan2");
@@ -263,7 +265,6 @@ public class TestFixedAndRecurringInvoiceItemGenerator extends InvoiceTestSuiteN
                                                                        SubscriptionBaseTransitionType.CHANGE);
         events.add(event2);
 
-
         final BigDecimal fixedPriceAmount3 = BigDecimal.ONE;
         final MockInternationalPrice fixedPrice3 = new MockInternationalPrice(new DefaultPrice(fixedPriceAmount3, Currency.USD));
         final Plan plan3 = new MockPlan("my-plan3");
@@ -283,4 +284,92 @@ public class TestFixedAndRecurringInvoiceItemGenerator extends InvoiceTestSuiteN
         assertEquals(proposedItems.get(0).getAmount().compareTo(fixedPriceAmount3), 0);
     }
 
+    @Test(groups = "fast")
+    public void testSafetyBounds() throws InvoiceApiException {
+        final int threshold = 15;
+        final LocalDate startDate = new LocalDate("2016-01-01");
+
+        final BillingEventSet events = new MockBillingEventSet();
+        final BigDecimal amount = BigDecimal.TEN;
+        final MockInternationalPrice price = new MockInternationalPrice(new DefaultPrice(amount, account.getCurrency()));
+        final Plan plan = new MockPlan("my-plan");
+        final PlanPhase planPhase = new MockPlanPhase(price, null, BillingPeriod.MONTHLY, PhaseType.EVERGREEN);
+        final BillingEvent event = invoiceUtil.createMockBillingEvent(account,
+                                                                      subscription,
+                                                                      startDate.toDateTimeAtStartOfDay(),
+                                                                      plan,
+                                                                      planPhase,
+                                                                      null,
+                                                                      amount,
+                                                                      account.getCurrency(),
+                                                                      planPhase.getRecurring().getBillingPeriod(),
+                                                                      1,
+                                                                      BillingMode.IN_ADVANCE,
+                                                                      "Billing Event Desc",
+                                                                      1L,
+                                                                      SubscriptionBaseTransitionType.CREATE);
+        events.add(event);
+
+        // Simulate a big catch-up
+        final List<Invoice> existingInvoices = new LinkedList<Invoice>();
+        for (int i = 0; i < threshold; i++) {
+            final Invoice invoice = new DefaultInvoice(account.getId(), clock.getUTCToday(), startDate.plusMonths(i), account.getCurrency());
+            invoice.addInvoiceItem(new RecurringInvoiceItem(UUID.randomUUID(),
+                                                            startDate.plusMonths(i).toDateTimeAtStartOfDay(), // Different days - should not trigger the safety bounds
+                                                            invoice.getId(),
+                                                            account.getId(),
+                                                            subscription.getBundleId(),
+                                                            subscription.getId(),
+                                                            event.getPlan().getName(),
+                                                            event.getPlanPhase().getName(),
+                                                            startDate.plusMonths(i),
+                                                            startDate.plusMonths(1 + i),
+                                                            amount,
+                                                            amount,
+                                                            account.getCurrency()));
+            existingInvoices.add(invoice);
+        }
+
+        assertEquals(fixedAndRecurringInvoiceItemGenerator.generateItems(account,
+                                                                         UUID.randomUUID(),
+                                                                         events,
+                                                                         existingInvoices,
+                                                                         startDate.plusMonths(threshold),
+                                                                         account.getCurrency(),
+                                                                         new HashMap<UUID, SubscriptionFutureNotificationDates>(),
+                                                                         internalCallContext).size(), 1);
+
+        // Simulate a big catch-up on that day
+        for (int i = threshold; i < 2 * threshold; i++) {
+            final Invoice invoice = new DefaultInvoice(account.getId(), clock.getUTCToday(), startDate.plusMonths(i), account.getCurrency());
+            invoice.addInvoiceItem(new RecurringInvoiceItem(UUID.randomUUID(),
+                                                            clock.getUTCNow(), // Same day
+                                                            invoice.getId(),
+                                                            account.getId(),
+                                                            subscription.getBundleId(),
+                                                            subscription.getId(),
+                                                            event.getPlan().getName(),
+                                                            event.getPlanPhase().getName(),
+                                                            startDate.plusMonths(i),
+                                                            startDate.plusMonths(1 + i),
+                                                            amount,
+                                                            amount,
+                                                            account.getCurrency()));
+            existingInvoices.add(invoice);
+        }
+
+        try {
+            final List<InvoiceItem> generatedItems = fixedAndRecurringInvoiceItemGenerator.generateItems(account,
+                                                                                                         UUID.randomUUID(),
+                                                                                                         events,
+                                                                                                         existingInvoices,
+                                                                                                         startDate.plusMonths(2 * threshold),
+                                                                                                         account.getCurrency(),
+                                                                                                         new HashMap<UUID, SubscriptionFutureNotificationDates>(),
+                                                                                                         internalCallContext);
+            fail();
+        } catch (final InvoiceApiException e) {
+            assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
+        }
+    }
 }
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteWithEmbeddedDB.java b/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteWithEmbeddedDB.java
index 2f6bf60..487fd07 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteWithEmbeddedDB.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteWithEmbeddedDB.java
@@ -42,6 +42,7 @@ import org.killbill.billing.util.config.definition.InvoiceConfig;
 import org.killbill.billing.util.dao.NonEntityDao;
 import org.killbill.bus.api.PersistentBus;
 import org.killbill.clock.Clock;
+import org.killbill.clock.ClockMock;
 import org.killbill.commons.locker.GlobalLocker;
 import org.killbill.notificationq.api.NotificationQueueService;
 import org.slf4j.Logger;
@@ -91,7 +92,7 @@ public abstract class InvoiceTestSuiteWithEmbeddedDB extends GuicyKillbillTestSu
     @Inject
     protected GlobalLocker locker;
     @Inject
-    protected Clock clock;
+    protected ClockMock clock;
     @Inject
     protected InternalCallContextFactory internalCallContextFactory;
     @Inject
@@ -127,6 +128,7 @@ public abstract class InvoiceTestSuiteWithEmbeddedDB extends GuicyKillbillTestSu
         controllerDispatcher.clearAll();
         bus.start();
         restartInvoiceService(invoiceService);
+        clock.resetDeltaFromReality();
     }
 
     private void restartInvoiceService(final InvoiceService invoiceService) throws Exception {
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.java
index 0e2a8ab..ab12d94 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.java
@@ -18,23 +18,19 @@ package org.killbill.billing.subscription.engine.dao;
 
 import java.util.Date;
 import java.util.List;
-import java.util.UUID;
-
-import org.joda.time.DateTime;
-import org.killbill.billing.entity.EntityPersistenceException;
-import org.skife.jdbi.v2.sqlobject.Bind;
-import org.skife.jdbi.v2.sqlobject.BindBean;
-import org.skife.jdbi.v2.sqlobject.SqlQuery;
-import org.skife.jdbi.v2.sqlobject.SqlUpdate;
 
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.subscription.engine.dao.model.SubscriptionEventModelDao;
 import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
 import org.killbill.billing.util.audit.ChangeType;
-import org.killbill.billing.callcontext.InternalCallContext;
-import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.util.entity.dao.Audited;
 import org.killbill.billing.util.entity.dao.EntitySqlDao;
 import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
 
 @EntitySqlDaoStringTemplate
 public interface SubscriptionEventSqlDao extends EntitySqlDao<SubscriptionEventModelDao, SubscriptionBaseEvent> {
@@ -51,8 +47,8 @@ public interface SubscriptionEventSqlDao extends EntitySqlDao<SubscriptionEventM
 
     @SqlQuery
     public List<SubscriptionEventModelDao> getFutureOrPresentActiveEventForSubscription(@Bind("subscriptionId") String subscriptionId,
-                                                                               @Bind("now") Date now,
-                                                                               @BindBean final InternalTenantContext context);
+                                                                                        @Bind("now") Date now,
+                                                                                        @BindBean final InternalTenantContext context);
 
     @SqlQuery
     public List<SubscriptionEventModelDao> getEventsForSubscription(@Bind("subscriptionId") String subscriptionId,
diff --git a/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.sql.stg b/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.sql.stg
index 4b0aa5a..a924d70 100644
--- a/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.sql.stg
+++ b/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.sql.stg
@@ -70,6 +70,7 @@ and effective_date > :now
 ;
 >>
 
+
 getFutureOrPresentActiveEventForSubscription() ::= <<
 select <allTableFields()>
 , record_id as total_ordering
diff --git a/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java b/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java
index 5e05cad..776621b 100644
--- a/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java
+++ b/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java
@@ -36,6 +36,16 @@ public interface InvoiceConfig extends KillbillConfig {
     @Description("Maximum target date to consider when generating an invoice")
     int getNumberOfMonthsInFuture(@Param("dummy") final InternalTenantContext tenantContext);
 
+    @Config("org.killbill.invoice.maxDailyNumberOfItemsSafetyBound")
+    @Default("15")
+    @Description("Maximum daily number of invoice items to generate for a subscription id")
+    int getMaxDailyNumberOfItemsSafetyBound();
+
+    @Config("org.killbill.invoice.maxDailyNumberOfItemsSafetyBound")
+    @Default("15")
+    @Description("Maximum daily number of invoice items to generate for a subscription id")
+    int getMaxDailyNumberOfItemsSafetyBound(@Param("dummy") final InternalTenantContext tenantContext);
+
     @Config("org.killbill.invoice.dryRunNotificationSchedule")
     @Default("0s")
     @Description("DryRun invoice notification time before targetDate (ignored if set to 0s)")