Ash 3.0 Teasers!

Ash 3.0 Teaser #3: Better Defaults, Less Surprises, Part 1

I meant to make this post a few days ago, but I got a bit busy handling a heisenbug that a user found when using aggregates in policies :bug:. That is all sorted now so I can focus on making progress on 3.0 again! :partying_face:

You may also notice that, from here on out, we will be exchanging the term api with domain. See teaser #2 for more :slight_smile:

A big part of the purpose of 3.0 is changing default behaviors. Ash 2.0 had a lot of “permissive” defaults that could make things extremely quick to get started but could easily bite you later down the road. Ultimately, with Ash, we care more about “year five” than “day one”, so we don’t really want to make design choices that will be a foot-gun later down the road for the sake of easy initial adoption.

There are a lot of these changes, so this will be a two parter!

domain.authorization.authorize now defaults to :by_default

The original default was :when_requested, which would cause authorization to trigger when an actor option was provided, or when authorize?: true was set. This makes it very easy to accidentally forget to authorize an action invocation. :by_default always sets authorize? true (has no effect if you are not using authorizers), unless you explicitly say authorize?: false

We’ve known for a very long time that this was not the ideal default behavior, but it was a significant breaking change and so had to wait for 3.0. You can revert to the old behavior (but should eventually update) by setting the value in each domain module back to :when_requested.

unknown action inputs now produce errors

When passing parameters to actions, we would previously ignore unknown parameters. This made it very easy to misspell an input and not realize it, or otherwise believe that an input was being used when it wasn’t.

Bulk update/destroy strategy defaults to :atomic

When calling a bulk update/destroy, it may not be able to be done atomically (i.e one single UPDATE query + after hooks). You can specify a list of allowed strategies when calling a bulk action. The strategies available are

  • atomic - Must be doable as a single operation at the data layer (plus after action/after batch logic)
  • atomic_batches - Can be a series of atomic operations (as above). This can be great for massive inputs where you want to do them in batches. What we do is stream the provided query or list into batches, and update by primary key as a single atomic operation.
  • stream - We stream each record and do the logic to update each record one at a time.

Ash will use the “best” strategy that it can use (i.e the first one that can be used in the list above starting at the top). In 2.0, the default for the strategy option is [:atomic, :atomic_batches, :stream], allowing all three by default.

In 3.0 the default for the strategy option is [:atomic]. This is in line with making defaults something that will help you choose the safest option. If an action cannot be done atomically, you will be told a reason, and you can adjust the strategy option or modify the action.

require_atomic? on update/destroy actions defaults to true

When writing actions, we want them to be concurrency safe. What this means is that no update/destroy action will be performed non atomically (in this context, to be done atomically means that all changes, validations, and attribute changes can be done atomically. You will get a warning at compile time if your action is known not to be able to be done atomically, and an error at runtime if you attempt to run them.

For example, what might happen is something like this:

update :update do
  # I add an anonymous function change
  change fn changeset, _ -> 
    Ash.Changeset.change_attribute(changeset, :attribute, :value)
  end
end

I get an error at compile time, like this:

warning: [Resource]
 actions -> update:
  update cannot be done atomically, because the changes `[Ash.Resource.Change.Function]` cannot be done atomically

So I adjust the action to use a builtin change that has an atomic implementation. I could also extract the logic into its own module and use Ash.Resource.Change and add the atomic/2 callback.

update :update do
  # I add an anonymous function change
  change set_attribute(:attribute, :value)
end

And now we’re good! I can also add require_atomic? false to the action if I know that the changes on this action are safe to run non-atomically.

update :update do
  require_atomic? false
  
  # I add an anonymous function change
  change fn changeset, _ -> 
    Ash.Changeset.change_attribute(changeset, :attribute, :value)
  end
end

Closing

This one was a lot, and there are more to come! Keep in mind that not all breaking changes will be included in these teasers, but the goal is to include all major/significant changes. Can’t wait for 3.0 to get out there.

Teaser #4: Ash 3.0 Teasers! - #28 by zachdaniel

22 Likes