

本文為英文版的機器翻譯版本，如內容有任何歧義或不一致之處，概以英文版為準。

# 中的衝突偵測和解決 AWS AppSync
<a name="conflict-detection-and-resolution"></a>

當 AWS AppSync 發生並行寫入時，您可以設定衝突偵測和衝突解決策略，以適當處理更新。「衝突偵測」會判斷變動是否與資料來源中的實際寫入項目衝突。藉由將 `conflictDetection` 欄位之 SyncConfig 中的值設為 `VERSION`，即可啟用「衝突偵測」。

「衝突解決」是偵測到衝突時所採取的動作。這是透過在 SyncConfig 中設定「Conflict Handler (衝突處理常式)」欄位來決定的。有三種衝突解決策略：
+ OPTIMISTIC\$1CONCURRENCY
+ AUTOMERGE
+ LAMBDA

在寫入操作期間， AWS AppSync 會自動遞增版本，且不應由用戶端或使用已啟用版本之資料來源設定的解析程式外部修改。這樣做會改變系統的一致性行為，並可能造成資料遺失。

## 樂觀並行
<a name="optimistic-concurrency"></a>

樂觀並行是 AWS AppSync 為版本控制資料來源提供的衝突解決策略。當衝突解析程式設為「開放式並行存取」時，如果偵測到傳入變動的版本與物件的實際版本不同，衝突處理常式將僅拒絕傳入請求。在 GraphQL 回應中，將提供最新版本伺服器上的現有項目。然後，預期用戶端在本機處理此衝突，並使用該項目的更新版本重試變動。

## 自動合併
<a name="automerge"></a>

自動合併提供開發人員一種簡單的方法來設定衝突解決策略，而不需撰寫用戶端邏輯以手動合併其他策略無法處理的衝突。自動合併在合併資料來解決衝突時，遵守嚴格的規則集。自動合併的原則是以 GraphQL 欄位的基礎資料類型為主。如下所示：
+ 純量欄位上的衝突：GraphQL 純量或非集合的任何欄位 （即 List、Set、Map)。拒絕純量欄位的傳入值，並選取伺服器中的現有值。
+ List 的衝突：GraphQL 類型和資料庫類型為 List。串連傳入清單與伺服器中的現有清單。傳入變動的清單值將附加到伺服器列表的末尾。將保留重複的值。
+ Set 的衝突：GraphQL 類型為 List，資料庫類型為 Set。使用傳入集合和伺服器中的現有集合套用集合聯集。這符合 Set 的屬性，表示沒有重複的項目。
+ 當傳入變動將新欄位新增至項目，或針對值為 的欄位進行時`null`，請將該欄位合併到現有項目。
+ Map 的衝突：當資料庫中的基礎資料類型是 Map (即索引鍵-值文件) 時，會套用上述規則，因為其剖析和處理 Map 的每個屬性。

自動合併是為了自動偵測、合併和重試具有更新版本的請求而設計，讓用戶端無需手動合併任何衝突的資料。

為了展現自動合併如何處理純量類型衝突的範例。我們將使用以下記錄做為起點。

```
{
  "id" : 1,
  "name" : "Nadia",
  "jersey" : 5,
  "_version" : 4
}
```

現在，由於用戶端尚未與伺服器同步，傳入變動可能試圖使用舊版本來更新該項目。如下所示：

```
{
  "id" : 1,
  "name" : "Nadia",
  "jersey" : 55,
  "_version" : 2
}
```

請注意，傳入請求中的過時版本 2。在此流程中，自動合併拒絕將 ‘jersey’ 欄位更新為 ‘55’ ，並保留值 ‘5’，導致以下項目影像儲存在伺服器中。

```
{
  "id" : 1,
  "name" : "Nadia",
  "jersey" : 5,
  "_version" : 5 # version is incremented every time automerge performs a merge that is stored on the server.
}
```

假設如上所示的項目狀態為版本 5，現在假設傳入變動嘗試使用以下影像對項目進行變動：

```
{
  "id" : 1,
  "name" : "Shaggy",
  "jersey" : 5,
  "interests" : ["breakfast", "lunch", "dinner"] # underlying data type is a Set
  "points": [24, 30, 27] # underlying data type is a List
  "_version" : 3
}
```

傳入變動中有三個參考資訊。名稱 (純量) 已變更，但增加兩個新欄位，“interests” (Set) 和 “points” (List)。在這種情況下，因為版本不符，會偵測到衝突。自動合併遵循其屬性，並拒絕名稱變更，因為它是純量，而且是非衝突的欄位。這將導致儲存在伺服器中的項目顯示如下。

