Search This Blog

Friday 26 June 2020

Dynamo Db - locks

When I studies Databases in college, we learnt the concept of locks
A database lock is used to “lock” some data in a database so that only one 
database user/session may update that particular data. So, database locks 
exist to prevent two or more database users from updating the same exact 
piece of data at the same exact time.
The idea was to achieve transactional isolation. Or two operations operating on the same record did not have any side effects on the other.

We studied pessimistic locking and optimistic locking approaches. I also came across this terms more frequently when working with Hibernate and SQL databases.
Quoting liberally from Jboss docs:
Pessimistic locking:
With pessimistic locking, locks are applied in a fail-safe way.A resource is
locked from the time it is first accessed in a transaction until the 
transaction is finished, making it inaccessible to other transactions during 
that time. 
If most transactions simply look at the resource and never change it, an 
exclusive lock may be overkill as it may cause lock contention, and optimistic
locking may be a better approach. 
Optimistic Locking:
With optimistic locking, a resource is not actually locked when it is first
is accessed by a transaction. Instead, the state of the resource at the time
when it would have been locked with the pessimistic locking approach is saved.
Other transactions are able to concurrently access to the resource and the
possibility of conflicting changes is possible. At commit time, when the 
resource is about to be updated in persistent storage, the state of the resource
is read from storage again and compared to the state that was saved when the 
resource was first accessed in the transaction. If the two states differ, a 
conflicting update was made, and the transaction will be rolled back.
Consider a banking application example
With Pessimistic  Locking, an account is locked as soon as it is accessed in a transaction. Attempts to use the account in other transactions while it is locked will either result in the other process being delayed until the account lock is released, or that the process transaction will be rolled back. The lock exists until the transaction has either been committed or rolled back.
With Optimistic locking the amount of an account is saved when the account is first accessed in a transaction. If the transaction changes the account amount, the amount is read from the store again just before the amount is about to be updated. If the amount has changed since the transaction began, the transaction will fail itself, otherwise the new amount is written to persistent storage.
With Dynamo Db (which is noSql) what are my options ?
Optimistic Locking: This approach is supported in Dynamo Db API. I decided to try out an example for myself:
I created a Pie and added a host of eaters. Each trying to eat a piece of the pie. Since we have several simultaneous consumers, we need to ensure the Pie consumption is managed in a synchronous manner.
The Pie here is an DynamoDb instance:
My Java POJO is
@DynamoDBTable(tableName = "Pie")
public class Pie {
    @DynamoDBHashKey(attributeName = "id")
    private String id;
    private int noOfSlices;
    private String lastEatenBy;
    /**
     * Version field - used by DynamoDbMapper for optimistic version
     */
    @DynamoDBVersionAttribute
    private Long version;
    //setters and getters
}
The Version field is used to ensure Optimistic locking of the Pie when multiple consumers try to eat it. My Consumer class is as below:
static class PieEater implements Callable<Void> {

    private String name;
    private DynamoDBMapper mapper;
    private String pieKey;

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

    @Override
    public Void call() {
        System.out.println(this.name + " Starting up on " + this.pieKey);
        Pie pieKey = new Pie();
        pieKey.setId(this.pieKey);

        while (true) {
            Pie pie = mapper.load(pieKey);

            if (pie != null && pie.getNoOfSlices() > 0) {
                System.out.println(name + " looking to eat a piece of pie. Available Pieces " + pie.getNoOfSlices()
                        + ". Last eaten by " + pie.getLastEatenBy() + " [Version] " + pie.getVersion());
                pie.setNoOfSlices(pie.getNoOfSlices() - 1);
                pie.setLastEatenBy(name);
                try {
                    mapper.save(pie); // processing complete
                } catch (Exception e) {
                    System.err.println("Saving pie for " + name + " failed. " + e.getMessage());
                    //e.printStackTrace();
                }
            } else {
                System.out.println("Pie is over. " + name + " stopping");
                break;
            }
        }
        return null;
    }
}
The class attempts to fetch the Pie and eat a piece of it. This continues until any pie pieces remain.
The Main class that sets it all up together is as below:
private static DynamoDBMapper mapper;

