Hone logo
Hone
Problems

Implementing Snapshot Isolation in JavaScript

Snapshot isolation is a concurrency control technique that provides a consistent view of data to each transaction, preventing phenomena like dirty reads, non-repeatable reads, and phantom reads. This challenge asks you to simulate snapshot isolation using JavaScript, focusing on the core concepts of versioning and read-only snapshots. Implementing this will help you understand how databases manage concurrent access to data.

Problem Description

You are tasked with creating a simple database system that supports snapshot isolation. The system should allow multiple transactions to run concurrently, each operating on a consistent snapshot of the database's state. The database will be represented as a JavaScript object where keys are data identifiers (e.g., "item1", "item2") and values are the corresponding data.

What needs to be achieved:

  1. Snapshot Creation: A createSnapshot() function should return a copy of the current database state. This snapshot represents a consistent view of the database at a specific point in time.
  2. Transaction Execution: A executeTransaction(snapshot, transactionLogic) function should take a snapshot and a transaction logic function as input. The transactionLogic function should operate on the snapshot and return an object containing updates to be applied to the database after the transaction completes. The transaction logic must not modify the snapshot directly.
  3. Commit: A commitTransaction(database, updates) function should apply the updates returned by the transaction logic to the main database.
  4. Concurrency: The system should allow multiple transactions to run concurrently without interfering with each other's snapshots. Each transaction should see the database as it was when the snapshot was created.

Key Requirements:

  • The snapshot should be a deep copy of the database. Changes to the database after the snapshot is created should not affect the snapshot.
  • Transactions should operate on their own snapshots and should not directly modify the database.
  • The executeTransaction function should ensure that the transaction logic is executed in a way that prevents external modifications to the database during its execution. (Simulate this by preventing any commits while the transaction is running).
  • The commitTransaction function should apply the updates to the main database.

Expected Behavior:

  • createSnapshot(): Returns a new object that is a deep copy of the database.
  • executeTransaction(): Executes the provided transaction logic function within the context of the snapshot, preventing external modifications. Returns an object containing updates.
  • commitTransaction(): Applies the updates to the main database.

Edge Cases to Consider:

  • Empty database: The snapshot should be an empty object.
  • Updates that modify existing keys: The commitTransaction function should correctly update the values associated with existing keys.
  • Updates that add new keys: The commitTransaction function should correctly add new keys to the database.
  • Concurrent transactions: Multiple transactions should be able to run concurrently without interfering with each other's snapshots.

Examples

Example 1:

Input:
database = { item1: 10, item2: 20 }
transactionLogic = (snapshot) => ({ item1: snapshot.item1 + 5 })

Output:
database = { item1: 10, item2: 20 }  (Initially)
snapshot = { item1: 10, item2: 20 } (Created)
updates = { item1: 15 } (Returned by transactionLogic)
database = { item1: 15, item2: 20 } (After commitTransaction)

Explanation: A snapshot is created, the transaction logic increments item1 in the snapshot, and the updated value is committed to the database.

Example 2:

Input:
database = { item1: 10 }
transactionLogic = (snapshot) => ({ item2: 30 })

Output:
database = { item1: 10 } (Initially)
snapshot = { item1: 10 } (Created)
updates = { item2: 30 } (Returned by transactionLogic)
database = { item1: 10, item2: 30 } (After commitTransaction)

Explanation: A snapshot is created, the transaction logic adds a new item (item2) to the snapshot, and the new item is committed to the database.

Example 3: (Concurrent Transactions)

Input:
database = { item1: 10 }
transactionLogic1 = (snapshot) => ({ item1: snapshot.item1 + 5 })
transactionLogic2 = (snapshot) => ({ item1: snapshot.item1 * 2 })

Output:
database = { item1: 10 } (Initially)
snapshot1 = { item1: 10 } (Created for transaction1)
snapshot2 = { item1: 10 } (Created for transaction2)
updates1 = { item1: 15 } (Returned by transactionLogic1)
updates2 = { item1: 20 } (Returned by transactionLogic2)
database = { item1: 20 } (After commitTransaction2 - assuming it commits last)

Explanation: Both transactions operate on the same initial snapshot. The order of commits can affect the final state, but each transaction sees a consistent snapshot.

Constraints

  • The database will be a JavaScript object with string keys and numeric values.
  • The transaction logic function will accept a snapshot object and return an object containing updates.
  • The createSnapshot function must perform a deep copy. Using JSON.parse(JSON.stringify(obj)) is acceptable for this purpose.
  • The solution should be reasonably efficient. Avoid unnecessary iterations or complex data structures.
  • The solution must be implemented in JavaScript.

Notes

  • Focus on the core concepts of snapshot isolation: creating consistent snapshots and preventing external modifications during transaction execution.
  • You don't need to implement a full-fledged database system. The focus is on simulating snapshot isolation.
  • Consider using closures to encapsulate the snapshot and transaction logic.
  • Think about how to prevent external modifications to the database while a transaction is running. A simple approach is to disable commits during transaction execution.
  • Error handling is not required for this challenge.
  • The order of commits is not strictly defined, but it should be deterministic given the same inputs.
Loading editor...
javascript