<!-- livebook:{"persist_outputs":true} -->

# Optimistic Locking

```elixir
Mix.install([{:ash, "~> 3.0.0-rc"}], consolidate_protocols: false)
Logger.configure(level: :debug)
```

## What is optimistic locking?

Optimistic Locking is the process of only allowing an update to occur if the version of a record that you have in memory is the same as the version in the database. Otherwise, an error is returned.

Optimistic locking may used for two primary purposes:

### User Experience

For example, if a user is editing a form that contains `State` and `County` fields, and they change the `County`, while another user has used the form to change the `State`, you could end up with a mismatch between `State` and `County`.

With optimistic locking, the user will instead get an error message that the record has been changed since they last looked.

### Concurrency Safety

Optimistic locking can make actions safe to run concurrently even if they can't be performed in a single query(atomically), by returning an error if the resource in the data layer does not have the same version as the one being edited.

This tells the user that they need to reload and try again.

### Want to see how it works

Modify the setup block and configure the log level to debug to see logs from the ETS data layer.

<!-- livebook:{"force_markdown":true} -->

```elixir
Logger.configure(level: :debug)
```

## Define a resource with a version attribute

```elixir
defmodule Address do
  use Ash.Resource,
    domain: Domain,
    data_layer: Ash.DataLayer.Ets

  attributes do
    uuid_primary_key(:id)
    attribute(:version, :integer, allow_nil?: false, default: 1)
    attribute(:state, :string, allow_nil?: false)
    attribute(:county, :string, allow_nil?: false)
  end

  actions do
    defaults([:read, create: [:state, :county]])

    update :update do
      accept([:state, :county])
      change(optimistic_lock(:version))
    end
  end
end

defmodule Domain do
  use Ash.Domain,
    validate_config_inclusion?: false

  resources do
    resource Address do
      define(:get_address, action: :read, get_by: [:id])
      define(:create_address, action: :create, args: [:state, :county])
      define(:update_address, action: :update)
    end
  end
end
```

<!-- livebook:{"output":true} -->

```
{:module, Domain, <<70, 79, 82, 49, 0, 1, 254, ...>>,
 [
   Ash.Domain.Dsl.Resources.Resource,
   Ash.Domain.Dsl.Resources.Options,
   Ash.Domain.Dsl,
   %{opts: [], entities: [...]},
   Ash.Domain.Dsl,
   Ash.Domain.Dsl.Resources.Options,
   ...
 ]}
```

## Trigger a StaleRecord error

```elixir
address = Domain.create_address!("FL", "Pinellas")
Domain.update_address!(address, %{state: "NC", county: "Guilford"})

# `address` still has a version of `1`, so our optimistic lock should catch it!
Domain.update_address(address, %{county: "Miami-Dade"})
```

<!-- livebook:{"output":true} -->

