Serialized access to instance of resource for `AshPostgres`
Pursuant to the discussion in #general , I'd like to create a macro which uses the
changes
hook to ensure that change actions to an instance (row) of a resource are serialized. For now I'm only looking at Postgres support, but if the lock
primitive comes into play, happy to update. Primary changes are the addition of a definitely-not-working locking statement and or changeset.type == :read
to not block read
actions.
Usage:
Two things:
- This relies on the changeset and its children happening inside a transaction. Is that the case?
- I'm using the resource as if it's an Ecto schema. This is...maybe not legit. The Ash changeset (or parts of it) will end up as an Ecto changeset at some point, though. Is there an escape hatch I can use here to inject this or raw SQL into the changeset?39 Replies
I don’t think the macro is necessary honestly. You can define a module and refer to it.
change SerializeAccess
And then use Ash.Resource.Change
In that module.Sure, I'm happy to do it that way as well.
Some notes/answers:
1. Changesets will never have an action type of
:read
. Queries are used for that, and go through a different flow.
2. By default global changes only happen on create/update. To include destroy, do change Module, on: [:create, :update, :destroy]
3. By default all create/update/destroy actions happen in a transaction. Anything nested is therefore, by default, also in a transaction.
4. You can’t really inject raw sql into the changeset, but you can make the action “manual” which gives you an Ash changeset and leaves you to your own devices on how to actually do the action (all hooks/changes still happen around it though)1. Makes sense.
2. Good note, will do.
3. Great!
4. Alright, so I guess this means implementing
Manual{Create,Update,Destroy}
to do the locking? Does this mix with non-manual things inside a single transaction? Can I just do:
well
yes and no
a couple things
You'll get an Ash changeset with all of those attributes set, yes
in your manual action
but you can't do what you're doing there with
code = generate_code()
that block of code is all happening at compile time
You'd need to do something like
change SetCode
and generate code in the change functionRight, ok. Same for
SendEmail
.So is the
enter_email
action actually a create? like its creating a new user or something like that?Although...how do I give the code to
SendEmail
?
It's an entrypoint into that persistent flow, so it's a create.So what do you need to serialize in that case?
There won't be anything in the db to lock
Ok sure, for the following steps.
verify_code
, enter_password
, etc.Yeah.
Anyway, if you wanted to do something like first generate a code, and then send an email with that code (FYI we have
ash_authentication
if you are building an authentication system)
I used anonymous functions in that example just for brevity
but you can do that or modules
You can also set context on the changeset and pick it up in subsequent changes
Ash.Changeset.set_context/2
and changeset.context...
Right, with you so far.
Other than that, I don't think you necessarily need a manual action for your locking
You don't want to lock when you do the update anyway, you want to lock before that, right?
So if you have a change, that locks the row before_action, then you ought to be good to go.
Under a lot of circumstances, I'd agree, but the goal is to be able to use this for creating an FSM. Without
select ... for update
multiple transactions will be able to run over each other.
Postgres won't enforce the FSM invariants without a little bit of help.Yeah but you can issue that select in a before action hook
Or are you planning on doing the create action thing we mentioned to simulate it
Where everything is a manual create action
No, hopefully not!
I guess that's what I was trying to do at the top, no?
But the manual action is like…taking over the actual write to the db
It replaces our insert statement
I think the piece missing is how to do a
select ... for update
with Ash. I know how to do it with Ecto, but I didn't see an equivalent in the Ash docs yet.Yeah, there really isn’t one
You should just use ecto for that
Given the conversation before about
lock
I figured it wasn't there.
Ok, so a before_action
hook would be more appropriate for this than a manual
?
How does one issue an Ecto query in a before_action
hook? 😁You can just run an ecto query as normal
Like in the before action hook do a query with a lock as if you were just using ecto
I think that's what I was doing up top.
Could probably get rid of
__CALLER__.module
and repo:
if this is somewhere in the context/changeset passed to before_action
.Yeah, I don’t think I had any problem with that, was just advising against using a macro for it
Because any change can add a before action and after action hook
So the macro isn’t really helping
Ok, so the strategy makes sense, but I can replace the macro with a
change
that adds a before_action
hook?
So if you had that (no need to serialize a create).
Didn’t write the whole thing out cuz I’m on my phone now 😂
No worries, mate, I appreciate the time.
Can I just stick this in there, though?
Is an
Ash.Resource
also an Ecto.Schema
?
Forget the unquote
stuff ofc.
Or where in the changeset can I extract the Ecto.Schema
and Repo
?Gold.
I forget if thats how you actually do ecto locking but otherwies that should work
It's right, but yeah need the prefix.
are you doing multitenant stuff?
or jsut storing your data in the non-public schema
Multitenant in nature, yes, but can't use the multitenancy in Ash because users can cross tenant boundaries.
in what way?
Just users specifically?
Or like in general you'll be mixing tenant data?
Think GH; there are organizations whose data is siloed, which would make a lot of sense to use at least attribute-based MT, but user accounts have data attached which is outside the siloes and can be members of multiple orgs.
You can generally mix and match tenant and non-tenant data
but thats a conversation for another time 🙂
I'm going to hit the sack
Well, I guess
is that still an unanswered question for you? Like how to set the query prefix?
Oh no, I was just saying you need
Ecto.Query.
before lock
because it's not going to be in scope in that context.oh
haha
okay
gotcha
Thanks a lot for all the answers mate, have a good rest!
My pleasure. Have a good one!