Two-Phased Commit and eXtended Architecture: The Basics

Two-Phased Commit and eXtended Architecture: The Basics

Understanding Distributed Transactions with 2PC and XA with Example.

Two-phase commit (2PC) and XA (eXtended Architecture) are two important concepts in database transactions and distributed systems. They both provide a way to ensure that transactions involving multiple resources are either completed successfully or rolled back in case of failure, thus maintaining the integrity of the data. In this article, we will explain the two-phase commit protocol and XA in detail and discuss their use cases and limitations.

Introduction

In distributed transaction processing, a commit operation finalizes a transaction and makes it visible to other participants. In an extended two-phase commit (2PC) protocol, the commit action is split into two phases: Prepare and Commit. The first phase is called prepare because the participant prepares to commit by checking some pre-conditions. If those conditions are not satisfied, the participant can not continue in the second phase by committing but has to roll back their work. A failure at this point results in aborting the transaction and starting again from the beginning of the process. The main advantage of a 2PC protocol is that it enables automatic recovery from failures during transactions.

What is a Transaction?

Before we delve into 2PC and XA, it is important to understand what a transaction is. A transaction is a sequence of operations that are performed as a single unit of work. The main goal of a transaction is to ensure that the data remains consistent and reliable, even in the face of failures or errors.

In database systems, a transaction can consist of multiple database operations, such as inserts, updates, and deletes, that are performed on one or more tables. Transactions allow us to ensure that the data remains consistent and correct, even if some of the operations fail. For example, if we are transferring money from one bank account to another, we want to make sure that the money is deducted from the first account and added to the second account, or that no changes are made at all if something goes wrong.

Why Distributed Transaction Processing?

Distributed transaction processing has become an important requirement in many application scenarios. The reasons are simple:

  • first, we want to achieve scalability by increasing the size of the computing clusters to handle larger workloads.

  • Second, we want to achieve availability by ensuring that no single point of failure can bring down the system.

Achieving scalability and availability requires distributed systems with atomic transactions.

What is the Two-Phase Commit Protocol (2PC)?

The two-phase commit protocol is a distributed transaction protocol that ensures that a transaction is either completed successfully or rolled back in case of failure. It is called "two-phase" because it consists of two phases: a prepare phase and a commit phase.

The Prepare Phase

In the prepare phase, the transaction coordinator (also known as the "transaction manager") sends a request to all the participating resources (such as databases or message queues) to prepare for the commit. The resources then perform any necessary checks and updates, and return a response indicating whether they are ready to commit or not. If all the resources are ready to commit, the transaction coordinator moves on to the commit phase.

The Commit Phase

In the commit phase, the transaction coordinator sends a commit request to all the resources. If all the resources respond successfully, the transaction is considered committed and the changes are made permanent. If any of the resources fail to commit, the transaction coordinator sends a rollback request to all the resources and the transaction is considered failed.


The two-phase commit protocol is used to ensure that all the participating resources are in sync and that the changes are made consistently across all the resources. It is a reliable and widely used protocol, but it has some limitations, which we will discuss later.

Distributed Transaction Processing with 2PC

For distributed transaction processing, a two-phase commit protocol is necessary for ensuring that transactions are managed and controlled by more than one participant. Since all participants can't communicate with each other directly, a distributed transaction manager is required for controlling the transaction.

Transaction Manager

The transaction manager is responsible for controlling the transaction and coordinating the communication between the distributed resource managers.It does this by using a two-phase commit protocol

A two-phase commit protocol ensures that at least two participants have to be involved in every transaction:

  • the transaction manager and

  • at least one resource manager.

This means that a two-phase commit protocol requires a network connection between the transaction manager and the resource managers.

What is the eXtended Architecture Protocol?

XA is an extension of the two-phase commit protocol that allows transactions to span multiple resources, such as databases, message queues, and file systems. It is used to coordinate the commit or rollback of a transaction across multiple resources, ensuring that the changes are made consistently and reliably.

In XA, each resource participating in the transaction is represented by an XA resource manager. The XA resource manager is responsible for managing the transactions on the resource and communicating with the transaction manager. The transaction manager is responsible for coordinating the commit or rollback of the transaction across all the participating resources.

The XA protocol defines a set of APIs (Application Programming Interfaces) that the transaction manager and the XA resource managers use to communicate and coordinate the transaction. These APIs include functions for starting, committing, and rolling back a transaction, as well as for checking the status of a transaction.