```
{
  "id" : 1,
  "name" : "Nadia",
  "jersey" : 5,
  "interests" : ["breakfast", "lunch", "dinner"] # underlying data type is a Set
  "points": [24, 30, 27] # underlying data type is a List
  "_version" : 6
}
```

現在，使用版本 6 的項目更新影像，現在假設傳入變動 (具有另一個不相符的版本) 嘗試將項目轉換為以下內容：

```
{
  "id" : 1,
  "name" : "Nadia",
  "jersey" : 5,
  "interests" : ["breakfast", "lunch", "brunch"] # underlying data type is a Set
  "points": [30, 35] # underlying data type is a List
  "_version" : 5
}
```

我們在這裡觀察到 “interests” 的傳入欄位具有伺服器中的一個重複值和兩個新值。在這種情況下，由於基礎資料類型是 Set，所以自動合併會將伺服器中的現有值與傳入請求中的值合併，並刪除任何重複項目。同樣地，“points” 欄位也有衝突，有一個重複值和一個新值。但由於這裡的基礎資料類型是 List，自動合併會簡單地將傳入請求中的所有值附加到伺服器中已存在值的結尾。存放在伺服器上的產生合併影像顯示如下：

```
{
  "id" : 1,
  "name" : "Nadia",
  "jersey" : 5,
  "interests" : ["breakfast", "lunch", "dinner", "brunch"] # underlying data type is a Set
  "points": [24, 30, 27, 30, 35] # underlying data type is a List
  "_version" : 7
}
```

現在，假設存放在伺服器中的項目在版本 8 中如下所示。

```
{
  "id" : 1,
  "name" : "Nadia",
  "jersey" : 5,
  "interests" : ["breakfast", "lunch", "dinner", "brunch"] # underlying data type is a Set
  "points": [24, 30, 27, 30, 35] # underlying data type is a List
  "stats": {
      "ppg": "35.4",
      "apg": "6.3"
  }
  "_version" : 8
}
```

但傳入的請求嘗試使用以下影像來更新項目，再次發生版本不相符：

```
{
  "id" : 1,
  "name" : "Nadia",
  "stats": {
      "ppg": "25.7",
      "rpg": "6.9"
  }
  "_version" : 3
}
```

在這種情況下，我們可以看到伺服器中已存在的欄位遺失 (interests、points、jersey)。此外，已編輯映射 “stats” 中的 “ppg” 值，已新增新值 “rpg”，而且已省略 “apg”。自動合併會保留已省略的欄位 (注意：如果要移除欄位，必須使用相符的版本再次嘗試該請求)，以便不會遺失欄位。它也會將相同的規則套用至映射中的欄位，因此將拒絕對 “ppg” 進行的變更，而保留 “apg” ，並新增欄位 “rpg”。存放在伺服器中的產生項目現在顯示如下：

```
{
  "id" : 1,
  "name" : "Nadia",
  "jersey" : 5,
  "interests" : ["breakfast", "lunch", "dinner", "brunch"] # underlying data type is a Set
  "points": [24, 30, 27, 30, 35] # underlying data type is a List
  "stats": {
      "ppg": "35.4",
      "apg": "6.3",
      "rpg": "6.9"
  }
  "_version" : 9
}
```

## Lambdas
<a name="lambda"></a>

有多種 Lambda 解析策略可供選擇：
+  `RESOLVE`：將現有項目取代為回應承載中提供的新項目。您一次只能對單ㄧ項目重試相同的操作。目前支援 DynamoDB `PutItem` 和 `UpdateItem`。
+  `REJECT`：拒絕變動，並傳回 GraphQL 回應中現有項目的錯誤。目前支援 DynamoDB `PutItem`、`UpdateItem` 和 `DeleteItem`。
+  `REMOVE`：移除現有項目。目前支援 DynamoDB `DeleteItem`。

 **Lambda 叫用請求** 

The AWS AppSync DynamoDB 解析程式會叫用 中指定的 Lambda 函數`LambdaConflictHandlerArn`。它會使用資料來源上所設定的相同 `service-role-arn`。叫用承載的結構如下：

```
{
    "newItem": { ... },
    "existingItem": {... },
    "arguments": { ... },
    "resolver": { ... },
    "identity": { ... }
}
```

欄位定義如下：

** `newItem` **  
預覽項目，如果變動成功。

** `existingItem` **  
目前位於 DynamoDB 資料表中的項目。

** `arguments` **  
來自 GraphQL 變動的引數。

** `resolver` **  
 AWS AppSync 解析程式的相關資訊。

** `identity` **  
發起人的相關資訊。如果使用 API 金鑰存取，則此欄位會設為 Null。

承載範例：

