killbill-memoizeit

Details

diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestBasic.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestBasic.java
index 47f754e..0e548b7 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestBasic.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestBasic.java
@@ -185,12 +185,12 @@ public class TestBasic {
         testBasePlanComplete(clock.getUTCNow().minusDays(1).getDayOfMonth());
     }
 
-    @Test(groups = "fast", enabled = false)
+    @Test(groups = "fast", enabled = true)
     public void testBasePlanCompleteWithBillingDayPresent() throws Exception {
         testBasePlanComplete(clock.getUTCNow().getDayOfMonth());
     }
 
-    @Test(groups = "fast", enabled = true)
+    @Test(groups = "fast", enabled = false)
     public void testBasePlanCompleteWithBillingDayAlignedWithTrial() throws Exception {
         testBasePlanComplete(clock.getUTCNow().plusDays(30).getDayOfMonth());
     }
@@ -200,10 +200,10 @@ public class TestBasic {
         testBasePlanComplete(clock.getUTCNow().plusDays(1).getDayOfMonth());
     }
 
-
-    private void waitForDebug() throws Exception {
-        Thread.sleep(600000);
-    }
+//
+//    private void waitForDebug() throws Exception {
+//        Thread.sleep(600000);
+//    }
 
 
     @Test(groups = "stress", enabled = false)