```

06:59:29.507 [debug] Creating Address:

%{
  id: "8b79d3c4-807d-4756-a46e-eae7da020122",
  version: 1,
  state: "FL",
  county: "Pinellas"
}

:error
{:ok,
 %{
   extra: %{},
   name: :if,
   arguments: [
     %{
       left: %{
         attribute: %{
           default: 1,
           name: :version,
           type: Ash.Type.Integer,
           description: nil,
           source: :version,
           __struct__: Ash.Resource.Attribute,
           constraints: [],
           primary_key?: false,
           public?: false,
           sensitive?: false,
           allow_nil?: false,
           update_default: nil,
           writable?: true,
           generated?: false,
           always_select?: false,
           match_other_defaults?: false,
           filterable?: true,
           sortable?: true
         },
         resource: Address,
         __struct__: Ash.Query.Ref,
         simple_equality?: nil,
         relationship_path: [],
         input?: false,
         bare?: nil
       },
       right: 1,
       operator: :==,
       __struct__: Ash.Query.Operator.Eq,
       embedded?: false,
       __predicate__?: true,
       __operator__?: true
     },
     %{
       left: %{
         attribute: %{
           default: 1,
           name: :version,
           type: Ash.Type.Integer,
           description: nil,
           source: :version,
           __struct__: Ash.Resource.Attribute,
           constraints: [],
           primary_key?: false,
           public?: false,
           sensitive?: false,
           allow_nil?: false,
           update_default: nil,
           writable?: true,
           generated?: false,
           always_select?: false,
           match_other_defaults?: false,
           filterable?: true,
           sortable?: true
         },
         resource: Address,
         __struct__: Ash.Query.Ref,
         simple_equality?: nil,
         relationship_path: [],
         input?: false,
         bare?: nil
       },
       right: 1,
       operator: :+,
       __struct__: Ash.Query.Operator.Basic.Plus,
       embedded?: false,
       __predicate__?: false,
       __operator__?: true
     },
     %{
       extra: %{},
       name: :error,
       arguments: [
         Ash.Error.Changes.StaleRecord,
         %{filter: %{version: 1}, resource: "Address"}
       ],
       __struct__: Ash.Query.Function.Error,
       embedded?: false,
       __predicate__?: false,
       __function__?: true
     }
   ],
   __struct__: Ash.Query.Function.If,
   embedded?: false,
   __predicate__?: false,
   __function__?: true
 }}
%{
  extra: %{},
  name: :if,
  arguments: [
    %{
      left: %{
        attribute: %{
          default: 1,
          name: :version,
          type: Ash.Type.Integer,
          description: nil,
          source: :version,
          __struct__: Ash.Resource.Attribute,
          constraints: [],
          primary_key?: false,
          public?: false,
          sensitive?: false,
          allow_nil?: false,
          update_default: nil,
          writable?: true,
          generated?: false,
          always_select?: false,
          match_other_defaults?: false,
          filterable?: true,
          sortable?: true
        },
        resource: Address,
        __struct__: Ash.Query.Ref,
        simple_equality?: nil,
        relationship_path: [],
        input?: false,
        bare?: nil
      },
      right: 1,
      operator: :==,
      __struct__: Ash.Query.Operator.Eq,
      embedded?: false,
      __predicate__?: true,
      __operator__?: true
    },
    %{
      left: %{
        attribute: %{
          default: 1,
          name: :version,
          type: Ash.Type.Integer,
          description: nil,
          source: :version,
          __struct__: Ash.Resource.Attribute,
          constraints: [],
          primary_key?: false,
          public?: false,
          sensitive?: false,
          allow_nil?: false,
          update_default: nil,
          writable?: true,
          generated?: false,
          always_select?: false,
          match_other_defaults?: false,
          filterable?: true,
          sortable?: true
        },
        resource: Address,
        __struct__: Ash.Query.Ref,
        simple_equality?: nil,
        relationship_path: [],
        input?: false,
        bare?: nil
      },
      right: 1,
      operator: :+,
      __struct__: Ash.Query.Operator.Basic.Plus,
      embedded?: false,
      __predicate__?: false,
      __operator__?: true
    },
    %{
      extra: %{},
      name: :error,
      arguments: [
        Ash.Error.Changes.StaleRecord,
        %{filter: %{version: 1}, resource: "Address"}
      ],
      __struct__: Ash.Query.Function.Error,
      embedded?: false,
      __predicate__?: false,
      __function__?: true
    }
  ],
  __struct__: Ash.Query.Function.If,
  embedded?: false,
  __predicate__?: false,
  __function__?: true
}
true
:error

06:59:29.515 [debug] ETS: Updating Address matching filter `id == "8b79d3c4-807d-4756-a46e-eae7da020122"`:

%{
  version: required!(if version == 1 do
    version + 1
  else
    error(
      Ash.Error.Changes.StaleRecord,
      %{filter: %{version: 1}, resource: "Address"}
    )
  end),
  state: "NC",
  county: "Guilford"
}

:error
{:ok,
 %{
   extra: %{},
   name: :if,
   arguments: [
     %{
       left: %{
         attribute: %{
           default: 1,
           name: :version,
           type: Ash.Type.Integer,
           description: nil,
           source: :version,
           __struct__: Ash.Resource.Attribute,
           constraints: [],
           primary_key?: false,
           public?: false,
           sensitive?: false,
           allow_nil?: false,
           update_default: nil,
           writable?: true,
           generated?: false,
           always_select?: false,
           match_other_defaults?: false,
           filterable?: true,
           sortable?: true
         },
         resource: Address,
         __struct__: Ash.Query.Ref,
         simple_equality?: nil,
         relationship_path: [],
         input?: false,
         bare?: nil
       },
       right: 1,
       operator: :==,
       __struct__: Ash.Query.Operator.Eq,
       embedded?: false,
       __predicate__?: true,
       __operator__?: true
     },
     %{
       left: %{
         attribute: %{
           default: 1,
           name: :version,
           type: Ash.Type.Integer,
           description: nil,
           source: :version,
           __struct__: Ash.Resource.Attribute,
           constraints: [],
           primary_key?: false,
           public?: false,
           sensitive?: false,
           allow_nil?: false,
           update_default: nil,
           writable?: true,
           generated?: false,
           always_select?: false,
           match_other_defaults?: false,
           filterable?: true,
           sortable?: true
         },
         resource: Address,
         __struct__: Ash.Query.Ref,
         simple_equality?: nil,
         relationship_path: [],
         input?: false,
         bare?: nil
       },
       right: 1,
       operator: :+,
       __struct__: Ash.Query.Operator.Basic.Plus,
       embedded?: false,
       __predicate__?: false,
       __operator__?: true
     },
     %{
       extra: %{},
       name: :error,
       arguments: [
         Ash.Error.Changes.StaleRecord,
         %{filter: %{version: 1}, resource: "Address"}
       ],
       __struct__: Ash.Query.Function.Error,
       embedded?: false,
       __predicate__?: false,
       __function__?: true
     }
   ],
   __struct__: Ash.Query.Function.If,
   embedded?: false,
   __predicate__?: false,
   __function__?: true
 }}
%{
  extra: %{},
  name: :if,
  arguments: [
    %{
      left: %{
        attribute: %{
          default: 1,
          name: :version,
          type: Ash.Type.Integer,
          description: nil,
          source: :version,
          __struct__: Ash.Resource.Attribute,
          constraints: [],
          primary_key?: false,
          public?: false,
          sensitive?: false,
          allow_nil?: false,
          update_default: nil,
          writable?: true,
          generated?: false,
          always_select?: false,
          match_other_defaults?: false,
          filterable?: true,
          sortable?: true
        },
        resource: Address,
        __struct__: Ash.Query.Ref,
        simple_equality?: nil,
        relationship_path: [],
        input?: false,
        bare?: nil
      },
      right: 1,
      operator: :==,
      __struct__: Ash.Query.Operator.Eq,
      embedded?: false,
      __predicate__?: true,
      __operator__?: true
    },
    %{
      left: %{
        attribute: %{
          default: 1,
          name: :version,
          type: Ash.Type.Integer,
          description: nil,
          source: :version,
          __struct__: Ash.Resource.Attribute,
          constraints: [],
          primary_key?: false,
          public?: false,
          sensitive?: false,
          allow_nil?: false,
          update_default: nil,
          writable?: true,
          generated?: false,
          always_select?: false,
          match_other_defaults?: false,
          filterable?: true,
          sortable?: true
        },
        resource: Address,
        __struct__: Ash.Query.Ref,
        simple_equality?: nil,
        relationship_path: [],
        input?: false,
        bare?: nil
      },
      right: 1,
      operator: :+,
      __struct__: Ash.Query.Operator.Basic.Plus,
      embedded?: false,
      __predicate__?: false,
      __operator__?: true
    },
    %{
      extra: %{},
      name: :error,
      arguments: [
        Ash.Error.Changes.StaleRecord,
        %{filter: %{version: 1}, resource: "Address"}
      ],
      __struct__: Ash.Query.Function.Error,
      embedded?: false,
      __predicate__?: false,
      __function__?: true
    }
  ],
  __struct__: Ash.Query.Function.If,
  embedded?: false,
  __predicate__?: false,
  __function__?: true
}
true
:error

06:59:29.527 [debug] ETS: Updating Address matching filter `id == "8b79d3c4-807d-4756-a46e-eae7da020122"`:

%{
  version: required!(if version == 1 do
    version + 1
  else
    error(
      Ash.Error.Changes.StaleRecord,
      %{filter: %{version: 1}, resource: "Address"}
    )
  end),
  county: "Miami-Dade"
}


```

