Detecção e resolução de conflitos em AWS AppSync - AWS AppSync

As traduções são geradas por tradução automática. Em caso de conflito entre o conteúdo da tradução e da versão original em inglês, a versão em inglês prevalecerá.

Detecção e resolução de conflitos em AWS AppSync

Quando ocorrem gravações simultâneas AWS AppSync, você pode configurar estratégias de detecção e resolução de conflitos para lidar com as atualizações de forma adequada. A detecção de conflitos determina se a mutação está em conflito com o item real gravado na fonte de dados. A Detecção de Conflitos é ativada definindo o valor no conflictDetection campo paraVERSION. SyncConfig

A resolução de conflitos é a ação tomada caso um conflito seja detectado. Isso é determinado definindo o campo Conflict Handler no SyncConfig. Existem três estratégias de resolução de conflitos:

  • OPTIMISTIC_CONCURRENCY

  • AUTOMERGE

  • LAMBDA

As versões são incrementadas automaticamente AWS AppSync durante as operações de gravação e não devem ser modificadas pelos clientes ou fora de um resolvedor configurado com uma fonte de dados habilitada para versão. Isso muda o comportamento de consistência do sistema e pode ocasionar perda de dados.

Concorrência otimista

A simultaneidade otimista é uma estratégia de resolução de conflitos que AWS AppSync fornece fontes de dados versionadas. Quando o resolvedor de conflitos é definido como Simultaneidade otimista, se uma mutação de entrada é detectada com uma versão diferente da versão real do objeto, o handler de conflitos simplesmente rejeita a solicitação recebida. Dentro da resposta do GraphQL, será fornecido o item existente no servidor que tem a versão mais recente. Espera-se então que o cliente processe esse conflito localmente e repita a mutação com a versão atualizada do item.

Fusões automáticas

O Automerge fornece aos desenvolvedores uma maneira fácil de configurar uma estratégia de resolução de conflitos sem gravar lógica no lado do cliente para mesclar manualmente conflitos incapazes de serem resolvidos com outras estratégias. O Automerge adere a um conjunto de regras rigoroso ao mesclar dados para resolver conflitos. Os princípios do Automerge giram em torno do tipo de dados subjacente do campo GraphQL. Eles são os seguintes:

  • Conflito em um campo escalar: escalar do GraphQL ou qualquer campo que não seja uma coleção (ou seja, lista, conjunto, mapa). Rejeitar o valor de entrada para o campo escalar e selecionar o valor existente no servidor.

  • Conflito em uma lista: o tipo do GraphQL e o tipo do banco de dados são listas. Concatenar a lista de entrada com a lista existente no servidor. Os valores da lista na mutação de entrada serão anexados ao final da lista no servidor. Valores duplicados serão retidos.

  • Conflito em um conjunto: o tipo do GraphQL é uma lista e o tipo do banco de dados é um conjunto. Aplique uma união de conjunto usando a entrada do conjunto e o conjunto existente no servidor. Isso adere às propriedades de um conjunto, o que significa que não haverão entradas duplicadas.

  • Quando uma mutação de entrada adiciona um novo campo ao item ou é feita em relação a um campo com o valor de null, ocorre a mescla no item existente.

  • Conflito em um mapa: quando o tipo de dados subjacente no banco de dados é um mapa (ou seja, documento chave/valor), aplique as regras acima à medida que ele analisa e processa cada propriedade do mapa.

O Automerge foi projetado para detectar, mesclar e repetir solicitações automaticamente com uma versão atualizada, isentando o cliente da necessidade de mesclar manualmente quaisquer dados conflitantes.

Para mostrar um exemplo de como o Automerge lida com um conflito em um tipo escalar. Usaremos o seguinte registro como nosso ponto de partida.

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

Agora, uma mutação de entrada pode estar tentando atualizar o item, mas com uma versão mais antiga, uma vez que o cliente ainda não efetuou a sincronização com o servidor. O resultado se parece com:

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

Observe a versão desatualizada do 2 na solicitação recebida. Durante este fluxo, o Automerge irá mesclar os dados rejeitando a atualização de campo 'jersey' para '55', além de manter o valor em '5', resultando na seguinte imagem do item que está sendo salvo no servidor.

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

Considerando o estado do item mostrado acima na versão 5, vamos supor que uma mutação de entrada tente alterar o item com a seguinte imagem:

