Skip to content

Commit

Permalink
Ecto.Repo.transaction support
Browse files Browse the repository at this point in the history
  • Loading branch information
JesseStimpson committed Mar 30, 2024
1 parent 132825c commit 8a94dcc
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 26 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ Roughly in order of priority.
- [x] Index caching
- [x] Mapped Range Indexes
- [x] Hierarchical multi index
- [x] Ecto Transactions
- [ ] FDB Watches
- [ ] Ecto Transactions
- [ ] Logging for all Adapter behaviours
- [ ] Composite primary key
36 changes: 31 additions & 5 deletions lib/ecto/adapters/foundationdb.ex
Original file line number Diff line number Diff line change
Expand Up @@ -112,21 +112,36 @@ defmodule Ecto.Adapters.FoundationDB do
### Transactions
`ecto_foundationdb` implements its own transaction API. This was decided early on
to make sure we can support tenants and custom indexes. A transaction always
executes on a single tenant, and so individual Repo calls inside your
transaction do not need to specify a `:prefix`.
`ecto_foundationdb` exposes the Ecto.Repo transaction function, and it also exposes
a FoundationDB-specific transaction. Typically you will use the Ecto.Repo transaction.
```elixir
TestRepo.transaction(fn ->
# Ecto work
end, prefix: tenant)
```
```elixir
FoundationDB.transactional(tenant,
fn tx ->
# :erlfdb work
end)
```
### Migrations
At first glance, `:ecto_foundationdb` migrations may look similar to that of `:ecto_sql`,
but the actual execution of migrations and how your app must be configured are very
differently, so please read this section in full.
different, so please read this section in full.
If your app uses indexes on any of your schemas, you must define a `:migrator`
option on your repo that is a module implementing the `Ecto.Adapters.FoundationDB.Migrator`
behaviour.
Migrations are only used for index management. There are no tables to add, drop, or modify.
When you add a field to your Schema, any read requests on old objects will return `nil` in
that field. Right now there is no way to rename a field.
As tenants are opened during your application runtime, migrations will be executed
automatically. This distributes the migration across a potentially long period of time,
as migrations will not be executed unless the tenant is opened.
Expand Down Expand Up @@ -204,6 +219,7 @@ defmodule Ecto.Adapters.FoundationDB do
@behaviour Ecto.Adapter.Storage
@behaviour Ecto.Adapter.Schema
@behaviour Ecto.Adapter.Queryable
@behaviour Ecto.Adapter.Transaction
@behaviour Ecto.Adapters.FoundationDB.Migration

alias Ecto.Adapters.FoundationDB.Database
Expand All @@ -212,6 +228,7 @@ defmodule Ecto.Adapters.FoundationDB do
alias Ecto.Adapters.FoundationDB.EctoAdapterQueryable
alias Ecto.Adapters.FoundationDB.EctoAdapterSchema
alias Ecto.Adapters.FoundationDB.EctoAdapterStorage
alias Ecto.Adapters.FoundationDB.EctoAdapterTransaction
alias Ecto.Adapters.FoundationDB.Layer.Tx
alias Ecto.Adapters.FoundationDB.Options
alias Ecto.Adapters.FoundationDB.Tenant
Expand Down Expand Up @@ -325,6 +342,15 @@ defmodule Ecto.Adapters.FoundationDB do
defdelegate stream(adapter_meta, query_meta, query_cache, params, options),
to: EctoAdapterQueryable

@impl Ecto.Adapter.Transaction
defdelegate transaction(adapter_meta, options, function), to: EctoAdapterTransaction

@impl Ecto.Adapter.Transaction
defdelegate in_transaction?(adapter_meta), to: EctoAdapterTransaction

@impl Ecto.Adapter.Transaction
defdelegate rollback(adapter_meta, value), to: EctoAdapterTransaction

@impl Ecto.Adapters.FoundationDB.Migration
defdelegate supports_ddl_transaction?(), to: EctoAdapterMigration

Expand Down
56 changes: 56 additions & 0 deletions lib/ecto/adapters/foundationdb/ecto_adapter_transaction.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
defmodule Ecto.Adapters.FoundationDB.EctoAdapterTransaction do
@moduledoc """
Implemenation of Ecto.Adapter.Transaction
"""
alias Ecto.Adapters.FoundationDB
alias Ecto.Adapters.FoundationDB.Layer.Tx
@behaviour Ecto.Adapter.Transaction

