-module(clockwork_schedule). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/clockwork_schedule.gleam"). -export([new/3, with_logging/1, with_time_offset/2, stop/1, start/1, supervised/2]). -export_type([schedule/0, message/0, state/0, scheduler/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( "////////////////////////////////////////////////////////////////////////////////\n" "////////////////////////////////////////////////////////////////////////////////\n" ). -type schedule() :: {schedule, gleam@erlang@process:subject(message())}. -type message() :: run | stop. -type state() :: {state, binary(), gleam@erlang@process:subject(message()), clockwork:cron(), fun(() -> nil), gleam@time@duration:duration()}. -opaque scheduler() :: {scheduler, binary(), clockwork:cron(), fun(() -> nil), boolean(), gleam@time@duration:duration()}. -file("src/clockwork_schedule.gleam", 120). ?DOC( " Creates a new scheduler configuration.\n" " \n" " ## Parameters\n" " \n" " - `id`: A unique identifier for this scheduler (used in logging)\n" " - `cron`: A cron expression defining when the job should run\n" " - `job`: The function to execute on each scheduled occurrence\n" " \n" " ## Example\n" " \n" " ```gleam\n" " import clockwork\n" " \n" " let assert Ok(cron) = clockwork.from_string(\"0 */2 * * *\") // Every 2 hours\n" " let scheduler = clockwork_schedule.new(\"data_sync\", cron, fn() {\n" " database.sync_remote_data()\n" " })\n" " ```\n" ). -spec new(binary(), clockwork:cron(), fun(() -> nil)) -> scheduler(). new(Id, Cron, Job) -> {scheduler, Id, Cron, Job, false, {duration, 0, 0}}. -file("src/clockwork_schedule.gleam", 139). ?DOC( " Enables logging for the scheduler.\n" " \n" " When logging is enabled, the scheduler will log:\n" " - When a job starts executing (with timestamp)\n" " - When the scheduler is stopped\n" " \n" " Logging uses the `logging` library and outputs at the `Info` level.\n" " \n" " ## Example\n" " \n" " ```gleam\n" " let scheduler = \n" " clockwork_schedule.new(\"cleanup\", cron, cleanup_fn)\n" " |> clockwork_schedule.with_logging() // Enable logging\n" " ```\n" ). -spec with_logging(scheduler()) -> scheduler(). with_logging(Scheduler) -> {scheduler, erlang:element(2, Scheduler), erlang:element(3, Scheduler), erlang:element(4, Scheduler), true, erlang:element(6, Scheduler)}. -file("src/clockwork_schedule.gleam", 172). ?DOC( " Sets a time zone offset for the scheduler.\n" " \n" " By default, schedulers use the system's UTC offset. Use this function\n" " to run scheduled tasks in a specific time zone.\n" " \n" " ## Parameters\n" " \n" " - `scheduler`: The scheduler to configure\n" " - `offset`: The UTC offset as a Duration (positive for east, negative for west)\n" " \n" " ## Example\n" " \n" " ```gleam\n" " import gleam/time/duration\n" " \n" " // Configure for UTC+9 (Tokyo)\n" " let tokyo_offset = duration.from_hours(9)\n" " \n" " let scheduler = \n" " clockwork_schedule.new(\"tokyo_job\", cron, job_fn)\n" " |> clockwork_schedule.with_time_offset(tokyo_offset)\n" " \n" " // Configure for UTC-5 (New York)\n" " let ny_offset = duration.from_hours(-5)\n" " \n" " let ny_scheduler = \n" " clockwork_schedule.new(\"ny_job\", cron, job_fn)\n" " |> clockwork_schedule.with_time_offset(ny_offset)\n" " ```\n" ). -spec with_time_offset(scheduler(), gleam@time@duration:duration()) -> scheduler(). with_time_offset(Scheduler, Offset) -> {scheduler, erlang:element(2, Scheduler), erlang:element(3, Scheduler), erlang:element(4, Scheduler), erlang:element(5, Scheduler), Offset}. -file("src/clockwork_schedule.gleam", 360). ?DOC( " Gracefully stops a running scheduler.\n" " \n" " Sends a stop message to the scheduler, which will:\n" " 1. Cancel any pending job executions\n" " 2. Log a stop message (if logging is enabled)\n" " 3. Terminate the scheduler actor\n" " \n" " ## Parameters\n" " \n" " - `schedule`: The Schedule handle returned from `start` or `supervised`\n" " \n" " ## Example\n" " \n" " ```gleam\n" " let assert Ok(schedule) = clockwork_schedule.start(scheduler)\n" " \n" " // Run for some time...\n" " process.sleep(60_000) // 1 minute\n" " \n" " // Gracefully stop\n" " clockwork_schedule.stop(schedule)\n" " ```\n" " \n" " ## Note\n" " \n" " After calling `stop`, the Schedule handle becomes invalid and cannot\n" " be reused. To restart scheduling, create and start a new scheduler.\n" ). -spec stop(schedule()) -> nil. stop(Schedule) -> gleam@erlang@process:send(erlang:element(2, Schedule), stop). -file("src/clockwork_schedule.gleam", 388). -spec enqueue_job(clockwork:cron(), state()) -> gleam@erlang@process:timer(). enqueue_job(Cron, State) -> Now = gleam@time@timestamp:system_time(), Next_occurrence = begin _pipe = clockwork:next_occurrence(Cron, Now, erlang:element(6, State)), _pipe@1 = gleam@time@timestamp:difference(Now, _pipe), _pipe@2 = gleam@time@duration:to_seconds_and_nanoseconds(_pipe@1), begin {Seconds, Nanoseconds} = _pipe@2, (Seconds * 1000) + (Nanoseconds div 1000000) end end, gleam@erlang@process:send_after( erlang:element(3, State), Next_occurrence, run ). -file("src/clockwork_schedule.gleam", 364). -spec loop(state(), message()) -> gleam@otp@actor:next(state(), any()). loop(State, Message) -> case Message of run -> logging:log( info, <<<<<<"[CLOCKWORK] Running job: "/utf8, (erlang:element(2, State))/binary>>/binary, " at "/utf8>>/binary, (begin _pipe = gleam@time@timestamp:system_time(), _pipe@1 = gleam@time@timestamp:add( _pipe, erlang:element(6, State) ), _pipe@2 = gleam@time@timestamp:to_unix_seconds(_pipe@1), gleam_stdlib:float_to_string(_pipe@2) end)/binary>> ), proc_lib:spawn_link(erlang:element(5, State)), enqueue_job(erlang:element(4, State), State), gleam@otp@actor:continue(State); stop -> logging:log( info, <<"[CLOCKWORK] Stopping job: "/utf8, (erlang:element(2, State))/binary>> ), gleam@otp@actor:stop() end. -file("src/clockwork_schedule.gleam", 185). -spec start_actor(scheduler()) -> {ok, gleam@otp@actor:started(gleam@erlang@process:subject(message()))} | {error, gleam@otp@actor:start_error()}. start_actor(Scheduler) -> case erlang:element(5, Scheduler) of true -> logging_ffi:configure(); false -> nil end, _pipe@4 = gleam@otp@actor:new_with_initialiser( 100, fun(Self) -> State = {state, erlang:element(2, Scheduler), Self, erlang:element(3, Scheduler), erlang:element(4, Scheduler), erlang:element(6, Scheduler)}, Selector = begin _pipe = gleam_erlang_ffi:new_selector(), gleam@erlang@process:select(_pipe, Self) end, enqueue_job(erlang:element(3, Scheduler), State), _pipe@1 = gleam@otp@actor:initialised(State), _pipe@2 = gleam@otp@actor:selecting(_pipe@1, Selector), _pipe@3 = gleam@otp@actor:returning(_pipe@2, Self), {ok, _pipe@3} end ), _pipe@5 = gleam@otp@actor:on_message(_pipe@4, fun loop/2), gleam@otp@actor:start(_pipe@5). -file("src/clockwork_schedule.gleam", 259). ?DOC( " Starts an unsupervised scheduler.\n" " \n" " This function starts a scheduler as a standalone actor that will run\n" " according to its cron expression. For production use, prefer `supervised`\n" " to run the scheduler under OTP supervision for better fault tolerance.\n" " \n" " ## Parameters\n" " \n" " - `scheduler`: The scheduler configuration to start\n" " \n" " ## Returns\n" " \n" " - `Ok(Schedule)`: A handle to control the running scheduler\n" " - `Error(actor.StartError)`: If the scheduler fails to start\n" " \n" " ## Example\n" " \n" " ```gleam\n" " import clockwork\n" " import clockwork_schedule\n" " \n" " pub fn main() {\n" " let assert Ok(cron) = clockwork.from_string(\"*/30 * * * *\") // Every 30 minutes\n" " \n" " let scheduler = \n" " clockwork_schedule.new(\"metrics\", cron, fn() {\n" " metrics.collect_and_report()\n" " })\n" " |> clockwork_schedule.with_logging()\n" " \n" " let assert Ok(schedule) = clockwork_schedule.start(scheduler)\n" " \n" " // The scheduler is now running\n" " // Stop it when done:\n" " clockwork_schedule.stop(schedule)\n" " }\n" " ```\n" " \n" " ## Note\n" " \n" " The scheduler will continue running until explicitly stopped with `stop`\n" " or until the process crashes. For automatic restart on failure, use\n" " `supervised` instead.\n" ). -spec start(scheduler()) -> {ok, schedule()} | {error, gleam@otp@actor:start_error()}. start(Scheduler) -> _pipe = start_actor(Scheduler), case _pipe of {ok, X} -> {ok, {schedule, erlang:element(3, X)}}; {error, E} -> {error, E} end. -file("src/clockwork_schedule.gleam", 322). ?DOC( " Creates a child specification for running the scheduler under OTP supervision.\n" " \n" " This is the recommended way to run schedulers in production. The scheduler\n" " will be automatically restarted if it crashes, ensuring your scheduled\n" " tasks remain reliable.\n" " \n" " ## Parameters\n" " \n" " - `scheduler`: The scheduler configuration\n" " - `schedule_receiver`: A subject to receive the Schedule handle once started\n" " \n" " ## Returns\n" " \n" " A `supervision.ChildSpec` that can be added to your supervision tree.\n" " \n" " ## Example\n" " \n" " ```gleam\n" " import clockwork\n" " import clockwork_schedule\n" " import gleam/erlang/process\n" " import gleam/otp/static_supervisor as supervisor\n" " \n" " pub fn main() {\n" " let assert Ok(cron) = clockwork.from_string(\"0 * * * *\") // Every hour\n" " \n" " let scheduler = \n" " clockwork_schedule.new(\"hourly_task\", cron, fn() {\n" " perform_hourly_maintenance()\n" " })\n" " |> clockwork_schedule.with_logging()\n" " \n" " // Create a receiver for the schedule handle\n" " let schedule_receiver = process.new_subject()\n" " \n" " // Create the child spec\n" " let schedule_child_spec = \n" " clockwork_schedule.supervised(scheduler, schedule_receiver)\n" " \n" " // Add to supervision tree\n" " let assert Ok(sup) =\n" " supervisor.new()\n" " |> supervisor.add(schedule_child_spec)\n" " |> supervisor.start()\n" " \n" " // Receive the schedule handle\n" " let assert Ok(schedule) = process.receive(schedule_receiver, 1000)\n" " \n" " // The scheduler is now running under supervision\n" " process.sleep_forever()\n" " }\n" " ```\n" " \n" " ## Fault Tolerance\n" " \n" " If the scheduler crashes, the supervisor will automatically restart it.\n" " The new instance will recalculate the next occurrence and continue\n" " scheduling jobs as expected.\n" ). -spec supervised(scheduler(), gleam@erlang@process:subject(schedule())) -> gleam@otp@supervision:child_specification(gleam@erlang@process:subject(message())). supervised(Scheduler, Schedule_receiver) -> gleam@otp@supervision:worker( fun() -> gleam@result:'try'( start_actor(Scheduler), fun(Started) -> gleam@erlang@process:send( Schedule_receiver, {schedule, erlang:element(3, Started)} ), {ok, Started} end ) end ).