<!-- livebook:{"output":true} -->

```
{:error,
 %Ash.Error.Invalid{
   changeset: nil,
   query: nil,
   action_input: nil,
   errors: [
     %Ash.Error.Changes.StaleRecord{
       resource: "Address",
       filter: %{"version" => 1},
       splode: Ash.Error,
       bread_crumbs: [],
       vars: [],
       path: [],
       stacktrace: #Splode.Stacktrace<>,
       class: :invalid
     }
   ],
   splode: Ash.Error,
   bread_crumbs: [],
   vars: [],
   path: [],
   stacktrace: #Splode.Stacktrace<>,
   class: :invalid
 }}
```

## Refetching a record to get the latest version

```elixir
address = Domain.create_address!("FL", "Pinellas")
Domain.update_address!(address, %{state: "NC", county: "Guilford"})

case Domain.update_address(address, %{county: "Miami-Dade"}) do
  {:error, %Ash.Error.Invalid{errors: [%Ash.Error.Changes.StaleRecord{}]}} ->
    # In a liveview, you wouldn't jsut refetch and resubmit
    # you would show the user an error and have them submit the form again
    address = Domain.get_address!(address.id)
    Domain.update_address!(address, %{county: "Miami-Dade"})

  {:ok, domain} ->
    domain
end
```

<!-- livebook:{"output":true} -->

```
#Address<
  __meta__: #Ecto.Schema.Metadata<:loaded>,
  id: "f2bdbd9e-4946-4df5-8608-0777900e1b47",
  version: 3,
  state: "NC",
  county: "Miami-Dade",
  aggregates: %{},
  calculations: %{},
  ...
>
```