{ "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 }

Há três pontos de interesse na mutação de entrada. O nome, um escalar, foi alterado, mas dois novos campos “interesses”, um conjunto, e “pontos”, uma lista, foram adicionados. Neste cenário, um conflito será detectado devido à incompatibilidade de versão. O Automerge adere às suas propriedades e rejeita a alteração de nome por ele ser um escalar e adicionar campos não conflitantes. Isso resulta no item que é salvo no servidor de modo a aparecer da seguinte forma.

{ "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 }

Com a imagem atualizada do item com a versão 6, vamos supor que uma mutação de entrada (com outra incompatibilidade de versão) tente transformar o item para o seguinte:

{ "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 }

Aqui observamos que o campo de entrada para “interesses” tem um valor duplicado que existe no servidor e dois novos valores. Neste caso, como o tipo de dados subjacente é um conjunto, o Automerge combinará os valores existentes no servidor com os da solicitação recebida e eliminará quaisquer duplicatas. Da mesma forma, há um conflito no campo “pontos” onde há um valor duplicado e um novo valor. Entretanto, como o tipo de dados subjacente aqui é uma lista, o Automerge simplesmente acrescentará todos os valores na solicitação de entrada ao final dos valores já existentes no servidor. A imagem mesclada resultante armazenada no servidor aparecerá da seguinte forma:

{ "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 }

Agora vamos supor que o item armazenado no servidor aparece da seguinte forma, na versão 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 }

Entretanto, uma solicitação de entrada tenta atualizar o item com a imagem a seguir, mais uma vez com uma incompatibilidade de versão:

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

Agora, neste cenário, podemos ver que os campos que já existem no servidor estão ausentes (interesses, pontos, jersey). Além disso, o valor para “ppg” dentro do mapa “stats” está sendo editado, um novo valor “rpg” está sendo adicionado e “apg” é omitido. O Automerge preserva os campos que foram omitidos (observação: se os campos tiverem de ser removidos, será preciso tentar a solicitação novamente com a versão correspondente) para que eles não sejam perdidos. Ele também aplicará as mesmas regras aos campos dentro dos mapas e, portanto, a alteração em “ppg” será rejeitada enquanto “apg” será preservada e “rpg”, um novo campo, será adicionado. Agora, o item resultante armazenado no servidor aparecerá como:

{ "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

Há várias estratégias de resolução Lambda para escolher:

  • RESOLVE: substitui o item existente pelo novo item fornecido na carga útil de resposta. Você só pode repetir a mesma operação em um único item de cada vez. No momento, é compatível com PutItem e UpdateItem para DynamoDB.

  • REJECT: rejeita a mutação e retorna um erro com o item existente na resposta do GraphQL. No momento, é compatível com PutItem, UpdateItem e DeleteItem para DynamoDB.

  • REMOVE: remove o item existente. No momento, é compatível com DeleteItem para DynamoDB.

A solicitação de invocação do Lambda

O AWS AppSync resolvedor do DynamoDB invoca a função Lambda especificada no. LambdaConflictHandlerArn Ele usa o mesmo service-role-arn configurado na fonte de dados. A carga da invocação tem a seguinte estrutura:

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

Os campos são definidos da seguinte forma:

newItem

O item de visualização, se a mutação foi bem-sucedida.

existingItem

O item que reside na tabela do DynamoDB.

arguments

Os argumentos da mutação do GraphQL.

resolver

Informações sobre o AWS AppSync resolvedor.

identity

Informações sobre o chamador. Esse campo é definido como nulo, se for acessado com API chave.

Exemplo de carga:

{ "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" } }

A resposta de invocação do Lambda

Para resolução de conflitos PutItem e UpdateItem

RESOLVE a mutação. A resposta deve estar no seguinte formato.

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

O campo item representa um objeto que será usado para substituir o item existente na fonte de dados subjacente. A chave primária e os metadados de sincronização serão ignorados se incluídos no item.

REJECT a mutação. A resposta deve estar no seguinte formato.

{ "action": "REJECT" }

Para resolução de conflitos DeleteItem

REMOVE o item. A resposta deve estar no seguinte formato.

{ "action": "REMOVE" }

REJECT a mutação. A resposta deve estar no seguinte formato.

{ "action": "REJECT" }

O exemplo de função do Lambda abaixo verifica quem faz a chamada e o nome do resolvedor. Se for feito porjeffTheAdmin, REMOVE o objeto para o DeletePost resolvedor ou o conflito com RESOLVE o novo item para os resolvedores Update/Put. Se não, a mutação é 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; }

Erros

Abaixo está uma lista de possíveis erros que podem ocorrer durante um processo de resolução de conflitos:

ConflictUnhandled

A detecção de conflitos localiza uma incompatibilidade de versão e o handler de conflitos rejeita a mutação.

Exemplo: resolução de conflitos com um handler de conflitos de simultaneidade otimista. Ou, o handler de conflitos do Lambda retornou REJECT.

ConflictError

Ocorre um erro interno ao tentar resolver um conflito.

Exemplo: o handler de conflitos do Lambda retornou uma resposta mal-formada. Ou, não é possível invocar o handler de conflitos do Lambda porque o recurso LambdaConflictHandlerArn fornecido não foi encontrado.

MaxConflicts

O máximo de tentativas de repetição foram atingidas para a resolução de conflitos.

Exemplo: excesso de solicitações simultâneas no mesmo objeto. Antes que o conflito seja resolvido, o objeto é atualizado para uma nova versão por outro cliente.

BadRequest

O cliente tenta atualizar campos de metadados (_version, _ttl, _lastChangedAt, _deleted).

Exemplo: o cliente tenta atualizar _version um objeto com uma mutação de atualização.

DeltaSyncWriteError

Falha ao gravar o registro de sincronização delta.

Exemplo: a mutação foi bem-sucedida, mas ocorreu um erro interno ao tentar gravar na tabela de sincronização delta.

InternalFailure

Ocorreu um erro interno.

UnsupportedOperation

Operação não suportada 'X'. O controle de versão da fonte de dados suporta somente as seguintes operações (TransactGetItems,,, ScanPutItem, QueryBatchGetItem,,,, GetItem SyncDeleteItem). UpdateItem

Exemplo: uso de determinadas operações de transação e lote com a detecção/resolução de conflitos ativada. Essas operações não são suportadas atualmente.

CloudWatch Registros

Se um AWS AppSync API tiver ativado CloudWatch os registros com as configurações de registro definidas como Registros em nível de campo enabled e em nível de registro para os registros em nível de campo definidas comoALL, então AWS AppSync emitirá informações de detecção e resolução de conflitos para o grupo de registros. Para obter informações sobre o formato das mensagens de log, consulte a documentação de Detecção de conflitos e Registro em log de sincronização.