# Global Transaction (Seata Server)

Transaction management is a significant challenge in a microservice architecture. 

Previously, in a monolithic architecture, every call was local within one virtual machine. Now, calls between different microservices become remote, which sometimes might get lost if the network is unstable. This raises the question of how to ensure transaction consistency.

To address this, we have a back-end service called Seata Server. With proper annotations in the source code, it will help to record unfinished transactions. The whole process is known as "TCC" (Try/Confirm/Cancel). You can find relevant information about TCC on the Internet. 

Since it's related to code development, if you encounter any issues, please reach out to your developer or platform team for assistance.

![Overview of a global transaction](./image/global_transaction/seata_tcc-1.png)



## Basic Mechanism

### Start Distributed Transaction

After starting a Spring transaction, call the following method.
```java
globalTransactionService.beginGlobalTransaction("transactionName");
```
When the Spring transaction is committed, the global transaction will also be automatically committed.

After initiating the distributed transaction, all business operations, including initiating global transactions, should provide compensation capabilities (AT/TCC/reliable messages). Otherwise, the corresponding data cannot be rolled back after the global transaction is rolled back.

### AT Transaction Compensation Mode

This mode is applicable to REST API calls of most core systems. We recommend its usage. Features are as follows:
- Best performance
- Simple compensation implementation
- Synchronous call
- No impact on procedure flow

The compensation implementation of AT mode relies on the reverse operation at the persistence layer:
- Data granularity is an independent key business object, such as policy, endorsement, underwriting, and charge/payment interfaces.

- Business objects support Create and Update operations only, excluding Delete operations. Delete operations must be converted to Update operations by logical deletion.

- Create operations are compensated by Delete operations.

- Update operations compensate only for key properties specified by the business side, not all properties.

- Multiple Create/Update operations on the same business object will be merged.

In the AT mode, the service provider API needs to record relevant rollback information during execution. Upon global transaction rollback, the framework automatically invokes the corresponding rollback method. The service caller simply needs to initiate the global transaction without regard to the server's compensation implementation.

The framework will automatically add a global transaction-level lock to all compensable objects. Until the current global transaction completes, other global transactions cannot update these objects. If exceptions occur during the rollback process, it's not allowed to update the objects until problems are resolved.

#### Use AT Model with simple config
The simplest way to implement the AT model is use the configable AT feature.
* Implement the ConfigurableUndoSupport interface at JPA entity bean, and implement the getUndoFields method to provide the list of key columns need to be rollback if transaction failed.
* There is JPA intecepter capture the changed field value & it's orignial value, generate the UndoData, and save to UndoLog table.
* If the entity is extended in project implementation, and want to add more fields for undo support, the developer can configure them in data table: TccAdditionalUndoFields

**Example:** 

```
@Entity
@Table(name = "T_PA_ENDO_ENDORSEMENT")
public class Endorsement extends DomainModelImplWithDynamicModel
        implements ConfigurableUndoSupport {
            @Override
    public List<String> getUndoFields() {
        return Arrays.asList("endoStatus", "afterRevisionId", "policyDiff");
    }
    ...
}        
```


#### Use AT Model with advanced coding

**Example 1: Create compensation**

```java
//For data creation, you need to use EntityName and PrimaryKey in BaseUndoData
public class MyEndoCreateUndoData extends BaseUndoData {
    @Override
    @JsonIgnore
    public String getUndoActionSpringBeanName() {
        return MyEndoCreateUndoAction.BEAN_NAME;
    }
    @Override
    protected String getEntityName() {return "Endorsement";}
    @Override
    public int getActionType() {return BaseUndoData.ACTION_TYPE__CREATE;}
}
@Component(MyEndoCreateUndoAction.BEAN_NAME)
public class MyEndoCreateUndoAction extends BaseUndoAction {
    public final static String BEAN_NAME="endorsementCreateUndoAction";
    @PersistenceContext(unitName = "JPAUnit")
    private EntityManager entityManager;

    @Override
    public void undo(BaseUndoData subUndoData) {
        Endorsement endo=entityManager.find(Endorsement.class,
((MyEndoCreateUndoData) subUndoData).getPrimaryKey());
        entityManager.remove(endo);//compensation for creation by deletion
    }
}
```
**Example 2: Update compensation**
```java
//For data update operation, it is necessary to record the key field information. When you roll back, these field values will be restored​
public class MyEndoUpdateUndoData extends BaseUndoData {
    private String preEndoStatus;
    @Override
    @JsonIgnore
    public String getUndoActionSpringBeanName() {
        return MyEndoUpdateUndoAction.BEAN_NAME;
    }
    @Override
    protected String getEntityName() {return "Endorsement";}
    @Override
    public int getActionType() {return BaseUndoData.ACTION_TYPE__UPDATE;}

    public String getPreEndoStatus() {return preEndoStatus; }
    public void setPreEndoStatus(String preEndoStatus) {
        this.preEndoStatus = preEndoStatus;
    }
}

@Component(MyEndoUpdateUndoAction.BEAN_NAME)
public class MyEndoUpdateUndoAction extends BaseUndoAction {
    public final static String BEAN_NAME="endoUpdateSubCompensationAction";
    @PersistenceContext(unitName = "JPAUnit")
    private EntityManager entityManager;

    @Override
    public void undo(BaseUndoData subUndoData) {
        Endorsement endo=entityManager.find(Endorsement.class, 
((MyEndoUpdateUndoData) subUndoData).getPrimaryKey());
//compensation for update operation by restoring key field values
        endo.setEndoStatus(endoUpdateSubCompensationData.getPreEndoStatus());
    }
}
```
### Reliable Message Mode
The reliable message mode is mainly utilized in the following two scenarios:
- Send MQ messages.
- Call the third-party interfaces without compensation capabilities.

