killbill-memoizeit

Changes

.travis.yml 37(+0 -37)

NEWS 8(+7 -1)

Details

diff --git a/.circleci/config.yml b/.circleci/config.yml
index ca7b9fc..ccde348 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -174,46 +174,18 @@ workflows:
   version: 2
   build-and-test:
     jobs:
-      - build:
-          filters:
-            branches:
-              only:
-                - master
-                - work-for-release-0.19.x
-                - circle-ci-experiment
+      - build
       - test-h2:
           requires:
             - build
-          filters:
-            branches:
-              only:
-                - master
-                - circle-ci-experiment
       - test-mysql:
           requires:
             - build
-          filters:
-            branches:
-              only:
-                - master
-                - work-for-release-0.19.x
-                - circle-ci-experiment
       - test-postgresql:
           requires:
             - build
-          filters:
-            branches:
-              only:
-                - master
-                - circle-ci-experiment
       - integration-tests:
           requires:
             - test-h2
             - test-mysql
             - test-postgresql
-          filters:
-            branches:
-              only:
-                - master
-                - work-for-release-0.19.x
-                - circle-ci-experiment
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
index 808e0c5..c812bfe 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -352,10 +352,11 @@ public class InvoiceDispatcher {
                     }
                 }
             } else /* Dry run use cases */ {
-
                 final NotificationQueue notificationQueue = notificationQueueService.getNotificationQueue(DefaultInvoiceService.INVOICE_SERVICE_NAME,
                                                                                                           DefaultNextBillingDateNotifier.NEXT_BILLING_DATE_NOTIFIER_QUEUE);
-                final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications = notificationQueue.getFutureNotificationForSearchKeys(context.getAccountRecordId(), context.getTenantRecordId());
+                final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotificationsIterable = notificationQueue.getFutureNotificationForSearchKeys(context.getAccountRecordId(), context.getTenantRecordId());
+                // Copy the results as retrieving the iterator will issue a query each time. This also makes sure the underlying JDBC connection is closed.
+                final List<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications = ImmutableList.<NotificationEventWithMetadata<NextBillingDateNotificationKey>>copyOf(futureNotificationsIterable);
 
                 final Map<UUID, DateTime> nextScheduledSubscriptionsEventMap = getNextTransitionsForSubscriptions(billingEvents);
 
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java
index fb269c5..6cfcb83 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -19,6 +19,7 @@
 package org.killbill.billing.invoice.notification;
 
 import java.io.IOException;
+import java.util.Iterator;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
@@ -84,19 +85,27 @@ public class DefaultNextBillingDatePoster implements NextBillingDatePoster {
             final Iterable<NotificationEventWithMetadata<NextBillingDateNotificationKey>> futureNotifications = nextBillingQueue.getFutureNotificationFromTransactionForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId(), entitySqlDaoWrapperFactory.getHandle().getConnection());
 
             NotificationEventWithMetadata<NextBillingDateNotificationKey> existingNotificationForEffectiveDate = null;
-            for (final NotificationEventWithMetadata<NextBillingDateNotificationKey> input : futureNotifications) {
-                final boolean isEventDryRunForNotifications = input.getEvent().isDryRunForInvoiceNotification() != null ?
-                                                              input.getEvent().isDryRunForInvoiceNotification() : false;
-
-                final LocalDate notificationEffectiveLocaleDate = internalCallContext.toLocalDate(futureNotificationTime);
-                final LocalDate eventEffectiveLocaleDate = internalCallContext.toLocalDate(input.getEffectiveDate());
-
-                if (notificationEffectiveLocaleDate.compareTo(eventEffectiveLocaleDate) == 0 &&
-                    ((isDryRunForInvoiceNotification && isEventDryRunForNotifications) ||
-                     (!isDryRunForInvoiceNotification && !isEventDryRunForNotifications))) {
-                    existingNotificationForEffectiveDate = input;
+            final Iterator<NotificationEventWithMetadata<NextBillingDateNotificationKey>> iterator = futureNotifications.iterator();
+            try {
+                while (iterator.hasNext()) {
+                    final NotificationEventWithMetadata<NextBillingDateNotificationKey> input = iterator.next();
+                    final boolean isEventDryRunForNotifications = input.getEvent().isDryRunForInvoiceNotification() != null ?
+                                                                  input.getEvent().isDryRunForInvoiceNotification() : false;
+
+                    final LocalDate notificationEffectiveLocaleDate = internalCallContext.toLocalDate(futureNotificationTime);
+                    final LocalDate eventEffectiveLocaleDate = internalCallContext.toLocalDate(input.getEffectiveDate());
+
+                    if (notificationEffectiveLocaleDate.compareTo(eventEffectiveLocaleDate) == 0 &&
+                        ((isDryRunForInvoiceNotification && isEventDryRunForNotifications) ||
+                         (!isDryRunForInvoiceNotification && !isEventDryRunForNotifications))) {
+                        existingNotificationForEffectiveDate = input;
+                    }
                 }
+            } finally {
                 // Go through all results to close the connection
+                while (iterator.hasNext()) {
+                    iterator.next();
+                }
             }
 
             if (existingNotificationForEffectiveDate == null) {
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentPoster.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentPoster.java
index 4a5d7e9..b8c7d76 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentPoster.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/ParentInvoiceCommitmentPoster.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -18,6 +18,7 @@
 package org.killbill.billing.invoice.notification;
 
 import java.io.IOException;
+import java.util.Iterator;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
@@ -58,17 +59,22 @@ public class ParentInvoiceCommitmentPoster {
             final Iterable<NotificationEventWithMetadata<ParentInvoiceCommitmentNotificationKey>> futureNotifications = commitInvoiceQueue.getFutureNotificationFromTransactionForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId(), entitySqlDaoWrapperFactory.getHandle().getConnection());
 
             boolean existingFutureNotificationWithSameDateAndInvoiceId = false;
-            for (final NotificationEventWithMetadata<ParentInvoiceCommitmentNotificationKey> input : futureNotifications) {
-
-
-
-                final LocalDate notificationEffectiveLocaleDate = internalCallContext.toLocalDate(futureNotificationTime);
-                final LocalDate eventEffectiveLocaleDate = internalCallContext.toLocalDate(input.getEffectiveDate());
-
-                if (notificationEffectiveLocaleDate.compareTo(eventEffectiveLocaleDate) == 0 && input.getEvent().getUuidKey().equals(invoiceId)) {
-                    existingFutureNotificationWithSameDateAndInvoiceId = true;
+            final Iterator<NotificationEventWithMetadata<ParentInvoiceCommitmentNotificationKey>> iterator = futureNotifications.iterator();
+            try {
+                while (iterator.hasNext()) {
+                    final NotificationEventWithMetadata<ParentInvoiceCommitmentNotificationKey> input = iterator.next();
+                    final LocalDate notificationEffectiveLocaleDate = internalCallContext.toLocalDate(futureNotificationTime);
+                    final LocalDate eventEffectiveLocaleDate = internalCallContext.toLocalDate(input.getEffectiveDate());
+
+                    if (notificationEffectiveLocaleDate.compareTo(eventEffectiveLocaleDate) == 0 && input.getEvent().getUuidKey().equals(invoiceId)) {
+                        existingFutureNotificationWithSameDateAndInvoiceId = true;
+                    }
                 }
+            } finally {
                 // Go through all results to close the connection
+                while (iterator.hasNext()) {
+                    iterator.next();
+                }
             }
 
             if (!existingFutureNotificationWithSameDateAndInvoiceId) {
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TestResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TestResource.java
index a73a9b9..5e01d3c 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TestResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TestResource.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -18,12 +18,15 @@
 
 package org.killbill.billing.jaxrs.resources;
 
+import java.util.Iterator;
+
 import javax.inject.Inject;
 import javax.servlet.ServletRequest;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.DefaultValue;
 import javax.ws.rs.GET;
+import javax.ws.rs.HEAD;
 import javax.ws.rs.HeaderParam;
 import javax.ws.rs.POST;
 import javax.ws.rs.PUT;
@@ -253,12 +256,21 @@ public class TestResource extends JaxRsResourceBase {
 
     private boolean areAllNotificationsProcessed(final Long tenantRecordId) {
         int nbNotifications = 0;
-        for (final NotificationQueue notificationQueue : notificationQueueService.getNotificationQueues()) {
-            for (final NotificationEventWithMetadata<NotificationEvent> notificationEvent : notificationQueue.getFutureOrInProcessingNotificationForSearchKey2(null, tenantRecordId)) {
-                if (!notificationEvent.getEffectiveDate().isAfter(clock.getUTCNow())) {
-                    nbNotifications += 1;
+        final Iterator<NotificationQueue> iterator = notificationQueueService.getNotificationQueues().iterator();
+        try {
+            while (iterator.hasNext()) {
+                final NotificationQueue notificationQueue = iterator.next();
+                for (final NotificationEventWithMetadata<NotificationEvent> notificationEvent : notificationQueue.getFutureOrInProcessingNotificationForSearchKey2(null, tenantRecordId)) {
+                    if (!notificationEvent.getEffectiveDate().isAfter(clock.getUTCNow())) {
+                        nbNotifications += 1;
+                    }
                 }
             }
+        } finally {
+            // Go through all results to close the connection
+            while (iterator.hasNext()) {
+                iterator.next();
+            }
         }
         if (nbNotifications != 0) {
             log.info("TestResource: {} queue(s) with more notification(s) to process", nbNotifications);
@@ -268,6 +280,7 @@ public class TestResource extends JaxRsResourceBase {
 
     private boolean areAllBusEventsProcessed(final Long tenantRecordId) {
         final Iterable<BusEventWithMetadata<BusEvent>> availableBusEventForSearchKey2 = persistentBus.getAvailableOrInProcessingBusEventsForSearchKey2(null, tenantRecordId);
+        // This will go through all results to close the connection
         final int nbBusEvents = Iterables.size(availableBusEventForSearchKey2);
         if (nbBusEvents != 0) {
             log.info("TestResource: at least {} more bus event(s) to process", nbBusEvents);

NEWS 8(+7 -1)

diff --git a/NEWS b/NEWS
index 4827589..1be25b9 100644
--- a/NEWS
+++ b/NEWS
@@ -3,7 +3,13 @@
     Fix connection leak (#558)
     Fix limitation where catalog plan name cannot end with an number (#842)
     Fix missing Invoice Notification when we have future billing events (#846)
-    Rreduce log level of InvoiceItemGeneratorLogger (#851)
+    Reduce log level of InvoiceItemGeneratorLogger (#851)
+
+0.18.17
+    Relax sanity checks for STANDALONE subscriptions #840
+    Fix JDBC connection leak in pagination API #853
+    Fix limitation where catalog plan name cannot end with an number #842
+    Reduce log level of InvoiceItemGeneratorLogger #851
 
 0.18.16
     See https://github.com/killbill/killbill/releases/tag/killbill-0.18.16
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/notification/DefaultOverduePosterBase.java b/overdue/src/main/java/org/killbill/billing/overdue/notification/DefaultOverduePosterBase.java
index d06c167..fc14f47 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/notification/DefaultOverduePosterBase.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/notification/DefaultOverduePosterBase.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -18,6 +18,7 @@
 
 package org.killbill.billing.overdue.notification;
 
+import java.util.Iterator;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
@@ -93,10 +94,18 @@ public abstract class DefaultOverduePosterBase implements OverduePoster {
                 public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
                     final Iterable<NotificationEventWithMetadata<T>> futureNotifications = getFutureNotificationsForAccountInTransaction(entitySqlDaoWrapperFactory, checkOverdueQueue,
                                                                                                                                          clazz, context);
-                    for (final NotificationEventWithMetadata<T> notification : futureNotifications) {
-                        checkOverdueQueue.removeNotificationFromTransaction(entitySqlDaoWrapperFactory.getHandle().getConnection(), notification.getRecordId());
+                    final Iterator<NotificationEventWithMetadata<T>> iterator = futureNotifications.iterator();
+                    try {
+                        while (iterator.hasNext()) {
+                            final NotificationEventWithMetadata<T> notification = iterator.next();
+                            checkOverdueQueue.removeNotificationFromTransaction(entitySqlDaoWrapperFactory.getHandle().getConnection(), notification.getRecordId());
+                        }
+                    } finally {
+                        // Go through all results to close the connection
+                        while (iterator.hasNext()) {
+                            iterator.next();
+                        }
                     }
-
                     return null;
                 }
             });
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueCheckPoster.java b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueCheckPoster.java
index 25d7eff..3fc227a 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueCheckPoster.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueCheckPoster.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -18,6 +18,8 @@
 
 package org.killbill.billing.overdue.notification;
 
+import java.util.Iterator;
+
 import org.joda.time.DateTime;
 import org.killbill.billing.util.cache.CacheControllerDispatcher;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
@@ -48,23 +50,32 @@ public class OverdueCheckPoster extends DefaultOverduePosterBase {
         boolean shouldInsertNewNotification = true;
         int minIndexToDeleteFrom = 0;
         int index = 0;
-        for (final NotificationEventWithMetadata<T> cur : futureNotifications) {
-            // Results are ordered by effective date asc
-            if (index == 0) {
-                if (cur.getEffectiveDate().isBefore(futureNotificationTime)) {
-                    // We don't have to insert a new one. For sanity, delete any other future notification
-                    minIndexToDeleteFrom = 1;
-                    shouldInsertNewNotification = false;
-                } else {
-                    // We win - we are before any other already recorded. Delete all others.
-                    minIndexToDeleteFrom = 0;
+        final Iterator<NotificationEventWithMetadata<T>> iterator = futureNotifications.iterator();
+        try {
+            while (iterator.hasNext()) {
+                final NotificationEventWithMetadata<T> cur = iterator.next();
+                // Results are ordered by effective date asc
+                if (index == 0) {
+                    if (cur.getEffectiveDate().isBefore(futureNotificationTime)) {
+                        // We don't have to insert a new one. For sanity, delete any other future notification
+                        minIndexToDeleteFrom = 1;
+                        shouldInsertNewNotification = false;
+                    } else {
+                        // We win - we are before any other already recorded. Delete all others.
+                        minIndexToDeleteFrom = 0;
+                    }
                 }
-            }
 
-            if (minIndexToDeleteFrom <= index) {
-                overdueQueue.removeNotificationFromTransaction(entitySqlDaoWrapperFactory.getHandle().getConnection(), cur.getRecordId());
+                if (minIndexToDeleteFrom <= index) {
+                    overdueQueue.removeNotificationFromTransaction(entitySqlDaoWrapperFactory.getHandle().getConnection(), cur.getRecordId());
+                }
+                index++;
+            }
+        } finally {
+            // Go through all results to close the connection
+            while (iterator.hasNext()) {
+                iterator.next();
             }
-            index++;
         }
 
         return shouldInsertNewNotification;
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/notification/TestDefaultOverdueCheckPoster.java b/overdue/src/test/java/org/killbill/billing/overdue/notification/TestDefaultOverdueCheckPoster.java
index 1a7b3bd..ca991a2 100644
--- a/overdue/src/test/java/org/killbill/billing/overdue/notification/TestDefaultOverdueCheckPoster.java
+++ b/overdue/src/test/java/org/killbill/billing/overdue/notification/TestDefaultOverdueCheckPoster.java
@@ -92,6 +92,7 @@ public class TestDefaultOverdueCheckPoster extends OverdueTestSuiteWithEmbeddedD
         return entitySqlDaoTransactionalJdbiWrapper.execute(new EntitySqlDaoTransactionWrapper<List<NotificationEventWithMetadata<OverdueCheckNotificationKey>>>() {
             @Override
             public List<NotificationEventWithMetadata<OverdueCheckNotificationKey>> inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
+                // This will go through all results to close the connection
                 return ImmutableList.<NotificationEventWithMetadata<OverdueCheckNotificationKey>>copyOf(((OverdueCheckPoster) checkPoster).getFutureNotificationsForAccountInTransaction(entitySqlDaoWrapperFactory, overdueQueue, OverdueCheckNotificationKey.class, internalCallContext));
             }
         });
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
index 4bd018a..ecaa6d8 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -24,6 +24,7 @@ import java.util.Collection;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -411,11 +412,19 @@ public class PaymentProcessor extends ProcessorBase {
             final Iterable<NotificationEventWithMetadata<NotificationEvent>> notificationEventWithMetadatas =
                     retryQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
 
-            for (final NotificationEventWithMetadata<NotificationEvent> notificationEvent : notificationEventWithMetadatas) {
-                if (((PaymentRetryNotificationKey) notificationEvent.getEvent()).getAttemptId().equals(lastPaymentAttemptId)) {
-                    retryQueue.removeNotification(notificationEvent.getRecordId());
+            final Iterator<NotificationEventWithMetadata<NotificationEvent>> iterator = notificationEventWithMetadatas.iterator();
+            try {
+                while (iterator.hasNext()) {
+                    final NotificationEventWithMetadata<NotificationEvent> notificationEvent = iterator.next();
+                    if (((PaymentRetryNotificationKey) notificationEvent.getEvent()).getAttemptId().equals(lastPaymentAttemptId)) {
+                        retryQueue.removeNotification(notificationEvent.getRecordId());
+                    }
                 }
+            } finally {
                 // Go through all results to close the connection
+                while (iterator.hasNext()) {
+                    iterator.next();
+                }
             }
         } catch (final NoSuchNotificationQueue noSuchNotificationQueue) {
             log.error("ERROR Loading Notification Queue - " + noSuchNotificationQueue.getMessage());
diff --git a/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java b/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java
index 8e0f0fe..27a8bde 100644
--- a/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java
+++ b/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -26,6 +26,7 @@ import java.util.UUID;
 
 import org.joda.time.DateTime;
 import org.killbill.billing.account.api.Account;
+import org.killbill.billing.api.FlakyRetryAnalyzer;
 import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.payment.PaymentTestSuiteWithEmbeddedDB;
 import org.killbill.billing.payment.api.Payment;
@@ -94,7 +95,8 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
         assertEquals(retrievedAttempts.get(0).getPluginName(), pluginName);
     }
 
-    @Test(groups = "slow")
+    // Flaky, see https://github.com/killbill/killbill/issues/860
+    @Test(groups = "slow", retryAnalyzer = FlakyRetryAnalyzer.class)
     public void testPaymentAndTransactions() {
         final UUID paymentMethodId = UUID.randomUUID();
         final UUID accountId = UUID.randomUUID();
@@ -293,7 +295,8 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
         assertEquals(deletedPaymentMethod.getPluginName(), pluginName);
     }
 
-    @Test(groups = "slow")
+    // Flaky, see https://github.com/killbill/killbill/issues/860
+    @Test(groups = "slow", retryAnalyzer = FlakyRetryAnalyzer.class)
     public void testPendingTransactions() {
 
         final UUID paymentMethodId = UUID.randomUUID();
diff --git a/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java b/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
index d09c923..086918e 100644
--- a/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
+++ b/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -20,6 +20,7 @@ package org.killbill.billing.payment;
 import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
@@ -28,6 +29,7 @@ import java.util.concurrent.TimeUnit;
 
 import org.joda.time.LocalDate;
 import org.killbill.billing.account.api.Account;
+import org.killbill.billing.api.FlakyRetryAnalyzer;
 import org.killbill.billing.api.TestApiListener;
 import org.killbill.billing.api.TestApiListener.NextEvent;
 import org.killbill.billing.callcontext.InternalCallContext;
@@ -414,7 +416,8 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
     }
 
     // The test will check that when a PENDING entry stays PENDING, we go through all our retries and eventually give up (no infinite loop of retries)
-    @Test(groups = "slow")
+    // Flaky, see https://github.com/killbill/killbill/issues/860
+    @Test(groups = "slow", retryAnalyzer = FlakyRetryAnalyzer.class)
     public void testPendingEntriesThatDontMove() throws Exception {
 
         final BigDecimal requestedAmount = BigDecimal.TEN;
@@ -447,13 +450,13 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
             // Verify there is a notification to retry updating the value
             assertEquals(getPendingNotificationCnt(internalCallContext), 1);
 
-            clock.addDeltaFromReality(cur.getMillis() + 1);
+            clock.addDeltaFromReality(cur.getMillis() + 1000);
 
             assertNotificationsCompleted(internalCallContext, 5);
             // We add a sleep here to make sure the notification gets processed. Note that calling assertNotificationsCompleted alone would not work
             // because there is a point in time where the notification queue is empty (showing notification was processed), but the processing of the notification
             // will itself enter a new notification, and so the synchronization is difficult without writing *too much code*.
-            Thread.sleep(1000);
+            Thread.sleep(1500);
             assertNotificationsCompleted(internalCallContext, 5);
 
             final Payment updatedPayment = paymentApi.getPayment(payment.getId(), false, false, ImmutableList.<PluginProperty>of(), callContext);
@@ -510,11 +513,19 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
                 @Override
                 public Boolean call() throws Exception {
                     boolean completed = true;
-                    for (final NotificationEventWithMetadata<NotificationEvent> notificationEvent : notificationQueueService.getNotificationQueue(DefaultPaymentService.SERVICE_NAME, Janitor.QUEUE_NAME).getFutureOrInProcessingNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId())) {
-                        if (!notificationEvent.getEffectiveDate().isAfter(clock.getUTCNow())) {
-                            completed = false;
+                    final Iterator<NotificationEventWithMetadata<NotificationEvent>> iterator = notificationQueueService.getNotificationQueue(DefaultPaymentService.SERVICE_NAME, Janitor.QUEUE_NAME).getFutureOrInProcessingNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId()).iterator();
+                    try {
+                        while (iterator.hasNext()) {
+                            final NotificationEventWithMetadata<NotificationEvent> notificationEvent = iterator.next();
+                            if (!notificationEvent.getEffectiveDate().isAfter(clock.getUTCNow())) {
+                                completed = false;
+                            }
                         }
+                    } finally {
                         // Go through all results to close the connection
+                        while (iterator.hasNext()) {
+                            iterator.next();
+                        }
                     }
                     return completed;
                 }
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPushNotification.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPushNotification.java
index 4b32eba..5c53689 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPushNotification.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPushNotification.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -31,6 +31,7 @@ import javax.servlet.http.HttpServletResponse;
 import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.servlet.ServletContextHandler;
 import org.eclipse.jetty.servlet.ServletHolder;
+import org.killbill.billing.api.FlakyRetryAnalyzer;
 import org.killbill.billing.client.KillBillClientException;
 import org.killbill.billing.client.model.TenantKey;
 import org.killbill.billing.jaxrs.json.NotificationJson;
@@ -152,7 +153,8 @@ public class TestPushNotification extends TestJaxrsBase {
         return callback;
     }
 
-    @Test(groups = "slow")
+    // Flaky, see https://github.com/killbill/killbill/issues/860
+    @Test(groups = "slow", retryAnalyzer = FlakyRetryAnalyzer.class)
     public void testPushNotificationRetries() throws Exception {
         final String callback = registerTenantForCallback();
 
@@ -205,7 +207,8 @@ public class TestPushNotification extends TestJaxrsBase {
         unregisterTenantForCallback(callback);
     }
 
-    @Test(groups = "slow")
+    // Flaky, see https://github.com/killbill/killbill/issues/860
+    @Test(groups = "slow", retryAnalyzer = FlakyRetryAnalyzer.class)
     public void testPushNotificationRetriesMaxAttemptNumber() throws Exception {
         final String callback = registerTenantForCallback();
 
@@ -231,16 +234,16 @@ public class TestPushNotification extends TestJaxrsBase {
 
         resetCallbackStatusProperties();
 
-        // move clock 15 minutes and get 1st retry
-        clock.addDeltaFromReality(900000);
+        // move clock 15 minutes (+10s for flakiness) and get 1st retry
+        clock.addDeltaFromReality(910000);
 
         assertAllCallbacksCompleted();
         Assert.assertTrue(callbackCompletedWithError);
 
         resetCallbackStatusProperties();
 
-        // move clock an hour and get 2nd retry
-        clock.addDeltaFromReality(3600000);
+        // move clock an hour (+10s for flakiness) and get 2nd retry
+        clock.addDeltaFromReality(3610000);
 
         assertAllCallbacksCompleted();
         Assert.assertTrue(callbackCompletedWithError);
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 3af29b0..c56df6e 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
@@ -295,7 +295,8 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
 
             @Override
             public SubscriptionBaseBundle inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
-                final List<SubscriptionBundleModelDao> existingBundles = entitySqlDaoWrapperFactory.become(BundleSqlDao.class).getBundlesForLikeKey(bundle.getExternalKey(), context);
+                final List<SubscriptionBundleModelDao> existingBundles = bundle.getExternalKey() == null ? ImmutableList.<SubscriptionBundleModelDao>of()
+                                                                                                         : entitySqlDaoWrapperFactory.become(BundleSqlDao.class).getBundlesForLikeKey(bundle.getExternalKey(), context);
 
                 final SubscriptionBaseBundle unusedBundle = findExistingUnusedBundleForExternalKeyAndAccount(existingBundles, entitySqlDaoWrapperFactory);
                 if (unusedBundle != null) {
diff --git a/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/BundleSqlDao.sql.stg b/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/BundleSqlDao.sql.stg
index 369469b..ecfeb0f 100644
--- a/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/BundleSqlDao.sql.stg
+++ b/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/BundleSqlDao.sql.stg
@@ -49,15 +49,10 @@ where id = :id
 
 
 renameBundleExternalKey(prefix)  ::= <<
-update bundles b
-join (select
-     record_id
-     , external_key
-     from
-     bundles
-     where external_key = :externalKey <AND_CHECK_TENANT("")>) t
-on b.record_id = t.record_id
-set b.external_key = concat('kb', '<prefix>', '-', t.record_id, ':', t.external_key)
+update bundles
+set external_key = concat('kb', '<prefix>', '-', record_id, ':', external_key)
+where external_key = :externalKey
+<AND_CHECK_TENANT("")>
 ;
 >>
 
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java
index 4ed3a8f..8174045 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java
@@ -24,6 +24,7 @@ import org.joda.time.DateTime;
 import org.joda.time.Interval;
 import org.joda.time.LocalDate;
 import org.killbill.billing.ErrorCode;
+import org.killbill.billing.api.FlakyRetryAnalyzer;
 import org.killbill.billing.api.TestApiListener.NextEvent;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.BillingPeriod;
@@ -205,7 +206,8 @@ public class TestUserApiCancel extends SubscriptionTestSuiteWithEmbeddedDB {
 
     // Similar test to testCancelSubscriptionEOTWithChargeThroughDate except we uncancel and check things
     // are as they used to be and we can move forward without hitting cancellation
-    @Test(groups = "slow")
+    // Flaky, see https://github.com/killbill/killbill/issues/860
+    @Test(groups = "slow", retryAnalyzer = FlakyRetryAnalyzer.class)
     public void testUncancel() throws SubscriptionBillingApiException, SubscriptionBaseApiException {
         final String prod = "Shotgun";
         final BillingPeriod term = BillingPeriod.MONTHLY;
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCreate.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCreate.java
index 143041e..ba1229a 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCreate.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCreate.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * 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:
  *
@@ -16,6 +18,7 @@
 
 package org.killbill.billing.subscription.api.user;
 
+import java.sql.SQLException;
 import java.sql.SQLIntegrityConstraintViolationException;
 import java.util.List;
 
@@ -34,7 +37,6 @@ import org.killbill.billing.subscription.DefaultSubscriptionTestInitializer;
 import org.killbill.billing.subscription.SubscriptionTestSuiteWithEmbeddedDB;
 import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
 import org.killbill.billing.subscription.events.phase.PhaseEvent;
-import org.mariadb.jdbc.internal.util.dao.QueryException;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -82,7 +84,7 @@ public class TestUserApiCreate extends SubscriptionTestSuiteWithEmbeddedDB {
             subscriptionInternalApi.createBundleForAccount(bundle.getAccountId(), DefaultSubscriptionTestInitializer.DEFAULT_BUNDLE_KEY, false, internalCallContext);
             Assert.fail("createBundleForAccount should fail because key already exists");
         } catch (final RuntimeException e) {
-            assertTrue(e.getCause() instanceof SQLIntegrityConstraintViolationException);
+            assertTrue(e.getCause() instanceof SQLException && (e.getCause() instanceof SQLIntegrityConstraintViolationException || "23505".compareTo(((SQLException) e.getCause()).getSQLState()) == 0));
         }
 
         final SubscriptionBaseBundle newBundle = subscriptionInternalApi.createBundleForAccount(bundle.getAccountId(), DefaultSubscriptionTestInitializer.DEFAULT_BUNDLE_KEY, true, internalCallContext);
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
index 04f0ab5..79bb057 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
@@ -25,6 +25,7 @@ import javax.annotation.Nullable;
 import org.joda.time.DateTime;
 import org.joda.time.Interval;
 import org.killbill.billing.ErrorCode;
+import org.killbill.billing.api.FlakyRetryAnalyzer;
 import org.killbill.billing.api.TestApiListener.NextEvent;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.BillingPeriod;
@@ -106,7 +107,8 @@ public class TestUserApiError extends SubscriptionTestSuiteNoDB {
         }
     }
 
-    @Test(groups = "fast")
+    // Flaky, see https://github.com/killbill/killbill/issues/860
+    @Test(groups = "fast", retryAnalyzer = FlakyRetryAnalyzer.class)
     public void testChangeSubscriptionNonActive() throws SubscriptionBaseApiException {
         final SubscriptionBase subscription = testUtil.createSubscription(bundle, "Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME);
 
diff --git a/util/src/main/resources/trimTenant.sql b/util/src/main/resources/trimTenant.sql
new file mode 100644
index 0000000..d0bfb0c
--- /dev/null
+++ b/util/src/main/resources/trimTenant.sql
@@ -0,0 +1,75 @@
+drop procedure if exists trimTenant;
+DELIMITER //
+CREATE PROCEDURE trimTenant(p_api_key varchar(36))
+BEGIN
+
+    DECLARE v_tenant_record_id bigint /*! unsigned */;
+
+    select record_id from tenants WHERE api_key = p_api_key into v_tenant_record_id;
+
+    DELETE FROM analytics_account_fields WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_account_tags WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_account_transitions WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_accounts WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_bundle_fields WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_bundle_tags WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_bundles WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_invoice_adjustments WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_invoice_credits WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_invoice_fields WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_invoice_item_adjustments WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_invoice_items WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_invoice_payment_fields WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_invoice_tags WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_invoices WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_notifications WHERE search_key2 = v_tenant_record_id;
+    DELETE FROM analytics_notifications_history WHERE search_key2 = v_tenant_record_id;
+    DELETE FROM analytics_payment_auths WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_payment_captures WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_payment_chargebacks WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_payment_credits WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_payment_fields WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_payment_method_fields WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_payment_purchases WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_payment_refunds WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_payment_tags WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_payment_voids WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_subscription_transitions WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM analytics_transaction_fields WHERE tenant_record_id = v_tenant_record_id;
+
+    DELETE FROM account_email_history WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM account_emails WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM account_history WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM accounts WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM audit_log WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM blocking_states WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM bundles WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM bus_events WHERE search_key2 = v_tenant_record_id;
+    DELETE FROM bus_events_history WHERE search_key2 = v_tenant_record_id;
+    DELETE FROM bus_ext_events WHERE search_key2 = v_tenant_record_id;
+    DELETE FROM bus_ext_events_history WHERE search_key2 = v_tenant_record_id;
+    DELETE FROM custom_field_history WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM custom_fields WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM invoice_items WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM invoice_parent_children WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM invoice_payments WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM invoices WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM notifications WHERE search_key2 = v_tenant_record_id;
+    DELETE FROM notifications_history WHERE search_key2 = v_tenant_record_id;
+    DELETE FROM payment_attempt_history WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM payment_attempts WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM payment_history WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM payment_method_history WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM payment_methods WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM payment_transaction_history WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM payment_transactions WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM payments WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM rolled_up_usage WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM subscription_events WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM subscriptions WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM tag_history WHERE tenant_record_id = v_tenant_record_id;
+    DELETE FROM tags WHERE tenant_record_id = v_tenant_record_id;
+
+    END;
+//
+DELIMITER ;
diff --git a/util/src/test/java/org/killbill/billing/DBTestingHelper.java b/util/src/test/java/org/killbill/billing/DBTestingHelper.java
index 2808866..d940406 100644
--- a/util/src/test/java/org/killbill/billing/DBTestingHelper.java
+++ b/util/src/test/java/org/killbill/billing/DBTestingHelper.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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
@@ -19,18 +19,27 @@
 package org.killbill.billing;
 
 import java.io.IOException;
+import java.io.PrintWriter;
 import java.net.URL;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.sql.SQLNonTransientConnectionException;
 import java.util.Enumeration;
-import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.sql.DataSource;
 
 import org.killbill.billing.platform.test.PlatformDBTestingHelper;
 import org.killbill.billing.util.glue.IDBISetup;
 import org.killbill.billing.util.io.IOUtils;
 import org.killbill.commons.embeddeddb.EmbeddedDB;
+import org.killbill.commons.jdbi.guice.DBIProvider;
 import org.skife.jdbi.v2.DBI;
 import org.skife.jdbi.v2.IDBI;
 import org.skife.jdbi.v2.ResultSetMapperFactory;
 import org.skife.jdbi.v2.tweak.ResultSetMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.google.common.base.MoreObjects;
 
@@ -38,7 +47,7 @@ public class DBTestingHelper extends PlatformDBTestingHelper {
 
     private static DBTestingHelper dbTestingHelper = null;
 
-    private AtomicBoolean initialized;
+    private DBI dbi;
 
     public static synchronized DBTestingHelper get() {
         if (dbTestingHelper == null) {
@@ -49,18 +58,18 @@ public class DBTestingHelper extends PlatformDBTestingHelper {
 
     private DBTestingHelper() {
         super();
-        initialized = new AtomicBoolean(false);
     }
 
     @Override
-    public IDBI getDBI() {
-        final DBI dbi = (DBI) super.getDBI();
-        // Register KB specific mappers
-        if (initialized.compareAndSet(false, true)) {
+    public synchronized IDBI getDBI() {
+        if (dbi == null) {
+            final RetryableDataSource retryableDataSource = new RetryableDataSource(getDataSource());
+            dbi = (DBI) new DBIProvider(null, retryableDataSource, null).get();
+
+            // Register KB specific mappers
             for (final ResultSetMapperFactory resultSetMapperFactory : IDBISetup.mapperFactoriesToRegister()) {
                 dbi.registerMapper(resultSetMapperFactory);
             }
-
             for (final ResultSetMapper resultSetMapper : IDBISetup.mappersToRegister()) {
                 dbi.registerMapper(resultSetMapper);
             }
@@ -202,4 +211,73 @@ public class DBTestingHelper extends PlatformDBTestingHelper {
             }
         }
     }
+
+    // DataSource which will retry recreating a connection once in case of a connection exception.
+    // This is useful for transient network errors in tests when using a separate database (e.g. Docker container),
+    // as we don't use a connection pool.
+    private static final class RetryableDataSource implements DataSource {
+
+        private static final Logger logger = LoggerFactory.getLogger(RetryableDataSource.class);
+
+        private final DataSource delegate;
+
+        private RetryableDataSource(final DataSource delegate) {
+            this.delegate = delegate;
+        }
+
+        @Override
+        public Connection getConnection() throws SQLException {
+            try {
+                return delegate.getConnection();
+            } catch (final SQLNonTransientConnectionException e) {
+                logger.warn("Unable to retrieve connection, attempting to retry", e);
+                return delegate.getConnection();
+            }
+        }
+
+        @Override
+        public Connection getConnection(final String username, final String password) throws SQLException {
+            try {
+                return delegate.getConnection(username, password);
+            } catch (final SQLNonTransientConnectionException e) {
+                logger.warn("Unable to retrieve connection, attempting to retry", e);
+                return delegate.getConnection(username, password);
+            }
+        }
+
+        @Override
+        public <T> T unwrap(final Class<T> iface) throws SQLException {
+            return delegate.unwrap(iface);
+        }
+
+        @Override
+        public boolean isWrapperFor(final Class<?> iface) throws SQLException {
+            return delegate.isWrapperFor(iface);
+        }
+
+        @Override
+        public PrintWriter getLogWriter() throws SQLException {
+            return delegate.getLogWriter();
+        }
+
+        @Override
+        public void setLogWriter(final PrintWriter out) throws SQLException {
+            delegate.setLogWriter(out);
+        }
+
+        @Override
+        public void setLoginTimeout(final int seconds) throws SQLException {
+            delegate.setLoginTimeout(seconds);
+        }
+
+        @Override
+        public int getLoginTimeout() throws SQLException {
+            return delegate.getLoginTimeout();
+        }
+
+        //@Override
+        public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException {
+            throw new SQLFeatureNotSupportedException("javax.sql.DataSource.getParentLogger() is not currently supported by " + this.getClass().getName());
+        }
+    }
 }