Search This Blog

Sunday 5 July 2020

Dynamo Db - locks continued

In the last post I used optimistic locking with the Version attribute provided by DynamoDBMapper. Here I am going to look at the pessimistic locking method
Pessimistic locking techniques require us to acquire a lock on the resource before we can modify it. As only one process can acquire the lock, e are guaranteed to be working with the latest version of the object.
Dynamo Db inherently does not provide a lock implementation, Amazon has however released an open source library called the Amazon DynamoDB Lock Client, which can be used for this purpose.
Similar to our previous post I created a simple Cake class:

@DynamoDBTable(tableName = "Cake")
public class Cake {
    @DynamoDBHashKey(attributeName = "id")
    private String id;
    private int noOfSlices;
    private String lastEatenBy;
//setter getters
}
Notice there is no version attribute here. The DynamoDb representation is as below:

The next step is to setup the Lock Client. The Lock client uses a separate DynamoDb table to manage locks. Instead of manually setting up the table, I used a utility method provided by the LockClient library:

CreateDynamoDBTableOptions lockTableOptions = CreateDynamoDBTableOptions.builder(amazonDynamoDB,
       new ProvisionedThroughput(25L, 25L), "CakeLock").build();
AmazonDynamoDBLockClient.createLockTableInDynamoDB(lockTableOptions);
The table created is as below:
A table is created with the hash key as 'key'. This table is used to determine if a Cake item is locked and who owns the lock.
The Cake consumer threads is as below:

static class CakeEater implements Callable<Void> {

    private String name;
    private DynamoDBMapper mapper;
    private String cakeKey;

    public CakeEater(String name, DynamoDBMapper mapper, String cakeKey) {
        this.name = name;
        this.mapper = mapper;
        this.cakeKey = cakeKey;
        System.out.println("Eater " + name + " ready to start eating");
    }

    @Override
    public Void call() throws IOException {

        // Whether or not to create a heartbeating background thread
        final boolean createHeartbeatBackgroundThread = false;
        //build the lock client
        long leaseDuration = 2L;
        final AmazonDynamoDBLockClient client = new AmazonDynamoDBLockClient(
                AmazonDynamoDBLockClientOptions.builder(amazonDynamoDB, "CakeLock")
                        .withTimeUnit(TimeUnit.MILLISECONDS)
                        .withLeaseDuration(leaseDuration)
                        .withHeartbeatPeriod(2L)
                        .withCreateHeartbeatBackgroundThread(createHeartbeatBackgroundThread)
                        .build());
        while (true) {
            //try to acquire a lock on the partition key - cakeKey
            try {
                System.out.println("Attempt to acquire lock by " + name);
                final Optional<LockItem> lockItem = Optional.ofNullable(
                        client.acquireLock(AcquireLockOptions.builder(cakeKey).build()));
                if (lockItem.isPresent()) {
                    System.out.println("Acquired lock by " + name);
                    Cake cakeId = new Cake();
                    cakeId.setId(this.cakeKey);

                    //Time to eat the cake quickly - within 200 millis
                    Cake cake = mapper.load(cakeId);
                    if (cake != null && cake.getNoOfSlices() > 0) {
                        System.out.println(name + " looking to eat. Available Pieces " + cake.getNoOfSlices()
                                + ".Last eaten by " + cake.getLastEatenBy());
                        cake.setNoOfSlices(cake.getNoOfSlices() - 1);
                        cake.setLastEatenBy(name);
                        try {
                            mapper.save(cake); // processing complete
                        } catch (Exception e) {
                            System.err.println("Saving Cake for " + name + " failed. " + e.getMessage());
                        }
                    } else {
                        System.out.println("Cake is over. " + name + " stopping");
                        break;
                    }
                    client.releaseLock(lockItem.get()); //lock released
                    System.out.println(name + " taking a break");
                    Thread.sleep(leaseDuration * 3);
                } else {
                    System.out.println(name + " failed to acquire lock");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        client.close();
        return null;
    }
}
The code works as below:

  1. Code tries to acquire a lock for 2 milliseconds. 
  2. If lock is acquired, code will fetch the cake record and eat a piece. 
  3. Once done the code releases the lock. 

The change in lock values can be seen in the Cake Locks table:
The assumption here is that code will finish its operations within 2 milliseconds. How does the locking logic work ?
The way locks are expired is that a call to acquireLock reads in the current lock,
checks the RecordVersionNumber of the lock (which is a GUID) and starts a timer.
If the lock still has the same GUID after the lease duration time has passed, the
client will determine that the lock is stale and expire it
So any code that reads the Lock table - if it finds an entry will always assume that someone is holding the lock. The assumption is also that lock is being held for atleast 'DurationTime' as specified in the Table.
So in our code, Lets say Thread 0 wins the lock and will hold it for 2 milliseconds. It after 1 millisecond, Thread 1 arrives it will see that the lock is being held for a duration of 2 milliseconds and will therefore wait for 2 milliseconds before trying to acquire again. In effect this is 3 milliseconds since Thread 0 acquired the lock.
When Thread 1 finally checks again, the record would have been deleted (as Thread 0 had released the lock after 2 milliseconds). So Thread 1 will acquire the lock.
What if Thread 0 had crashed while holding the lock ?
If Thread  had crashed without releasing the lock, an entry would remain in table. When Thread 1 checks again, the recordVersionNumber would be same as when it checked earlier. This means,  lock is stale and so Thread 0 will acquire the lock.
The lock client has additional features - non blocking wait, automatic heartbeats that extend your lock duration etc.
My code output is as below:
Cake has been saved
Eater Eater0 ready to start eating
Eater Eater1 ready to start eating
Eater Eater2 ready to start eating
Attempt to acquire lock by Eater1
Attempt to acquire lock by Eater0
Attempt to acquire lock by Eater2
Acquired lock by Eater0
Eater0 looking to eat. Available Pieces 10.Last eaten by null
Eater0 taking a break
Attempt to acquire lock by Eater0
Acquired lock by Eater0
Eater0 looking to eat. Available Pieces 9.Last eaten by Eater0
Eater0 taking a break
Attempt to acquire lock by Eater0
Acquired lock by Eater0
Eater0 looking to eat. Available Pieces 8.Last eaten by Eater0
Eater0 taking a break
Attempt to acquire lock by Eater0
Acquired lock by Eater0
What fits my use case better ? I compared both locking mechanisms - for my usecase, Optimistic Locking is a better fit. The DynamoMapper can handle the contention among multiple write threads ensuring none of them updates stale information. Pessimistic locking is more of an advisory locking mechanism, with more responsibility on the code to get it right. A more appropriate use case for this would be when I need to perform some operations if I have a lock on the record.

3 comments:

  1. Splendid post. Also visit here: Feeta.pk house for sale in Islamabad . it is very helpful for us thanks for sharing.

    ReplyDelete
  2. Wow amazing, i will definately go for these helpful tips. Redspider classified script  is about to tell you that thanks for sharing.

    ReplyDelete
  3. Cheapest Flight Booking Travels with SkyGoTrip

    ReplyDelete