A deadlock occurs when two or more processes are stuck indefinitely, each waiting for a resource that the other process holds.

Let’s see a deadlock in action within a simplified distributed system. Imagine two services, ServiceA and ServiceB, both needing to acquire locks on two resources, ResourceX and ResourceY.

Here’s a potential scenario:

  1. ServiceA acquires a lock on ResourceX.
  2. ServiceB acquires a lock on ResourceY.
  3. ServiceA now attempts to acquire a lock on ResourceY, but ServiceB already holds it. ServiceA waits.
  4. ServiceB now attempts to acquire a lock on ResourceX, but ServiceA already holds it. ServiceB waits.

Both services are now waiting for each other indefinitely. Neither can proceed. This is a classic deadlock. In a real distributed system, this could involve database transactions, distributed locks managed by ZooKeeper or etcd, or even network requests waiting for responses.

To detect and break deadlocks, we need to understand the "wait-for" graph. Each node in this graph represents a process (or transaction, or service instance), and a directed edge from A to B means A is waiting for a resource held by B. A deadlock exists if and only if there is a cycle in this graph.

Detecting Deadlocks

In a distributed system, detecting cycles in a global wait-for graph is challenging because there’s no single point of control or a universally shared view of all locks and dependencies. Common detection strategies include:

  1. Distributed Deadlock Detection Algorithms: These algorithms typically involve a coordinator process or a distributed graph traversal.

    • Path-Pushing (e.g., Chandy-Misra-Haas): When a process P waits for a process Q, P sends a probe message to Q. This probe message carries a path of processes it has traversed. If Q receives a probe message that already contains Q, a cycle is detected.
      • Diagnosis: Monitor probe message traffic. If a probe message returns to its origin or a process already in its path, a cycle is found.
      • Fix: The process that initiated the probe (or a designated victim) aborts its transaction. This breaks the cycle by releasing its held resources.
    • Edge-Chasing: Processes maintain lists of processes they are waiting for and processes that are waiting for them. Periodically, or upon certain events, a "detector" process initiates a traversal to find cycles.
      • Diagnosis: Look for specific "cycle detection" messages or states within the distributed system’s logging or monitoring.
      • Fix: A chosen process within the cycle is terminated or rolled back.
  2. Timeouts: While not a true deadlock detection mechanism, timeouts are a practical, albeit imperfect, way to resolve situations that might be deadlocks. If a process has been waiting for a resource for an unusually long time, it’s assumed to be part of a deadlock (or a very slow operation) and is aborted.

    • Diagnosis: Observe processes or transactions that exceed their configured timeout thresholds for acquiring locks or receiving responses. In Kubernetes, for example, you might see Pods stuck in a ContainerCreating state if a dependent resource (like a PVC) is stuck, or Services with no endpoints if the backing Pods are unhealthy. For database deadlocks, you’d see specific error messages like "SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction" in application logs.
    • Fix: For database deadlocks, the database typically picks a "victim" transaction to roll back. Application-level timeouts require your application code to detect the timeout error and retry the operation, potentially with backoff. For Kubernetes, you might need to manually intervene by deleting the stuck Pod or Service to allow the controller to recreate it.
      • Example (Application Retry Logic):
        import time
        import random
        
        MAX_RETRIES = 5
        RETRY_DELAY_SECONDS = 2
        
        for attempt in range(MAX_RETRIES):
            try:
                # Attempt to acquire lock or perform operation
                acquire_lock(resource_id)
                print("Lock acquired successfully.")
                break # Success! Exit loop
            except DeadlockError: # Assume this exception is raised on deadlock
                print(f"Deadlock detected. Retrying in {RETRY_DELAY_SECONDS} seconds...")
                time.sleep(RETRY_DELAY_SECONDS + random.uniform(0, 1)) # Add jitter
            except Exception as e:
                print(f"An unexpected error occurred: {e}")
                # Handle other errors or re-raise
                raise
        else: # This else block executes if the loop completes without a 'break'
            print("Max retries reached. Operation failed.")
            # Handle final failure
        
        This code snippet demonstrates a common pattern: attempting an operation, catching a specific DeadlockError (or a more general timeout/serialization failure), and retrying after a delay. The random.uniform(0, 1) adds jitter to the delay, which is crucial in distributed systems to prevent multiple clients from retrying simultaneously and causing a thundering herd.
  3. Centralized Lock Manager/Database: If your distributed system uses a central service to manage all locks (e.g., a dedicated distributed lock manager or a database with strong transaction isolation), that central component can often detect cycles directly.

    • Diagnosis: Query the lock manager’s status or logs. Many databases provide tools to view active transactions, locks held, and locks waited for. For PostgreSQL, you can use pg_locks and pg_stat_activity. For MySQL, SHOW ENGINE INNODB STATUS provides deadlock information.
      • PostgreSQL Example Diagnosis:
        SELECT
            a.pid AS waiting_pid,
            a.usename AS waiting_user,
            a.query AS waiting_query,
            b.pid AS holding_pid,
            b.usename AS holding_user,
            b.query AS holding_query,
            blocked_locks.locktype,
            blocked_locks.mode
        FROM
            pg_stat_activity a
        JOIN
            pg_locks ON a.pid = pg_locks.pid
        JOIN
            pg_locks AS blocked_locks ON pg_locks.locktype = blocked_locks.locktype
            AND pg_locks.database IS NOT DISTINCT FROM blocked_locks.database
            AND pg_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
            AND pg_locks.page IS NOT DISTINCT FROM blocked_locks.page
            AND pg_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
            AND pg_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
            AND pg_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
            AND pg_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
            AND pg_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
            AND pg_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
            AND pg_locks.pid != blocked_locks.pid
        JOIN
            pg_stat_activity b ON blocked_locks.pid = b.pid
        WHERE
            NOT a.waiting AND pg_locks.granted IS FALSE;
        
        This query attempts to identify processes waiting for locks held by others. A true cycle requires more sophisticated graph traversal, but this shows the building blocks.
    • Fix: The database or lock manager typically automatically aborts one of the transactions involved in the deadlock. If you’re using a custom lock manager, you’ll need to implement this logic.

Breaking Deadlocks

Once detected, a deadlock must be broken to allow the system to proceed. This is usually achieved by aborting one or more processes involved in the cycle. The choice of which process to abort (the "victim") is critical and often based on factors like:

  • Transaction age: Aborting the oldest transaction.
  • Number of locks held: Aborting the process holding the fewest locks.
  • Resource usage: Aborting the process that has done the least work.
  • Priority: Aborting a lower-priority process.

The victim process is rolled back, releasing its locks. The other processes can then proceed.

The next issue you’ll likely encounter after resolving deadlocks is dealing with livelocks, where processes repeatedly attempt to resolve a conflict but end up in a repeating sequence of actions that prevent progress.

Want structured learning?

Take the full Distributed Systems course →