Offline Concurrency Patterns 

time

Offline Concurrency Patterns 

ARKADIUSZ KONDAS

Lead Software Architect
@ Proget Sp. z o.o.
Poland

Zend Certified Engineer

Code Craftsman

Blogger

Ultra Runner

@ ArkadiuszKondas

arkadiuszkondas.com

Zend Certified Architect

Agenda

  1. The concurrency
     
  2. The patterns (with code examples)

Concurrency

Concurrency

  • Thread level concurrency
    • Memory access (race conditions, memory consistency)
  • Application level concurrency
    • Database transactions
  • Offline concurrency
    • Business transactions

Problems

  • lost updates 
  • inconsistent read
  • failure of correctness
  • liveness
    • how much concurrent activity system can handle

Execution Contexts

Thread

Process

Request

Session

Solutions

  • Immutability
    • data that cannot be modified

everyone wants to modify everything

Solutions

  • Isolation
    • any piece of data can only be accessed by one thread or process

everyone wants to do everything at once

Solutions

  • mutable data than cannot be isolated
    • Optimistic Locking
    • Pessimistic Locking

Example:

Two agents who use the flight ticket system want to work on one flight at the same time.

Optimistic Concurrency Control

Martin

David

fetch

fetch

David

save

Martin

error

Optimistic Concurrency Control

  • Conflict detection
  • Lock during commit (on write)
  • Supports concurrency
  • Low frequency of conflicts
  • Used for not critical features when consequences are low

Pessimistic Concurrency Control

Martin

fetch

David

error

Martin

save

Pessimistic Concurrency Control

  • Conflict prevention
  • Lock during entire transaction
  • Does not suport concurrency
  • Used for critical features when consequences are high

But what about the reading?

Inconsistent Reads

Martin

count

David

3

4

update

2

count

Martin

 SUM = 6 

WRONG
 
SUM = 5 

Preventing Inconsistent Reads

  • Optimistic approach
    • Versioning
  • Pessimistic approach
    • Read  ➡️ shared lock
    • Write ➡️ exclusive lock
  • Temporal Reads

Deadlocks

Martin

Account A

lock

Account B

David

lock

wait

wait

Deadlocks

  • Choose a victim
    • someone must blame
  • Lock with TTL
    • double check (failure)
  • Techniques:
    • Force to acquire all the necessary locks at the beginning
    • Enforce a coherent strategy to grant lock (ex. alphabetical order of the files)

Transactions

  • Bounded sequence of work
  • ACID
    • Atomicity
      • everything or nothing (prevent dirty writes)
    • Consistency
      • before and after (constraints)
    • Isolation
      • forever alone (level ⚠️) - SQL-92
    • Durability
      • must survive a crash of any sort (permanent)

Durability

Tables

Tables

mmap

fsync

Storage Engine

CRUD

Undo log

append

Redo log

append

memory

storage

Isolation level

  • Serializable
    • full isolation
    • execute transactions sequentially
    • loss liveness and performance

BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Isolation level

  • Repeatable reads
    • phantoms are allowed
    • applies usually for inserts

BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;

Isolation level

  • Read committed
    • unrepeatable reads allowed
    • statement can only see rows committed before it began

BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;

Isolation level

  • Read uncommitted
    • dirty reads allowed (transactions)
Isolation level Dirty reads? Unrepeatable reads? Phantom reads?
Read uncommitted ✔️ ✔️ ✔️
Read committed ✔️ ✔️
Repeatable reads
✔️
Serializable

Transactions

Request

Session

Business transaction

System Transaction

Request

Request

Time

Offline concurrency

business transactions divided into system transactions

How to provide ACID between system calls?

Patterns

https://github.com/akondas/flighthub

Optimistic Offline Lock

Concurrency Control

Optimistic Offline Lock

Martin's session

Application

David's session

load flight 123

return flight 123

load flight 123

return flight 123

edit

update flight 123

update flight 123

wrong version

system transaction

business 

transaction

Optimistic Offline Lock

UPDATE flight
SET blocked_seat = '{...}'
WHERE id = '22c6e025-d62e-4561-920e-999d7ce83ad8'
AND version = 2

false positive

what about columns that not overlap?

Write-based schema mapping

Versionless Optimistic Locking

import org.hibernate.annotations.OptimisticLocking;
import org.hibernate.annotations.OptimisticLockType;

@Entity
@OptimisticLocking(type = OptimisticLockType.DIRTY)
@DynamicUpdate
@Getter
@Setter
public class Car {
    
    @Id
    private Integer id;

    private String model;

    private String brand;
}
UPDATE CAR 
SET MODEL = ?
WHERE ID = ? AND MODEL = ?

Optimistic Offline Lock

  • Detects conflict
     
  • Use when chance of conflict is low and resolution is easy

Versioning on UPDATE and DELETE does not prevent inconsistent reads

Pessimistic Offline Lock