@rollback :__ectofdbtxrollback__

@doc """
Runs the given function inside a transaction.
Returns `{:ok, value}` if the transaction was successful where `value`
is the value return by the function or `{:error, value}` if the transaction
was rolled back where `value` is the value given to `rollback/1`.
"""
@impl true
def transaction(_adapter_meta, options, function) when is_function(function, 0) do
FoundationDB.transactional(options[:prefix], fn ->
function.()
end)
catch
{@rollback, value} -> {:error, value}
end

def transaction(_adapter_meta, options, function) when is_function(function, 1) do
FoundationDB.transactional(options[:prefix], fn repo ->
function.(repo)
end)
catch
{:__ectofdbtxrollback__, value} -> {:error, value}
end

@doc """
Returns true if the given process is inside a transaction.
"""
@impl true
def in_transaction?(_adapter_meta) do
Tx.in_tx?()
end

@doc """
Rolls back the current transaction.
The transaction will return the value given as `{:error, value}`.
See `c:Ecto.Repo.rollback/1`.
"""
@impl true
def rollback(adapter_meta, value) do
if in_transaction?(adapter_meta) do
throw({@rollback, value})
end
end
end
5 changes: 1 addition & 4 deletions lib/ecto/adapters/foundationdb/layer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,7 @@ defmodule Ecto.Adapters.FoundationDB.Layer do
...> Repo.insert!(%User{name: "John"})
...> Repo.insert!(%Event{timestamp: ~N[2024-02-18 12:34:56], data: "Welcome John"})
...> end
iex> FoundationDB.transactional(tenant, fun)
For now, this Transaction lives separate from Ecto's own Transaction, so please be mindful when using
this feature to use `Ecto.Adapters.FoundationDB.transactional`.
iex> Repo.transaction(fun, prefix: tenant)
"""
end
4 changes: 1 addition & 3 deletions lib/ecto/adapters/foundationdb/migrator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,7 @@ defmodule Ecto.Adapters.FoundationDB.Migrator do
opts
|> Keyword.put(:log, migrator_log(opts))

FoundationDB.transactional(tenant, fn ->
tx_do_up(repo, config, version, module, opts)
end)
repo.transaction(fn -> tx_do_up(repo, config, version, module, opts) end, prefix: tenant)
end

def tx_do_up(repo, config, version, module, opts) do
Expand Down
24 changes: 12 additions & 12 deletions test/ecto/integration/crud_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -79,18 +79,17 @@ defmodule Ecto.Integration.CrudTest do

# Crossing a struct into another tenant is not allowed when using a transaction.
assert_raise(IncorrectTenancy, ~r/original transaction context .* did not match/, fn ->
FoundationDB.transactional(
other_tenant,
TestRepo.transaction(
fn ->
TestRepo.insert(user)
end
end,
prefix: other_tenant
)
end)

# Here are 2 ways to force a struct into a different tenant's transaction
assert :ok =
FoundationDB.transactional(
other_tenant,
TestRepo.transaction(
fn ->
# Specify the equivalent tenant
%User{user | id: nil}
Expand All @@ -103,7 +102,8 @@ defmodule Ecto.Integration.CrudTest do
|> TestRepo.insert()

:ok
end
end,
prefix: other_tenant
)
end

Expand Down Expand Up @@ -201,8 +201,7 @@ defmodule Ecto.Integration.CrudTest do
# Operations inside a FoundationDB Adapater Transaction have the tenant applied
# automatically.
user =
FoundationDB.transactional(
tenant,
TestRepo.transaction(
fn ->
{:ok, jesse} =
%User{name: "Jesse"}
Expand All @@ -213,7 +212,8 @@ defmodule Ecto.Integration.CrudTest do
|> TestRepo.insert()

TestRepo.get(User, jesse.id)
end
end,
prefix: tenant
)

assert user.name == "Jesse"
Expand All @@ -224,13 +224,13 @@ defmodule Ecto.Integration.CrudTest do

names = ~w/John James Jesse Sarah Bob Steve/

FoundationDB.transactional(
tenant,
TestRepo.transaction(
fn ->
for n <- names do
TestRepo.insert(%User{name: n})
end
end
end,
prefix: tenant
)

# Each chunk of the stream is retrieved in a separate FDB transaction
Expand Down

0 comments on commit 8a94dcc

Please sign in to comment.