question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Optimistic Concurrency Control

See original GitHub issue

Problem statement

The current API doesn’t allow to implement application-level Optimistic Concurrency Control, which is a pattern commonly implemented by applications with high concurrency needs, to avoid creating a bottleneck on the database, while obtaining certain guarantees about the integrity of the modified data.

Use cases

Imagine data that is bound to a given user profile, which only the said user can modify, or their admin. There are few chances that the same user would try to update the same data at the same time, which makes the use of pessimistic locking or long-running transaction solutions suboptimal. Implementing OCC, in this case, will be likely more performant.

Context about Optimistic Concurrency Control

OCC assumes that multiple transactions can frequently complete without interfering with each other, hence proposes to not block concurrent transactions and require the application to rollback in case of a detected conflict:

This SO answer gives a great summary

Optimistic Concurrency Control implies reading a record, taking note of a version number (other methods to do this involve dates, timestamps or checksums/hashes), and checking that the version hasn’t changed before the record gets written back. When writing the record back, one filters the update on the version to make sure it’s atomic. (i.e. hasn’t been updated between when checking the version and writing the record to the disk) and update the version in one hit.

If the record is dirty (i.e. has a different version to the user’s) the transaction should abort and require the process to be restarted.

Pessimistic Concurrency Control

PCC is assuming that data will often be accessed by multiple processes at the same time and proposes to lock data to sequence how these processes access it.

In practice, database vendors like Postgres or MySQL implement a concept of table- or row-level locking, giving control over what can be read and written (depending on the transaction’s isolation level):

  • SELECT ... FOR SHARE which leaves the data available to read, but not to update outside the transaction.
  • SELECT ... FOR UPDATE which prevents the selected data from being read or updated outside of the transaction.
  • the NOWAIT variant to fail if a row is locked
  • the SKIP LOCKED variant to skip locked rows

This approach comes with risk of deadlocks, which can block the application, so should be considered carefully.

Scope

While we should consider an API design allowing us to evolve towards supporting and controlling both policies, the first iteration should focus on supporting Optimistic Concurrency Control.

Pessimistic Concurrency Control requires a deeper analysis of combinations of transaction isolation levels along with the locking strategy for each record, which makes it significantly more complex.

Solution ideas

The API should offer a way to specify, on update and in $transaction calls, whether to check on a certain field used to implement Optimistic Concurrency Control (version, timestamp…).

Proposal 1

A single attribute defining the locking policy, so that people can implement Optimistic Concurrency Control:

lockingPolicy: {
  optimistic: 'fieldName'
} 

This design would possibly allow defining a pessimistic policy in the future:

lockingPolicy: {
  pessimistic: 'ROW' // or 'TABLE', depends on database vendor
}

Applied example

Imagine users detaining a number of points in a competition. And that points can be updated based on how each user behaves in the application.

model User {
  team        Team
  numPoints   Int
  updatedAt   DateTime @updatedAt
}

The exchange of points needs to be done in a consistent fashion across users to ensure a fair game.

Pseudo code:

// Optimistic
const scored = 2
prisma.$transaction([
	prisma.user.update({
		data: { 
		  numPoints: {
		    decrement: scored
		  } 
		}, 
		where: {
		  id: 'loser-user-id'
		},
		lockingPolicy: {
      optimistic: 'updatedAt'
    } 
  ),
	prisma.user.update({
		data: { 
		  numPoints: {
		    increment: scored
		  } 
		}, 
		where: {
		  id: 'winner-user-id'
		},
		lockingPolicy: {
      optimistic: 'updatedAt'
    } 
  )
])

// Pessimistic (used to proof the API design, not meant for implementation)
const scored = 2
prisma.$transaction([
	prisma.user.update({
		data: { 
		  numPoints: {
		    decrement: scored
		  } 
		}, 
		where: {
		  id: 'loser-user-id'
		},
		lockingPolicy: {
      pessimistic: 'ROW'
    } 
  ),
	prisma.user.update({
		data: { 
		  numPoints: {
		    increment: scored
		  } 
		}, 
		where: {
		  id: 'winner-user-id'
		},
		pessimisticLock: {
      on: 'ROW'
    } 
  )
], {
  isolation: 'REPEATABLE READ'
})

Proposition 2

// Optimistic
const scored = 2
const user = prisma.user.findOne({ where: { id: 'loser-user-id' } })
const controlPoints = user.points

try {
	// Throws if `numPoints` has changed between the fetching and the update
	prisma.user.update({
		data: { 
		  numPoints: {
		    decrement: scored
		  } 
		},
		where: {
		  id: 'loser-user-id'
		},
		if: {
	      numPoints: controlPoints
	    }
	  )
} catch (e) {
  // Consider redoing, or error out.
}

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:50
  • Comments:16 (5 by maintainers)

github_iconTop GitHub Comments

15reactions
hugbubbycommented, May 9, 2021

Anybody working on this?

6reactions
vimutti77commented, Jun 22, 2021

My current approach is to use prisma.$executeRaw

const tableName = 'User'
const userId = 1
const version = 1

prisma.$executeRaw(`
  do $$
  begin
    update "${tableName}" set "version" = "version" + 1 where "id" = '${userId}' and "version" = '${version}';
    if not found then raise exception 'Data version mismatch';
    end if;
  end $$;
`)
Read more comments on GitHub >

github_iconTop Results From Across the Web

Optimistic concurrency control
Optimistic concurrency control (OCC), also known as optimistic locking, is a concurrency control method applied to transactional systems such as relational ...
Read more >
Optimistic Concurrency - ADO.NET
In an optimistic concurrency model, a violation is considered to have occurred if, after a user receives a value from the database, another...
Read more >
What is an optimistic concurrency control in DBMS?
What is an optimistic concurrency control in DBMS? - All data items are updated at the end of the transaction, at the end,...
Read more >
PESSIMISTIC vs. OPTIMISTIC concurrency control
Optimistic concurrency control (or optimistic locking) assumes that although conflicts are possible, they will be very rare. Instead of locking every record ...
Read more >
Optimistic concurrency control | Elasticsearch Guide [8.5]
Optimistic concurrency control edit ... Elasticsearch is distributed. When documents are created, updated, or deleted, the new version of the document has to...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found