1

Most AWS Amplify/AppSync tutorials and examples explain how conflict resolution is usually automatic with this tech stack, and in extreme examples show how to custom handle a conflict when saving an instance of a single Model. What happens when you need to enforce more complicated relationships between your Models, and something goes wrong "down stream"?

(Contrived) Example:

Imagine a number of easter eggs holding some number of jelly beans each, and a group of children trying to empty them into their easter baskets.

type Child @model {
  id: ID!
  name: String!
  jellybeans: Int!
}

type Egg @model {
  id: ID!
  jellybeans: Int!
}

Jellybeans are a conserved quantity - if we add up all the jellybeans across the children and the eggs it should be constant. Additionally, neither children nor eggs can have "negative jellybeans" - so a CRDT model is not possible.

As the children find eggs, they add them to their baskets:

async function findEgg(childId, eggId) {
  const child = await DataStore.query(Child, childId);
  const egg = await DataStore.query(Egg, eggId);

  if (!child || !egg) {
    return;
  }

  const count = egg.jellybeans;

  // Call 1
  await DataStore.save(
    Child.copyOf(child, updated => {
      updated.jellybeans += count;
    })
  );

  // Call 2
  await DataStore.save(
    Egg.copyOf(egg, updated => {
      updated.jellybeans -= count;
    })
  );  
}

John, in offline mode, finds egg #ABC, and his app calls findEgg() - he got 13 jellybeans! However, unfortunately, Suzie actually had already found egg #ABC while John was offline, and called findEgg(), and AppSync GraphQL had already given her the 13 jellybeans.

I'm not sure what happens here, but I don't think I like it under any circumstances:

  • John and Suzie both tried to set #ABC to a count of 0. AppSync may say "no conflict", while letting John and Suzie both increment their own jellybean count, ruining conservation of jelly beans.
  • A conflict was identified and "successfully" resolved as #ABC now has -13 jellybeans. I can imagine how to identify this and prevent it at the resolver level, so let's skip discussion of that.
  • AppSync GraphQL rejects John's Call 2 attempt to update #ABC when John later comes online. Presumably Call 1 already resolved without issue, though, which is a problem.

Question Restatement So - how do you structure the models and/or the code in this contrived example, so that AppSync not only notices an issue, but then also backs out mutations on other models that should not succeed in isolation?

What I've tried:

Or rather, not tried, but wondered (because I can't find good examples of this), would be to create something like:

type JellybeanTransfer @model {
  child: Child
  egg: Egg
}

and then try to save a "new" one of these. The GraphQL resolver doesn't do a braindead CRUD operation, but instead either (A) saves one of these in addition to updating Child and Egg, all via some sort of BEGIN TRANSACTION / END TRANSACTION sql; or (B) doesn't even create a JellybeanTransfer instance anywhere and treats it as a sort of meta/ephemeral concept. It updates the Child and the Egg, and if it can't do both, backs out the change and rejects the new JellybeanTransfer.

jdowdell
  • 1,578
  • 12
  • 24
  • We've got the same issue. The documentation (https://docs.aws.amazon.com/whitepapers/latest/amplify-datastore-implementation/use-case-and-implementation.html#retail-inventory-counting) gives a similar example - surely they have to combine the subtotals into a correct total? We tried a custom resolver with a TransactWriteItems but that led to errors warning it was unsupported if versioning was enabled (and surely you want versioning for synchronisation?). Perhaps you also optimistically update the summaries, storing a from/to value, then you can conflict-resolve to get the right answer? – user611942 Jul 06 '23 at 09:19
  • Our custom resolver approach is really a variation of your more general approach - you "compress" the transaction into their unit of replication and "decompress" in the resolver. This is where the lack of TransactWriteItems support hit us. – user611942 Jul 06 '23 at 09:24
  • Sounds like we did something similar. I haven't had time to write this as a formal answer, but we ended up going way down the rabbit hole on this and coming up with a solution. It wasn't too far off from the "(B)" `JellybeanTransfer` approach. The key was backing the GraphQL Resolver with a Lambda function that let us do whatever we needed to do, and then triggering change event on both the Child and Egg subscription channels. Not a typical use case in their various docs, makes me want to go make my own DataStore clone that's better... – jdowdell Jul 08 '23 at 02:25

0 Answers0