private static void init() {
    AWSCredentials awsCredentials = new BasicAWSCredentials("AccessKey",
            "SecretKey");
    AWSCredentialsProvider awsCredentialsProvider = new AWSStaticCredentialsProvider(awsCredentials);

    AmazonDynamoDB client = AmazonDynamoDBClientBuilder.standard()
            .withRegion("us-east-1")
            .withCredentials(awsCredentialsProvider).build();
    mapper = new DynamoDBMapper(client);
}


public static void main(String[] args) {
    init();
    Pie pie = new Pie();
    pie.setNoOfSlices(10);
    pie.setId("StrawBerryPie");
    mapper.save(pie);
    System.out.println("Pie has been saved");

    //Start Worker Threads
    List<Thread> threads = new ArrayList<>(10);
    for (int i = 0; i < 10; i++) {
        PieEater pieEater = new PieEater("Eater" + i, mapper, pie.getId());
        FutureTask<Void> futureTask = new FutureTask<>(pieEater);
        Thread thread = new Thread(futureTask);
        thread.start();
        threads.add(thread);
    }

    for (Thread thread : threads) {
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
The logs are where it gets interesting:
Pie has been saved
Eater Eater0 ready to start eating
Eater Eater1 ready to start eating
Eater0 Starting up on StrawBerryPie
Eater Eater2 ready to start eating
Eater1 Starting up on StrawBerryPie
Eater2 Starting up on StrawBerryPie
Eater2 looking to eat. Available Pieces 10.Last eaten by null [Version] 1
Eater2 looking to eat. Available Pieces 9.Last eaten by Eater2 [Version] 2
Eater0 looking to eat. Available Pieces 8.Last eaten by Eater2 [Version] 3
Eater1 looking to eat. Available Pieces 8.Last eaten by Eater2 [Version] 3
Eater2 looking to eat. Available Pieces 8.Last eaten by Eater2 [Version] 3
Saving pie for Eater1 failed. The conditional request failed (Service: 
AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException;
 Request ID: D2DHJL8RIN0VG91JMN1PJ23FDNVV4KQNSO5AEMVJF66Q9ASUAAJG; Proxy: null)
Saving pie for Eater2 failed. The conditional request failed (Service: 
AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException;
 Request ID: 70FQ1PMILO5VDH37V019ONDA8RVV4KQNSO5AEMVJF66Q9ASUAAJG; Proxy: null)
Eater0 looking to eat. Available Pieces 7.Last eaten by Eater0 [Version] 4
Eater1 looking to eat. Available Pieces 7.Last eaten by Eater0 [Version] 4
Eater2 looking to eat. Available Pieces 7.Last eaten by Eater0 [Version] 4
Saving pie for Eater1 failed. The conditional request failed (Service: 
AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException;
 Request ID: Q6OJPQVPA9TO8PKGCNFCEDGU6RVV4KQNSO5AEMVJF66Q9ASUAAJG; Proxy: null)
Saving pie for Eater2 failed. The conditional request failed (Service: 
AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException;
 Request ID: 05RMKQ3UIE3I27U34DNPJUNF7BVV4KQNSO5AEMVJF66Q9ASUAAJG; Proxy: null)
Eater0 looking to eat. Available Pieces 6.Last eaten by Eater0 [Version] 5
Eater1 looking to eat. Available Pieces 6.Last eaten by Eater0 [Version] 5
Eater2 looking to eat. Available Pieces 6.Last eaten by Eater0 [Version] 5
Saving pie for Eater1 failed. The conditional request failed (Service: 
AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException;
 Request ID: 9I5RF7P75UHCT8Q836ENQ8UHDFVV4KQNSO5AEMVJF66Q9ASUAAJG; Proxy: null)
Saving pie for Eater2 failed. The conditional request failed (Service: 
AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException;
 Request ID: JQIFHNBA6O5J4V0CTIQ1LBQCJ3VV4KQNSO5AEMVJF66Q9ASUAAJG; Proxy: null)
Eater0 looking to eat. Available Pieces 5.Last eaten by Eater0 [Version] 6
Eater1 looking to eat. Available Pieces 5.Last eaten by Eater0 [Version] 6
Eater2 looking to eat. Available Pieces 5.Last eaten by Eater0 [Version] 6
Saving pie for Eater1 failed. The conditional request failed (Service: 
AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException;
 Request ID: V7KE115NFLH2HJ6DCU2J5JHP33VV4KQNSO5AEMVJF66Q9ASUAAJG; Proxy: null)
Saving pie for Eater2 failed. The conditional request failed (Service: 
AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException;
 Request ID: HA1P2B0SL1GV210AD5ROTCKE07VV4KQNSO5AEMVJF66Q9ASUAAJG; Proxy: null)
Eater0 looking to eat. Available Pieces 4.Last eaten by Eater0 [Version] 7
Eater1 looking to eat. Available Pieces 4.Last eaten by Eater0 [Version] 7
Eater2 looking to eat. Available Pieces 4.Last eaten by Eater0 [Version] 7
Saving pie for Eater1 failed. The conditional request failed (Service: 
AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException;
 Request ID: CP0834PJJ5G5TQ21I9PJV6JMRJVV4KQNSO5AEMVJF66Q9ASUAAJG; Proxy: null)
Saving pie for Eater2 failed. The conditional request failed (Service:
 AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException;
 Request ID: LASU1G7US4B5HVK3D8KBNUKEQ3VV4KQNSO5AEMVJF66Q9ASUAAJG; Proxy: null)
Eater0 looking to eat. Available Pieces 3.Last eaten by Eater0 [Version] 8
Eater1 looking to eat. Available Pieces 3.Last eaten by Eater0 [Version] 8
Eater2 looking to eat. Available Pieces 3.Last eaten by Eater0 [Version] 8
Saving pie for Eater1 failed. The conditional request failed (Service: 
AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException;
 Request ID: 98LIVGVP3MET3C8TRNIT5AQT7FVV4KQNSO5AEMVJF66Q9ASUAAJG; Proxy: null)
Saving pie for Eater2 failed. The conditional request failed (Service: 
AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException;
 Request ID: VT6O08ORKITUJTPFS0A9LC6QN7VV4KQNSO5AEMVJF66Q9ASUAAJG; Proxy: null)
Eater0 looking to eat. Available Pieces 2.Last eaten by Eater0 [Version] 9
Eater1 looking to eat. Available Pieces 2.Last eaten by Eater0 [Version] 9
Eater2 looking to eat. Available Pieces 2.Last eaten by Eater0 [Version] 9
Saving pie for Eater1 failed. The conditional request failed (Service: 
AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException;
 Request ID: TVQDNGK3MTOQJAMIL7MJEUS9DNVV4KQNSO5AEMVJF66Q9ASUAAJG; Proxy: null)
Saving pie for Eater2 failed. The conditional request failed (Service: 
AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException;
 Request ID: 8TABGOACGFN2TFP11GT02VD0L7VV4KQNSO5AEMVJF66Q9ASUAAJG; Proxy: null)
Eater0 looking to eat. Available Pieces 1.Last eaten by Eater0 [Version] 10
Eater1 looking to eat. Available Pieces 1.Last eaten by Eater0 [Version] 10
Eater2 looking to eat. Available Pieces 1.Last eaten by Eater0 [Version] 10
Saving pie for Eater1 failed. The conditional request failed (Service:
 AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException;
 Request ID: M814TCUGGRMV8LCIH37A3C30A3VV4KQNSO5AEMVJF66Q9ASUAAJG; Proxy: null)
Saving pie for Eater2 failed. The conditional request failed (Service: 
AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException;
 Request ID: FF6O9NCI1AQ9A48AL83B4RLFTVVV4KQNSO5AEMVJF66Q9ASUAAJG; Proxy: null)
Pie is over. Eater0 stopping
Pie is over. Eater1 stopping
Pie is over. Eater2 stopping
The Bakery item looks as below:

As seen whenever a Thread attempts to save the Pie Record and its 'read version' does not match what  version exists in Dynamo, the code throws a ConditionalCheckFailedException.
When the save succeeds, the mapper updates the record along with a new versionNumber.
DynamoDb actually provides a graph of the exception count

In the next post, Ill look to use pessimistic locking

No comments:

Post a Comment