Why require_atomic? defaults to true?

Just one use of “manage_relationship” makes action not able to be atomic.
I think the default value of require_atomic? should be “false”.
Why is it “true”?

Actions that cannot be performed atomically have some inherent issues. Specifically they are susceptible to having problems when operating concurrently. For example, if you insert a record, and then in between inserting it and doing some next step, that record has been deleted, you can find yourself in a bad state.

By setting require_atomic? to true, we make it more clear to users that they may need to handle these cases.

Is there any way to use “manage_relationship” with “require_atomic? true”?

No, not currently. There may be, with certain sets of options in the future though.

Ok, then can I think “In the future, most of the actions could be atomic with proper implementation.”?

It really just depends on the action. Being atomic is a property of the action. Some actions can be, some can’t be, it’s entirely based on what the action does.

Hmm… Then I still cannot understand why require_atomic? defaults to true…
I think this would be the last question.

config :ash, :require_atomic_by_default?, false
Is this option “not recommended” or just “up to my choice”? @zachdaniel

I think most apps are

  1. not that simple
  2. has many actions that can’t be atomic

so most apps would use config above.

When this argument is brought up, in a lot of cases it points to poor design. If you cobble code together OFC you will end up with actions that are not atomic because you are always tempted to take the easy way out and do side effects as much as possible.

Happily enough, elixir and the VM limits drastically how many shortcuts of this type you can have by making the values immutable and having a clear concurrency concept.

I think you are not an ash framework user.
You don’t get what “not that simple” means.

Just one use of manage_relationship makes action not able to be atomic.
And manage_relationship is a basic feature in ash,
which is for creating two related resources in one action.

1 Like

“Keep It Simple” is not “Always be simple”.
In complex problems, you can’t stay in simple.

require_atomic? is a safety guard. It’s like marking code as safe or unsafe in another language. It has little to no value if it defaults to false. It is expected that you will have to turn it off relatively often. That is still important though, as when you see that error you should consider if there is anything to be done for it.

This configuration:

config :ash, :require_atomic_by_default?, false, was made only to assist with the upgrade and will be removed in the future. I would not suggest using it at all.

2 Likes

Thanks for your explanation and advice. I really appreciate it.

Writing atomic code is harder, but here is an example of a case where it will pay off significantly:

update :add_ten_to_score do
  change fn changeset, _ -> 
    Ash.Changeset.change_attribute(changeset, :score, changeset.data.score + 10)
  end
end

The action above raises an error about it not being atomic. In this case, the effect here is that if you ran this same update concurrently on the same record, it’s possible to have inconsistent results.

i.e with a record that has a score of 0
Action 1: record is read, current score: 0
Action 2: record is read, current score: 0
Action 1: new score is determined, new score 10
Action 2: new score is determined, new score 10
Action 1: data is written, new score 10
Action 2: data is written, new score 10

So now even though the action was called twice, and you’d expect a new score of 20, you actually have a new score of 10.

By using an action like this:

update :add_ten_to_score do
  change atomic_update(:score, expr(score + 10))
  # or use the built in
  change increment(:score, amount: 10)
end

you now have a fully atomic update that does not have the same problems as described above

I’ve made some tweaks to the atomic update docs to potentially help with clarity. Update Actions — ash v3.3.3

3 Likes

So in a way, its supposed to be slightly annoying, and if it drives folks to understand what it means for an action to be atomic and why it matters then its doing its job :slight_smile:

Its worth highlighting a snippet from those docs though:

Not all actions can reasonably be made atomic, and not all non-atomic actions are problematic for concurrency. The goal is only to make sure that you are aware and have considered the implications.

I am also a race condition worrying guy.
But I think many folks would write a spark dsl for “automatically setting require_atomic? false”,
because require_atomic? true is rare case for most apps. :rofl: