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