diff --git a/guides/introduction/packages_glossary.md b/guides/introduction/packages_glossary.md index f066943fe4..f42870f9cd 100644 --- a/guides/introduction/packages_glossary.md +++ b/guides/introduction/packages_glossary.md @@ -33,6 +33,9 @@ You will also work with the following: * [Swoosh](https://hexdocs.pm/swoosh) - a library for composing, delivering and testing emails, also used by `mix phx.gen.auth` + * [Oban](https://hexdocs.pm/oban) - async, distributed background job + system that uses SQL for job persistence + When peeking under the covers, you will find these libraries play an important role in Phoenix applications: diff --git a/installer/lib/mix/tasks/phx.new.ex b/installer/lib/mix/tasks/phx.new.ex index dc4807b652..63af0ad5a0 100644 --- a/installer/lib/mix/tasks/phx.new.ex +++ b/installer/lib/mix/tasks/phx.new.ex @@ -58,6 +58,11 @@ defmodule Mix.Tasks.Phx.New do * `--no-mailer` - do not generate Swoosh mailer files + * `--no-oban` — do not include Oban for async background jobs. + Oban requires `ecto`, and either `postgres` or `sqlite3` for job + persistence. Oban won't be included if the `--no-ecto` flag or an + incompatible database is selected + * `--no-tailwind` - do not include tailwind dependencies and assets. The generated markup will still include Tailwind CSS classes, those are left-in as reference for the subsequent styling of your layout @@ -137,6 +142,7 @@ defmodule Mix.Tasks.Phx.New do verbose: :boolean, live: :boolean, dashboard: :boolean, + oban: :boolean, install: :boolean, prefix: :string, mailer: :boolean, diff --git a/installer/lib/phx_new/generator.ex b/installer/lib/phx_new/generator.ex index b0c0a9f0d1..b420efc251 100644 --- a/installer/lib/phx_new/generator.ex +++ b/installer/lib/phx_new/generator.ex @@ -188,6 +188,12 @@ defmodule Phx.New.Generator do {adapter_app, adapter_module, adapter_config} = get_ecto_adapter(db, String.downcase(project.app), project.app_mod) + # Oban requires ecto, and is only compatible with `postgres` + # or `sqlite3` adapters. + oban = ecto and adapter_app in ~w(postgrex ecto_sqlite3)a and Keyword.get(opts, :oban, true) + + oban_engine = get_oban_engine(adapter_app) + {web_adapter_app, web_adapter_vsn, web_adapter_module, web_adapter_docs} = get_web_adapter(web_adapter) pubsub_server = get_pubsub_server(project.app_mod) @@ -228,6 +234,8 @@ defmodule Phx.New.Generator do live: live, live_comment: if(live, do: nil, else: "// "), dashboard: dashboard, + oban: oban, + oban_engine: inspect(oban_engine), gettext: gettext, adapter_app: adapter_app, adapter_module: adapter_module, @@ -304,6 +312,10 @@ defmodule Phx.New.Generator do Mix.raise("Unknown database #{inspect(db)}") end + defp get_oban_engine(:postgrex), do: Oban.Engines.Basic + defp get_oban_engine(:ecto_sqlite3), do: Oban.Engines.Lite + defp get_oban_engine(_other), do: :none + defp get_web_adapter("cowboy"), do: {:plug_cowboy, "~> 2.7", Phoenix.Endpoint.Cowboy2Adapter, "https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html"} defp get_web_adapter("bandit"), do: {:bandit, "~> 1.5", Bandit.PhoenixAdapter, "https://hexdocs.pm/bandit/Bandit.html#t:options/0"} defp get_web_adapter(other), do: Mix.raise("Unknown web adapter #{inspect(other)}") diff --git a/installer/lib/phx_new/oban.ex b/installer/lib/phx_new/oban.ex new file mode 100644 index 0000000000..8a419d5fc2 --- /dev/null +++ b/installer/lib/phx_new/oban.ex @@ -0,0 +1,26 @@ +defmodule Phx.New.Oban do + @moduledoc false + + use Phx.New.Generator + + alias Phx.New.Project + + template(:new, [ + {:eex, :app, + "phx_oban/oban.ex": "lib/:app/oban.ex", + "phx_oban/migration.exs": "priv/repo/migrations/0_add_oban_tables.exs"} + ]) + + def prepare_project(%Project{} = project) do + app_path = Path.expand(project.base_path) + project_path = Path.dirname(Path.dirname(app_path)) + + %Project{project | in_umbrella?: true, app_path: app_path, project_path: project_path} + end + + def generate(%Project{} = project) do + inject_umbrella_config_defaults(project) + copy_from(project, __MODULE__, :new) + project + end +end diff --git a/installer/lib/phx_new/project.ex b/installer/lib/phx_new/project.ex index da093e465a..9ef916f8de 100644 --- a/installer/lib/phx_new/project.ex +++ b/installer/lib/phx_new/project.ex @@ -65,6 +65,10 @@ defmodule Phx.New.Project do Keyword.fetch!(binding, :mailer) end + def oban?(%Project{binding: binding}) do + Keyword.fetch!(binding, :oban) + end + def verbose?(%Project{opts: opts}) do Keyword.get(opts, :verbose, false) end diff --git a/installer/lib/phx_new/single.ex b/installer/lib/phx_new/single.ex index 989c4d7255..db6cc21a16 100644 --- a/installer/lib/phx_new/single.ex +++ b/installer/lib/phx_new/single.ex @@ -98,6 +98,12 @@ defmodule Phx.New.Single do {:eex, :app, "phx_mailer/lib/app_name/mailer.ex": "lib/:app/mailer.ex"} ]) + template(:oban, [ + {:eex, :app, + "phx_oban/oban.ex": "lib/:app/oban.ex", + "phx_oban/migration.exs": "priv/repo/migrations/0_add_oban_tables.exs"} + ]) + def prepare_project(%Project{app: app, base_path: base_path} = project) when not is_nil(app) do if in_umbrella?(base_path) do %Project{project | in_umbrella?: true, project_path: Path.dirname(Path.dirname(base_path))} @@ -138,6 +144,7 @@ defmodule Phx.New.Single do if Project.html?(project), do: gen_html(project) if Project.mailer?(project), do: gen_mailer(project) if Project.gettext?(project), do: gen_gettext(project) + if Project.oban?(project), do: gen_oban(project) gen_assets(project) project @@ -177,4 +184,8 @@ defmodule Phx.New.Single do def gen_mailer(%Project{} = project) do copy_from(project, __MODULE__, :mailer) end + + def gen_oban(%Project{} = project) do + copy_from(project, __MODULE__, :oban) + end end diff --git a/installer/templates/phx_ecto/data_case.ex b/installer/templates/phx_ecto/data_case.ex index 3ec498c660..3a5ab5ae54 100644 --- a/installer/templates/phx_ecto/data_case.ex +++ b/installer/templates/phx_ecto/data_case.ex @@ -20,6 +20,8 @@ defmodule <%= @app_module %>.DataCase do quote do alias <%= @app_module %>.Repo + use Oban.Testing, repo: <%= @app_module %>.Repo + import Ecto import Ecto.Changeset import Ecto.Query diff --git a/installer/templates/phx_oban/migration.exs b/installer/templates/phx_oban/migration.exs new file mode 100644 index 0000000000..e40de4518e --- /dev/null +++ b/installer/templates/phx_oban/migration.exs @@ -0,0 +1,13 @@ +defmodule <%= @app_module %>.Repo.Migrations.AddObanTables do + use Ecto.Migration + + def up do + Oban.Migration.up(version: 12) + end + + # We specify `version: 1` in `down`, ensuring that we'll roll all the way back down if + # necessary, regardless of which version we've migrated `up` to. + def down do + Oban.Migration.down(version: 1) + end +end diff --git a/installer/templates/phx_oban/oban.ex b/installer/templates/phx_oban/oban.ex new file mode 100644 index 0000000000..ee6d64eedd --- /dev/null +++ b/installer/templates/phx_oban/oban.ex @@ -0,0 +1,3 @@ +defmodule <%= @app_module %>.Oban do + use Oban, otp_app: :<%= @app_name %> +end diff --git a/installer/templates/phx_single/config/config.exs b/installer/templates/phx_single/config/config.exs index 97c1bbf53b..ec86b633ad 100644 --- a/installer/templates/phx_single/config/config.exs +++ b/installer/templates/phx_single/config/config.exs @@ -31,7 +31,20 @@ config :<%= @app_name %>, <%= @endpoint_module %>, # # For production it's recommended to configure a different adapter # at the `config/runtime.exs`. -config :<%= @app_name %>, <%= @app_module %>.Mailer, adapter: Swoosh.Adapters.Local<% end %><%= if @javascript do %> +config :<%= @app_name %>, <%= @app_module %>.Mailer, adapter: Swoosh.Adapters.Local<% end %><%= if @oban do %> + +# Configures Oban for background jobs +# +# A single `default` queue is configured to run 10 jobs concurrently, +# and executed jobs are retained for an hour before deletion. +config :<%= @app_name %>, <%= @app_module %>.Oban, + engine: <%= @oban_engine %>, + repo: <%= @app_module %>.Repo, + queues: [default: 10], + plugins: [ + {Oban.Plugins.Pruner, max_age: 3_600}, + {Oban.Plugins.Cron, crontab: []} + ]<% end %><%= if @javascript do %> # Configure esbuild (the version is required) config :esbuild, diff --git a/installer/templates/phx_single/config/test.exs b/installer/templates/phx_single/config/test.exs index c880db8b9e..dc9f5eb1dc 100644 --- a/installer/templates/phx_single/config/test.exs +++ b/installer/templates/phx_single/config/test.exs @@ -12,7 +12,10 @@ config :<%= @app_name %>, <%= @app_module %>.Mailer, adapter: Swoosh.Adapters.Test # Disable swoosh api client as it is only required for production adapters -config :swoosh, :api_client, false<% end %> +config :swoosh, :api_client, false<% end %><%= if @oban do %> + +# In test we don't run queues, plugins, or execute jobs +config :<%= @app_name %>, <%= @app_module %>.Oban, testing: :manual<% end %> # Print only warnings and errors during test config :logger, level: :warning diff --git a/installer/templates/phx_single/lib/app_name/application.ex b/installer/templates/phx_single/lib/app_name/application.ex index 65aad283e9..1723edad69 100644 --- a/installer/templates/phx_single/lib/app_name/application.ex +++ b/installer/templates/phx_single/lib/app_name/application.ex @@ -12,7 +12,8 @@ defmodule <%= @app_module %>.Application do <%= @app_module %>.Repo,<% end %><%= if @adapter_app == :ecto_sqlite3 do %> {Ecto.Migrator, repos: Application.fetch_env!(<%= inspect(String.to_atom(@app_name)) %>, :ecto_repos), - skip: skip_migrations?()},<% end %> + skip: skip_migrations?()},<% end %><%= if @oban do %> + <%= @app_module %>.Oban,<% end %> {DNSCluster, query: Application.get_env(<%= inspect(String.to_atom(@app_name)) %>, :dns_cluster_query) || :ignore}, {Phoenix.PubSub, name: <%= @app_module %>.PubSub}, # Start a worker by calling: <%= @app_module %>.Worker.start_link(arg) diff --git a/installer/templates/phx_single/mix.exs b/installer/templates/phx_single/mix.exs index ed631a2521..241120823b 100644 --- a/installer/templates/phx_single/mix.exs +++ b/installer/templates/phx_single/mix.exs @@ -46,7 +46,8 @@ defmodule <%= @app_module %>.MixProject do # TODO bump on release to {:phoenix_live_view, "~> 1.0.0"}, {:phoenix_live_view, "~> 1.0.0-rc.1", override: true}, {:floki, ">= 0.30.0", only: :test},<% end %><%= if @dashboard do %> - {:phoenix_live_dashboard, "~> 0.8.3"},<% end %><%= if @javascript do %> + {:phoenix_live_dashboard, "~> 0.8.3"},<% end %><%= if @oban do %> + {:oban, "~> 2.18"},<% end %><%= if @javascript do %> {:esbuild, "~> 0.8", runtime: Mix.env() == :dev},<% end %><%= if @css do %> {:tailwind, "~> 0.2", runtime: Mix.env() == :dev}, {:heroicons, diff --git a/installer/templates/phx_umbrella/apps/app_name/mix.exs b/installer/templates/phx_umbrella/apps/app_name/mix.exs index 5ba7b9323e..3b676e90ae 100644 --- a/installer/templates/phx_umbrella/apps/app_name/mix.exs +++ b/installer/templates/phx_umbrella/apps/app_name/mix.exs @@ -42,7 +42,8 @@ defmodule <%= @app_module %>.MixProject do {:<%= @adapter_app %>, ">= 0.0.0"}, {:jason, "~> 1.2"}<% end %><%= if @mailer do %>, {:swoosh, "~> 1.16"}, - {:req, "~> 0.5.4"}<% end %> + {:req, "~> 0.5.4"}<% end %><%= if @oban do %>, + {:oban, "~> 2.18"}<% end %> ] end diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs index d33502a688..10a4c89bb8 100644 --- a/installer/test/phx_new_test.exs +++ b/installer/test/phx_new_test.exs @@ -284,6 +284,32 @@ defmodule Mix.Tasks.Phx.NewTest do "config :swoosh, api_client: Swoosh.ApiClient.Req" end) + # Oban + assert_file("phx_blog/mix.exs", fn file -> + assert file =~ "{:oban, \"~> 2.18\"}" + end) + + assert_file("phx_blog/lib/phx_blog/oban.ex", fn file -> + assert file =~ "defmodule PhxBlog.Oban do" + assert file =~ "use Oban, otp_app: :phx_blog" + end) + + assert_file("phx_blog/config/config.exs", fn file -> + assert file =~ "config :phx_blog, PhxBlog.Oban," + assert file =~ "engine: Oban.Engines.Basic" + assert file =~ "repo: PhxBlog.Repo" + end) + + assert_file("phx_blog/config/test.exs", fn file -> + assert file =~ "config :phx_blog, PhxBlog.Oban, testing: :manual" + end) + + assert_file("phx_blog/priv/repo/migrations/0_add_oban_tables.exs", fn file -> + assert file =~ "defmodule PhxBlog.Repo.Migrations.AddObanTables" + assert file =~ "Oban.Migration.up(version: 12)" + assert file =~ "Oban.Migration.down(version: 1)" + end) + # Install dependencies? assert_received {:mix_shell, :yes?, ["\nFetch and install dependencies?"]}