Concurrent read/writes and locking

Hi - we’re building Dragginz, a big virtual pet game on the IC (from the founders of Neopets etc.)

We have a data structure called Item which looks like this

  public type Item = {
    name : Text;
    bulk : BulkVariant;
    maxStack : ItemStack;
    quality : QualityVariant;
    secret : Secret;
    flavourText : ?Text;
    rarity : ?RarityVariant;
    scarcity : ?Scarcity;
    uses : ?Dice;
    attributes : [ItemAttribute];
    live : OneToOne<ItemLive>;
    icon : ManyToOne<Icon>;
    model : ManyToOne<Model>;
  };
  public type ItemMUT = {
    var name : Text;
    var bulk : BulkVariant;
    var quality : QualityVariant;
    var secret : Secret;
    var maxStack : ItemStack;
    var flavourText : ?Text;
    var rarity : ?RarityVariant;
    var scarcity : ?Scarcity;
    var uses : ?DiceMUT;
    var attributes : [ItemAttributeMUT];
    var live : OneToOne<ItemLive>;
    var icon : ManyToOne<Icon>;
    var model : ManyToOne<Model>;
  };

and I have “live” mutable data stored in the ItemLive type. This records every time an Item is created in-game, so for instance there can only ever be 10 copies of a certain magic sword.

  public type ItemLive = {
    item : OneToOne<Item>;
    created : Nat;
    destroyed : Nat;
  };
  public type ItemLiveMUT = {
    item : OneToOne<Item>;
    var created : Nat;
    var destroyed : Nat;
  };

Item is a definition of an item, so probably a handful of changes every week. Millions of reads every second. ItemLive however, millions of writes a second.

I’ve split them because that’s what I would do on a traditional RDBMS (not saying that’s the correct approach, but what I’ve done in the past). Do I need to do this on the IC? Are there any read/write locks that are going to impact performance?

1 Like

I don’t think you need to split it, if the reason is performance. (You may want to for other reasons, e.g. you need a shared type and var fields aren’t permitted in shared types.)

Canister updates (i.e. writes) are atomic, provided your canister update function doesn’t call other canisters. As long as you don’t do that, there will only be one write executing at any given time.

Whereas updates go through subnet consensus (and are thus “locked” to a single writer), canister queries (i.e. reads) happen in parallel on multiple threads. A query first gets routed to a node in a subnet, and then gets handled by a query thread on that node. These query calls run against the snapshot of memory recorded in the last finalized state of the canister. Since state updates are atomic and don’t commit their changes until a commit point (like return) is reached, there isn’t a problem of isolation like dirty reads or lost updates.

At least to me, it seems like the IC canister actor model is in effect similar to a single-writer, multi-reader lock. Please correct me if I’m wrong.

This might be a helpful read.

1 Like

Yes, thanks. I was overthinking it. I’m still going to split them up but there’s not a technical reason to do so, it’s just separation of concerns.

I feel a bit uneasy when you’ve got two different uses like that in the same record type.

1 Like