XA is a powerful tool for managing distributed transactions, but it has some limitations, which we will discuss later.

Use Cases for 2PC and XA

2PC and XA are used in a variety of scenarios where transactions involve multiple resources, such as databases, message queues, and file systems. Some common use cases include:

  1. Financial transactions: 2PC and XA are widely used in the financial industry to ensure the integrity of financial transactions, such as money transfers, stock trades, and credit card payments.

  2. E-commerce: In e-commerce systems, 2PC and XA are used to ensure that orders, payments, and inventory updates are all completed consistently and reliably.

  3. Supply chain management: In supply chain management systems, 2PC and XA are used to ensure that orders, shipments, and inventory updates are all coordinated and consistent across multiple resources.

  4. Healthcare: In healthcare systems, 2PC and XA are used to ensure that patient records, treatments, and billing information are all consistent and accurate.

Limitations of 2PC and XA

While 2PC and XA are powerful tools for managing distributed transactions, they have some limitations:

  1. Performance: 2PC and XA can have a significant impact on performance, as they involve multiple round-trips and communication between the participating resources and the transaction manager. This can make them slower than other transaction protocols.

  2. Complexity: 2PC and XA are complex protocols that require a significant amount of programming and infrastructure to implement.

  3. Single point of failure: The transaction manager is a single point of failure in the 2PC and XA protocols. If the transaction manager fails, the entire transaction will fail.

  4. Limited scalability: 2PC and XA can be challenging to scale, as they involve multiple round-trips and communication between the participating resources and the transaction manager.

2PC With No Rollback

In a 2PC scenario where no rollback occurs, the prepare phase proceeds and all participants agree to commit. Since no participant is executing a rollback at this point, the transaction can be committed. A 2PC with no rollback is an optimistic implementation where the transaction participants proceed with the commit action in the second phase. If, however, some participants were not able to satisfy the conditions, they won’t proceed and will roll back their work. This is called an optimistic approach because the participants proceed with committing their work without necessarily knowing whether their work will be visible to the other participants. The advantage of an optimistic approach is that it can lead to faster throughput in distributed transactions since no participants will be delaying the completion of their work.

2PC With Rollback

The main difference between a 2PC with no rollback and a 2PC with a rollback is that a 2PC with a rollback can proceed only if all participants agree to commit the transaction. If any participant fails to meet the pre-conditions and is unable to continue with the commit in the second phase, these participants have to roll back their work. An advantage of a 2PC with rollback is that it is more conservative and is therefore likely to lead to slower throughput because many distributed transactions may take longer to complete. For example, if a transaction cannot proceed because a resource manager is down, the participants will not be able to commit, and they will have to roll back their work.

Example: XA with NodeJS, TypeScript & Express

Here is an example of using XA with Node.js, TypeScript, and Express:

NOTE: These code examples are for illustration purposes only and do not represent a complete or real-world implementation of a distributed transaction management system. They are meant to provide a general understanding of the concepts involved and should not be used as is in a production environment.

  • Firstly, We're going to create an XA class to manage distributed transactions. This class will help us create and manage transactions that involve multiple resources. We'll be using MySQL client, to persist and coordinate the state of different resources and ensure the transaction is either committed or rolled back as needed.
import { Client } from 'pg';

class XA {
  private client: Client;
  private transaction: Transaction | null = null;

  constructor(client: Client) {
    this.client = client;
  }

  async beginTransaction(): Promise<Transaction> {
    // Begin a new transaction
    this.transaction = new Transaction(this.client);
    return this.transaction;
  }
}
  • Then we're going to create a Transaction class. The Transaction class is an important part of a distributed transaction management system because it helps to coordinate the actions of multiple resources involved in a transaction. It is responsible for managing the lifecycle of a distributed transaction, including the prepare, commit, and rollback phases.
class Transaction {
  private client: Client;
  private transactionId: string;
  private resourceManagers: ResourceManager[] = [];

  constructor(client: Client) {
    this.client = client;
    this.transactionId = Math.random().toString(36).substr(2, 10);
  }

  async addResourceManager(url: string, resourceName: string): Promise<void> {
    // Add a new resource manager to the list
    this.resourceManagers.push(new ResourceManager(url, resourceName));
  }

