Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

locker: naive lock implementation on Redis #57

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions locker/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@
<version>1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>it.ozimov</groupId>
<artifactId>embedded-redis</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-mxj</artifactId>
Expand Down Expand Up @@ -76,6 +81,10 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<parent>
<artifactId>killbill-oss-parent</artifactId>
<groupId>org.kill-bill.billing</groupId>
<version>0.142.5-SNAPSHOT</version>
<version>0.142.5</version>
</parent>
<groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-commons</artifactId>
Expand Down