Skip to content

Commit 03b4305

Browse files
authored
Merge pull request #99 from moshegood/moshe/ShouldSkipBlockingWait/eventually.should.work
Fix: ShouldSkipBlockingWait should still acquire a dead lock if tried for longer than TTL
2 parents 0cd3fe8 + f620e0a commit 03b4305

File tree

2 files changed

+58
-3
lines changed

2 files changed

+58
-3
lines changed

src/main/java/com/amazonaws/services/dynamodbv2/AmazonDynamoDBLockClient.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ public class AmazonDynamoDBLockClient implements Runnable, Closeable {
233233
private final boolean holdLockOnServiceUnavailable;
234234
private final String ownerName;
235235
private final ConcurrentHashMap<String, LockItem> locks;
236+
private final ConcurrentHashMap<String, LockItem> notMyLocks = new ConcurrentHashMap<>();
236237
private final ConcurrentHashMap<String, Thread> sessionMonitors;
237238
private final Optional<Thread> backgroundThread;
238239
private final Function<String, ThreadFactory> namedThreadCreator;
@@ -470,11 +471,30 @@ public LockItem acquireLock(final AcquireLockOptions options) throws LockNotGran
470471
}
471472

472473
if (options.shouldSkipBlockingWait() && existingLock.isPresent() && !existingLock.get().isExpired()) {
474+
String id = existingLock.get().getUniqueIdentifier();
475+
// Let's check to see if this existingLock expired based on old data we cached.
476+
// Or cache it if we haven't seen this recordVersion before.
477+
boolean isReallyExpired = false;
478+
if (notMyLocks.containsKey(id) &&
479+
notMyLocks.get(id).getRecordVersionNumber()
480+
.equals(existingLock.get().getRecordVersionNumber())) {
481+
482+
isReallyExpired = notMyLocks.get(id).isExpired();
483+
if (isReallyExpired) {
484+
// short circuit the waiting that we normally do.
485+
lockTryingToBeAcquired = notMyLocks.get(id);
486+
}
487+
} else {
488+
notMyLocks.put(id, existingLock.get());
489+
}
490+
473491
/*
474492
* The lock is being held by some one and is still not expired. And the caller explicitly said not to perform a blocking wait;
475493
* We will throw back a lock not grant exception, so that the caller can retry if needed.
476494
*/
477-
throw new LockCurrentlyUnavailableException("The lock being requested is being held by another client.");
495+
if (!isReallyExpired) {
496+
throw new LockCurrentlyUnavailableException("The lock being requested is being held by another client.");
497+
}
478498
}
479499

480500
Optional<ByteBuffer> newLockData = Optional.empty();

src/test/java/com/amazonaws/services/dynamodbv2/AmazonDynamoDBLockClientTest.java

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -443,10 +443,45 @@ public void acquireLock_whenLockAlreadyExistsAndIsNotReleased_andSkipBlockingWai
443443
.thenReturn(GetItemResponse.builder().item(item).build())
444444
.thenReturn(GetItemResponse.builder().build());
445445
AcquireLockOptions acquireLockOptions = AcquireLockOptions.builder("customer1")
446-
.withShouldSkipBlockingWait(true)
447-
.withDeleteLockOnRelease(false).build();
446+
.withShouldSkipBlockingWait(true)
447+
.withDeleteLockOnRelease(false).build();
448448
client.acquireLock(acquireLockOptions);
449449
}
450+
/*
451+
* Test case for the scenario, where the lock is being held by the first owner and the lock duration has not past
452+
* the lease duration. In this case, We should expect a LockAlreadyOwnedException when shouldSkipBlockingWait is set.
453+
* But if we try again later, we should get the lock.
454+
*/
455+
@Test
456+
public void acquireLock_whenLockAlreadyExistsAndIsNotReleased_andSkipBlockingWait_eventuallyGetsTheLock()
457+
throws InterruptedException {
458+
UUID uuid = setOwnerNameToUuid();
459+
AmazonDynamoDBLockClient client = getLockClient();
460+
Map<String, AttributeValue> item = new HashMap<>(5);
461+
item.put("customer", AttributeValue.builder().s("customer1").build());
462+
item.put("ownerName", AttributeValue.builder().s("foobar").build());
463+
item.put("recordVersionNumber", AttributeValue.builder().s(uuid.toString()).build());
464+
item.put("leaseDuration", AttributeValue.builder().s("100").build());
465+
when(dynamodb.getItem(Mockito.<GetItemRequest>any()))
466+
.thenReturn(GetItemResponse.builder().item(item).build())
467+
.thenReturn(GetItemResponse.builder().build());
468+
AcquireLockOptions acquireLockOptions = AcquireLockOptions.builder("customer1")
469+
.withShouldSkipBlockingWait(true)
470+
.withDeleteLockOnRelease(false).build();
471+
472+
try {
473+
client.acquireLock(acquireLockOptions);
474+
} catch (LockCurrentlyUnavailableException e) {
475+
// This is expected
476+
} catch (RuntimeException e) {
477+
Assert.fail("Expected LockCurrentlyUnavailableException, but got " + e.getClass().getName());
478+
}
479+
480+
// Now wait for the TTL to expire and try to acquire the lock again
481+
Thread.sleep(101);
482+
LockItem lockItem = client.acquireLock(acquireLockOptions);
483+
Assert.assertNotNull("Failed to get lock item, when the lock is not present in the db", lockItem);
484+
}
450485

451486
@Test(expected = IllegalArgumentException.class)
452487
public void sendHeartbeat_whenDeleteDataTrueAndDataNotNull_throwsIllegalArgumentException() {

0 commit comments

Comments
 (0)