diff --git a/locker/pom.xml b/locker/pom.xml
index 7be9a49a..f5de5880 100644
--- a/locker/pom.xml
+++ b/locker/pom.xml
@@ -49,6 +49,11 @@
1.0
test
+
+ it.ozimov
+ embedded-redis
+ test
+
mysql
mysql-connector-mxj
@@ -76,6 +81,10 @@
+
+ org.redisson
+ redisson
+
org.slf4j
slf4j-api
diff --git a/locker/src/main/java/org/killbill/commons/locker/redis/RedisGlobalLocker.java b/locker/src/main/java/org/killbill/commons/locker/redis/RedisGlobalLocker.java
new file mode 100644
index 00000000..ac4a33c5
--- /dev/null
+++ b/locker/src/main/java/org/killbill/commons/locker/redis/RedisGlobalLocker.java
@@ -0,0 +1,87 @@
+/*
+ * 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
+ * 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 org.killbill.commons.locker.redis;
+
+import java.util.concurrent.TimeUnit;
+
+import org.killbill.commons.locker.GlobalLock;
+import org.killbill.commons.locker.GlobalLocker;
+import org.killbill.commons.locker.GlobalLockerBase;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+
+public class RedisGlobalLocker extends GlobalLockerBase implements GlobalLocker {
+
+ private final RedissonClient redissonClient;
+
+ public RedisGlobalLocker(final RedissonClient redissonClient) {
+ super(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+ this.redissonClient = redissonClient;
+ }
+
+ @Override
+ public synchronized boolean isFree(final String service, final String lockKey) {
+ final String lockName = getLockName(service, lockKey);
+ final RLock redisLock = redissonClient.getLock(lockName);
+ return !redisLock.isLocked();
+ }
+
+ @Override
+ protected synchronized GlobalLock doLock(final String lockName) {
+ final RLock redisLock = redissonClient.getLock(lockName);
+ if (redisLock.isLocked()) {
+ return null;
+ }
+
+ final boolean acquired;
+ try {
+ // waitTime=1ms (retry done ourselves)
+ // leaseTime=5min
+ acquired = redisLock.tryLock(1, 300000, TimeUnit.MILLISECONDS);
+ } catch (final InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return null;
+ }
+
+ if (!acquired) {
+ return null;
+ } else if (redisLock.getHoldCount() > 1) {
+ // Someone beat us to it?
+ redisLock.forceUnlock();
+ return null;
+ }
+
+ final GlobalLock lock = new GlobalLock() {
+ @Override
+ public void release() {
+ if (lockTable.releaseLock(lockName)) {
+ redisLock.forceUnlock();
+ }
+ }
+ };
+
+ lockTable.createLock(lockName, lock);
+
+ return lock;
+ }
+
+ @Override
+ protected String getLockName(final String service, final String lockKey) {
+ return service + "-" + lockKey;
+ }
+}
diff --git a/locker/src/test/java/org/killbill/commons/locker/redis/TestRedisGlobalLocker.java b/locker/src/test/java/org/killbill/commons/locker/redis/TestRedisGlobalLocker.java
new file mode 100644
index 00000000..bb44de9e
--- /dev/null
+++ b/locker/src/test/java/org/killbill/commons/locker/redis/TestRedisGlobalLocker.java
@@ -0,0 +1,104 @@
+/*
+ * 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
+ * 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 org.killbill.commons.locker.redis;
+
+import java.io.IOException;
+import java.util.UUID;
+
+import org.killbill.commons.locker.GlobalLock;
+import org.killbill.commons.locker.GlobalLocker;
+import org.killbill.commons.locker.LockFailedException;
+import org.killbill.commons.request.Request;
+import org.killbill.commons.request.RequestData;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import redis.embedded.RedisServer;
+
+public class TestRedisGlobalLocker {
+
+ private RedissonClient redissonClient;
+ private GlobalLocker locker;
+ private RedisServer redisServer;
+
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ redisServer = new RedisServer(56379);
+ redisServer.start();
+
+ final Config config = new Config();
+ config.useSingleServer().setAddress("redis://127.0.0.1:56379").setConnectionMinimumIdleSize(10);
+ redissonClient = Redisson.create(config);
+ locker = new RedisGlobalLocker(redissonClient);
+ Request.resetPerThreadRequestData();
+ }
+
+ @AfterMethod(groups = "slow")
+ public void afterMethod() throws Exception {
+ redissonClient.shutdown();
+ Request.resetPerThreadRequestData();
+ redisServer.stop();
+ }
+
+ @Test(groups = "slow")
+ public void testSimpleLocking() throws IOException, LockFailedException {
+ final String serviceLock = "MY_AWESOME_LOCK";
+ final String lockName = UUID.randomUUID().toString();
+
+ final GlobalLock lock = locker.lockWithNumberOfTries(serviceLock, lockName, 3);
+ Assert.assertFalse(locker.isFree(serviceLock, lockName));
+
+ boolean gotException = false;
+ try {
+ locker.lockWithNumberOfTries(serviceLock, lockName, 1);
+ } catch (final LockFailedException e) {
+ gotException = true;
+ }
+ Assert.assertTrue(gotException);
+
+ lock.release();
+ Assert.assertTrue(locker.isFree(serviceLock, lockName));
+ }
+
+ @Test(groups = "slow")
+ public void testReentrantLock() throws IOException, LockFailedException {
+ final String serviceLock = "MY_SHITTY_LOCK";
+ final String lockName = UUID.randomUUID().toString();
+
+ final String requestId = "12345";
+
+ Request.setPerThreadRequestData(new RequestData(requestId));
+
+ final GlobalLock lock = locker.lockWithNumberOfTries(serviceLock, lockName, 3);
+ Assert.assertFalse(locker.isFree(serviceLock, lockName));
+
+ // Re-aquire the createLock with the same requestId, should work
+ final GlobalLock reentrantLock = locker.lockWithNumberOfTries(serviceLock, lockName, 1);
+
+ lock.release();
+ Assert.assertFalse(locker.isFree(serviceLock, lockName));
+
+ reentrantLock.release();
+ Assert.assertTrue(locker.isFree(serviceLock, lockName));
+ }
+}
diff --git a/pom.xml b/pom.xml
index 0f88ee04..350e6894 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,7 +21,7 @@
killbill-oss-parent
org.kill-bill.billing
- 0.142.5-SNAPSHOT
+ 0.142.5
org.kill-bill.commons
killbill-commons