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);
+ }
+ }
+}