killbill-memoizeit

Merge pull request #952 from killbill/perf-updates subscription:

4/20/2018 6:55:56 PM

Details

diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestCatalogRetireElements.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestCatalogRetireElements.java
index 0f3c7e1..965a5fb 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestCatalogRetireElements.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestCatalogRetireElements.java
@@ -53,8 +53,7 @@ public class TestCatalogRetireElements extends TestIntegrationBase {
         return super.getConfigSource("/beatrixCatalogRetireElements.properties");
     }
 
-    // Flaky, see https://github.com/killbill/killbill/issues/860
-    @Test(groups = "slow", retryAnalyzer = FlakyRetryAnalyzer.class)
+    @Test(groups = "slow")
     public void testRetirePlan() throws Exception {
         // Catalog v1 starts in 2011-01-01
         // Catalog v2 starts in 2015-12-01
@@ -111,8 +110,7 @@ public class TestCatalogRetireElements extends TestIntegrationBase {
 
     }
 
-    // Flaky, see https://github.com/killbill/killbill/issues/860
-    @Test(groups = "slow", retryAnalyzer = FlakyRetryAnalyzer.class)
+    @Test(groups = "slow")
     public void testRetirePlanWithUncancel() throws Exception {
         // Catalog v1 starts in 2011-01-01
         // Catalog v2 starts in 2015-12-01
@@ -176,8 +174,7 @@ public class TestCatalogRetireElements extends TestIntegrationBase {
         }
     }
 
-    // Flaky, see https://github.com/killbill/killbill/issues/860
-    @Test(groups = "slow", retryAnalyzer = FlakyRetryAnalyzer.class)
+    @Test(groups = "slow")
     public void testRetirePlanAfterChange() throws Exception {
         // Catalog v1 starts in 2011-01-01
         // Catalog v3 starts in 2016-01-01
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
index 4e8ecce..33fe5ab 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
@@ -308,6 +308,11 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB implemen
     public void beforeClass() throws Exception {
         final InvoiceConfig defaultInvoiceConfig = new ConfigurationObjectFactory(skifeConfigSource).build(InvoiceConfig.class);
         invoiceConfig = new ConfigurableInvoiceConfig(defaultInvoiceConfig);
+        // The default value is 50, i.e. wait 50 x 100ms = 5s to get the lock. This isn't always enough and can lead to random tests failures
+        // in the listener status: after moving the clock, if there are two notifications triggering an invoice run, we typically expect
+        // both an INVOICE and a NULL_INVOICE event. If the invoice generation takes too long, the NULL_INVOICE event is never generated
+        // (LockFailedException): the test itself doesn't fail (the correct invoice is generated), but assertListenerStatus() would.
+        invoiceConfig.setMaxGlobalLockRetries(150);
         final Injector g = Guice.createInjector(Stage.PRODUCTION, new BeatrixIntegrationModule(configSource, invoiceConfig));
         g.injectMembers(this);
     }
@@ -973,10 +978,12 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB implemen
 
         private final InvoiceConfig defaultInvoiceConfig;
 
+        private int maxGlobalLockRetries;
         private boolean isInvoicingSystemEnabled;
 
         public ConfigurableInvoiceConfig(final InvoiceConfig defaultInvoiceConfig) {
             this.defaultInvoiceConfig = defaultInvoiceConfig;
+            maxGlobalLockRetries = defaultInvoiceConfig.getMaxGlobalLockRetries();
             isInvoicingSystemEnabled = defaultInvoiceConfig.isInvoicingSystemEnabled();
         }
 
@@ -1032,7 +1039,7 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB implemen
 
         @Override
         public int getMaxGlobalLockRetries() {
-            return defaultInvoiceConfig.getMaxGlobalLockRetries();
+            return maxGlobalLockRetries;
         }
 
         @Override
@@ -1080,6 +1087,10 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB implemen
             return getItemResultBehaviorMode();
         }
 
+        public void setMaxGlobalLockRetries(final int maxGlobalLockRetries) {
+            this.maxGlobalLockRetries = maxGlobalLockRetries;
+        }
+
         public void setInvoicingSystemEnabled(final boolean invoicingSystemEnabled) {
             isInvoicingSystemEnabled = invoicingSystemEnabled;
         }
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
index a6ed103..dc76f96 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
@@ -643,10 +643,6 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
         }
     }
 