  async prepare(data: any): Promise<void> {
    // Send a prepare request to all the resource managers, including the transaction ID and necessary data
    const results = await Promise.all(this.resourceManagers.map(rm => rm.prepare(this.transactionId, data)));

    // Update the transaction status in the database
    await this.client.query('INSERT INTO transactions (id, status) VALUES ($1, $2)', [this.transactionId, 'prepared']);

    // If any of the resource managers failed to prepare, rollback the transaction
    if (results.some(result => !result)) {
      await this.rollback();
      throw new Error('Transaction failed to prepare');
    }
  }

  async commit(): Promise<void> {
    // Send a commit request to all the resource managers, including the transaction ID
    await Promise.all(this.resourceManagers.map(rm => rm.commit(this.transactionId)));

    // Update the transaction status in the database
    await this.client.query('UPDATE transactions SET status = $1 WHERE id = $2', ['committed', this.transactionId]);
  }

  async rollback(): Promise<void> {
    // Send a rollback request to all the resource managers, including the transaction ID
    await Promise.all(this.resourceManagers.map(rm => rm.rollback(this.transactionId)));

// Update the transaction status in the database
await this.client.query('UPDATE transactions SET status = $1 WHERE id = $2', ['reverted', this.transactionId]);
  }
}
  • Now, to the resource manager class which is another important part of the distributed transaction management system. The ResourceManager class is typically responsible for receiving requests from the Transaction class to prepare, commit, or rollback a transaction, and for interacting with the shared resource to perform these actions. It may also be responsible for other tasks related to managing the shared resource, such as creating and releasing locks on the resource, or handling errors that occur during the transaction.
class ResourceManager {
  private url: string;

  constructor(url: string) {
    this.url = url;
  }

  async prepare(transactionId: string, data: any): Promise<boolean> {
    try {
      // Send a prepare request to the resource manager, including the transaction ID and necessary data
      await axios.post(`${this.url}/prepare`, { transactionId, data });
      return true;
    } catch (error) {
      return false;
    }
  }

  async commit(transactionId: string): Promise<void> {
    // Send a commit request to the resource manager, including the transaction ID
    await axios.post(`${this.url}/commit`, { transactionId });
  }

  async rollback(transactionId: string): Promise<void> {
    // Send a rollback request to the resource manager, including the transaction ID
    await axios.post(`${this.url}/rollback`, { transactionId });
  }
}

Here is an example of how to use the updated XA and Transaction classes to manage a distributed transaction in a Node.js application using TypeScript and the Express framework:

import express, { Router } from 'express';
import { Client } from 'pg';
import { XA, Transaction } from './xa';

const app = express();
const router = Router();
const client = new Client();
const xa = new XA(client);

router.post('/transfer', async (req, res) => {
  try {
    // Begin a new transaction
    const transaction = await xa.beginTransaction();

    // Add the necessary resource managers to the transaction
    await transaction.addResourceManager('http://debit.service/api');
    await transaction.addResourceManager('http://credit.service/api');

    // Prepare the transaction, including the necessary data
    const data = {
      fromAccount: req.body.fromAccount,
      toAccount: req.body.toAccount,
      amount: req.body.amount,
    };
    await transaction.prepare(data);

    // Commit the transaction
    await transaction.commit();

    res.sendStatus(200);
  } catch (error) {
    res.sendStatus(500);
  }
});

app.use(router);

app.listen(3000, () => {
  console.log('Listening on port 3000');
});

In this example, the prepare method of the Transaction class will send a POST request to the /prepare endpoint of each of the resource managers, passing along the transactionId and the necessary data. The resource managers will then use this data to prepare for the transaction.

The commit method of the Transaction class will then send a POST request to the /commit endpoint of each of the resource managers, passing along the transactionId. The resource managers will use this request to commit the actions they prepared for in the previous step.

If any errors occur during the transaction, the rollback method of the Transaction class will be called, which will send a POST request to the /rollback endpoint of each of the resource managers, passing along the transactionId. The resource managers will use this request to roll back any actions they took during the prepare phase.

This is a basic example of how to use the XA and Transaction classes to manage a distributed transaction in a Node.js application. You may need to modify these classes and the example code to fit the specific needs of your application.


Warning: These code examples are for illustration purposes only and do not represent a complete or real-world implementation of a distributed transaction management system. They are meant to provide a general understanding of the concepts involved and should not be used as is in a production environment.


Conclusion

In conclusion, 2PC and XA are important concepts in database transactions and distributed systems. They provide a way to ensure that transactions involving multiple resources are either completed successfully or rolled back in case of failure, thus maintaining the integrity of the data. However, they have some limitations, including performance, complexity, and scalability, which should be taken into consideration when deciding whether to use them in a particular application.