This mode adheres to the original TxOutCommand mode without interface changes, but it has been internally transformed into the Seata TCC mode. The TxOutCommand will be executed after the global transaction, rather than the original local transaction commits. If no global transaction is started, the system will automatically create one that will be committed after the local Spring transaction completes.

### TCC Transaction Mode
When employing the TCC transaction mode, you need to add two annotations: `@LocalTCC` and `@TwoPhaseBusinessAction`. Due to the complexity of implementing TCC, it is generally not recommended. Instead, the AT mode and reliable message mode are preferred.

```java
@LocalTCC
public interface TransactionEventuallyConsistentTCCService {

    @TwoPhaseBusinessAction(name = "TransactionEventuallyConsistent" , commitMethod = "commit", rollbackMethod = "rollback")
    public boolean prepare(BusinessActionContext actionContext, AbstractTxOutCommand command);

    public boolean commit(BusinessActionContext actionContext);


    public boolean rollback(BusinessActionContext actionContext);
}
```


## TCC for BFF without InsureMO Appframework and Foundation

All above-mentioned java methods rely on InsureMO Appframework. If the user application operates with its own foundation framework and only requires calling InsureMO's atomic APIs, there are alternative methods for usage.

Option 1: HTTP API Call

* When initiating a global transaction, you can invoke the start global transaction API. 

    ```
    - /seata-server/seata-server/beginGlobal POST Method
    {  
    "ClientAppliationName":"proposal-v2", //whatever string  
    "ClientVersion":2, //fix value  
    "Timeout":600000, //fix value  
    "TransactionName":"createPolicy" //whatever string  
    }
    ```

* For all platform APIs that requires being in the same transaction, the user needs to append XID in the header.
* When the global transaction is complete, you can invoke the end global transaction API.

    ```
    - /seata-server/seata-server/commitGlobal POST Method
    {
        "ClientAppliationName": "proposal-v2",  
        "ClientVersion": 2,  
        "Xid": "24266702816"  // return from previous API
    }
    ```
* If the there is error during global transaction , you can invoke the rollback global transaction API.

    ```
    - /seata-server/seata-server/rollbackGlobal POST Method
    {
        "ClientAppliationName": "proposal-v2",  
        "ClientVersion": 2,  
        "Xid": "24266702816"  // return from previous API
    }
    ```

Option 2: Lightweight SDK (not recommended)

* Add dependencies: `vela-seata-client` and `vela-sdk-base`, without relying on other foundation JARs.
* Implement bean interfaces in `vela-sdk-base`: `ThreadContextAPI`, `SequenceAPI`. Register them as Spring beans.
* Ensure beans for `DataSource`, `Transaction`, `RestTemplate` are provided.
* When calling platform APIs, ensure that the HTTP headers, including the user name, tenant code, trace ID, and XID, are set correctly.


## TCC Compensation

Our TCC management system supports the logging of failed TCC transactions. For any failed transactions, you can find them in the system and click **Retry** to re-commit the transaction. Normally, the suggested operation steps can be:

1. Locate all relevant transactions based on the application ID or transaction name.
2. Retry them from the earliest to the latest transaction, one by one.

![TCC Management](./image/monitor/tcc.png)


## TCC Transaction Timeout Setting

If a transaction encounters an issue indicating exceeding the specified timeout limit, it will be automatically stopped by the timeout setting.

For all environments, we recommend the following default timeout settings:

- `seata.defaultGlobalTransactionTimeout=60000`
- `ribbon.ReadTimeout=30000`

While for development environments, we have specifically adjusted the parameters to a longer timing since such environments can sometimes require longer processing times:

- `seata.defaultGlobalTransactionTimeout=600000`
- `ribbon.ReadTimeout=600000`

## Question and Answer

### When customer portal tries to issue a policy A, it receives an error, and then creates another policy B. However, because the error occurs during the TCC commit timeout, which does not rollback policy issuance, policy A is issued as well. Is this the right behavior?


This is a complex issue and we need to analyze it from various aspects.

1.	TCC mechanism

    In the TCC transaction mode, which is used for managing distributed transactions, the process is divided into three distinct phases: **Try**, **Confirm**, and **Cancel**.

    * If it's in the **Try** stage before **Confirm** (commit), the Seata Server will cancel (rollback) once any error, including timeout, occurs. 
    * However, if it goes into the final **Confirm** (commit) stage, if any error occurs, including timeout, there’s no way to rollback, but the Seata Server will adhere to the current strategy of retrying until success.

    This is the basic TCC mechanism and is a standard industry practice.

2.	PA system case analysis

    If any front end calls our APIs, there must be a unique transaction number to complete the transaction anyway. In a policy admin system, for example, if a user has already reached the bind stage and click **Issuance**, the system will error out. Typically, users will find and enter the same proposal to continue processing. If the user finds the proposal has already been issued, it cannot be redone.

3.	Customer portal integration case analysis

    To prevent such duplicate policy issuances, the front-end system should always finish the same transaction instead of creating a new one. There are multiple ways to achieve it:

    * For any transactions, call the proposal number generation API first, and then enter the proposal number in the request to issue the policy. If any error occurs, retry with the same proposal number to complete the transaction.

    * Alternatively, create something like an order number and pass it to us. Then we need to add transaction logic to search based on the order number. If there is none, create a new proposal. If there is one, update it based on the existing proposal.  
    
       Note that while ES can be used for searching, due to update latency, it's advisable to rely on DB-level searches. We are testing whether configurations can work at the DB level to enhance search accuracy, and will update you accordingly.