Pessimistic Offline Lock

Martin's session

Application

David's session

load flight 123

return flight 123

load flight 123

error: flight locked

update flight 123

Pessimistic Offline Lock

  • Prevent conflict
  • Use when chance of conflict is high and resolution is hard

How to build?

  • know what type of locks you need
  • build a lock manager
  • define procedures for a business transaction to use locks

Lock manager

  • Implementation
    • table that maps locks to owners conflict
       
  • Contract
    • what to lock
    • when to lock
    • when release
    • what to do when lock cannot be acquired

Lock manager

<?php

declare(strict_types=1);

namespace FlightHub\Application;

interface LockManager
{
    public function acquireLock(Uuid $lockable, Uuid $owner): void;

    public function releaseLock(Uuid $lockable, Uuid $owner): void;

    public function releaseAllLocks(Uuid $owner): void;
}

Lock manager

<?php

final class SharedReadLockManager implements LockManager
{
    public function acquireLock(Uuid $lockable, Uuid $owner): void
    {
        if($this->hasLock($lockable, $owner)) {
            return;
        }

        $stm = $this->connection->prepare(
            'INSERT INTO read_locks WHERE lockable = :lockable AND owner = :owner'
        );

        try {
            $stm->execute([':lockable' => $lockable, ':owner' => $owner]);
        } catch (\PDOException $exception) {
            throw new ConcurrencyException(sprintf(
                'Can\'t get a lock for %s with owner %s',
                $lockable,
                $owner
            ));
        }
    }
}

Lock manager

<?php

final class SharedReadLockManager implements LockManager
{
    public function releaseLock(Uuid $lockable, Uuid $owner): void
    {
        $stm = $this->connection->prepare(
            'DELETE FROM read_locks 
                WHERE lockable = :lockable AND owner = :owner'
        );

        try {
            $stm->execute([':lockable' => $lockable, ':owner' => $owner]);
        } catch (\PDOException $exception) {
            throw new ConcurrencyException(sprintf(
                'Can\'t release lock for %s with owner %s',
                $lockable,
                $owner
            ));
        }
    }
}

Lock manager

<?php

final class SharedReadLockManager implements LockManager
{
    public function releaseAllLocks(Uuid $owner): void
    {
        $stm = $this->connection->prepare(
            'DELETE FROM read_locks WHERE owner = :owner'
        );

        try {
            $stm->execute([':owner' => $owner]);
        } catch (\PDOException $exception) {
            throw new ConcurrencyException(sprintf(
                'Can\'t release lock for owner %s',
                $owner
            ));
        }
    }
}

Coarse-Grained Lock

Lock a set of related objects with a single lock

Coarse-Grained Lock

Customer

Address

Version

Lock

1

*

*

1

1

1

1

*

Coarse-Grained Lock

Customer
(aggregate)

Address

Lock

1

*

1

1

boundary

Implicit Lock

Implicit Lock

Business transaction

Application

Lock manager

load flight

acquire lock

success

return flight

Command/Request Lock

Command/Request Lock

Business transaction

Handler

Lock manager

cancel reservation

acquire lock

success

cancel reservation

acquire lock

wait or fail?

Command/Request Lock

public function handle(CancelReservation $command): void
{
    $this->lockManager->lock($command->reservationId(), 60);

    try {
        $flight = $this->flights
            ->getByReservationId($command->reservationId());
        $flight->cancelReservation($command->reservationId());

        $this->paymentService->returnFunds($command->reservationId());

    } catch (\Throwable $exception) {
        $this->lockManager->unlock($command->reservationId());

        throw $exception;
    }

    $this->lockManager->unlock($command->reservationId());
}

Command/Request Lock

    public function lock(string $lockId, int $time) : bool
    {
        $res = $this->client->setnx($this->lockId($lockId), true);

        if (!$res) {
            return false;
        }

        $this->client->expire($this->lockId($lockId), $time);

        return true;
    }

https://redis.io/commands/setnx

https://redis.io/topics/distlock - redlock

Summary

Summary

  • Concurrency is hard
  • Locking always has a cost
  • Avoid whenever you can
  • Avoid deadlock using timeout
  • Concurrency is not parallelism
  • Start with patterns then explore more

Sources

  • Chapters:
    • 5 - Concurrency
    • 16 - Offline Concurrency Patterns

Q&A

Thanks for listening

@ ArkadiuszKondas

https://slides.com/arkadiuszkondas

https://github.com/akondas/flighthub

Offline Concurrency Patterns

By Arkadiusz Kondas

Offline Concurrency Patterns

Imaging the situation where you have an flight ticket booking system where you can edit available seats. Then think what if two people are are editing the same flight. Both introduce changes simultaneously but which ones will be saved? What if one saves over the other data? These are concurrency issues. I will present a few patterns that represent the technique of controlling concurrent work in situations where it is necessary to apply more than one system transaction.

  • 2,098