```
{
    "newItem": {
        "id": "1",
        "author": "Jeff",
        "title": "Foo Bar",
        "rating": 5,
        "comments": ["hello world"],
    },
    "existingItem": {
        "id": "1",
        "author": "Foo",
        "rating": 5,
        "comments": ["old comment"]
    },
    "arguments": {
        "id": "1",
        "author": "Jeff",
        "title": "Foo Bar",
        "comments": ["hello world"]
    },
    "resolver": {
        "tableName": "post-table",
        "awsRegion": "us-west-2",
        "parentType": "Mutation",
        "field": "updatePost"
    },
    "identity": {
         "accountId": "123456789012",
         "sourceIp": "x.x.x.x",
         "username": "AIDAAAAAAAAAAAAAAAAAA",
         "userArn": "arn:aws:iam::123456789012:user/appsync"
    }
}
```

 **Lambda 叫用回應** 

適用於 `PutItem` 和 `UpdateItem` 衝突解決

 `RESOLVE` 變動。回應必須採用下列格式。

```
{
    "action": "RESOLVE",
    "item": { ... }
}
```

`item` 欄位代表將用來取代基礎資料來源中現有項目的物件。如果包含在 `item` 中，則會忽略主索引鍵和同步中繼資料。

 `REJECT` 變動。回應必須採用下列格式。

```
{
    "action": "REJECT"
}
```

適用於 `DeleteItem` 衝突解決

 `REMOVE` 項目。回應必須採用下列格式。

```
{
    "action": "REMOVE"
}
```

 `REJECT` 變動。回應必須採用下列格式。

```
{
    "action": "REJECT"
}
```

以下 Lambda 函數範例會檢查進行呼叫的對象和解析程式名稱。如果是由 `jeffTheAdmin` 產生，則會 `REMOVE` DeletePost 解析程式的物件，或針對「更新/放置」解析程式 `RESOLVE` 與新項目的衝突。如果不是，則變動為 `REJECT`。

```
exports.handler = async (event, context, callback) => {
    console.log("Event: "+ JSON.stringify(event));

    // Business logic goes here.
    var response;
    if ( event.identity.user == "jeffTheAdmin" ) {
        let resolver = event.resolver.field;

        switch(resolver) {
            case "deletePost":
                response = {
                    "action" : "REMOVE"
                }
                break;

            case "updatePost":
            case "createPost":
                response = {
                    "action" : "RESOLVE",
                    "item": event.newItem
                }
                break;
            default:
                response = { "action" : "REJECT" };
        }
    } else {
        response = { "action" : "REJECT" };
    }

    console.log("Response: "+ JSON.stringify(response));
    return response;
}
```

## 錯誤
<a name="errors"></a>

以下是衝突解決程序期間可能發生的錯誤清單：

** `ConflictUnhandled` **  
衝突偵測發現版本不相符，而且衝突處理常式拒絕變動。  
範例：具有開放式並行存取衝突處理常式的衝突解決機制。或者，Lambda 衝突處理常式傳回 `REJECT`。

** `ConflictError` **  
嘗試解決衝突時，發生內部錯誤。  
範例：Lambda 衝突處理常式傳回格式錯誤的回應。或者，無法叫用 Lambda 衝突處理常式，因為找不到提供的資源 `LambdaConflictHandlerArn`。

** `MaxConflicts` **  
已達到衝突解決的重試次數上限。  
範例：同一個物件上有太多並行請求。解決衝突之前，另一個用戶端已將物件更新為新版本。

** `BadRequest` **  
用戶端嘗試更新中繼資料欄位 (`_version`、`_ttl`、`_lastChangedAt`、`_deleted`)。  
範例：用戶端嘗試更新具有更新變動`_version`的物件。

** `DeltaSyncWriteError` **  
無法寫入差異同步記錄。  
範例：變動成功，但嘗試寫入差異同步資料表時發生內部錯誤。

** `InternalFailure` **  
發生內部錯誤。

**`UnsupportedOperation`**  
不支援的操作 '*X*'。資料來源版本控制僅支援下列操作 (TransactGetItems、PutItem、BatchGetItem、Scan、Query、GetItem、DeleteItem、UpdateItem、Sync)。  
範例：在啟用衝突偵測/解決的情況下使用特定交易和批次操作。目前不支援這些操作。

## CloudWatch Logs
<a name="cloudwatch-logs"></a>

如果 an AWS AppSync API 已啟用 CloudWatch Logs，並將記錄設定為欄位層級日誌`enabled`，並將欄位層級日誌的日誌層級設定為 `ALL`，則 AWS AppSync 會將衝突偵測和解決資訊發出至日誌群組。如需日誌訊息格式的相關資訊，請參閱[衝突偵測與同步記錄的文件](monitoring.md#aws-appsync-monitoring-conflict-detection-and-sync-logging)。