Atomic operators for database writes?

Coming from MongoDB, one of the nice things I discovered were the atomic operators. For example:

An excerpt example from the MongoDB docs:

Consider a collection with this document:

db.games.insertOne( { _id: 1, score: 80 } )

Consider if these update operations occur concurrently:

// Update A
db.games.updateOne(
   { _id: 1 },
   {
      $inc: { score: 10 }
   }
)
// Update B
db.games.updateOne(
   { _id: 1 },
   {
      $inc: { score: 20 }
   }
)

After one update operation completes, the remaining operation still matches the query predicate { _id: 1 }. However, because the operations modify the current value of score, they don’t overwrite each other. Both updates are reflected and the resulting score is 110.


Does Nakama support such atomic operators?

From my understanding, Nakama will error out if conflicting database values are attempted to be written (using an internal version field).

However, the ability to use atomic operators should allow for smoother cases and avoid retrying an entire function for conflicting database writes that could be resolved with atomic operators.

The only thing I found in Nakama that has a similar approach is Virtual wallet, but I need it for other cases too.

Hello @KamilDev,

As you mention, Nakama uses Optimistic Concurrency Control (OCC) making use of the version field. If the update tries to overwrite an “unseen” change, it’ll fail.

There’s no concept of atomic operators, if you’d like to achieve something similar it is possible but you’d have to use custom SQL.

Example:

INSERT INTO 
    storage AS s (collection, "key", user_id, "value", version, "write")
VALUES
    ('counter', 'count', $1, '{"count":1}'::JSONB, md5('{"count":1}'::TEXT), 0)
ON CONFLICT
    (collection, "key", user_id)
DO
    UPDATE SET
        version = md5(excluded."value"::TEXT),
        "value" = jsonb_build_object('count', (s."value"->>'count')::INT + 1)

docs: PostgreSQL: Documentation: 16: 9.16. JSON Functions and Operators

The peril of writing custom queries is optimization and correctness, the Nakama provided APIs ensure that indices are used correctly and that queries will perform optimally. The OCC model also avoids exposing transactions because using them incorrectly can also cause severe performance issues at scale. This is why we recommend against writing custom queries unless it’s truly warranted and the queries are sound.

In practice, the OCC model works well because updates for a given user should rarely cause parallel writes to the same row unless the client is acting incorrectly.

For shared objects, one must be careful about frequency of updates - globally shared objects (owned by the global nil user) that are updated very frequently are often a bad idea and can cause issues at scale as they cause a lot of contention on that row.

Best.

Does Nakama support transactions, or what would the suitable way to do it Nakama?

Lets say I have a function that lets players gift an energy point to a different player, and then they also get rewarded 100 gold. Like this:

  1. Get receiver player data
  2. Increment energy by 1, save to DB
  3. Get this current player data
  4. Increment gold by 100, save to DB

If an error happens halfway, how should I handle it? With transactions, I think it would safely discard and revert all the changes, so the function can be retried if needed.

Otherwise, it could end up like the player gifted the energy, but due to error, they never received their 100 gold reward. We can’t simply retry the function as it would give an extra energy point to receiver player.

The multiUpdate API performs updates to multiple objects/wallets/accounts transactionally.