├── README.md ├── tips ├── v2 │ ├── 001_glossary_instance.md │ ├── 002_glossary_node.md │ ├── 004_glossary_queue.md │ ├── 000_reboot.md │ ├── 006_glossary_states.md │ ├── 007_design_registry.md │ ├── 003_glossary_worker.md │ ├── 015_oss_string_keys.md │ ├── 005_glossary_job.md │ ├── 008_oss_ok_return.md │ ├── 011_oss_crash_error.md │ ├── 017_pro_advanced_structured.md │ ├── 010_oss_error_return.md │ ├── 016_pro_structured.md │ ├── 012_oss_stored_errors.md │ ├── 009_pro_recorded.md │ ├── 014_oss_unsaved_errors.md │ └── 013_oss_backoff.md └── v1 │ ├── 001_snooze.md │ ├── 030_crontab_uniquness.md │ ├── 029_crontab_timezones.md │ ├── 024_starting_queues.md │ ├── 025_stopping_queues.md │ ├── 013_graceful_shutdown.md │ ├── 017_replace_args.md │ ├── 009_draining_queues.md │ ├── 019_default_logging.md │ ├── 027_staging_jobs.md │ ├── 012_pausing_queues.md │ ├── 016_unique_jobs.md │ ├── 015_priority.md │ ├── 003_timeout.md │ ├── 028_crontab_extras.md │ ├── 005_contextual_backoff.md │ ├── 004_custom_backoff.md │ ├── 022_using_multis.md │ ├── 010_draining_failures.md │ ├── 014_initially_paused.md │ ├── 023_insert_all_with_multi.md │ ├── 018_unique_keys.md │ ├── 006_assert_enqueued.md │ ├── 008_testing_prefix.md │ ├── 002_discard.md │ ├── 020_error_reporting.md │ ├── 026_pruning_jobs.md │ ├── 011_recording_errors.md │ ├── 007_perform_job.md │ └── 021_customized_logging.md └── LICENSE.txt /README.md: -------------------------------------------------------------------------------- 1 | # Oban Tips 2 | 3 | The aggregated source for the Oban Tips twitter series. 4 | 5 | Images are generated by with [carbon.now.sh](https://carbon.now.sh). 6 | -------------------------------------------------------------------------------- /tips/v2/001_glossary_instance.md: -------------------------------------------------------------------------------- 1 | 📖 Oban Glossary 📖 2 | 3 | An Oban supervision tree is called an "instance," and applications can have multiple instances as long as they have a unique name (e.g. Oban.A, Oban.B, Oban.C) 4 | 5 | #MyElixirStatus #ObanTips 6 | -------------------------------------------------------------------------------- /tips/v2/002_glossary_node.md: -------------------------------------------------------------------------------- 1 | 📖 Oban Glossary 📖 2 | 3 | A "node" is a BEAM host for one or more Oban instances. Nodes don't need to be clustered, but they must have unique names and connect to the same PostgreSQL database. 4 | 5 | #MyElixirStatus #ObanTips 6 | -------------------------------------------------------------------------------- /tips/v2/004_glossary_queue.md: -------------------------------------------------------------------------------- 1 | 📖 Oban Glossary 📖 2 | 3 | Oban segments jobs into named "queues", each of which runs a configurable number of concurrent jobs. Every queue's jobs live in the same oban_jobs PostgreSQL table. 4 | 5 | #MyElixirStatus #ObanTips 6 | -------------------------------------------------------------------------------- /tips/v2/000_reboot.md: -------------------------------------------------------------------------------- 1 | 📣 We're rebooting Oban Tips to refine old tips, cover new features (💎), share architecture notes (🏛️), define mysterious terms (📖), and include bits of Pro (🌟). 2 | 3 | Watch for new tips week daily(ish)! 4 | 5 | #MyElixirStatus #ObanTips 6 | -------------------------------------------------------------------------------- /tips/v1/001_snooze.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #1 — Snooze 💎 2 | 3 | Did you know you can snooze a job to reschedule it for some time in the future? Return a snooze tuple from `perform/1` to back off and run the job again, without any errors. 4 | 5 | #myelixirstatus #elixirlang #obanbg 6 | -------------------------------------------------------------------------------- /tips/v2/006_glossary_states.md: -------------------------------------------------------------------------------- 1 | 📖 Oban Glossary 📖 2 | 3 | Jobs flow through "states" indicating their place in a finite state machine. They start inserted or scheduled, transition to executing, then to an end state, or back to retryable if there are retries available. 4 | 5 | #MyElixirStatus #ObanTips 6 | -------------------------------------------------------------------------------- /tips/v2/007_design_registry.md: -------------------------------------------------------------------------------- 1 | 🏛️ Oban Design 🏛️ 2 | 3 | The Oban application starts a Registry used for process lookup. That makes it possible to find isntances, child processes, and to name an instance any term. 4 | 5 | #MyElixirStatus #ObanTips 6 | 7 | ```elixir 8 | 9 | name = {:local, "name"} 10 | 11 | {:ok, pid} = Oban.start_link(name: name, repo: MyApp.Repo) 12 | 13 | ^pid = Oban.Registry.whereis(name) 14 | 15 | %Oban.Config{} = Oban.Registry.config(name) 16 | 17 | ``` 18 | -------------------------------------------------------------------------------- /tips/v2/003_glossary_worker.md: -------------------------------------------------------------------------------- 1 | 📖 Oban Glossary 📖 2 | 3 | A "worker" module performs one-off tasks, also called "jobs". The worker's perform/1 function receives a job with arguments and executes your application's business logic. 4 | 5 | #MyElixirStatus #ObanTips 6 | 7 | ```elixir 8 | defmodule MyApp.OnboardWorker do 9 | use Oban.Worker 10 | 11 | @impl Oban.Worker 12 | def perform(%{args: %{"user_id" => user_id}}) do 13 | user_id 14 | |> MyApp.fetch_user() 15 | |> MyApp.onboard_user() 16 | end 17 | end 18 | ``` 19 | -------------------------------------------------------------------------------- /tips/v2/015_oss_string_keys.md: -------------------------------------------------------------------------------- 1 | 💎 Oban OSS 💎 2 | 3 | The `args` passed to `perform/1` will always have string keys. That's because `args` are stored as JSON in the database and serialization stringifies all keys. 4 | 5 | https://hexdocs.pm/oban/Oban.Worker.html#c:perform/1 6 | 7 | #MyElixirStatus #ObanTips 8 | 9 | ```elixir 10 | 11 | %{id: 123, account_id: 456} 12 | |> MyApp.Worker.new() 13 | |> Oban.insert() 14 | 15 | def perform(%{args: %{"id" => id, "account_id" => aid}) do 16 | ... 17 | end 18 | 19 | ``` 20 | -------------------------------------------------------------------------------- /tips/v1/030_crontab_uniquness.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #30 — Crontab Uniquness 💎 2 | 3 | Did you know that you can override the unique settings for cron jobs? For 4 | example, using a larger period allows at-most-once-per-n jobs. 5 | 6 | https://hexdocs.pm/oban/Oban.Plugins.Cron.html#content 7 | 8 | #MyElixirStatus #ObanBG 9 | 10 | ```elixir 11 | # Ensure that a job is enqueued at most once a day on reboot 12 | plugins: [ 13 | {Oban.Plugins.Cron, 14 | crontab: [ 15 | {"@reboot", MyApp.AtMostOnceDaily, unique: [period: 86_400]} 16 | ] 17 | } 18 | ] 19 | ``` 20 | -------------------------------------------------------------------------------- /tips/v2/005_glossary_job.md: -------------------------------------------------------------------------------- 1 | 📖 Oban Glossary 📖 2 | 3 | An Oban "job" wraps up the queue name, worker, arguments, state, and other options into a serializable struct (and Ecto schema) persisted as rows in the oban_jobs table. 4 | 5 | #MyElixirStatus #ObanTips 6 | 7 | ```elixir 8 | 9 | %Oban.Job{ 10 | args: %{"account_id" => 123, "agree_to_terms" => true}, 11 | attempt: 1, 12 | max_attempts: 10, 13 | queue: "default", 14 | state: "retryable", 15 | worker: "MyApp.HardWorker", 16 | ... 17 | 18 | Oban.Job 19 | |> where(state: "scheduled") 20 | |> MyApp.Repo.all() 21 | 22 | ``` 23 | -------------------------------------------------------------------------------- /tips/v2/008_oss_ok_return.md: -------------------------------------------------------------------------------- 1 | 💎 Oban OSS 💎 2 | 3 | Workers should return :ok or {:ok, value} from perform/1 to indicate success. Technically, any non-error/snooze/cancel value also works, but you'll get a warning for every job. 4 | 5 | #MyElixirStatus #ObanTips 6 | 7 | ```elixir 8 | 9 | @type result :: :ok | {:ok, ignored :: term()} 10 | 11 | def perform(%Oban.Job{args: args}) do 12 | case args do 13 | %{"action" => "noop"} -> 14 | :ok 15 | 16 | %{"action" => "mult", "int" => int} -> 17 | {:ok, int * int} 18 | 19 | _ -> 20 | {:this, :completes, "but logs a warning"} 21 | end 22 | end 23 | 24 | ``` 25 | -------------------------------------------------------------------------------- /tips/v1/029_crontab_timezones.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #29 — Crontab Timezones 💎 2 | 3 | Did you know that you can set a timezone for your cron schedule? Typically, 4 | crontab entries are evaluated as UTC, but you can override it within the Cron 5 | plugin. 6 | 7 | https://hexdocs.pm/oban/Oban.Plugins.Cron.html#content 8 | 9 | #MyElixirStatus #ObanBG 10 | 11 | ```elixir 12 | # Change the timezone to America/Chicago (GMT-6) 13 | plugins: [ 14 | {Oban.Plugins.Cron, 15 | timezone: "America/Chicago", 16 | crontab: [ 17 | {"0 0 * * *", MyApp.MidnightWorker}, 18 | {"0 8 * * *", MyApp.MorningWorker}, 19 | ] 20 | } 21 | ] 22 | ``` 23 | -------------------------------------------------------------------------------- /tips/v2/011_oss_crash_error.md: -------------------------------------------------------------------------------- 1 | 💎 Oban OSS 💎 2 | 3 | Unhandled thrown values or exits will also fail a job. For consistency, caught values are normalized and wrapped in a CrashError. 4 | 5 | #MyElixirStatus #ObanTips 6 | 7 | 8 | ```elixir 9 | 10 | defmodule MyApp.CrashyWorker do 11 | use Oban.Worker 12 | 13 | @impl Oban.Worker 14 | def perform(%Job{args: %{"crashy" => crashy?) do 15 | if crashy? do 16 | exit(:time_to_crash) 17 | else 18 | :ok 19 | end 20 | end 21 | end 22 | 23 | %Oban.CrashError{ 24 | message: "** (exit) :time_to_crash", 25 | reason: :time_to_crash 26 | } 27 | 28 | ``` 29 | -------------------------------------------------------------------------------- /tips/v1/024_starting_queues.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #24 — Starting Queues 💎 2 | 3 | Did you know that you can start a queue at runtime? With `start_queue/2` you can 4 | start a queue locally or across all connected nodes with one command. 5 | 6 | https://hexdocs.pm/oban/Oban.html#start_queue/2 7 | 8 | #MyElixirStatus #ObanBG 9 | 10 | ```elixir 11 | # Creation is global by default, start a priority queue on all connected nodes: 12 | Oban.start_queue(queue: :priority, limit: 10) 13 | 14 | # Start one queue per account, only on the local node 15 | for account <- MyApp.paying_accounts() do 16 | Oban.start_queue(queue: "account-#{account.id}", limit: 1, local_only: true) 17 | end 18 | ``` 19 | -------------------------------------------------------------------------------- /tips/v1/025_stopping_queues.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #25 — Stopping Queues 💎 2 | 3 | Naturally, as you're able to start queues at runtime you can also stop them at 4 | runtime. Using `stop_queue/2` you can stop a running queue locally or across all 5 | connected nodes. 6 | 7 | https://hexdocs.pm/oban/Oban.html#stop_queue/2 8 | 9 | #MyElixirStatus #ObanBG 10 | 11 | ```elixir 12 | # Stop all "expensive" queues across all connected nodes 13 | Oban.stop_queue(queue: :expensive) 14 | 15 | # Stop all dedicated queues for cancelled accounts on the local node only 16 | for account <- MyApp.cancelled_accounts() do 17 | Oban.stop_queue(queue: "account-#{account.id}", local_only: true) 18 | end 19 | ``` 20 | -------------------------------------------------------------------------------- /tips/v2/017_pro_advanced_structured.md: -------------------------------------------------------------------------------- 1 | 🌟 Oban Pro 🌟 2 | 3 | Oban Pro's `args_schema` also supports convenient extensions such as `enum` for values and `embeds_one/many` for nested maps. 4 | 5 | https://getoban.pro/docs/pro/Oban.Pro.Worker.html#module-structured-jobs 6 | 7 | #MyElixirStatus #ObanTips 8 | 9 | ```elixir 10 | 11 | defmodule MyApp.StructuredWorker do 12 | use Oban.Pro.Worker 13 | 14 | args_schema do 15 | field :uuid, :uuid, required: true 16 | field :mode, :enum, values: ~w(foo bar baz)a 17 | 18 | embeds_one :data, required: true do 19 | field :name, :string 20 | field :number, :integer 21 | end 22 | end 23 | 24 | ``` 25 | 26 | -------------------------------------------------------------------------------- /tips/v1/013_graceful_shutdown.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #13 — Graceful Shutdown 💎 2 | 3 | Did you know that when an app shuts down Oban pauses all queues and waits for 4 | jobs to finish? The time is configurable and defaults to 15 seconds, 5 | short enough for most deployments. 6 | 7 | https://hexdocs.pm/oban/Oban.html#start_link/1-twiddly-options 8 | 9 | #myelixirstatus #obanbg 10 | 11 | ```elixir 12 | # Change the default to 30 seconds 13 | config :my_app, Oban, 14 | repo: MyApp.Repo, 15 | queues: [default: 10], 16 | shutdown_grace_period: :timer.seconds(30) 17 | 18 | # Wait up to an hour for long running jobs in a blue-green style deploy 19 | config :my_app, Oban, 20 | shutdown_grace_period: :timer.minutes(60), 21 | ... 22 | ``` 23 | -------------------------------------------------------------------------------- /tips/v1/017_replace_args.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #17 — Replace Args 💎 2 | 3 | Did you know that you can selectively replace args when inserting a unique job? 4 | With `replace_args`, when an existing job matches some unique keys all other 5 | args are replaced. 6 | 7 | #myelixirstatus #obanbg 8 | 9 | ```elixir 10 | # Given an existing job with these args: 11 | %{some_value: 1, other_value: 1, id: 123} 12 | 13 | # Attempting to insert a new job with the same `id` key and different values: 14 | %{some_value: 2, other_value: 2, id: 123} 15 | |> MyJob.new(schedule_in: 10, replace_args: true unique: [keys: [:id]]) 16 | |> Oban.insert() 17 | 18 | # Will result in a single job with the args: 19 | %{some_value: 2, other_value: 2, id: 123} 20 | ``` 21 | -------------------------------------------------------------------------------- /tips/v2/010_oss_error_return.md: -------------------------------------------------------------------------------- 1 | 💎 Oban OSS 💎 2 | 3 | When a job encounters an exception, it fails and may be retried. You can also return an {:error, reason} tuple to fail (the reason is auto-wrapped in a PerformError). 4 | 5 | #MyElixirStatus #ObanTips 6 | 7 | ```elixir 8 | 9 | defmodule MyApp.SketchyWorker do 10 | use Oban.Worker 11 | 12 | @impl Oban.Worker 13 | def perform(%Job{args: %{"sketchy" => sketchy?}) do 14 | if sketchy? do 15 | {:error, "something sketchy happened"} 16 | else 17 | :ok 18 | end 19 | end 20 | end 21 | 22 | %Oban.PerformError{ 23 | message: "MyApp.SketchyWorker failed with \"something sketchy happened\"", 24 | reason: "something sketchy happened" 25 | } 26 | 27 | ``` 28 | -------------------------------------------------------------------------------- /tips/v1/009_draining_queues.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #9 — Draining Queues 💎 2 | 3 | Did you know that you can execute all jobs in a queue, in sequence, within the current process? The `drain_queue/2` function is perfect for integration tests. 4 | 5 | https://hexdocs.pm/oban/Oban.html#drain_queue/2 6 | 7 | #myelixirstatus #obanbg 8 | 9 | ```elixir 10 | defmodule MyApp.BusinessTest do 11 | use MyApp.DataCase, async: true 12 | 13 | alias MyApp.{Business, Worker} 14 | 15 | test "we stay in the business of doing business" do 16 | :ok = Business.schedule_a_meeting(%{email: "monty@brewster.com"}) 17 | 18 | assert %{success: 1, failure: 0} = Oban.drain_queue(queue: :mailer) 19 | 20 | # Now, make an assertion about the email delivery 21 | end 22 | end 23 | ``` 24 | -------------------------------------------------------------------------------- /tips/v1/019_default_logging.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #19 — Default Logging 💎 2 | 3 | Did you know Oban ships with a JSON based logger for job and circuit breaker events? The logger is powered by telemetry and provided by the Oban.Telemetry module. 4 | 5 | https://hexdocs.pm/oban/Oban.Telemetry.html#module-default-logger 6 | 7 | #MyElixirStatus #ObanBG 8 | 9 | ```elixir 10 | # Attach the logger with the default level 11 | :ok = Oban.Telemetry.attach_default_logger() 12 | 13 | # Attach the logger with the :debug level 14 | :ok = Oban.Telemetry.attach_default_logger(:debug) 15 | 16 | # Only attach the logger if you aren't running in an iex console 17 | unless Code.ensure_loaded?(IEx) and IEx.started?() do 18 | :ok = Oban.Telemetry.attach_default_logger(:debug) 19 | end 20 | ``` 21 | -------------------------------------------------------------------------------- /tips/v2/016_pro_structured.md: -------------------------------------------------------------------------------- 1 | 🌟 Oban Pro 🌟 2 | 3 | Oban Pro's `args_schema` define which fields are allowed, required, their expected types, and generate structs for compile-time checks and friendly dot access. 4 | 5 | https://getoban.pro/docs/pro/Oban.Pro.Worker.html#module-structured-jobs 6 | 7 | #MyElixirStatus #ObanTips 8 | 9 | ```elixir 10 | 11 | defmodule MyApp.StructuredWorker do 12 | use Oban.Pro.Worker 13 | 14 | args_schema do 15 | field :a, :id, required: true 16 | field :b, :any 17 | filed :c, :string, required: true 18 | end 19 | 20 | @impl Oban.Pro.Worker 21 | def process(%Job{args: %__MODULE__{a: a, c: c} = args}) do 22 | # Use the matched keys or access them on args 23 | end 24 | end 25 | 26 | ``` 27 | -------------------------------------------------------------------------------- /tips/v1/027_staging_jobs.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #27 — Staging Jobs 💎 2 | 3 | Did you know that you can configure how frequently Oban checks for scheduled 4 | jobs? The Stager plugin normally checks every second, but you can set the 5 | interval to as much time as you like. 6 | 7 | https://hexdocs.pm/oban/Oban.Plugins.Stager.html#module-options 8 | 9 | #MyElixirStatus #ObanBG 10 | 11 | ```elixir 12 | # Reduce the frequency of staging checks to once every 10 seconds 13 | config :my_app, Oban, 14 | plugins: [{Oban.Plugins.Stager, interval: :timer.seconds(10)}], 15 | ... 16 | 17 | # Do the exact opposite and check every 500ms to enable scheduling all the way 18 | # down to half a second 19 | config :my_app, Oban, 20 | plugins: [{Oban.Plugins.Stager, interval: 500}], 21 | ... 22 | ``` 23 | -------------------------------------------------------------------------------- /tips/v1/012_pausing_queues.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #12 — Pausing Queues 💎 2 | 3 | Did you know that you can pause a queue to stop it from processing more jobs? 4 | Calling `pause_queue/2` allows executing jobs to keep running while preventing 5 | the queue from fetching more. 6 | 7 | https://hexdocs.pm/oban/Oban.html#pause_queue/2 8 | 9 | #myelixirstatus #obanbg 10 | 11 | ```elixir 12 | # Pause all instances of the :default queue across all nodes 13 | Oban.pause_queue(queue: :default) 14 | 15 | # Pause only the local instance, leaving instances on any other nodes running 16 | Oban.pause_queue(queue: :default, local_only: true) 17 | 18 | # Queues are namespaced by prefix, so you can pause the :default queue for an 19 | # isolated supervisor 20 | Oban.pause_queue(MyApp.A.Oban, queue: :default) 21 | ``` 22 | -------------------------------------------------------------------------------- /tips/v2/012_oss_stored_errors.md: -------------------------------------------------------------------------------- 1 | 🏛️ Oban Design 🏛️ 2 | 3 | To aid in debugging without external exception trackers, all crashes, exceptions, and errors are stored along with an attempt and timestamp in a job's `errors` column. 4 | 5 | #MyElixirStatus #ObanTips 6 | 7 | ```json 8 | 9 | [ 10 | { 11 | "at": "2022-10-12T00:43:11.559809Z", 12 | "attempt": 1, 13 | "error": "** (RuntimeError) Something went wrong!\n ..." 14 | }, 15 | { 16 | "at": "2022-10-12T08:14:33.696247Z", 17 | "attempt": 2, 18 | "error": "** (RuntimeError) Something else went wrong!\n ..." 19 | }, 20 | { 21 | "at": "2022-10-15T14:02:00.790205Z", 22 | "attempt": 3, 23 | "error": "** (RuntimeError) Something went wrong again!\n ..." 24 | } 25 | ] 26 | 27 | ``` 28 | -------------------------------------------------------------------------------- /tips/v1/016_unique_jobs.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #16 — Unique Jobs 💎 2 | 3 | Did you know that Oban lets you specify constraints to prevent enqueueing 4 | duplicate jobs? Uniqueness is enforced as jobs are inserted, dynamically and 5 | atomically. 6 | 7 | https://hexdocs.pm/oban/Oban.html#module-unique-jobs 8 | 9 | #myelixirstatus #obanbg 10 | 11 | ```elixir 12 | # Configure 60 seconds of uniqueness within the worker 13 | defmodule MyApp.BusinessWorker do 14 | use Oban.Worker, unique: [period: 60] 15 | 16 | ... 17 | end 18 | 19 | # Manually override the unique period for a single job 20 | MyApp.BusinessWorker.new(%{id: 1}, unique: [period: 120]) 21 | 22 | # Override a job to have an infinite unique period, which lasts as long as jobs 23 | # are persisted 24 | MyApp.BusinessWorker.new(%{id: 2}, unique: [period: :infinity]) 25 | ``` 26 | -------------------------------------------------------------------------------- /tips/v1/015_priority.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #15 — Priority 💎 2 | 3 | Did you know that you can prioritize or de-prioritize jobs in a queue by setting 4 | a priority from 0-3? Rather than executing in the order they were scheduled, 5 | higher priority jobs are executed first. 6 | 7 | https://hexdocs.pm/oban/Oban.html#module-prioritizing-jobs 8 | 9 | #myelixirstatus #obanbg 10 | 11 | ```elixir 12 | # Default to a lower priority for most jobs 13 | defmodule MyApp.BusinessWorker do 14 | use Oban.Worker, queue: :events, priority: 1 15 | 16 | ... 17 | end 18 | 19 | # Manually set a higher priority for a job on the "top-tier" plan 20 | MyApp.BusinessWorker.new(%{id: 1, plan: "top-tier"}, priority: 0) 21 | 22 | # Manually set a lower priority for a job on the "free" plan 23 | MyApp.BusinessWorker.new(%{id: 2, plan: "free"}, priority: 3) 24 | ``` 25 | -------------------------------------------------------------------------------- /tips/v1/003_timeout.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #3 — Timeout 💎 2 | 3 | Did you know you can limit how long a job can execute? A worker's `timeout/1` callback calculates set how many milliseconds a job can execute before it is stopped. 4 | 5 | https://hexdocs.pm/oban/Oban.Worker.html#module-limiting-execution-time 6 | 7 | #myelixirstatus #elixirlang #obanbg 8 | 9 | ```elixir 10 | # Set a fixed timeout of 10 seconds 11 | def timeout(_job), do: :timer.seconds(10) 12 | 13 | # Allow 10 more seconds for each successive attempt 14 | def timeout(%Job{attempt: attempt}), do: :timer.seconds(attempt * 10) 15 | 16 | # Allow jobs for paying customers to run longer 17 | def timeout(%Job{args: %{"customer_id" => customer_id}) do 18 | if Customer.payming?(customer_id) do 19 | :timer.minutes(1) 20 | else 21 | :timer.seconds(15) 22 | end 23 | end 24 | ``` 25 | -------------------------------------------------------------------------------- /tips/v1/028_crontab_extras.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #28 — Crontab Extras 💎 2 | 3 | Did you know you can pass additional options to jobs scheduled by the Cron 4 | plugin? Anything you can pass to `Worker.new` is a valid option! 5 | 6 | https://hexdocs.pm/oban/Oban.Plugins.Cron.html#content 7 | 8 | #MyElixirStatus #ObanBG 9 | 10 | ```elixir 11 | # Specify fixed args for the cron instance 12 | crontab: [ 13 | {"0 * * * *", MyApp.BusinessWorker, args: %{mode: "business"}} 14 | ] 15 | 16 | # Only allow the worker to run once when scheduled via cron 17 | {"0 0 * * *", MyApp.DailyWorker, max_attempts: 1} 18 | 19 | # Use an alternate queue when the job is scheduled via cron 20 | {"0 12 * * MON", MyApp.MondayWorker, queue: :scheduled} 21 | 22 | # Set some tags when the job is scheduled via cron 23 | {"0 0 1 * *", MyApp.InfrequentWorker, tags: ["scheduled"]} 24 | ``` 25 | -------------------------------------------------------------------------------- /tips/v1/005_contextual_backoff.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #5 — Unsaved Errors 💎 2 | 3 | Did you know that errors are temporarily recorded on a job after execution? Errors are put in the `unsaved_error` map, which is then available in the backoff callback or telemetry events. 4 | 5 | https://hexdocs.pm/oban/Oban.Worker.html#module-contextual-backoff 6 | 7 | #myelixirstatus #elixirlang #obanbg 8 | 9 | ```elixir 10 | # Conditionally back off for different classes of error 11 | def backoff(%Job{unsaved_error: unsaved_error}) do 12 | case unsaved_error.reason do 13 | # Five minutes for "408 Request Timeout" 14 | %MyApp.ApiError{status: 408} -> 15 | @five_minutes 16 | 17 | # Twenty minutes for "429 Too Many Requests" error 18 | %MyApp.ApiError{status: 429} -> 19 | @twenty_minutes 20 | 21 | _ -> 22 | @one_minute 23 | end 24 | end 25 | ``` 26 | -------------------------------------------------------------------------------- /tips/v2/009_pro_recorded.md: -------------------------------------------------------------------------------- 1 | 🌟 Oban Pro 🌟 2 | 3 | The "result" part of {:ok, result} is typically ignored and only used for testing. Oban Pro's worker changes that with a `recorded` option that persists the result for retrieval later. 4 | 5 | https://getoban.pro/docs/pro/Oban.Pro.Worker.html#module-recorded-jobs?utm_source=twitter&utm_campaign=tips 6 | 7 | #MyElixirStatus #ObanTips 8 | 9 | ```elixir 10 | 11 | defmodule MyApp.RecordedWorker do 12 | use Oban.Pro.Worker, recorded: true 13 | 14 | @impl Oban.Pro.Worker 15 | def process(%Job{args: args}) do 16 | result = MyApp.resource_intensive_work(args) 17 | 18 | {:ok, result} 19 | end 20 | end 21 | 22 | # Use the result later, maybe from another worker 23 | 24 | {:ok, stored_result} = 25 | Oban.Job 26 | |> MyApp.Repo.get(job_id) 27 | |> MyApp.RecordedWorker.fetch_recorded() 28 | 29 | ``` 30 | -------------------------------------------------------------------------------- /tips/v1/004_custom_backoff.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #4 — Custom Backoff 💎 2 | 3 | Did you know you can customize the backoff between job retries? Add a 4 | `backoff/1` callback to your worker to calculate the seconds between retries 5 | based on any job attributes. 6 | 7 | https://hexdocs.pm/oban/Oban.Worker.html#module-customizing-backoff 8 | 9 | #myelixirstatus #elixirlang #obanbg 10 | 11 | ```elixir 12 | # Use a linear backoff based on the attempt 13 | def backoff(%Job{attempt: attempt}), do: attempt 14 | 15 | # Clamp the backoff to compensate for a high max_attempts 16 | def backoff(%Job{attempt: attempt}) do 17 | :math.pow(2, min(attempt, 15)) 18 | end 19 | 20 | # Use a fixed period for a particular tag, otherwise use the default 21 | def backoff(%Job{attempt: attempt, tags: tags} = job) do 22 | if "rush_job" in tags do 23 | 30 24 | else 25 | Worker.backoff(job) 26 | end 27 | end 28 | ``` 29 | -------------------------------------------------------------------------------- /tips/v1/022_using_multis.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #22 — Insert with Multi 💎 2 | 3 | Did you know that Oban has full integration with Ecto Multis? You can insert jobs directly into a multi chain using changesets or functions. 4 | 5 | https://hexdocs.pm/oban/Oban.html#insert/4 6 | 7 | #MyElixirStatus #ObanBG 8 | 9 | ```elixir 10 | alias Ecto.Multi 11 | 12 | Multi.new() 13 | |> Oban.insert("job-1", MyApp.Worker.new(%{id: 1})) 14 | |> Oban.insert("job-2", MyApp.Worker.new(%{id: 2})) 15 | |> MyApp.Repo.transaction() 16 | 17 | # You can also use a function rather than a changeset, which allows you to 18 | # reference previous changes. 19 | Multi.new() 20 | |> Multi.insert(:user, user_changeset) 21 | |> Multi.insert(:plan, plan_changeset) 22 | |> Oban.insert(:email, fn %{user: user, plan: plan} -> 23 | MyApp.SignupEmail.new(%{user_id: user.id, plan_id: plan.id}) 24 | end) 25 | |> MyApp.Repo.transaction() 26 | ``` 27 | -------------------------------------------------------------------------------- /tips/v1/010_draining_failures.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #10 — Draining Failures 💎 2 | 3 | Did you know it's possible to drain queues without crash safety? By default, 4 | `drain_queue/2` catches any errors, but the `with_safety: false` flag lets them 5 | bubble up to your test process. 6 | 7 | https://hexdocs.pm/oban/Oban.html#drain_queue/2-failures-retries 8 | 9 | #myelixirstatus #obanbg 10 | 11 | ```elixir 12 | # Assert that draining succeeds without any failures 13 | assert Oban.drain_queue(queue: :default, with_safety: false) 14 | 15 | # Assert that draining raises a specific exception 16 | assert_raise RuntimeError, fn -> 17 | Oban.drain_queue(queue: :risky, with_safety: false) 18 | end 19 | 20 | # Assert that a job crashes for a particular reason 21 | try do 22 | Oban.drain_queue(queue: :crashy, with_safety: false) 23 | catch 24 | :exit, value -> 25 | assert value =~ "Crashed for some reason" 26 | end 27 | ``` 28 | -------------------------------------------------------------------------------- /tips/v1/014_initially_paused.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #14 — Initially Paused 💎 2 | 3 | Lastly, in the queue-pause saga, did you know you can start a queue in the 4 | paused state? Passing `paused: true` as a queue option prevents the queue from 5 | processing jobs when it starts. 6 | 7 | https://hexdocs.pm/oban/Oban.html#start_link/1-primary-options 8 | 9 | #myelixirstatus #obanbg 10 | 11 | ```elixir 12 | # In a blue-green deployment it may be necessary to start queues when the node 13 | # boots yet prevent them from processing jobs until the node is rotated into 14 | # use. 15 | config :my_app, Oban, 16 | queues: [ 17 | mailer: 10, 18 | alpha: [limit: 10, paused: true], 19 | gamma: [limit: 20, paused: true], 20 | omega: [limit: 10, paused: true] 21 | ], 22 | ... 23 | 24 | # Once the app boots tell each queue to resume processing: 25 | for queue <- [:alpha, :gamma, :omega] do 26 | Oban.resume_queue(queue: queue) 27 | end 28 | ``` 29 | -------------------------------------------------------------------------------- /tips/v2/014_oss_unsaved_errors.md: -------------------------------------------------------------------------------- 1 | 💎 Oban OSS 💎 2 | 3 | Errors are temporarily recorded in an `unsaved_error` map after execution, which is then available in the backoff callback, engine functions, and telemetry events. 4 | 5 | #MyElixirStatus #ObanTips 6 | 7 | ```elixir 8 | 9 | @spec unsaved_error :: %{ 10 | kind: Exception.kind(), 11 | reason: term(), 12 | stacktrace: Exception.stacktrace() 13 | } 14 | 15 | # Conditionally back off for different classes of error 16 | def backoff(%Job{unsaved_error: unsaved_error}) do 17 | case unsaved_error.reason do 18 | # Five minutes for "408 Request Timeout" 19 | %MyApp.ApiError{status: 408} -> 20 | @five_minutes 21 | 22 | # Twenty minutes for "429 Too Many Requests" error 23 | %MyApp.ApiError{status: 429} -> 24 | @twenty_minutes 25 | 26 | _ -> 27 | @one_minute 28 | end 29 | end 30 | 31 | ``` 32 | -------------------------------------------------------------------------------- /tips/v2/013_oss_backoff.md: -------------------------------------------------------------------------------- 1 | 💎 Oban OSS 💎 2 | 3 | Failed jobs may be retried in the future based on a backoff algorithm (exponential with jitter by default). With the optional `backoff/1` callback you can customize the seconds between retries based on any job attributes. 4 | 5 | https://hexdocs.pm/oban/Oban.Worker.html#module-customizing-backoff 6 | 7 | #MyElixirStatus #ObanTips 8 | 9 | ```elixir 10 | 11 | # Use a linear backoff based on the attempt 12 | def backoff(%Job{attempt: attempt}), do: attempt 13 | 14 | # Clamp the backoff to compensate for a high max_attempts 15 | def backoff(%Job{attempt: attempt}) do 16 | :math.pow(2, min(attempt, 15)) 17 | end 18 | 19 | # Use a fixed period for a particular tag, otherwise use the default 20 | def backoff(%Job{attempt: attempt, tags: tags} = job) do 21 | if "rush_job" in tags do 22 | 30 23 | else 24 | Worker.backoff(job) 25 | end 26 | end 27 | 28 | ``` 29 | -------------------------------------------------------------------------------- /tips/v1/023_insert_all_with_multi.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #23 — Insert All with Multi 💎 2 | 3 | Did you know that you can also use insert_all with Ecto Multis? This is especially useful for enqueuing groups of related jobs. 4 | 5 | https://hexdocs.pm/oban/Oban.html#insert_all/4 6 | 7 | #MyElixirStatus #ObanBG 8 | 9 | ```elixir 10 | alias Ecto.Multi 11 | 12 | mail_jobs = Enum.map(users, &MailWorker.new(%{email: &1.email})) 13 | apns_jobs = Enum.map(users, &APNSWorker.new(%{token: &1.apns_token})) 14 | 15 | Multi.new() 16 | |> Oban.insert_all(:mail, mail_jobs) 17 | |> Oban.insert_all(:apns, apns_jobs) 18 | |> MyApp.Repo.transaction() 19 | 20 | # A function or a map with a `changesets` key also works: 21 | Multi.new() 22 | |> Oban.insert_all(:mail, %{changesets: mail_jobs}) 23 | |> Oban.insert_all(:apns, fn %{mail: mail_jobs} -> 24 | mail_jobs 25 | |> Enum.map(&get_in(&1, [:args, "email"])) 26 | |> Enum.map(&APNSWorker.new(%{email: &1})) 27 | end) 28 | |> MyApp.Repo.transaction() 29 | ``` 30 | -------------------------------------------------------------------------------- /tips/v1/018_unique_keys.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #18 — Unique Keys 💎 2 | 3 | Dy default job uniqueness is based on the queue, state and args. Did you know you can restrict 4 | args to only a subset of keys? 5 | 6 | https://hexdocs.pm/oban/Oban.html#module-unique-jobs 7 | 8 | #myelixirstatus #obanbg 9 | 10 | ```elixir 11 | # Configure uniqueness only based on the :id field 12 | defmodule MyApp.BusinessWorker do 13 | use Oban.Worker, unique: [keys: [:id]] 14 | 15 | ... 16 | end 17 | 18 | # With an existing job: 19 | %{id: 1, type: "business", url: "https://example.com"} 20 | |> MyApp.BusinessWorker.new() 21 | |> Oban.insert() 22 | 23 | # Inserting another job with a different type won't work 24 | %{id: 1, type: "solo", url: "https://example.com"} 25 | |> MyApp.BusinessWorker.new() 26 | |> Oban.insert() 27 | 28 | # Inserting the same attributes with a different :id will work 29 | %{id: 2, type: "business", url: "https://example.com"} 30 | |> MyApp.BusinessWorker.new() 31 | |> Oban.insert() 32 | ``` 33 | -------------------------------------------------------------------------------- /tips/v1/006_assert_enqueued.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #6 — Assert Enqueued 💎 2 | 3 | Did you know that you can make assertions about whether jobs are enqueued? The `Testing` module provides dynamic helpers that you can mix into tests. 4 | 5 | https://hexdocs.pm/oban/Oban.Testing.html#module-using-in-tests 6 | 7 | #myelixirstatus #obanbg 8 | 9 | ```elixir 10 | # Include the helpers into a test module 11 | use Oban.Testing, repo: MyApp.Repo 12 | 13 | # Assert that a job was already enqueued by checking the worker and args 14 | assert_enqueued worker: MyWorker, args: %{id: 1} 15 | 16 | # Check only a queue and some tags instead 17 | assert_enqueued queue: :default, tags: ["customer-1"] 18 | 19 | # Provide a timeout to assert that a job _will_ be enqueued within 250ms 20 | assert_enqueued [worker: MyWorker], 250 21 | 22 | # Refute that a worker as enqueued 23 | refute_enqueued worker: MyWorker 24 | 25 | # Refute that a worker _will be_ enqueued within 100ms 26 | refute_enqueued [worker: MyWorker], 100 27 | ``` 28 | -------------------------------------------------------------------------------- /tips/v1/008_testing_prefix.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #8 — Testing Prefix 💎 2 | 3 | Did you know that you can assert that jobs are enqueued in a particular prefix? The `Testing` module accepts a `prefix` option, or you can override it for individual assertions. 4 | 5 | #myelixirstatus #obanbg 6 | 7 | ```elixir 8 | # Set a prefix for all testing assertions 9 | use Oban.Testing, repo: MyApp.Repo, prefix: "business" 10 | 11 | # Override the prefix on a per-assertion basis 12 | test "a context function enqueues certain jobs" do 13 | {:ok, %{id: id}} = MyContext.do_the_thing() 14 | 15 | assert_enqueued worker: MyWorker, args: %{id: id}, prefix: "public" 16 | end 17 | 18 | # When an assertion fails the prefix is included in the message: 19 | """ 20 | Expected a job matching: 21 | 22 | %{args: %{id: 9}, worker: MyWorker} 23 | 24 | to be enqueued in the "business" schema. Instead found: 25 | 26 | [ 27 | %{args: %{"id" => 1}, worker: "MyWorker"}, 28 | %{args: %{"id" => 2}, worker: "MyWorker"} 29 | ] 30 | """ 31 | ``` 32 | -------------------------------------------------------------------------------- /tips/v1/002_discard.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #2 — Discard 💎 2 | 3 | Did you know you can discard a job to prevent it from retrying again? Return `{:discard, reason}` from a worker's `perform/1` and it will record the reason as an error while marking the job discarded 4 | 5 | #myelixirstatus #elixirlang #obanbg 6 | 7 | ```elixir 8 | # If the underlying record was deleted then the job can never complete, discard it right 9 | # away rather than retrying later. 10 | def perform(%Job{args: %{"id" => id}}) do 11 | case Account.fetch(id) do 12 | {:ok, account} -> 13 | do_stuff(account) 14 | 15 | :error -> 16 | {:discard, "record could not be found"} 17 | end 18 | end 19 | 20 | # Retry for most errors, but discard for something more severe 21 | def perform(%Job{args: %{"id" => id}}) do 22 | try do 23 | Video.process(id) 24 | rescue 25 | CorruptError -> 26 | {:discard, "video is corrupt, unable to process"} 27 | 28 | exception -> 29 | reraise exception, __STACKTRACE__ 30 | end 31 | end 32 | ``` 33 | -------------------------------------------------------------------------------- /tips/v1/020_error_reporting.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #20 — Error Reporting 💎 2 | 3 | Did you know you can use Oban's telemetry events to report errors? Define and attach a handler to capture exceptions, complete with the stacktrace. 4 | 5 | https://hexdocs.pm/oban/Oban.Telemetry.html#module-job-events 6 | 7 | #MyElixirStatus #ObanBG 8 | 9 | ```elixir 10 | defmodule ErrorReporter do 11 | def handle_event([:oban, :job, :exception], measure, %{job: job}, _) do 12 | extra = 13 | job 14 | |> Map.take([:id, :args, :meta, :queue, :worker]) 15 | |> Map.merge(measure) 16 | 17 | Sentry.capture_exception(meta.error, stacktrace: meta.stacktrace, extra: extra) 18 | end 19 | 20 | def handle_event([:oban, :circuit, :trip], _measure, meta, _) do 21 | Sentry.capture_exception(meta.error, stacktrace: meta.stacktrace, extra: meta) 22 | end 23 | end 24 | 25 | :telemetry.attach_many( 26 | "oban-errors", 27 | [[:oban, :job, :exception], [:oban, :circuit, :trip]], 28 | &ErrorReporter.handle_event/4, 29 | %{} 30 | ) 31 | ``` 32 | -------------------------------------------------------------------------------- /tips/v1/026_pruning_jobs.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #26 — Pruning Jobs 💎 2 | 3 | Did you know Oban keeps executed jobs? That allows you to review completed jobs 4 | and aggregate metrics, while the Pruner plugin deletes older jobs to prevent the 5 | table from growing forever. 6 | 7 | https://hexdocs.pm/oban/Oban.html#module-pruning-historic-jobs 8 | 9 | #MyElixirStatus #ObanBG 10 | 11 | ```elixir 12 | # Use the Pruner plugin with the default settings 13 | config :my_app, Oban, 14 | plugins: [Oban.Plugins.Pruner], 15 | ... 16 | 17 | # Override the max age so that it retains jobs for one hour 18 | config :my_app, Oban, 19 | plugins: [{Oban.Plugins.Pruner, max_age: 3600}], 20 | ... 21 | 22 | # The DynamicPruner from Oban Pro provides far more control, allowing per-state, 23 | # per-queue or even per-worker overrides. 24 | plugins: [{ 25 | Oban.Pro.Plugins.DynamicPruner, 26 | mode: {:max_age, {7, :days}}, 27 | queue_overrides: [ 28 | events: {:max_age, {10, :minutes}} 29 | ], 30 | state_overrides: [ 31 | discarded: {:max_age, {1, :month}} 32 | ] 33 | }] 34 | ``` 35 | -------------------------------------------------------------------------------- /tips/v1/011_recording_errors.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #11 — Recording Errors 💎 2 | 3 | Did you know that errors are recorded in the database when a job fails? A job's 4 | `errors` field contains a list of the time, attempt and a formatted error 5 | message for each failed attempt. 6 | 7 | https://hexdocs.pm/oban/Oban.Job.html#t:errors/0 8 | 9 | #myelixirstatus #obanbg 10 | 11 | ```elixir 12 | # Errors look like this, where `at` is a UTC timestamp and `error` is the blamed 13 | # and formatted error message. 14 | [ 15 | %{ 16 | "at" => "2021-02-11T17:01:13.517233Z", 17 | "attempt" => 1, 18 | "error" => "** (RuntimeError) Something went wrong!\n..." 19 | } 20 | ] 21 | 22 | # Check the errors for a job with multiple attempts to see if it failed before 23 | # or it was snoozed. 24 | def perform(%Job{attempt: attempt, errors: errors}) when attempt > 1 do 25 | case errors do 26 | [%{"error" => error} | _] -> 27 | IO.puts "This job failed with the error:\n\n" <> error 28 | 29 | [] -> 30 | IO.puts "This job snoozed, it doesn't have any errors" 31 | end 32 | 33 | :ok 34 | end 35 | ``` 36 | -------------------------------------------------------------------------------- /tips/v1/007_perform_job.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #7 — Perform Job 💎 2 | 3 | Did you know you can construct and execute a job for testing with a single command? Use `perform_job/3` from the Testing module to verify a worker, validate options, and execute a job inline. 4 | 5 | https://hexdocs.pm/oban/Oban.Testing.html#perform_job/3 6 | 7 | #myelixirstatus #obanbg 8 | 9 | ```elixir 10 | use Oban.Testing, repo: MyApp.Repo 11 | 12 | alias MyApp.MyWorker 13 | 14 | test "checking job execution" do 15 | # Assert that it doesn't accept random arguments 16 | assert {:error, _} = perform_job(MyWorker, %{"bad" => "arg"}) 17 | 18 | # Assert executing with string arguments 19 | assert :ok = perform_job(MyWorker, %{"id" => 1}) 20 | 21 | # Assert executing with automatically stringified keys 22 | assert :ok = perform_job(MyWorker, %{id: 1}) 23 | end 24 | 25 | test "raising assertion errors for invalid options" do 26 | # Fails assertion because priority is invalid 27 | perform_job(MyWorker, %{}, priority: 9) 28 | 29 | # Fails because the provided worker isn't a real worker module 30 | perform_job(MyVerker, %{"id" => 1}) 31 | end 32 | ``` 33 | -------------------------------------------------------------------------------- /tips/v1/021_customized_logging.md: -------------------------------------------------------------------------------- 1 | 💎 Oban Tip #21 — Customized Logging 💎 2 | 3 | Did you know you can build your own customized logger from Oban's telemetry events? For example, you can us events to log plugin activity, timing and exceptions. 4 | 5 | https://hexdocs.pm/oban/Oban.Telemetry.html 6 | 7 | #MyElixirStatus #ObanBG 8 | 9 | ```elixir 10 | defmodule MyApp.ObanLogger do 11 | def handle_event([:oban, :plugin, :stop], measure, meta, _) do 12 | extra = 13 | case meta.plugin do 14 | Oban.Plugins.Cron -> [inserted_count: length(meta.jobs)] 15 | Oban.Plugins.Pruner -> [pruned_count: meta.pruned_count] 16 | Oban.Plugins.Stager -> [staged_count: meta.staged_count] 17 | _ -> [] 18 | end 19 | 20 | entry = [ 21 | event: "plugin:stop", 22 | oban: meta.conf.name, 23 | plugin: meta.plugin, 24 | duration: measure.duration 25 | ] 26 | 27 | Logger.info(entry ++ extra) 28 | end 29 | 30 | def handle_event([:oban, :plugin, :exception], measure, meta, _) do 31 | Logger.error( 32 | event: "plugin:exception", 33 | oban: meta.conf.name, 34 | plugin: meta.plugin, 35 | duration: measure.duration, 36 | error: Exception.message(meta.error) 37 | ) 38 | end 39 | end 40 | 41 | :telemetry.attach_many( 42 | "oban-plugin-logger", 43 | [[:oban, :plugin, :stop], [:oban, :plugin, :exception]], 44 | &MyApp.ObanLogger.handle_event/4, 45 | [] 46 | ) 47 | ``` 48 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | --------------------------------------------------------------------------------