Introduction to managing conflicts in NunDB

Conflict resolution is now, is a first-class citizen of NunDB, and in this post, I will show how to explore the new capabilities and features.

In this post, we will show how to deal with conflict resolution and take the max you can from the conflict resolution.

Create the DB

nun-db --user $NUN_USER  -p $NUN_PWD --host "http://http.nundb.org" exec "create-db trelo-real-time-arbiter trelo-real-time-pwd arbiter;use-db trelo-real-time-arbiter trelo-real-time-pwd;keys;snapshot"

The important part of this command is create-db $user $pwd arbiter the arbiter is the vital part o this command. We also have two other strategies, the none or newer.

In this post, the goal is to explore deeper the option of arbiter, but we will fly to the others.

Consensus Strategy None

Sample create-db $user $pwd none or create-db $user $pwd. In this case there will not be any try to resolve conflicts. Any old version trying to be set, set will be rejected with a version error.


const key = 'obj';
const version = 1;
const save2 = await db1.setValueSafe(key, { name: 'name 2' }, version);// saved
const save1 = await db2.setValueSafe(key, { name: 'name 1' }, version);// Fails because verion did not pump
const value = await db1.getValueSafe(key);// Value { name: 'name 2'}

Consensus Strategy Newer

Sample create-db $user $pwd newer in this case, there is one out-of-the-box conflict resolution strategy. In this case, the latest set is always the valid.

const key = 'obj';
const version = 1;
const save2 = await db1.setValueSafe(key, { name: 'name 2' }, version);// saved
const save1 = await db2.setValueSafe(key, { name: 'name 1' }, version);// saved
const value = await db1.getValueSafe(key);// Value { name: 'name 1'}

Consensus Strategy Arbiter

Sample create-db $user $pwd arbiter.

This is a less simple one; in this case, the conflict will be resolved by an actor we call arbiter,

const key = 'obj';
const version = 1;
const save2 = await db1.setValueSafe(key, { name: 'name 2' }, version);// saved
const save1 = await db2.setValueSafe(key, { name: 'name 1' }, version);// works and triggers arbiter 
// What to do?
const value = await db1.getValueSafe(key);// Value { name: 'name 1'}

How to create the Arbiter

The Arbiter is a database level, which means you need to connect as an arbiter for each db.

1. Connect as usual

  const db = new NunDb(server_url, "test", "test-pwd");

Tell the server that you are an arbiter

  await db.becameArbiter(resolverCallBack);

Set the conflict resolution

In this example, I am implementing a very simple auto-conflict resolution. Here I am concatenating the name with the conflicted name. Of course, all applications will need different implementations. Here the idea is if two users change the name of the same person, we concatenate both and save that as the name.

The callback has to return a promise, so depending on the application, you can implement long tasks, like letting the user choose in the UI. While the key is in a resolution conflict state, it will stay with version -2, and any new change to the same key will be added to a stack of conflicts to be resolved one after the other.

const resolverCallBack = (e) => {
// Resolve the conflict by concatenating the names
  const concat = e.values.map( _ => _.value.name).join(', ');
  console.log('Will finally resolve to',concat);
  return Promise.resolve({ id: e.values.at(0).id, value: { name: concat } });
};
await db.becameArbiter(resolverCallBack);// Repeated from the last code

How will the code work in the end?

const key = 'obj';
const version = 1;
const save2 = await db1.setValueSafe(key, { name: 'name 2' }, version);// saved
const save1 = await db2.setValueSafe(key, { name: 'name 1' }, version);// works and triggers arbiter 
// What to do?
const value = await db1.getValueSafe(key);// Value { name: 'name 1'}

All together

const db = new NunDb("ws://nun-db-1.localhost:3058", "trelo-real-time-arbiter", "trelo-real-time-pwd");
const resolverCallBack = (e) => {
  const concat = e.values.map( _ => _.value.name).join(', ');
  console.log('Will finally resolve to',concat);
  return Promise.resolve({ id: e.values.at(0).id, value: { name: concat } });
};
await db.becameArbiter(resolverCallBack);// Repeated from the last code
const version = parseInt(new Date().getTime() / 1000, 10);// Changed to make sure the version is unic on each execution
const key = 'obj_new';
const save2 = await db.setValueSafe(key, { name: 'name 2' }, version);// saved
const save1 = await db.setValueSafe(key, { name: 'name 1' }, version);// works and triggers arbiter 
// A few secounds latter
const value = await db.getValueSafe(key);// Value { name: 'name 1, name 2, name 1'}

Conclusion

In this post, we quickly demonstrated how to use the new NunDB capabilities to resolve conflicts. Most applications may not need many details to resolve conflicts, and therefore the last write wing, Newer should cover most of the user cases. In the cases where users want fine-granted control and manipulate conflicts in a much deeper way, the Arbiter will be the tool for that.

In our next post, we will show an example of the implementation of that using also offline first new NunDB js capabilities, which will show the power of conflict resolution in more complex conditions, dealing with UI react components and others.

Written on January 12, 2023