- Notifications
You must be signed in to change notification settings - Fork19
Simple scheduling for Elixir
License
SchedEx/SchedEx
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
SchedEx is a simple yet deceptively powerful scheduling library for Elixir. Though it is almost trivially simple bydesign, it enables a number of very powerful use cases to be accomplished with very little effort.
SchedEx is written byMat Trudel, and development is generously supported by the fine folksatFunnelCloud.
For usage details, please refer to thedocumentation.
In most contextsSchedEx.run_every is the function most commonly used. There are two typical use cases:
This approach is useful when you want SchedEx to manage jobs whose configuration is static. At FunnelCloud, we use thisapproach to run things like our hourly reports, cleanup tasks and such. Typically, you will start jobs inside yourapplication.ex file:
defmoduleExample.ApplicationdouseApplicationdefstart(_type,_args)dochildren=[# Call Runner.do_frequent/0 every five minutes%{id:"frequent",start:{SchedEx,:run_every,[Example.Runner,:do_frequent,[],"*/5 * * * *"]}},# Call Runner.do_daily/0 at 1:01 UTC every day%{id:"daily",start:{SchedEx,:run_every,[Example.Runner,:do_daily,[],"1 1 * * *"]}},# You can also pass a function instead of an m,f,a:%{id:"hourly",start:{SchedEx,:run_every,[fn->IO.puts"It is the top of the hour"end,"0 * * * *"]}}]opts=[strategy::one_for_one,name:Example.Supervisor]Supervisor.start_link(children,opts)endend
This will cause the corresponding methods to be run according to the specified crontab entries. If the jobs crash theyalso take down the SchedEx process which ran them (SchedEx does not provide supervision by design). Your application'sSupervisor will then restart the relevant SchedEx process, which will continue to run according to its crontab entry.
SchedEx is especially suited to running tasks which run on a schedule and may be dynamically configured by the user.For example, at FunnelCloud we have aScheduledTask Ecto schema with a string field calledcrontab. At startup ourscheduled_task application reads entries from this table, determines themodule, function, argument whichshould be invoked when the task comes due, and adds a SchedEx job to a supervisor:
defstart_scheduled_tasks(sup,scheduled_tasks)doscheduled_tasks|>Enum.map(&child_spec_for_scheduled_task/1)|>Enum.map(&(DynamicSupervisor.start_child(sup,&1)))enddefpchild_spec_for_scheduled_task(%ScheduledTask{id:id,crontab:crontab}=task)do%{id:"scheduled-task-#{id}",start:{SchedEx,:run_every,mfa_for_task(task)++[crontab]}}enddefpmfa_for_task(task)do# Logic that returns the [m, f, a] that should be invoked when task comes due[IO,:puts,["Hello, scheduled task:#{inspecttask}"]]end
This will start one SchedEx process perScheduledTask, all supervised within aDynamicSupervisor. If either SchedEx orthe invoked function crashesDynamicSupervisor will restart it, making this approach robust to failures anywhere in theapplication. Note that the above is somewhat simplified - in production we have some additional logic to handlestarting / stopping / reloading tasks on user change.
You can optionally pass a name to the task that would allow you to lookup the task later with Registry or gproc and remove it like so:
child_spec=%{id:"scheduled-task-#{id}",start:{SchedEx,:run_every,mfa_for_task(task)++[crontab,[name:{:via,Registry,{RegistryName,"scheduled-task-#{id}"}}]]}}defget_scheduled_item(id)do#ie. "scheduled-task-1"list=Registry.lookup(RegistryName,id)iflength(list)>0do{pid,_}=hd(list){:ok,pid}else{:error,"does not exist"}endenddefcancel_scheduled_item(id)dowith{:ok,pid}<-get_scheduled_item(id)doDynamicSupervisor.terminate_child(DSName,pid)endend
Then in your children in application.ex
{Registry,keys::unique,name:RegistryName},{DynamicSupervisor,strategy::one_for_one,name:DSName},
In addition toSchedEx.run_every, SchedEx provides two other methods which serve to schedule jobs;SchedEx.run_at,andSchedEx.run_in. As the names suggest,SchedEx.run_at takes aDateTime struct which indicates the time at whichthe job should be executed, andSchedEx.run_in takes a duration in integer milliseconds from the time the function iscalled at which to execute the job. Similarly toSchedEx.run_every, these functions both come inmodule, function, argument andfn form.
The above functions have the same return values as standardstart_link functions ({:ok, pid} on success,{:error, error} on error). The returned pid can be passed toSchedEx.cancel to cancel any further invocations of the job.
SchedEx uses thecrontab library to parse crontab strings. If it is unable toparse the given crontab string, an error is returned from theSchedEx.run_every call and no jobs are scheduled.
Building on the support provided by the crontab library, SchedEx supportsextended crontabs. Such crontabs have7 segments instead of the usual 5; one is added to the beginning of the crontab and expresses a seconds value, and oneadded to the end expresses a year value. As such, it's possible to specify a unique instant down to the second, forexample:
5059233112*1999# You'd better be getting ready to party
Jobs scheduled viaSchedEx.run_every are implicitly recurring; they continue to to execute according to the crontabuntilSchedEx.cancel/1 is called or the original calling process terminates. If job execution takes longer than thescheduling interval, the job is requeued at the next matching interval (for example, if a job set to run every minute(crontab* * * * *) takes 61 seconds to run at minutex it will not run at minutex+1 and will next run at minutex+2).
SchedEx has a feature calledTimeScales which help provide a performant and high parity environment for testingscheduled code. When invokingSchedEx.run_every orSchedEx.run_in, you can pass an optionaltime_scale parameterwhich allows you to change the speed at which time runs within SchedEx. This allows you to run an entire day (or longer)worth of scheduling time in a much shorter amount of real time. For example:
defmoduleExampleTestdouseExUnit.CasedefmoduleAgentHelperdodefset(agent,value)doAgent.update(agent,fn_->valueend)enddefget(agent)doAgent.get(agent,&&1)endenddefmoduleTestTimeScaledodefnow(_)doDateTime.utc_now()enddefspeedupdo86400endendtest"updates the agent at 10am every morning"do{:ok,agent}=start_supervised({Agent,fn->nilend})SchedEx.run_every(AgentHelper,:set,[agent,:sched_ex_scheduled_time],"* 10 * * *",time_scale:TestTimeScale)# Let SchedEx run through a day's worth of scheduling timeProcess.sleep(1000)expected_time=Timex.now()|>Timex.beginning_of_day()|>Timex.shift(hours:34)assertDateTime.diff(AgentHelper.get(agent),expected_time)==0endend
will run through an entire day's worth of scheduling time in one second, and allows us to test against the expectationsof the called function quickly, while maintaining near-perfect parity with development. The only thing that changes inthe test environment is the passing of atime_scale; all other code is exactly as it is in production.
Note that in the above test, the atom:sched_ex_scheduled_time is passed as a value in the argument array. This atomis treated specially by SchedEx, and is replaced by the scheduled invocation time for which the function is beingcalled.
SchedEx can be installed by adding:sched_ex to your list of dependencies inmix.exs:
defdepsdo[{:sched_ex,"~> 1.0"}]end
Copyright (c) 2018 Mat Trudel on behalf of FunnelCloud Inc.
This work is free. You can redistribute it and/or modify it under theterms of the MIT License. See theLICENSE.md file for more details.
About
Simple scheduling for Elixir
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors8
Uh oh!
There was an error while loading.Please reload this page.