@@ -240,7 +240,7 @@ public class TestBasic {
         assertNotNull(subscription);
 
 
-        waitForDebug();
+        //waitForDebug();
 
         assertTrue(busHandler.isCompleted(DELAY));
         log.info("testSimple passed first busHandler checkpoint.");
diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoiceGenerator.java b/invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoiceGenerator.java
index 41a86e1..331372a 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoiceGenerator.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoiceGenerator.java
@@ -150,35 +150,37 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
         if (billingPeriod != BillingPeriod.NO_BILLING_PERIOD) {
             BillingMode billingMode = instantiateBillingMode(thisEvent.getBillingMode());
             DateTime startDate = thisEvent.getEffectiveDate();
-            DateTime endDate = (nextEvent == null) ? null : nextEvent.getEffectiveDate();
-            int billCycleDay = thisEvent.getBillCycleDay();
-
-            List<RecurringInvoiceItemData> itemData;
-            try {
-                itemData = billingMode.calculateInvoiceItemData(startDate, endDate, targetDate, billCycleDay, billingPeriod);
-            } catch (InvalidDateSequenceException e) {
-                throw new InvoiceApiException(ErrorCode.INVOICE_INVALID_DATE_SEQUENCE, startDate, endDate, targetDate);
-            }
+            if (!startDate.isAfter(targetDate)) {
+                DateTime endDate = (nextEvent == null) ? null : nextEvent.getEffectiveDate();
+                int billCycleDay = thisEvent.getBillCycleDay();
 
-            for (RecurringInvoiceItemData itemDatum : itemData) {
-                InternationalPrice price = thisEvent.getRecurringPrice();
-                if (price != null) {
-                    BigDecimal rate;
+                List<RecurringInvoiceItemData> itemData;
+                try {
+                    itemData = billingMode.calculateInvoiceItemData(startDate, endDate, targetDate, billCycleDay, billingPeriod);
+                } catch (InvalidDateSequenceException e) {
+                    throw new InvoiceApiException(ErrorCode.INVOICE_INVALID_DATE_SEQUENCE, startDate, endDate, targetDate);
+                }
 
-                    try {
-                        rate = thisEvent.getRecurringPrice().getPrice(currency);
-                    } catch (CatalogApiException e) {
-                        throw new InvoiceApiException(e, ErrorCode.CAT_NO_PRICE_FOR_CURRENCY, currency.toString());
+                for (RecurringInvoiceItemData itemDatum : itemData) {
+                    InternationalPrice price = thisEvent.getRecurringPrice();
+                    if (price != null) {
+                        BigDecimal rate;
+
+                        try {
+                            rate = thisEvent.getRecurringPrice().getPrice(currency);
+                        } catch (CatalogApiException e) {
+                            throw new InvoiceApiException(e, ErrorCode.CAT_NO_PRICE_FOR_CURRENCY, currency.toString());
+                        }
+
+                        BigDecimal amount = itemDatum.getNumberOfCycles().multiply(rate).setScale(NUMBER_OF_DECIMALS, ROUNDING_MODE);
+
+                        RecurringInvoiceItem recurringItem = new RecurringInvoiceItem(invoiceId, thisEvent.getSubscription().getId(),
+                                                                                      thisEvent.getPlan().getName(),
+                                                                                      thisEvent.getPlanPhase().getName(),
+                                                                                      itemDatum.getStartDate(), itemDatum.getEndDate(),
+                                                                                      amount, rate, currency);
+                        items.add(recurringItem);
                     }
-
-                    BigDecimal amount = itemDatum.getNumberOfCycles().multiply(rate).setScale(NUMBER_OF_DECIMALS, ROUNDING_MODE);
-
-                    RecurringInvoiceItem recurringItem = new RecurringInvoiceItem(invoiceId, thisEvent.getSubscription().getId(),
-                                                                                  thisEvent.getPlan().getName(),
-                                                                                  thisEvent.getPlanPhase().getName(),
-                                                                                  itemDatum.getStartDate(), itemDatum.getEndDate(),
-                                                                                  amount, rate, currency);
-                    items.add(recurringItem);
                 }
             }
         }
diff --git a/util/src/main/java/com/ning/billing/util/globallocker/GlobalLock.java b/util/src/main/java/com/ning/billing/util/globallocker/GlobalLock.java
new file mode 100644
index 0000000..12ebc0d
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/globallocker/GlobalLock.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.globallocker;
+
+public interface GlobalLock
+{
+    public void release();
+}
\ No newline at end of file
diff --git a/util/src/main/java/com/ning/billing/util/globallocker/GlobalLocker.java b/util/src/main/java/com/ning/billing/util/globallocker/GlobalLocker.java
new file mode 100644
index 0000000..5312e09
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/globallocker/GlobalLocker.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.globallocker;
+
+public interface GlobalLocker {
+
+    GlobalLock lockWithNumberOfTries(final LockerService service, final String lockKey, final int retry);
+    Boolean isFree(final LockerService service, final String lockKey);
+
+    public enum LockerService {
+
+        // Only service needing global lock
+        INVOICE("invoice");
+
+        private final String svcName;
+
+        LockerService(String svcName) {
+            this.svcName = svcName;
+        }
+
+        @Override
+        public String toString() {
+            return svcName;
+        }
+    }
+}
\ No newline at end of file
diff --git a/util/src/main/java/com/ning/billing/util/globallocker/LockFailedException.java b/util/src/main/java/com/ning/billing/util/globallocker/LockFailedException.java
new file mode 100644
index 0000000..67f83a4
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/globallocker/LockFailedException.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.globallocker;
+
+public class LockFailedException extends RuntimeException
+{
+}
diff --git a/util/src/main/java/com/ning/billing/util/globallocker/MySqlGlobalLocker.java b/util/src/main/java/com/ning/billing/util/globallocker/MySqlGlobalLocker.java
new file mode 100644
index 0000000..f9de358
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/globallocker/MySqlGlobalLocker.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.globallocker;
+
+import com.google.inject.Inject;
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.IDBI;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MySqlGlobalLocker implements GlobalLocker {
+
+    private final static Logger logger = LoggerFactory.getLogger(MySqlGlobalLocker.class);
+
+    private final static long DEFAULT_TIMEOUT = 3L; // 3 seconds
+
+    private final IDBI dbi;
+    private long timeout;
+
+    @Inject
+    public MySqlGlobalLocker(IDBI dbi) {
+        this.dbi = dbi;
+        this.timeout = DEFAULT_TIMEOUT;
+    }
+
+    public void setTimeout(final long timeout) {
+        this.timeout = timeout;
+    }
+
+    @Override
+    public GlobalLock lockWithNumberOfTries(final LockerService service, final String lockKey, final int retry) {
+
+        final String lockName = getLockName(service, lockKey);
+        int tries_left = retry;
+        while (tries_left-- > 0) {
+            GlobalLock lock = lock(lockName);
+            if (lock != null) {
+                return lock;
+            }
+        }
+        logger.error(String.format("Failed to acquire lock %s for service %s after %d retry", lockKey, service, retry));
+        throw new LockFailedException();
+    }
+
+    private GlobalLock lock(final String lockName) throws LockFailedException {
+
+        final Handle h = dbi.open();
+        final MySqlGlobalLockerDao dao = h.attach(MySqlGlobalLockerDao.class);
+
+        final boolean obtained = dao.lock(lockName, timeout);
+        if (obtained) {
+            return new GlobalLock() {
+                @Override
+                public void release() {
+                    try {
+                        dao.releaseLock(lockName);
+                    }
+                    finally {
+                        if (h != null) {
+                            h.close();
+                        }
+                    }
+                }
+            };
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public Boolean isFree(final LockerService service, final String lockKey) {
+
+        final String lockName = getLockName(service, lockKey);
+        final Handle h = dbi.open();
+        try {
+            final MySqlGlobalLockerDao dao = h.attach(MySqlGlobalLockerDao.class);
+            return dao.isFree(lockName);
+        } finally {
+            if (h != null) {
+                h.close();
+            }
+        }
+    }
+
+    private String getLockName(final LockerService service, final String lockKey) {
+        StringBuilder tmp = new StringBuilder()
+            .append(service.toString())
+            .append("-")
+            .append(lockKey);
+        return tmp.toString();
+    }
+}
diff --git a/util/src/main/java/com/ning/billing/util/globallocker/MySqlGlobalLockerDao.java b/util/src/main/java/com/ning/billing/util/globallocker/MySqlGlobalLockerDao.java
new file mode 100644
index 0000000..876f6e1
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/globallocker/MySqlGlobalLockerDao.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.globallocker;
+
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapper;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+@RegisterMapper(MySqlGlobalLockerDao.LockMapper.class)
+public interface MySqlGlobalLockerDao {
+
+    @SqlQuery("Select GET_LOCK(:lockName, :timeout);")
+    public Boolean lock(@Bind("lockName") final String lockName, @Bind("timeout") final long timeout);
+
+    @SqlQuery("Select RELEASE_LOCK(:lockName);")
+    public Boolean releaseLock(@Bind("lockName") final String lockName);
+
+    @SqlQuery("Select IS_FREE_LOCK(:lockName);")
+    public Boolean isFree(@Bind("lockName") final String lockName);
+
+    class LockMapper implements ResultSetMapper<Boolean> {
+         @Override
+         public Boolean map(int index, ResultSet r, StatementContext ctx) throws SQLException {
+            return (r.getByte(1) == 1);
+         }
+    }
+}
diff --git a/util/src/test/java/com/ning/billing/util/globallocker/MockGlobalLocker.java b/util/src/test/java/com/ning/billing/util/globallocker/MockGlobalLocker.java
new file mode 100644
index 0000000..b1f3378
--- /dev/null
+++ b/util/src/test/java/com/ning/billing/util/globallocker/MockGlobalLocker.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.globallocker;
+
+import com.ning.billing.util.globallocker.GlobalLock;
+import com.ning.billing.util.globallocker.GlobalLocker;
+
+public class MockGlobalLocker implements GlobalLocker {
+
+    @Override
+    public GlobalLock lockWithNumberOfTries(LockerService service,
+            String lockKey, int retry) {
+        return new GlobalLock() {
+            @Override
+            public void release() {
+            }
+        };
+    }
+
+    @Override
+    public Boolean isFree(LockerService service, String lockKey) {
+        return Boolean.TRUE;
+    }
+}
diff --git a/util/src/test/java/com/ning/billing/util/globallocker/TestMysqlGlobalLocker.java b/util/src/test/java/com/ning/billing/util/globallocker/TestMysqlGlobalLocker.java
new file mode 100644
index 0000000..1fd8290
--- /dev/null
+++ b/util/src/test/java/com/ning/billing/util/globallocker/TestMysqlGlobalLocker.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.globallocker;
+
+import java.io.IOException;
+import java.util.UUID;
+
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.IDBI;
+import org.skife.jdbi.v2.TransactionCallback;
+import org.skife.jdbi.v2.TransactionStatus;
+import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.ning.billing.dbi.MysqlTestingHelper;
+import com.ning.billing.util.globallocker.GlobalLock;
+import com.ning.billing.util.globallocker.GlobalLocker;
+import com.ning.billing.util.globallocker.LockFailedException;
+import com.ning.billing.util.globallocker.MySqlGlobalLocker;
+import com.ning.billing.util.globallocker.GlobalLocker.LockerService;
+
+@Guice(modules=TestMysqlGlobalLocker.TestMysqlGlobalLockerModule.class)
+public class TestMysqlGlobalLocker {
+
+    @Inject
+    private IDBI dbi;
+
+    @Inject
+    private MysqlTestingHelper helper;
+
+    @BeforeClass(alwaysRun=true)
+    public void setup() throws IOException  {
+        helper.startMysql();
+        createSimpleTable(dbi);
+    }
+
+    @AfterClass(alwaysRun=true)
+    public void tearDown() {
+        helper.stopMysql();
+    }
+
+    // Used as a manual test to validate the simple DAO by stepping through that locking is done and release correctly
+    @Test(groups= "slow", enabled = true)
+    public void testSimpleLocking() {
+
+        final String lockName = UUID.randomUUID().toString();
+
+        GlobalLocker locker = new MySqlGlobalLocker(dbi);
+        GlobalLock lock = locker.lockWithNumberOfTries(LockerService.INVOICE, lockName, 3);
+
+        dbi.inTransaction(new TransactionCallback<Void>() {
+            @Override
+            public Void inTransaction(Handle conn, TransactionStatus status)
+                    throws Exception {
+                conn.execute("insert into dummy (dummy_id) values ('" + UUID.randomUUID().toString()  + "')");
+                return null;
+            }
+        });
+        Assert.assertEquals(locker.isFree(LockerService.INVOICE, lockName), Boolean.FALSE);
+
+        boolean gotException = false;
+        try {
+            locker.lockWithNumberOfTries(LockerService.INVOICE, lockName, 1);
+        } catch (LockFailedException e) {
+            gotException = true;
+        }
+        Assert.assertTrue(gotException);
+
+        lock.release();
+
+        Assert.assertEquals(locker.isFree(LockerService.INVOICE, lockName), Boolean.TRUE);
+    }
+
+    private void createSimpleTable(IDBI dbi) {
+        dbi.inTransaction(new TransactionCallback<Void>() {
+
+            @Override
+            public Void inTransaction(Handle h, TransactionStatus status)
+                    throws Exception {
+                h.execute("create table dummy " +
+                        "(id int(11) unsigned NOT NULL AUTO_INCREMENT, " +
+                        "dummy_id char(36) NOT NULL, " +
+                        "PRIMARY KEY(id)" +
+                		") ENGINE=innodb;");
+                return null;
+            }
+        });
+    }
+
+    public final static class TestMysqlGlobalLockerModule extends AbstractModule {
+
+        @Override
+        protected void configure() {
+            MysqlTestingHelper helper = new MysqlTestingHelper();
+            bind(MysqlTestingHelper.class).toInstance(helper);
+            final IDBI dbi = helper.getDBI();
+            bind(IDBI.class).toInstance(dbi);
+        }
+    }
+}