-    protected void verifyNumberOfElements(int actual, int expected, String errorMessage) {
-        Preconditions.checkArgument(actual == expected, errorMessage);
-    }
-
     protected void logDeprecationParameterWarningIfNeeded(@Nullable final String deprecatedParam, final String... replacementParams) {
         if (deprecatedParam != null) {
             log.warn(String.format("Parameter %s is being deprecated: Instead use parameters %s", deprecatedParam, Joiner.on(",").join(replacementParams)));
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
index 9bacaf2..7fdf952 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
@@ -28,6 +28,7 @@ import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.TimeoutException;
 
+import javax.annotation.Nullable;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
@@ -107,6 +108,7 @@ import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.inject.Inject;
@@ -329,8 +331,8 @@ public class SubscriptionResource extends JaxRsResourceBase {
 
         final Account account = accountUserApi.getAccountById(entitlementsWithAddOns.get(0).getBaseEntitlementAndAddOns().get(0).getAccountId(), callContext);
 
-        final List<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifierList = new ArrayList<BaseEntitlementWithAddOnsSpecifier>();
-        for (BulkSubscriptionsBundleJson bulkBaseEntitlementWithAddOns : entitlementsWithAddOns) {
+        final Collection<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifierList = new ArrayList<BaseEntitlementWithAddOnsSpecifier>();
+        for (final BulkSubscriptionsBundleJson bulkBaseEntitlementWithAddOns : entitlementsWithAddOns) {
             final Iterable<SubscriptionJson> baseEntitlements = Iterables.filter(
                     bulkBaseEntitlementWithAddOns.getBaseEntitlementAndAddOns(), new Predicate<SubscriptionJson>() {
                         @Override
@@ -338,26 +340,44 @@ public class SubscriptionResource extends JaxRsResourceBase {
                             return ProductCategory.BASE.toString().equalsIgnoreCase(subscription.getProductCategory());
                         }
                     });
-            Preconditions.checkArgument(Iterables.size(baseEntitlements) > 0, "SubscriptionJson Base Entitlement needs to be provided");
-            verifyNumberOfElements(Iterables.size(baseEntitlements), 1, "Only one BASE product is allowed per bundle.");
-            final SubscriptionJson baseEntitlement = baseEntitlements.iterator().next();
 
-            final Iterable<SubscriptionJson> addonEntitlements = Iterables.filter(
-                    bulkBaseEntitlementWithAddOns.getBaseEntitlementAndAddOns(), new Predicate<SubscriptionJson>() {
-                        @Override
-                        public boolean apply(final SubscriptionJson subscription) {
-                            return ProductCategory.ADD_ON.toString().equalsIgnoreCase(subscription.getProductCategory());
-                        }
-                    }
-                                                                                 );
-
-            final List<EntitlementSpecifier> entitlementSpecifierList = buildEntitlementSpecifierList(baseEntitlement, addonEntitlements, account.getCurrency());
+            final List<EntitlementSpecifier> entitlementSpecifierList;
+            final String bundleExternalKey;
+            if (baseEntitlements.iterator().hasNext()) {
+                Preconditions.checkArgument(Iterables.size(baseEntitlements) == 1, "Only one BASE product is allowed per bundle.");
+
+                final SubscriptionJson baseEntitlement = baseEntitlements.iterator().next();
+                final Iterable<SubscriptionJson> addonEntitlements = Iterables.filter(bulkBaseEntitlementWithAddOns.getBaseEntitlementAndAddOns(),
+                                                                                      new Predicate<SubscriptionJson>() {
+                                                                                          @Override
+                                                                                          public boolean apply(final SubscriptionJson subscription) {
+                                                                                              return ProductCategory.ADD_ON.toString().equalsIgnoreCase(subscription.getProductCategory());
+                                                                                          }
+                                                                                      });
+
+                entitlementSpecifierList = buildEntitlementSpecifierList(baseEntitlement, addonEntitlements, account.getCurrency());
+                bundleExternalKey = baseEntitlement.getExternalKey();
+            } else {
+                final Collection<SubscriptionJson> standaloneEntitlements = Collections2.filter(bulkBaseEntitlementWithAddOns.getBaseEntitlementAndAddOns(),
+                                                                                                new Predicate<SubscriptionJson>() {
+                                                                                                    @Override
+                                                                                                    public boolean apply(final SubscriptionJson subscription) {
+                                                                                                        return ProductCategory.STANDALONE.toString().equalsIgnoreCase(subscription.getProductCategory());
+                                                                                                    }
+                                                                                                });
+                entitlementSpecifierList = buildEntitlementSpecifierList(standaloneEntitlements, account.getCurrency());
+                bundleExternalKey = standaloneEntitlements.isEmpty() ? null : standaloneEntitlements.iterator().next().getExternalKey();
+            }
 
             // create the baseEntitlementSpecifierWithAddOns
             final LocalDate resolvedEntitlementDate = requestedDate != null ? toLocalDate(requestedDate) : toLocalDate(entitlementDate);
             final LocalDate resolvedBillingDate = requestedDate != null ? toLocalDate(requestedDate) : toLocalDate(billingDate);
 
-            final BaseEntitlementWithAddOnsSpecifier baseEntitlementSpecifierWithAddOns = buildBaseEntitlementWithAddOnsSpecifier(entitlementSpecifierList, resolvedEntitlementDate, resolvedBillingDate, null, baseEntitlement, isMigrated);
+            final BaseEntitlementWithAddOnsSpecifier baseEntitlementSpecifierWithAddOns = buildBaseEntitlementWithAddOnsSpecifier(entitlementSpecifierList,
+                                                                                                                                  resolvedEntitlementDate,
+                                                                                                                                  resolvedBillingDate,
+                                                                                                                                  bundleExternalKey,
+                                                                                                                                  isMigrated);
             baseEntitlementWithAddOnsSpecifierList.add(baseEntitlementSpecifierWithAddOns);
         }
 
@@ -398,35 +418,15 @@ public class SubscriptionResource extends JaxRsResourceBase {
         return callCompletionCreation.withSynchronization(callback, timeoutSec, callCompletion, callContext);
     }
 
-    private List<EntitlementSpecifier> buildEntitlementSpecifierList(final SubscriptionJson baseEntitlement, final Iterable<SubscriptionJson> addonEntitlements, final Currency currency) {
+    private List<EntitlementSpecifier> buildEntitlementSpecifierList(final SubscriptionJson baseEntitlement,
+                                                                     final Iterable<SubscriptionJson> addonEntitlements,
+                                                                     final Currency currency) {
         final List<EntitlementSpecifier> entitlementSpecifierList = new ArrayList<EntitlementSpecifier>();
 
         //
-        // BASE is fully specified we can add it
+        // BASE or STANDALONE is fully specified, we can add it
         //
-        if (baseEntitlement.getPlanName() != null ||
-            (baseEntitlement.getProductName() != null &&
-             baseEntitlement.getProductCategory() != null &&
-            baseEntitlement.getBillingPeriod() != null &&
-            baseEntitlement.getPriceList() != null)) {
-            final PlanPhaseSpecifier planPhaseSpecifier = baseEntitlement.getPlanName() != null ?
-                                                          new PlanPhaseSpecifier(baseEntitlement.getPlanName(), null) :
-                                                          new PlanPhaseSpecifier(baseEntitlement.getProductName(),
-                                                                                 BillingPeriod.valueOf(baseEntitlement.getBillingPeriod()), baseEntitlement.getPriceList(), null);
-            final List<PlanPhasePriceOverride> overrides = PhasePriceOverrideJson.toPlanPhasePriceOverrides(baseEntitlement.getPriceOverrides(), planPhaseSpecifier, currency);
-
-            EntitlementSpecifier specifier = new EntitlementSpecifier() {
-                @Override
-                public PlanPhaseSpecifier getPlanPhaseSpecifier() {
-                    return planPhaseSpecifier;
-                }
-                @Override
-                public List<PlanPhasePriceOverride> getOverrides() {
-                    return overrides;
-                }
-            };
-            entitlementSpecifierList.add(specifier);
-        }
+        buildEntitlementSpecifier(baseEntitlement, currency, entitlementSpecifierList);
 
         for (final SubscriptionJson entitlement : addonEntitlements) {
             // verifications
@@ -438,49 +438,96 @@ public class SubscriptionResource extends JaxRsResourceBase {
                                      entitlement.getPriceList(), "SubscriptionJson priceList needs to be set for each element");
             }
             // create the entitlementSpecifier
-            final PlanPhaseSpecifier planPhaseSpecifier = entitlement.getPlanName() != null ?
-                                                          new PlanPhaseSpecifier(entitlement.getPlanName(), null) :
-                                                          new PlanPhaseSpecifier(entitlement.getProductName(),
-                                                                                 BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList(), null);
-            final List<PlanPhasePriceOverride> overrides = PhasePriceOverrideJson.toPlanPhasePriceOverrides(entitlement.getPriceOverrides(), planPhaseSpecifier, currency);
-
-            EntitlementSpecifier specifier = new EntitlementSpecifier() {
-                @Override
-                public PlanPhaseSpecifier getPlanPhaseSpecifier() {
-                    return planPhaseSpecifier;
-                }
-                @Override
-                public List<PlanPhasePriceOverride> getOverrides() {
-                    return overrides;
-                }
-            };
-            entitlementSpecifierList.add(specifier);
+            buildEntitlementSpecifier(entitlement, currency, entitlementSpecifierList);
+        }
+        return entitlementSpecifierList;
+    }
+
+    private List<EntitlementSpecifier> buildEntitlementSpecifierList(final Iterable<SubscriptionJson> standaloneEntitlements,
+                                                                     final Currency currency) {
+        final List<EntitlementSpecifier> entitlementSpecifierList = new ArrayList<EntitlementSpecifier>();
+
+        for (final SubscriptionJson standaloneEntitlement : standaloneEntitlements) {
+            // verifications
+            verifyNonNullOrEmpty(standaloneEntitlement, "SubscriptionJson body should be specified for each element");
+            if (standaloneEntitlement.getPlanName() == null) {
+                verifyNonNullOrEmpty(standaloneEntitlement.getProductName(), "SubscriptionJson productName needs to be set for each element",
+                                     standaloneEntitlement.getProductCategory(), "SubscriptionJson productCategory needs to be set for each element",
+                                     standaloneEntitlement.getBillingPeriod(), "SubscriptionJson billingPeriod needs to be set for each element",
+                                     standaloneEntitlement.getPriceList(), "SubscriptionJson priceList needs to be set for each element");
+            }
+            // create the entitlementSpecifier
+            buildEntitlementSpecifier(standaloneEntitlement, currency, entitlementSpecifierList);
         }
         return entitlementSpecifierList;
     }
 
-    private BaseEntitlementWithAddOnsSpecifier buildBaseEntitlementWithAddOnsSpecifier(final List<EntitlementSpecifier> entitlementSpecifierList, final LocalDate resolvedEntitlementDate, final LocalDate resolvedBillingDate, final UUID bundleId, final SubscriptionJson baseEntitlement, final @QueryParam(QUERY_MIGRATED) @DefaultValue("false") Boolean isMigrated) {
+    private void buildEntitlementSpecifier(final SubscriptionJson subscriptionJson,
+                                           final Currency currency,
+                                           final Collection<EntitlementSpecifier> entitlementSpecifierList) {
+        if (subscriptionJson.getPlanName() == null &&
+            (subscriptionJson.getProductName() == null ||
+             subscriptionJson.getProductCategory() == null ||
+             subscriptionJson.getBillingPeriod() == null ||
+             subscriptionJson.getPriceList() == null)) {
+            return;
+        }
+
+        final PlanPhaseSpecifier planPhaseSpecifier = subscriptionJson.getPlanName() != null ?
+                                                      new PlanPhaseSpecifier(subscriptionJson.getPlanName(), null) :
+                                                      new PlanPhaseSpecifier(subscriptionJson.getProductName(),
+                                                                             BillingPeriod.valueOf(subscriptionJson.getBillingPeriod()),
+                                                                             subscriptionJson.getPriceList(),
+                                                                             null);
+        final List<PlanPhasePriceOverride> overrides = PhasePriceOverrideJson.toPlanPhasePriceOverrides(subscriptionJson.getPriceOverrides(),
+                                                                                                        planPhaseSpecifier,
+                                                                                                        currency);
+
+        final EntitlementSpecifier specifier = new EntitlementSpecifier() {
+            @Override
+            public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+                return planPhaseSpecifier;
+            }
+
+            @Override
+            public List<PlanPhasePriceOverride> getOverrides() {
+                return overrides;
+            }
+        };
+        entitlementSpecifierList.add(specifier);
+    }
+
+    private BaseEntitlementWithAddOnsSpecifier buildBaseEntitlementWithAddOnsSpecifier(final Iterable<EntitlementSpecifier> entitlementSpecifierList,
+                                                                                       final LocalDate resolvedEntitlementDate,
+                                                                                       final LocalDate resolvedBillingDate,
+                                                                                       @Nullable final String bundleExternalKey,
+                                                                                       final Boolean isMigrated) {
         return new BaseEntitlementWithAddOnsSpecifier() {
             @Override
             public UUID getBundleId() {
-                return bundleId;
+                return null;
             }
+
             @Override
             public String getExternalKey() {
-                return baseEntitlement.getExternalKey();
+                return bundleExternalKey;
             }
+
             @Override
             public Iterable<EntitlementSpecifier> getEntitlementSpecifier() {
                 return entitlementSpecifierList;
             }
+
             @Override
             public LocalDate getEntitlementEffectiveDate() {
                 return resolvedEntitlementDate;
             }
+
             @Override
             public LocalDate getBillingEffectiveDate() {
                 return resolvedBillingDate;
             }
+
             @Override
             public boolean isMigrated() {
                 return isMigrated;
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
index e6bdb7b..9794ac6 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
@@ -148,7 +148,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
     }
 
     private List<SubscriptionSpecifier> verifyAndBuildSubscriptionSpecifiers(final SubscriptionBaseBundle bundle,
-                                                                             @Nullable final EntitlementSpecifier baseOrStandalonePlanSpecifier,
+                                                                             @Nullable final EntitlementSpecifier baseOrFirstStandalonePlanSpecifier,
                                                                              final Iterable<EntitlementSpecifier> entitlements,
                                                                              final boolean isMigrated,
                                                                              final InternalCallContext context,
@@ -188,7 +188,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
             }
 
             final DateTime bundleStartDate;
-            if (baseOrStandalonePlanSpecifier != null) {
+            if (baseOrFirstStandalonePlanSpecifier != null) {
                 bundleStartDate = effectiveDate;
             } else {
                 final SubscriptionBase baseSubscription = dao.getBaseSubscription(bundle.getId(), catalog, context);
@@ -219,16 +219,18 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
                                                                          final SubscriptionBaseWithAddOnsSpecifier subscriptionBaseWithAddOnsSpecifier,
                                                                          final DateTime effectiveDate,
                                                                          final Collection<EntitlementSpecifier> outputEntitlementSpecifier) throws SubscriptionBaseApiException {
-        EntitlementSpecifier baseOrStandalonePlanSpecifier = null;
+        EntitlementSpecifier basePlanSpecifier = null;
         final Collection<EntitlementSpecifier> addOnSpecifiers = new ArrayList<EntitlementSpecifier>();
+        final List<EntitlementSpecifier> standaloneSpecifiers = new ArrayList<EntitlementSpecifier>();
         try {
             for (final EntitlementSpecifier cur : subscriptionBaseWithAddOnsSpecifier.getEntitlementSpecifiers()) {
                 final boolean isBase = isBaseSpecifier(catalog, effectiveDate, cur);
                 final boolean isStandalone = isStandaloneSpecifier(catalog, effectiveDate, cur);
-                final boolean isBaseOrStandaloneSpecifier = isBase || isStandalone;
-                if (isBaseOrStandaloneSpecifier) {
-                    if (baseOrStandalonePlanSpecifier == null) {
-                        baseOrStandalonePlanSpecifier = cur;
+                if (isStandalone) {
+                    standaloneSpecifiers.add(cur);
+                } else if (isBase) {
+                    if (basePlanSpecifier == null) {
+                        basePlanSpecifier = cur;
                     } else {
                         throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
                     }
@@ -240,11 +242,21 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
             throw new SubscriptionBaseApiException(e);
         }
 
-        if (baseOrStandalonePlanSpecifier != null) {
-            outputEntitlementSpecifier.add(baseOrStandalonePlanSpecifier);
+        if (basePlanSpecifier != null) {
+            outputEntitlementSpecifier.add(basePlanSpecifier);
         }
         outputEntitlementSpecifier.addAll(addOnSpecifiers);
-        return baseOrStandalonePlanSpecifier;
+
+        if (!outputEntitlementSpecifier.isEmpty() && !standaloneSpecifiers.isEmpty()) {
+            throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
+        }
+
+        if (standaloneSpecifiers.isEmpty()) {
+            return basePlanSpecifier;
+        } else {
+            outputEntitlementSpecifier.addAll(standaloneSpecifiers);
+            return standaloneSpecifiers.get(0);
+        }
     }
 
     private boolean isBaseSpecifier(final Catalog catalog, final DateTime effectiveDate, final EntitlementSpecifier cur) throws CatalogApiException {
@@ -271,7 +283,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
 
                 final Collection<EntitlementSpecifier> reorderedSpecifiers = new ArrayList<EntitlementSpecifier>();
                 // Note: billingRequestedDateRaw might not be accurate here (add-on with a too early date passed)?
-                final EntitlementSpecifier baseOrStandalonePlanSpecifier = sanityAndReorderBPOrStandaloneSpecFirst(catalog, subscriptionBaseWithAddOnsSpecifier, billingRequestedDateRaw, reorderedSpecifiers);
+                final EntitlementSpecifier baseOrFirstStandalonePlanSpecifier = sanityAndReorderBPOrStandaloneSpecFirst(catalog, subscriptionBaseWithAddOnsSpecifier, billingRequestedDateRaw, reorderedSpecifiers);
 
                 DateTime billingRequestedDate = billingRequestedDateRaw;
                 SubscriptionBaseBundle bundle = null;
@@ -282,7 +294,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
                         throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
                     }
                 } else if (subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey() != null &&
-                           baseOrStandalonePlanSpecifier == null) { // Skip the expensive checks if we are about to create the bundle (validation will be done in SubscriptionDao#createSubscriptionBundle)
+                           baseOrFirstStandalonePlanSpecifier == null) { // Skip the expensive checks if we are about to create the bundle (validation will be done in SubscriptionDao#createSubscriptionBundle)
                     final List<SubscriptionBaseBundle> existingBundles = dao.getSubscriptionBundlesForKey(subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey(), context);
                     final SubscriptionBaseBundle tmp = getActiveBundleForKeyNotException(existingBundles, dao, clock, catalog, context);
                     if (tmp == null) {
@@ -303,12 +315,12 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
                     }
                 }
 
-                if (bundle == null && baseOrStandalonePlanSpecifier != null) {
+                if (bundle == null && baseOrFirstStandalonePlanSpecifier != null) {
                     bundle = createBundleForAccount(accountId,
                                                     subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey(),
                                                     renameCancelledBundleIfExist,
                                                     context);
-                } else if (bundle != null && baseSubscription != null && baseOrStandalonePlanSpecifier != null && isBaseSpecifier(catalog, billingRequestedDateRaw, baseOrStandalonePlanSpecifier)) {
+                } else if (bundle != null && baseSubscription != null && baseOrFirstStandalonePlanSpecifier != null && isBaseSpecifier(catalog, billingRequestedDateRaw, baseOrFirstStandalonePlanSpecifier)) {
                     throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_BP_EXISTS, bundle.getExternalKey());
                 } else if (bundle == null) {
                     throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
@@ -317,7 +329,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
                 final SubscriptionAndAddOnsSpecifier subscriptionAndAddOnsSpecifier = new SubscriptionAndAddOnsSpecifier(bundle,
                                                                                                                          billingRequestedDate,
                                                                                                                          verifyAndBuildSubscriptionSpecifiers(bundle,
-                                                                                                                                                              baseOrStandalonePlanSpecifier,
+                                                                                                                                                              baseOrFirstStandalonePlanSpecifier,
                                                                                                                                                               reorderedSpecifiers,
                                                                                                                                                               subscriptionBaseWithAddOnsSpecifier.isMigrated(),
                                                                                                                                                               context,
@@ -850,14 +862,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
     private CacheLoaderArgument createBundleIdFromSubscriptionIdCacheLoaderArgument(final InternalTenantContext internalTenantContext) {
         final BundleIdFromSubscriptionIdCacheLoader.LoaderCallback loaderCallback = new BundleIdFromSubscriptionIdCacheLoader.LoaderCallback() {
             public UUID loadBundleId(final UUID subscriptionId, final InternalTenantContext internalTenantContext) {
-                final SubscriptionBase subscriptionBase;
-                try {
-                    subscriptionBase = getSubscriptionFromId(subscriptionId, internalTenantContext);
-                } catch (final SubscriptionBaseApiException e) {
-                    log.warn("Unable to retrieve subscription for id='{}'", subscriptionId);
-                    return null;
-                }
-                return subscriptionBase.getBundleId();
+                return dao.getBundleIdFromSubscriptionId(subscriptionId, internalTenantContext);
             }
         };
 
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
index 27b31f3..29cedce 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
@@ -144,10 +144,15 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
             try {
                 final DefaultSubscriptionBase subscriptionBase = new DefaultSubscriptionBase(subscription.getBuilder(), this, clock);
                 final InternalCallContext internalCallContext = createCallContextFromBundleId(subscriptionBase.getBundleId(), context);
-                final List<SubscriptionBaseEvent> events = getEventsOnCreation(subscriptionBase.getId(), subscriptionBase.getAlignStartDate(),
-                                                                               subscriptionBase.getBundleStartDate(), subscription.getPlan(),
-                                                                               subscription.getInitialPhase(), subscription.getRealPriceList(),
-                                                                               subscription.getEffectiveDate(), fullCatalog, internalCallContext);
+                final List<SubscriptionBaseEvent> events = getEventsOnCreation(subscriptionBase.getId(),
+                                                                               subscriptionBase.getAlignStartDate(),
+                                                                               subscriptionBase.getBundleStartDate(),
+                                                                               subscription.getPlan(),
+                                                                               subscription.getInitialPhase(),
+                                                                               subscription.getRealPriceList(),
+                                                                               subscription.getEffectiveDate(),
+                                                                               fullCatalog,
+                                                                               internalCallContext);
                 eventsMap.put(subscriptionBase.getId(), events);
                 subscriptionBaseList.add(subscriptionBase);
             } catch (final CatalogApiException e) {
@@ -434,13 +439,23 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
     }
 
     @Override
-    public List<SubscriptionBaseEvent> getEventsOnCreation(final UUID subscriptionId, final DateTime alignStartDate, final DateTime bundleStartDate,
-                                                           final Plan plan, final PhaseType initialPhase,
-                                                           final String realPriceList, final DateTime effectiveDate,
+    public List<SubscriptionBaseEvent> getEventsOnCreation(final UUID subscriptionId,
+                                                           final DateTime alignStartDate,
+                                                           final DateTime bundleStartDate,
+                                                           final Plan plan,
+                                                           final PhaseType initialPhase,
+                                                           final String realPriceList,
+                                                           final DateTime effectiveDate,
                                                            final Catalog fullCatalog,
                                                            final InternalTenantContext internalTenantContext) throws CatalogApiException, SubscriptionBaseApiException {
-        final TimedPhase[] curAndNextPhases = planAligner.getCurrentAndNextTimedPhaseOnCreate(alignStartDate, bundleStartDate, plan, initialPhase,
-                                                                                              realPriceList, effectiveDate, fullCatalog, internalTenantContext);
+        final TimedPhase[] curAndNextPhases = planAligner.getCurrentAndNextTimedPhaseOnCreate(alignStartDate,
+                                                                                              bundleStartDate,
+                                                                                              plan,
+                                                                                              initialPhase,
+                                                                                              realPriceList,
+                                                                                              effectiveDate,
+                                                                                              fullCatalog,
+                                                                                              internalTenantContext);
 
         final ApiEventBuilder createBuilder = new ApiEventBuilder()
                 .setSubscriptionId(subscriptionId)
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 1c11dae..065e0c8 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
@@ -301,6 +301,7 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
 
                 final SubscriptionBaseBundle unusedBundle = findExistingUnusedBundleForExternalKeyAndAccount(existingBundles, entitySqlDaoWrapperFactory);
                 if (unusedBundle != null) {
+                    log.info("Found unused bundle for externalKey='{}': bundleId='{}'", bundle.getExternalKey(), unusedBundle.getId());
                     return unusedBundle;
                 }
                 final BundleSqlDao bundleSqlDao = entitySqlDaoWrapperFactory.become(BundleSqlDao.class);
@@ -319,6 +320,7 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
                             if (s.getState() != EntitlementState.CANCELLED) {
                                 throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_ACTIVE_BUNDLE_KEY_EXISTS, bundle.getExternalKey());
                             } else if (renameCancelledBundleIfExist) {
+                                log.info("Renaming bundles with externalKey='{}', prefix='cncl'", bundle.getExternalKey());
                                 // Note that if bundle belongs to a different account, context is not the context for this target account,
                                 // but the underlying sql operation does not use the account info
                                 bundleSqlDao.renameBundleExternalKey(bundle.getExternalKey(), "cncl", context);
@@ -361,6 +363,17 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
     }
 
     @Override
+    public UUID getBundleIdFromSubscriptionId(final UUID subscriptionId, final InternalTenantContext context) {
+        return transactionalSqlDao.execute(true, new EntitySqlDaoTransactionWrapper<UUID>() {
+            @Override
+            public UUID inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
+                final SubscriptionModelDao subscriptionModel = entitySqlDaoWrapperFactory.become(SubscriptionSqlDao.class).getById(subscriptionId.toString(), context);
+                return subscriptionModel.getBundleId();
+            }
+        });
+    }
+
+    @Override
     public List<SubscriptionBase> getSubscriptions(final UUID bundleId, final List<SubscriptionBaseEvent> dryRunEvents, final Catalog catalog, final InternalTenantContext context) throws CatalogApiException {
         return buildBundleSubscriptions(getSubscriptionFromBundleId(bundleId, context), null, dryRunEvents, catalog, context);
     }
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
index 3e6dd6c..242676b 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
@@ -59,6 +59,8 @@ public interface SubscriptionDao extends EntityDao<SubscriptionBundleModelDao, S
 
     public SubscriptionBase getSubscriptionFromId(UUID subscriptionId, final Catalog catalog, InternalTenantContext context) throws CatalogApiException;
 
+    public UUID getBundleIdFromSubscriptionId(UUID subscriptionId, InternalTenantContext context);
+
     // SubscriptionBase retrieval
     public SubscriptionBase getBaseSubscription(UUID bundleId, final Catalog catalog, InternalTenantContext context) throws CatalogApiException;
 
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
index 19dbef1..8b5a74e 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
@@ -198,6 +198,11 @@ public class MockSubscriptionDaoMemory extends MockEntityDaoBase<SubscriptionBun
     }
 
     @Override
+    public UUID getBundleIdFromSubscriptionId(final UUID subscriptionId, final InternalTenantContext context) {
+        return getSubscriptionFromId(subscriptionId, null, context).getBundleId();
+    }
+
+    @Override
     public List<SubscriptionBaseEvent> createSubscriptionsWithAddOns(final List<SubscriptionBaseWithAddOns> subscriptions,
                                                                      final Map<UUID, List<SubscriptionBaseEvent>> initialEventsMap,
                                                                      final Catalog catalog,