Packages
phoenix_kit
1.7.151
1.7.211
1.7.210
1.7.209
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit/scheduled_jobs.ex
defmodule PhoenixKit.ScheduledJobs do
@moduledoc """
Context module for managing scheduled jobs.
Provides functions to create, cancel, and process scheduled jobs. The system uses
a behaviour-based handler pattern for extensibility - any module implementing
`PhoenixKit.ScheduledJobs.Handler` can be used to handle scheduled tasks.
## Usage
# Schedule a job using a handler module
{:ok, job} = ScheduledJobs.schedule_job(
MyApp.Posts.ScheduledPostHandler,
post.uuid,
~U[2025-01-15 10:00:00Z],
%{notify: true}
)
# Cancel a scheduled job
{:ok, job} = ScheduledJobs.cancel_job(job)
# Process all pending jobs (called by cron worker)
{:ok, %{executed: 5, failed: 1}} = ScheduledJobs.process_pending_jobs()
## Handler Pattern
Handlers must implement the `PhoenixKit.ScheduledJobs.Handler` behaviour:
defmodule MyHandler do
@behaviour PhoenixKit.ScheduledJobs.Handler
def job_type, do: "my_job"
def resource_type, do: "my_resource"
def execute(resource_uuid, args), do: :ok
end
"""
import Ecto.Query
require Logger
alias PhoenixKit.Config
alias PhoenixKit.ScheduledJobs.ScheduledJob
alias PhoenixKit.Utils.Date, as: UtilsDate
# Get the configured repo
defp repo, do: Config.get_repo()
## Public API
@doc """
Schedules a new job.
## Parameters
- `handler_module` - Module implementing `PhoenixKit.ScheduledJobs.Handler`
- `resource_uuid` - UUID of the target resource
- `scheduled_at` - DateTime when the job should execute
- `args` - Optional map of additional arguments (default: %{})
- `opts` - Optional keyword list with:
- `:priority` - Job priority (default: 0)
- `:max_attempts` - Max retry attempts (default: 3)
- `:created_by_uuid` - UUID of user creating the job
## Returns
- `{:ok, %ScheduledJob{}}` - Job created successfully
- `{:error, changeset}` - Validation failed
## Examples
iex> schedule_job(PostHandler, post.uuid, ~U[2025-01-15 10:00:00Z])
{:ok, %ScheduledJob{}}
iex> schedule_job(EmailHandler, email.uuid, scheduled_at, %{template: "welcome"}, priority: 10)
{:ok, %ScheduledJob{}}
"""
def schedule_job(handler_module, resource_uuid, scheduled_at, args \\ %{}, opts \\ []) do
attrs = %{
job_type: handler_module.job_type(),
handler_module: to_string(handler_module),
resource_type: handler_module.resource_type(),
resource_uuid: resource_uuid,
scheduled_at: scheduled_at,
args: args,
priority: Keyword.get(opts, :priority, 0),
max_attempts: Keyword.get(opts, :max_attempts, 3),
created_by_uuid: Keyword.get(opts, :created_by_uuid)
}
%ScheduledJob{}
|> ScheduledJob.create_changeset(attrs)
|> repo().insert()
end
@doc """
Cancels a scheduled job.
Only pending jobs can be cancelled. Returns error if job is already executed or cancelled.
## Examples
iex> cancel_job(job)
{:ok, %ScheduledJob{status: "cancelled"}}
iex> cancel_job(already_executed_job)
{:error, :already_executed}
"""
def cancel_job(%ScheduledJob{status: "pending"} = job) do
job
|> ScheduledJob.cancel_changeset()
|> repo().update()
end
def cancel_job(%ScheduledJob{status: "executed"}), do: {:error, :already_executed}
def cancel_job(%ScheduledJob{status: "cancelled"}), do: {:error, :already_cancelled}
def cancel_job(%ScheduledJob{status: "failed"}), do: {:error, :already_failed}
@doc """
Cancels all pending jobs for a specific resource.
Useful when deleting a resource or when rescheduling.
## Examples
iex> cancel_jobs_for_resource("post", post.uuid)
{3, nil}
"""
def cancel_jobs_for_resource(resource_type, resource_uuid) do
from(j in ScheduledJob,
where: j.resource_type == ^resource_type,
where: j.resource_uuid == ^resource_uuid,
where: j.status == "pending"
)
|> repo().update_all(set: [status: "cancelled", updated_at: UtilsDate.utc_now()])
end
@doc """
Reschedules a job to a new time.
Resets attempts and clears any previous errors.
## Examples
iex> reschedule_job(job, ~U[2025-01-20 10:00:00Z])
{:ok, %ScheduledJob{}}
"""
def reschedule_job(%ScheduledJob{} = job, new_scheduled_at) do
job
|> ScheduledJob.reschedule_changeset(new_scheduled_at)
|> repo().update()
end
@doc """
Processes all pending jobs that are due for execution.
Called by the cron worker every minute. Jobs are processed in priority order
(highest first), then by scheduled_at (oldest first).
## Returns
- `{:ok, %{executed: count, failed: count}}` - Processing summary
## Examples
iex> process_pending_jobs()
{:ok, %{executed: 5, failed: 1}}
"""
def process_pending_jobs do
now = UtilsDate.utc_now()
pending_jobs = get_pending_jobs(now)
results =
Enum.map(pending_jobs, fn job ->
execute_job(job)
end)
executed = Enum.count(results, &match?(:ok, &1))
failed = Enum.count(results, &match?({:error, _}, &1))
if executed > 0 or failed > 0 do
Logger.info(
"ScheduledJobs: Processed #{executed + failed} jobs (#{executed} executed, #{failed} failed)"
)
end
{:ok, %{executed: executed, failed: failed}}
end
@doc """
Gets all pending jobs that are due for execution.
## Parameters
- `as_of` - DateTime to check against (default: now)
## Examples
iex> get_pending_jobs()
[%ScheduledJob{}, ...]
"""
def get_pending_jobs(as_of \\ UtilsDate.utc_now()) do
from(j in ScheduledJob,
where: j.status == "pending",
where: j.scheduled_at <= ^as_of,
order_by: [desc: j.priority, asc: j.scheduled_at]
)
|> repo().all(log: false)
end
@doc """
Gets a scheduled job by ID.
"""
def get_job(id) do
repo().get(ScheduledJob, id)
end
@doc """
Gets all scheduled jobs for a resource.
## Examples
iex> get_jobs_for_resource("post", post.uuid)
[%ScheduledJob{}, ...]
"""
def get_jobs_for_resource(resource_type, resource_uuid) do
from(j in ScheduledJob,
where: j.resource_type == ^resource_type,
where: j.resource_uuid == ^resource_uuid,
order_by: [desc: j.inserted_at]
)
|> repo().all()
end
@doc """
Gets the pending job for a resource (if any).
## Examples
iex> get_pending_job_for_resource("post", post.uuid)
%ScheduledJob{status: "pending"}
"""
def get_pending_job_for_resource(resource_type, resource_uuid) do
from(j in ScheduledJob,
where: j.resource_type == ^resource_type,
where: j.resource_uuid == ^resource_uuid,
where: j.status == "pending",
limit: 1
)
|> repo().one()
end
@doc """
Deletes old completed jobs to prevent table bloat.
## Parameters
- `days` - Retention period in days (default: 7)
- `statuses` - List of statuses to delete (default: ["executed", "failed", "cancelled"])
## Returns
- `{count, nil}` - Number of deleted records
## Examples
iex> delete_old_jobs(7)
{1234, nil}
iex> delete_old_jobs(30, ["executed"])
{500, nil}
"""
def delete_old_jobs(days \\ 7, statuses \\ ["executed", "failed", "cancelled"]) do
cutoff_date = DateTime.add(UtilsDate.utc_now(), -days * 24 * 3600, :second)
{count, _} =
from(j in ScheduledJob,
where: j.status in ^statuses,
where: j.updated_at < ^cutoff_date
)
|> repo().delete_all(log: false)
if count > 0 do
Logger.info("ScheduledJobs: Deleted #{count} old job(s) older than #{days} days")
end
{count, nil}
end
## Private Functions
defp execute_job(%ScheduledJob{} = job) do
Logger.info(
"ScheduledJobs: Executing job #{job.uuid} (#{job.job_type}) for #{job.resource_type}/#{job.resource_uuid}"
)
handler_module = String.to_existing_atom(job.handler_module)
Logger.debug("ScheduledJobs: Using handler module #{handler_module}")
case handler_module.execute(job.resource_uuid, job.args) do
:ok ->
Logger.info("ScheduledJobs: Job #{job.uuid} executed successfully")
mark_executed(job)
:ok
{:ok, _result} ->
Logger.info("ScheduledJobs: Job #{job.uuid} executed successfully")
mark_executed(job)
:ok
{:error, reason} = error ->
Logger.warning("ScheduledJobs: Job #{job.uuid} failed with reason: #{inspect(reason)}")
mark_failed(job, reason)
error
end
rescue
e ->
error_message = Exception.message(e)
mark_failed(job, error_message)
Logger.error(
"ScheduledJobs: Job #{job.uuid} (#{job.job_type}) failed with exception: #{error_message}"
)
{:error, e}
end
defp mark_executed(%ScheduledJob{} = job) do
job
|> ScheduledJob.execute_changeset()
|> repo().update()
end
defp mark_failed(%ScheduledJob{} = job, error) do
job
|> ScheduledJob.fail_changeset(error)
|> repo().update()
end
end