diff --git a/.formatter.exs b/.formatter.exs index b11432d8..d2cda26e 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,3 +1,4 @@ +# Used by "mix format" [ - inputs: [".formatter.exs", "mix.exs", "{config,lib,test}/**/*.{ex,exs}"] + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] ] diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index b9db3118..21a46e97 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions/setup-elixir@v1 + - uses: erlef/setup-elixir@v1 with: otp-version: 22.3 elixir-version: 1.10.3 @@ -36,7 +36,7 @@ jobs: with: name: deps_lock path: mix.lock - + compile_dev: name: Compile Dev Environment @@ -46,7 +46,7 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions/setup-elixir@v1 + - uses: erlef/setup-elixir@v1 with: otp-version: 22.3 elixir-version: 1.10.3 @@ -76,7 +76,7 @@ jobs: with: name: compile_dev path: _build/dev/ - + compile_docs: name: Compile Docs Environment @@ -86,7 +86,7 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions/setup-elixir@v1 + - uses: erlef/setup-elixir@v1 with: otp-version: 22.3 elixir-version: 1.10.3 @@ -116,7 +116,7 @@ jobs: with: name: compile_docs path: _build/docs/ - + compile_test: name: Compile Test Environment (OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}}) @@ -132,7 +132,7 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions/setup-elixir@v1 + - uses: erlef/setup-elixir@v1 with: otp-version: ${{matrix.otp}} elixir-version: ${{matrix.elixir}} @@ -162,7 +162,7 @@ jobs: with: name: compile_test-${{matrix.otp}}-${{matrix.elixir}} path: _build/test/ - + compile_prod: name: Compile Prod Environment @@ -172,7 +172,7 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions/setup-elixir@v1 + - uses: erlef/setup-elixir@v1 with: otp-version: 22.3 elixir-version: 1.10.3 @@ -205,14 +205,14 @@ jobs: format: name: Check Formatting - + runs-on: ubuntu-latest needs: ['deps'] steps: - uses: actions/checkout@v2 - - uses: actions/setup-elixir@v1 + - uses: erlef/setup-elixir@v1 with: otp-version: 22.3 elixir-version: 1.10.3 @@ -243,7 +243,7 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions/setup-elixir@v1 + - uses: erlef/setup-elixir@v1 with: otp-version: ${{matrix.otp}} elixir-version: ${{matrix.elixir}} @@ -272,7 +272,7 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions/setup-elixir@v1 + - uses: erlef/setup-elixir@v1 with: otp-version: 22.3 elixir-version: 1.10.3 @@ -301,7 +301,7 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions/setup-elixir@v1 + - uses: erlef/setup-elixir@v1 with: otp-version: 22.3 elixir-version: 1.10.3 @@ -342,7 +342,7 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions/setup-elixir@v1 + - uses: erlef/setup-elixir@v1 with: otp-version: 22.3 elixir-version: 1.10.3 @@ -375,7 +375,7 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions/setup-elixir@v1 + - uses: erlef/setup-elixir@v1 with: otp-version: 22.3 elixir-version: 1.10.3 @@ -397,4 +397,4 @@ jobs: - uses: actions/upload-artifact@v1 with: name: docs - path: doc \ No newline at end of file + path: doc diff --git a/.gitignore b/.gitignore index f5762b88..08946195 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,31 @@ -/_build -/deps -/doc -/docs +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. erl_crash.dump -mix.lock + +# Also ignore archive artifacts (built via "mix archive.build"). *.ez -/cover + +# Ignore package tarball (built via "mix hex.build"). +quantum-*.tar + +# Temporary files for e.g. tests. +/tmp/ + +# Misc. +mix.lock /priv/plts/*.plt -/priv/plts/*.plt.hash \ No newline at end of file +/priv/plts/*.plt.hash diff --git a/CHANGELOG.md b/CHANGELOG.md index fb0ba0d5..c0b152a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ This project adheres to [Semantic Versioning](http://semver.org/). Diff for [unreleased] +## 3.4.0 - 2021-08-10 + +### Added +- `telemetry` `v1.0.0` support (#483) +- Logger Metadata (#462 & #464) +- Support setting job `state` in config (#463) + +### Fixed +- Invalid Timezone fix in `ExecutionBroadcaster` (#468) + +Diff for [3.4.0] + ## 3.3.0 - 2020-09-25 ### Added @@ -68,7 +80,7 @@ Diff for [3.0.0-rc.2] ## 3.0.0-rc.1 - 2020-02-26 -## Changed +### Changed - A lot of function that were not for public use have been undocumented. Those are now considered internal and may break at any point in time. - `Quantum.Scheduler` has been renamed to `Quantum` @@ -618,7 +630,8 @@ Diff for [1.0.0] ### Added - Initial commit -[unreleased]: https://github.com/quantum-elixir/quantum-core/compare/v3.3.0...HEAD +[unreleased]: https://github.com/quantum-elixir/quantum-core/compare/v3.4.0...HEAD +[3.3.0]: https://github.com/quantum-elixir/quantum-core/compare/v3.3.0...v3.4.0 [3.3.0]: https://github.com/quantum-elixir/quantum-core/compare/v3.2.0...v3.3.0 [3.2.0]: https://github.com/quantum-elixir/quantum-core/compare/v3.1.0...v3.2.0 [3.1.0]: https://github.com/quantum-elixir/quantum-core/compare/v3.0.2...v3.1.0 diff --git a/README.md b/README.md index 16707821..11072a6c 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,29 @@ # Quantum -[Cron](https://en.wikipedia.org/wiki/Cron)-like job scheduler for [Elixir](http://elixir-lang.org/). - -[![Financial Contributors on Open Collective](https://opencollective.com/quantum/all/badge.svg?label=financial+contributors)](https://opencollective.com/quantum) [![Hex.pm Version](http://img.shields.io/hexpm/v/quantum.svg)](https://hex.pm/packages/quantum) -[![Hex docs](http://img.shields.io/badge/hex.pm-docs-green.svg?style=flat)](https://hexdocs.pm/quantum) -![.github/workflows/elixir.yml](https://github.com/quantum-elixir/quantum-core/workflows/.github/workflows/elixir.yml/badge.svg) +[![Financial Contributors on Open Collective](https://opencollective.com/quantum/all/badge.svg?label=financial+contributors)](https://opencollective.com/quantum) +[![CI](https://github.com/quantum-elixir/quantum-core/workflows/.github/workflows/elixir.yml/badge.svg)](https://github.com/quantum-elixir/quantum-core/actions/workflows/elixir.yml) [![Coverage Status](https://coveralls.io/repos/quantum-elixir/quantum-core/badge.svg?branch=master)](https://coveralls.io/r/quantum-elixir/quantum-core?branch=master) -[![Hex.pm](https://img.shields.io/hexpm/dt/quantum.svg)](https://hex.pm/packages/quantum) +[![Module Version](https://img.shields.io/hexpm/v/quantum.svg)](https://hex.pm/packages/quantum) +[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/quantum/) +[![Total Download](https://img.shields.io/hexpm/dt/quantum.svg)](https://hex.pm/packages/quantum) +[![License](https://img.shields.io/hexpm/l/quantum.svg)](https://github.com/quantum-elixir/quantum-core/blob/master/LICENSE) +[![Last Updated](https://img.shields.io/github/last-commit/quantum-elixir/quantum-core.svg)](https://github.com/quantum-elixir/quantum-core/commits/master) > **This README follows master, which may not be the currently published version**. Here are the [docs for the latest published version of Quantum](https://hexdocs.pm/quantum/readme.html). +[Cron](https://en.wikipedia.org/wiki/Cron)-like job scheduler for [Elixir](http://elixir-lang.org/). + ## Setup -To use Quantum in your project, edit the `mix.exs` file and add Quantum to +To use Quantum in your project, edit the `mix.exs` file and add `Quantum` to **1. the list of dependencies:** ```elixir defp deps do - [{:quantum, "~> 3.0"}] + [ + {:quantum, "~> 3.0"} + ] end ``` @@ -35,8 +40,6 @@ defmodule Acme.Application do use Application def start(_type, _args) do - import Supervisor.Spec, warn: false - children = [ # This is the new line Acme.Scheduler @@ -92,10 +95,9 @@ More details on the usage can be found in the [Documentation](https://hexdocs.pm This project uses the [Collective Code Construction Contract (C4)](http://rfc.zeromq.org/spec:42/C4/) for all code changes. -> "Everyone, without distinction or discrimination, SHALL have an equal right to become a Contributor under the -terms of this contract." +> "Everyone, without distinction or discrimination, SHALL have an equal right to become a Contributor under the terms of this contract." -### tl;dr +### TL;DR 1. Check for [open issues](https://github.com/quantum-elixir/quantum-core/issues) or [open a new issue](https://github.com/quantum-elixir/quantum-core/issues/new) to start a discussion around [a problem](https://www.youtube.com/watch?v=_QF9sFJGJuc). 2. Issues SHALL be named as "Problem: _description of the problem_". @@ -108,7 +110,8 @@ terms of this contract." ### Code Contributors This project exists thanks to all the people who contribute. - + +[![Contributors](https://opencollective.com/quantum/contributors.svg?width=890&button=false)](https://github.com/quantum-elixir/quantum-core/graphs/contributors) ### Financial Contributors @@ -116,23 +119,33 @@ Become a financial contributor and help us sustain our community. [[Contribute]( #### Individuals - +[![Individuals](https://opencollective.com/quantum/individuals.svg?width=890)](https://opencollective.com/quantum) #### Organizations Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/quantum/contribute)] - - - - - - - - - - - -## License - -[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) +[![Organization0](https://opencollective.com/quantum/organization/0/avatar.svg)](https://opencollective.com/quantum/organization/0/website) +[![Organization1](https://opencollective.com/quantum/organization/1/avatar.svg)](https://opencollective.com/quantum/organization/1/website) +[![Organization2](https://opencollective.com/quantum/organization/2/avatar.svg)](https://opencollective.com/quantum/organization/2/website) +[![Organization3](https://opencollective.com/quantum/organization/3/avatar.svg)](https://opencollective.com/quantum/organization/3/website) +[![Organization4](https://opencollective.com/quantum/organization/4/avatar.svg)](https://opencollective.com/quantum/organization/4/website) +[![Organization5](https://opencollective.com/quantum/organization/5/avatar.svg)](https://opencollective.com/quantum/organization/5/website) +[![Organization6](https://opencollective.com/quantum/organization/6/avatar.svg)](https://opencollective.com/quantum/organization/6/website) +[![Organization7](https://opencollective.com/quantum/organization/7/avatar.svg)](https://opencollective.com/quantum/organization/7/website) +[![Organization8](https://opencollective.com/quantum/organization/8/avatar.svg)](https://opencollective.com/quantum/organization/8/website) +[![Organization9](https://opencollective.com/quantum/organization/9/avatar.svg)](https://opencollective.com/quantum/organization/9/website) + +## Copyright and License + +Copyright (c) 2015 Constantin Rack + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/assets/quantum-elixir-logo.svg b/assets/quantum-elixir-logo.svg new file mode 100644 index 00000000..8e659297 --- /dev/null +++ b/assets/quantum-elixir-logo.svg @@ -0,0 +1,12 @@ + + + + quantum-elixir logo + Logo of quantum-elixir, a cron-like job scheduler for elixir + + + + + + + diff --git a/config/config.exs b/config/config.exs index ae722aea..895be551 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,2 +1,3 @@ use Mix.Config +config :logger, :console, metadata: [:all, :crash_reason] config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase diff --git a/lib/quantum/clock_broadcaster.ex b/lib/quantum/clock_broadcaster.ex index 0d2631f7..d84e134f 100644 --- a/lib/quantum/clock_broadcaster.ex +++ b/lib/quantum/clock_broadcaster.ex @@ -107,17 +107,18 @@ defmodule Quantum.ClockBroadcaster do new_remaining_demand = remaining_demand - Enum.count(events) if remaining_demand > 0 and new_remaining_demand == 0 do - log_catched_up(state) + log_caught_up(state) end {:noreply, events, %State{state | time: new_time, remaining_demand: new_remaining_demand}} end - defp log_catched_up(%State{debug_logging: false}), do: :ok + defp log_caught_up(%State{debug_logging: false}), do: :ok - defp log_catched_up(%State{debug_logging: true}), + defp log_caught_up(%State{debug_logging: true}), do: Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Clock Producer catched up with past times and is now running in normal time" + {"Clock Producer caught up with past times and is now running in normal time", + node: Node.self()} end) end diff --git a/lib/quantum/execution_broadcaster.ex b/lib/quantum/execution_broadcaster.ex index 5a0ea10b..b64b0789 100644 --- a/lib/quantum/execution_broadcaster.ex +++ b/lib/quantum/execution_broadcaster.ex @@ -101,9 +101,7 @@ defmodule Quantum.ExecutionBroadcaster do ) do debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Scheduling job for single reboot execution: #{ - inspect(name) - }" + {"Scheduling job for single reboot execution", node: Node.self(), name: name} end) {[%ExecuteEvent{job: job}], %{state | uninitialized_jobs: [job | uninitialized_jobs]}} @@ -115,7 +113,7 @@ defmodule Quantum.ExecutionBroadcaster do ) do debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Adding job #{inspect(name)}" + {"Adding job", node: Node.self(), name: name} end) {[], %{state | uninitialized_jobs: [job | uninitialized_jobs]}} @@ -127,7 +125,7 @@ defmodule Quantum.ExecutionBroadcaster do ) do debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Running job #{inspect(name)} once" + {"Running job once", node: Node.self(), name: name} end) {[%ExecuteEvent{job: job}], state} @@ -143,7 +141,7 @@ defmodule Quantum.ExecutionBroadcaster do ) do debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Removing job #{inspect(name)}" + {"Removing job", node: Node.self(), name: name} end) uninitialized_jobs = Enum.reject(uninitialized_jobs, &(&1.name == name)) @@ -218,9 +216,7 @@ defmodule Quantum.ExecutionBroadcaster do for %Job{name: job_name} = job <- jobs do debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Scheduling job for execution #{ - inspect(job_name) - }" + {"Scheduling job for execution", node: Node.self(), name: job_name} end) %ExecuteEvent{job: job} @@ -267,6 +263,8 @@ defmodule Quantum.ExecutionBroadcaster do job: job, error: e ) + + state end defp get_next_execution_time( diff --git a/lib/quantum/executor.ex b/lib/quantum/executor.ex index 59bdd9e0..de435cea 100644 --- a/lib/quantum/executor.ex +++ b/lib/quantum/executor.ex @@ -51,7 +51,7 @@ defmodule Quantum.Executor do ) do debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Start execution of job #{inspect(job_name)}" + {"Start execution of job", node: Node.self(), name: job_name} end) case TaskRegistry.mark_running(task_registry, job_name, node) do @@ -84,63 +84,37 @@ defmodule Quantum.Executor do ) do debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Task for job #{inspect(job_name)} started on node #{ - node - }" + {"Task for job started on node", node: Node.self(), name: job_name, started_on: node} end) Task.Supervisor.async_nolink({task_supervisor, node}, fn -> debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Execute started for job #{inspect(job_name)}" + {"Execute started for job", node: Node.self(), name: job_name} end) - # Note: we are intentionally mimicking the ":telemetry.span" here to keep current functionality - start_monotonic_time = :erlang.monotonic_time() - - :telemetry.execute([:quantum, :job, :start], %{system_time: start_monotonic_time}, %{ - job: job, - node: node, - scheduler: scheduler - }) - try do - execute_task(task) + :telemetry.span([:quantum, :job], %{job: job, node: node, scheduler: scheduler}, fn -> + result = execute_task(task) + {result, %{job: job, node: node, scheduler: scheduler, result: result}} + end) catch type, value -> debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Execution ended for job #{ - inspect(job_name) - }, which failed due to: #{Exception.format(type, value, __STACKTRACE__)}" + { + "Execution failed for job", + node: Node.self(), name: job_name, type: type, value: value + } end) - duration = :erlang.monotonic_time() - start_monotonic_time - - :telemetry.execute([:quantum, :job, :exception], %{duration: duration}, %{ - job: job, - node: node, - reason: value, - stacktrace: __STACKTRACE__, - scheduler: scheduler - }) + log_exception(type, value, __STACKTRACE__) else result -> debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Execution ended for job #{ - inspect(job_name) - }, which yielded result: #{inspect(result)}" + {"Execution ended for job", node: Node.self(), name: job_name, result: result} end) - - duration = :erlang.monotonic_time() - start_monotonic_time - - :telemetry.execute([:quantum, :job, :stop], %{duration: duration}, %{ - job: job, - node: node, - scheduler: scheduler, - result: result - }) end :ok @@ -156,4 +130,24 @@ defmodule Quantum.Executor do defp execute_task(fun) when is_function(fun, 0) do fun.() end + + def log_exception(kind, reason, stacktrace) do + reason = Exception.normalize(kind, reason, stacktrace) + + # TODO: Remove in a future version and make elixir 1.10 minimum requirement + if Version.match?(System.version(), "< 1.10.0") do + Logger.error(Exception.format(kind, reason, stacktrace)) + else + crash_reason = + case kind do + :throw -> {{:nocatch, reason}, stacktrace} + _ -> {reason, stacktrace} + end + + Logger.error( + Exception.format(kind, reason, stacktrace), + crash_reason: crash_reason + ) + end + end end diff --git a/lib/quantum/job_broadcaster.ex b/lib/quantum/job_broadcaster.ex index 03a8812f..608bd76c 100644 --- a/lib/quantum/job_broadcaster.ex +++ b/lib/quantum/job_broadcaster.ex @@ -49,7 +49,7 @@ defmodule Quantum.JobBroadcaster do :not_applicable -> debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Loading Initial Jobs from Config" + {"Loading Initial Jobs from Config", node: Node.self()} end) jobs @@ -57,7 +57,7 @@ defmodule Quantum.JobBroadcaster do storage_jobs when is_list(storage_jobs) -> debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Loading Initial Jobs from Storage, skipping config" + {"Loading Initial Jobs from Storage, skipping config", node: Node.self()} end) for %Job{state: :active} = job <- storage_jobs do @@ -104,17 +104,10 @@ defmodule Quantum.JobBroadcaster do %{^job_name => %Job{state: :active} = old_job} -> debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Replacing job #{inspect(job_name)}" + {"Replacing job", node: Node.self(), name: job_name} end) - # Send event to telemetry incase the end user wants to monitor events - :telemetry.execute([:quantum, :job, :update], %{}, %{ - job: job, - scheduler: state.scheduler - }) - - :ok = storage.delete_job(storage_pid, job_name) - :ok = storage.add_job(storage_pid, job) + :ok = update_job(storage, storage_pid, job, state.scheduler) {:noreply, [{:delete, old_job}, {:add, job}], %{state | jobs: Map.put(jobs, job_name, job)}} @@ -122,24 +115,17 @@ defmodule Quantum.JobBroadcaster do %{^job_name => %Job{state: :inactive}} -> debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Replacing job #{inspect(job_name)}" + {"Replacing job", node: Node.self(), name: job_name} end) - # Send event to telemetry incase the end user wants to monitor events - :telemetry.execute([:quantum, :job, :update], %{}, %{ - job: job, - scheduler: state.scheduler - }) - - :ok = storage.delete_job(storage_pid, job_name) - :ok = storage.add_job(storage_pid, job) + :ok = update_job(storage, storage_pid, job, state.scheduler) {:noreply, [{:add, job}], %{state | jobs: Map.put(jobs, job_name, job)}} _ -> debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Adding job #{inspect(job_name)}" + {"Adding job", node: Node.self(), name: job_name} end) # Send event to telemetry incase the end user wants to monitor events @@ -167,41 +153,27 @@ defmodule Quantum.JobBroadcaster do %{^job_name => %Job{state: :active} = old_job} -> debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Replacing job #{inspect(job_name)}" + {"Replacing job", node: Node.self(), name: job_name} end) - # Send event to telemetry incase the end user wants to monitor events - :telemetry.execute([:quantum, :job, :update], %{}, %{ - job: job, - scheduler: state.scheduler - }) - - :ok = storage.delete_job(storage_pid, job_name) - :ok = storage.add_job(storage_pid, job) + :ok = update_job(storage, storage_pid, job, state.scheduler) {:noreply, [{:delete, old_job}], %{state | jobs: Map.put(jobs, job_name, job)}} %{^job_name => %Job{state: :inactive}} -> debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Replacing job #{inspect(job_name)}" + {"Replacing job", node: Node.self(), name: job_name} end) - # Send event to telemetry incase the end user wants to monitor events - :telemetry.execute([:quantum, :job, :update], %{}, %{ - job: job, - scheduler: state.scheduler - }) - - :ok = storage.delete_job(storage_pid, job_name) - :ok = storage.add_job(storage_pid, job) + :ok = update_job(storage, storage_pid, job, state.scheduler) {:noreply, [], %{state | jobs: Map.put(jobs, job_name, job)}} _ -> debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Adding job #{inspect(job_name)}" + {"Adding job", node: Node.self(), name: job_name} end) # Send event to telemetry incase the end user wants to monitor events @@ -227,7 +199,7 @@ defmodule Quantum.JobBroadcaster do ) do debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Deleting job #{inspect(name)}" + {"Deleting job", node: Node.self(), name: name} end) case Map.fetch(jobs, name) do @@ -269,7 +241,7 @@ defmodule Quantum.JobBroadcaster do ) do debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Change job state #{inspect(name)}" + {"Change job state", node: Node.self(), name: name} end) case Map.fetch(jobs, name) do @@ -309,7 +281,7 @@ defmodule Quantum.JobBroadcaster do ) do debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Running job #{inspect(name)} once" + {"Running job once", node: Node.self(), name: name} end) case Map.fetch(jobs, name) do @@ -332,7 +304,7 @@ defmodule Quantum.JobBroadcaster do ) do debug_logging && Logger.debug(fn -> - "[#{inspect(Node.self())}][#{__MODULE__}] Deleting all jobs" + {"Deleting all jobs", node: Node.self()} end) for {_name, %Job{} = job} <- jobs do @@ -361,4 +333,19 @@ defmodule Quantum.JobBroadcaster do def handle_info(_message, state) do {:noreply, [], state} end + + defp update_job(storage, storage_pid, %Job{name: job_name} = job, scheduler) do + # Send event to telemetry incase the end user wants to monitor events + :telemetry.execute([:quantum, :job, :update], %{}, %{ + job: job, + scheduler: scheduler + }) + + if function_exported?(storage, :update_job, 2) do + :ok = storage.update_job(storage_pid, job) + else + :ok = storage.delete_job(storage_pid, job_name) + :ok = storage.add_job(storage_pid, job) + end + end end diff --git a/lib/quantum/normalizer.ex b/lib/quantum/normalizer.ex index ceb8814d..4afc18e1 100644 --- a/lib/quantum/normalizer.ex +++ b/lib/quantum/normalizer.ex @@ -85,6 +85,10 @@ defmodule Quantum.Normalizer do Job.set_timezone(job, normalize_timezone(timezone)) end + defp normalize_job_option({:state, state}, job) do + Job.set_state(job, state) + end + defp normalize_job_option(_, job), do: job @spec normalize_task(config_task) :: Job.task() | no_return diff --git a/lib/quantum/run_strategy/all.ex b/lib/quantum/run_strategy/all.ex index 3de4edef..f0be096e 100644 --- a/lib/quantum/run_strategy/all.ex +++ b/lib/quantum/run_strategy/all.ex @@ -1,6 +1,6 @@ defmodule Quantum.RunStrategy.All do @moduledoc """ - Run job on all node of the node list + Run job on all node of the node list. If the node list is `:cluster`, all nodes of the cluster will be used. diff --git a/lib/quantum/storage.ex b/lib/quantum/storage.ex index a55dac5f..f88e9af9 100644 --- a/lib/quantum/storage.ex +++ b/lib/quantum/storage.ex @@ -1,6 +1,6 @@ defmodule Quantum.Storage do @moduledoc """ - Bahaviour to be implemented by all Storage Adapters. + Behaviour to be implemented by all Storage Adapters. The calls to the storage are blocking, make sure they're fast to not block the job execution. """ @@ -76,4 +76,14 @@ defmodule Quantum.Storage do Purge all date from storage and go back to initial state. """ @callback purge(storage_pid :: storage_pid) :: :ok + + @doc """ + Updates existing job in storage. + + This callback is optional. If not implemented then the `c:delete_job/2` + and then the `c:add_job/2` callbacks will be called instead. + """ + @callback update_job(storage_pid :: storage_pid, job :: Job.t()) :: :ok + + @optional_callbacks update_job: 2 end diff --git a/lib/quantum/supervisor.ex b/lib/quantum/supervisor.ex index 0468d2f4..eb2a315a 100644 --- a/lib/quantum/supervisor.ex +++ b/lib/quantum/supervisor.ex @@ -3,8 +3,6 @@ defmodule Quantum.Supervisor do use Supervisor - require Logger - # Starts the quantum supervisor. @spec start_link(GenServer.server(), Keyword.t()) :: GenServer.on_start() def start_link(quantum, opts) do diff --git a/lib/quantum/task_registry.ex b/lib/quantum/task_registry.ex index 6826d41b..e9b1124c 100644 --- a/lib/quantum/task_registry.ex +++ b/lib/quantum/task_registry.ex @@ -3,8 +3,6 @@ defmodule Quantum.TaskRegistry do # Registry to check if a task is already running on a node. - require Logger - alias __MODULE__.StartOpts alias Quantum.Job diff --git a/mix.exs b/mix.exs index c5e431b7..93445556 100644 --- a/mix.exs +++ b/mix.exs @@ -3,7 +3,8 @@ defmodule Quantum.Mixfile do use Mix.Project - @version "3.3.0" + @source_url "https://github.com/quantum-elixir/quantum-core" + @version "3.4.0" def project do [ @@ -54,10 +55,10 @@ defmodule Quantum.Mixfile do "Jonatan Männchen" ], exclude_patterns: [~r[priv/plts]], - licenses: ["Apache License 2.0"], + licenses: ["Apache-2.0"], links: %{ - "Changelog" => "https://github.com/quantum-elixir/quantum-core/blob/master/CHANGELOG.md", - "GitHub" => "https://github.com/quantum-elixir/quantum-core" + "Changelog" => "#{@source_url}/blob/master/CHANGELOG.md", + "GitHub" => @source_url } } end @@ -66,13 +67,14 @@ defmodule Quantum.Mixfile do [ main: "readme", source_ref: "v#{@version}", - source_url: "https://github.com/quantum-elixir/quantum-core", + source_url: @source_url, + logo: "assets/quantum-elixir-logo.svg", extras: [ - "README.md", "CHANGELOG.md", + "README.md", "pages/supervision-tree.md", "pages/configuration.md", - "pages/runtime.md", + "pages/runtime-configuration.md", "pages/crontab-format.md", "pages/run-strategies.md", "pages/telemetry.md" @@ -97,13 +99,12 @@ defmodule Quantum.Mixfile do [ {:crontab, "~> 1.1"}, {:gen_stage, "~> 0.14 or ~> 1.0"}, + {:telemetry, "~> 0.4.3 or ~> 1.0.0"}, {:tzdata, "~> 1.0", only: [:dev, :test]}, - {:earmark, "~> 1.0", only: [:dev, :docs], runtime: false}, - {:ex_doc, "~> 0.19", only: [:dev, :docs], runtime: false}, + {:ex_doc, ">= 0.0.0", only: [:dev, :docs], runtime: false}, {:excoveralls, "~> 0.5", only: [:test], runtime: false}, {:dialyxir, "~> 1.0-rc", only: [:dev], runtime: false}, - {:credo, "~> 1.0", only: [:dev], runtime: false}, - {:telemetry, "~> 0.4"} + {:credo, "~> 1.0", only: [:dev], runtime: false} ] end end diff --git a/pages/configuration.md b/pages/configuration.md index 3e683c57..26e1162a 100644 --- a/pages/configuration.md +++ b/pages/configuration.md @@ -15,7 +15,7 @@ config :your_app, YourApp.Scheduler, # Runs on 18, 20, 22, 0, 2, 4, 6: {"0 18-6/2 * * *", fn -> :mnesia.backup('/var/backup/mnesia') end}, # Runs every midnight: - {"@daily", {Backup, :backup, []}} + {"@daily", {Backup, :backup, []}, state: :inactive} ] ``` @@ -72,6 +72,7 @@ Possible options: - `task` function to be performed, ex: `{Heartbeat, :send, []}` or `fn -> :something end` - `run_strategy` strategy on how to run tasks inside of cluster, default: `%Quantum.RunStrategy.Random{nodes: :cluster}` - `overlap` set to false to prevent next job from being executed if previous job is still running, default: `true` +- `state` set to `:inactive` to deactivate a job or `:active` to activate it It is possible to control the behavior of jobs at runtime. @@ -154,6 +155,3 @@ Timezones can also be configured on a per-job basis. This overrides the default timezone: "America/New_York" } ``` - -## Telemetry Support - diff --git a/pages/crontab-format.md b/pages/crontab-format.md index 4bc16da5..54893a62 100644 --- a/pages/crontab-format.md +++ b/pages/crontab-format.md @@ -11,7 +11,7 @@ | month | 1-12 (or names) | | day of week | 0-6 (0 is Sunday, or use abbreviated names) | -The `second` field can only be used in extended cron expressions. +The `second` field can only be used in extended Cron expressions. Names can also be used for the `month` and `day of week` fields. Use the first three letters of the particular day or month (case does not matter). @@ -43,5 +43,5 @@ Instead of the first five fields, one of these special strings may be used: All Cron Expressions are parsed and evaluated by [crontab](https://hex.pm/packages/crontab). -Issues with parsing a cron expression can be reported here: +Issues with parsing a Cron expression can be reported here: [crontab GitHub issues](https://github.com/jshmrtn/crontab/issues) diff --git a/pages/runtime.md b/pages/runtime-configuration.md similarity index 100% rename from pages/runtime.md rename to pages/runtime-configuration.md diff --git a/pages/supervision-tree.md b/pages/supervision-tree.md index 0f8e0ba6..40be512c 100644 --- a/pages/supervision-tree.md +++ b/pages/supervision-tree.md @@ -6,10 +6,10 @@ * `YourApp.Scheduler.JobBroadcaster` (`Quantum.JobBroadcaster`) - The `GenStage` that keeps track of all jobs. * `YourApp.Scheduler.ExecutionBroadcaster` (`Quantum.ExecutionBroadcaster`) - The `GenStage` that notifies execution of jobs. * `YourApp.Scheduler.ExecutorSupervisor` (`Quantum.ExecutorSupervisor`) - The `ConsumerSupervisor` that spawns an Executor for every execution. - - `no_name` (`YourApp.Scheduler.Executor`) - The `Task` that calls the `YourApp.Scheduler.Task.Supervisor` with the execution of the cron (per Node). - * `YourApp.Scheduler.Task.Supervisor` (`Task.Supervisor`) - The `Task.Supervisor` where all cron jobs run in. - - `Task` - The place where the defined cron job action gets called. + - `no_name` (`YourApp.Scheduler.Executor`) - The `Task` that calls the `YourApp.Scheduler.Task.Supervisor` with the execution of the Cron (per Node). + * `YourApp.Scheduler.Task.Supervisor` (`Task.Supervisor`) - The `Task.Supervisor` where all Cron jobs run in. + - `Task` - The place where the defined Cron job action gets called. ## Error Handling -The OTP Supervision Tree is initiated by the user of the library. Therefore the error handling can be implemented via normal OTP means. See `Supervisor.Spec` for more information. +The OTP Supervision Tree is initiated by the user of the library. Therefore the error handling can be implemented via normal OTP means. See `Supervisor` module for more information. diff --git a/pages/telemetry.md b/pages/telemetry.md index 818b4b3c..6dac35e8 100644 --- a/pages/telemetry.md +++ b/pages/telemetry.md @@ -1,6 +1,6 @@ # Telemetry -Sice version [`3.2.0`](https://github.com/quantum-elixir/quantum-core/releases/tag/v3.2.0) `quantum` supports [`:telemetry`](https://hexdocs.pm/telemetry) metrics. +Since version [`3.2.0`](https://github.com/quantum-elixir/quantum-core/releases/tag/v3.2.0) `quantum` supports [`:telemetry`](https://hexdocs.pm/telemetry) metrics.