Optimistic Locking

Jan 3, 2022 optimistic locking mongo

Purpose

The purpose of this document is to learn about optimistic locking and its use cases.

Use Case

Suppose there are multiple threads that are trying to increment a count field in a table.

How would you do it?

You could try to increment the count by

  1. reading the current count from the table.
  2. increment it.
  3. save it.

The problem is these 3 steps are not atomic. Example:

Time | Thread 1         | Thread 2
T1   | reads 50         | reads 50
T2   | increments to 51 | increments to 51
T3   | save 51          |  
T4   |                  | save 51

A count is missed here. We need some kind of locking or transactions.

Iteration 1: Optimistic Locking

Is a way to write to the DB only if the record hasn't changed since the previous read.

MongoDB does it with a @version field.

Updates to the table succeed only if the “read” version is the same when saving it back, otherwise, an OptimisticLockingFailureException is thrown.

Here is an example:

ExecutorService executorService = new ThreadPoolExecutor(
    5, 5, 5L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000), 
    new ThreadPoolExecutor.DiscardOldestPolicy()
);

List<Callable<Integer>> callableTasks = new ArrayList<>();
        
Callable<Integer> potentiallyFailingJob = () -> {
        TimeUnit.MILLISECONDS.sleep(300);
        Table table = tableRepository.findByRecordType(Table.RecordType.REQUEST_COUNT.name());
        Integer count = table.getCount();
        count += 1;
        table.setCount(count);
        return this.tableRepository.save(table).getCount();
    };

for (int i = 0; i < 10; i++) {
    callableTasks.add(potentiallyFailingJob);
}

List<Future<Integer>> futures = executorService.invokeAll(callableTasks);
for (Future<Integer> f : futures) {
    try {
        f.get();
    } catch (InterruptedException e) {
        System.out.println(System.currentTimeMillis() + " : Interrupted Exception catch.");
    } catch (ExecutionException e) {
        System.out.println(System.currentTimeMillis() + " : Execution Exception catch.");
    }
}
executorService.shutdown();

Table Class looks like

public class Table {
    public enum RecordType {
        REQUEST_COUNT;
    }
    @Id
    private String id;
    private RecordType recordType;
    @Version
    private Long version;
    private Integer count;
    public Table() {}
    // add getters and setters mate.
}

This works but OptimisticLockingFailureException will be thrown when multiple threads try to update the table after the version has changed.

Iteration 2: Optimistic Locking w/ retry

boolean runWithRetries(int maxRetries, Callable<Integer> t, ExecutorService executorService) {
    int count = 0;
    while (count < maxRetries) {
        try {
            System.out.println("Try: " + count);
            Integer result = executorService.submit(t).get();
            System.out.println("Try: " + count + ", result: " + result);
            return true;
        } catch (RejectedExecutionException | InterruptedException | ExecutionException e) {
            if (++count >= maxRetries) {
                return false;
            }
        }
    }
    return false;
}

and add
System.out.println("Was retry successful ? " + (runWithRetries(5, potentiallyFailingJob, executorService) ? "it was" : "no, it wasn't"));
when ExecutionException is caught.

Great. This will retry 5 times before giving up.

Conclusion

We have learned what is optimistic locking and how to use it.

Hope you enjoyed this newsletter.