diff --git a/LICENSE b/LICENSE index de1fa96..8d9f1d5 100644 --- a/LICENSE +++ b/LICENSE @@ -175,7 +175,7 @@ END OF TERMS AND CONDITIONS - Copyright 2019, zhongwencool . + Copyright 2019-2025, zhongwencool . Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 267a6ce..d8b277f 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,38 @@ - -# ecron [![GitHub Actions][action-img]][action] [![codeCov-img]][codeCov] [![hex-img]][hex] [![tag]][tag] [![License]][License] -[action]: https://github.com/zhongwencool/ecron -[action-img]: https://github.com/zhongwencool/ecron/actions/workflows/ci.yml/badge.svg -[codeCov]: https://codecov.io/gh/zhongwencool/ecron -[codeCov-img]: https://codecov.io/gh/zhongwencool/ecron/branch/master/graph/badge.svg?token=FI9WAQ6UG5 -[hex]: https://hex.pm/packages/ecron -[hex-img]: https://img.shields.io/hexpm/v/ecron.svg?style=flat -[tag]: https://img.shields.io/github/tag/zhongwencool/ecron.svg -[License]: https://img.shields.io/hexpm/l/ecron.svg +# ecron [![GitHub Actions](https://github.com/zhongwencool/ecron/actions/workflows/ci.yml/badge.svg)](https://github.com/zhongwencool/ecron) [![CodeCov](https://codecov.io/gh/zhongwencool/ecron/branch/master/graph/badge.svg?token=FI9WAQ6UG5)](https://codecov.io/gh/zhongwencool/ecron) [![Hex](https://img.shields.io/hexpm/v/ecron.svg?style=flat)](https://hex.pm/packages/ecron) [![Tag](https://img.shields.io/github/tag/zhongwencool/ecron.svg)](https://img.shields.io/github/tag/zhongwencool/ecron.svg) [![License](https://img.shields.io/hexpm/l/ecron.svg)](https://img.shields.io/hexpm/l/ecron.svg) [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/ecron/) A lightweight and efficient cron-like job scheduling library for Erlang. -## Overview +# Overview Ecron is designed to manage scheduled jobs within a single gen_server process, similar to the standard library's [stdlib's timer](http://erlang.org/doc/man/timer.html). It uses an ordered_set ETS table to organize jobs by the next run time, ensuring efficient execution. Unlike traditional cron, Ecoron does not poll the system every second, which reduces message overhead and process usage. -more detail see [Implementation](#Implementation). +## Key Features - -### Key Features - Supports both cron-like and interval-based scheduling. - Well-tested with [PropTest](https://github.com/proper-testing/proper) [![codecov](https://codecov.io/gh/zhongwencool/ecron/branch/master/graph/badge.svg?token=FI9WAQ6UG5)](https://codecov.io/gh/zhongwencool/ecron). - Utilizes gen_server timeout mechanism for precise timing. - Efficient process management, avoiding high memory usage. -You can find a collection of general practices in [Full Erlang Examples](https://github.com/zhongwencool/ecron/blob/master/examples/titan_erlang) and [Full Elixir Examples](https://github.com/zhongwencool/ecron/blob/master/examples/titan_elixir). - ## Installation - -**Erlang** - ```erlang + + + +### Erlang + +```erlang %% rebar.config {deps, [ecron]} - ``` +``` +### Elixir -**Elixir** - ```elixir +```elixir # mix.exs def deps do [{:ecron, "~> 1.0.1"}] end ``` + + ## Basic Usage Configure Ecoron in your sys.config file with timezone and job specifications: @@ -146,6 +138,9 @@ ok = ecron:delete(crontabuniqueName), EveryMFA = {io, format, ["Runs every 120 second.~n"]}, {ok, _} = ecron:add(everyUniqueName, 120, EveryMFA), ``` + +You can find a collection of general practices in [Full Erlang Examples](https://github.com/zhongwencool/ecron/blob/master/examples/titan_erlang) and [Full Elixir Examples](https://github.com/zhongwencool/ecron/blob/master/examples/titan_elixir). + ## Debug Support Ecron provides functions to assist with debugging: @@ -225,97 +220,98 @@ Additionally, you can use `ecron:statistic(Name)` to see the job's latest 16 res |<-------| ``` -[Check this for global_jobs workflow](https://github.com/zhongwencool/ecron/blob/master/doc/global.md#Implementation). +[Check this for global_jobs workflow](global.html#Implementation). -## Telemetry +## CRON Expression Guide -Ecron publishes events through telemetry, allowing for monitoring and alerting, -You can handle those events by [this guide](https://github.com/zhongwencool/ecron/blob/master/doc/telemetry.md). +### Basic Format -## CRON Expression Format +A cron expression consists of 5-6 fields representing time units: -A [cron expression](https://www.wikiwand.com/en/Cron) represents a set of times, using 5-6 space-separated fields. -Currently, W (nearest weekday), L (last day of month/week), and # (nth weekday of the month) are not supported. - -Most other features supported by popular cron implementations should work just fine. -```shell script - # ┌────────────── second (optional) - # │ ┌──────────── minute - # │ │ ┌────────── hour - # │ │ │ ┌──────── day of month - # │ │ │ │ ┌────── month - # │ │ │ │ │ ┌──── day of week - # │ │ │ │ │ │ - # │ │ │ │ │ │ - # 0 * * * * * +``` +# ┌────────────── second (optional) +# │ ┌──────────── minute +# │ │ ┌────────── hour +# │ │ │ ┌──────── day of month +# │ │ │ │ ┌────── month +# │ │ │ │ │ ┌──── day of week +# │ │ │ │ │ │ +# │ │ │ │ │ │ +# 0 * * * * * ``` -Field name | Mandatory? | Allowed values | Allowed special characters ----------- | ---------- | -------------- | -------------------------- -Seconds | No | 0-59 | * / , - -Minutes | Yes | 0-59 | * / , - -Hours | Yes | 0-23 | * / , - -Day of month | Yes | 1-31 | * / , - -Month | Yes | 1-12 or JAN-DEC | * / , - -Day of week | Yes | 0-6 or SUN-SAT | * / , - +### Field Values -Note: Month and Day-of-week field values are case-insensitive. "SUN", "Sun", and "sun" are equally accepted. +Field | Required | Values | Special Characters +Second | No | 0-59 | `* / , -` +Minute | Yes | 0-59 | `* / , -` +Hour | Yes | 0-23 | `* / , -` +Day of Month | Yes | 1-31 | `* / , -` +Month | Yes | 1-12 or JAN-DEC | `* / , -` +Day of Week | Yes | 0-6 or SUN-SAT | `* / , -` -When specifying your cron values you'll need to make sure that your values fall within the ranges. -For instance, some cron's use a 0-7 range for the day of week where both 0 and 7 represent Sunday. We do not. +> **Note**: Month and Day-of-week values are case-insensitive. ### Special Characters -#### Asterisk ( * ) -The asterisk indicates that the cron expression will match for all values of the field. -For example, using an asterisk in the `month` field would indicate every month. -#### Slash ( / ) -Slashes are used to describe increments of ranges. -For example, "3-59/15" in the `minutes` field would indicate the 3rd minute of the hour and every 15 minutes thereafter. -The form "*/..." is equivalent to the form "First-Last/...", that is, an increment over the largest possible range of the field. -The form "N/..." is accepted as meaning "N-Max/...", that is, starting at N, use the increment until the end of that specific range. -It does not wrap around. +- `*` - Any value +- `/` - Step values (e.g., `*/15` - every 15 units) +- `,` - Value list (e.g., `1,3,5`) +- `-` - Range (e.g., `1-5`) -#### Comma ( , ) -Commas are used to separate items of a list. -For example, using "MON,WED,FRI" in the `day_of_week` field would mean Mondays, Wednesdays and Fridays. +### Predefined Schedules -#### Hyphen ( - ) -Hyphens are used to define ranges. -For example, using "9-17" in the `hours`field would indicate every hour between 9am and 5pm inclusive. +Expression | Description | Equivalent +`@yearly` | Once a year (midnight, Jan 1) | `0 0 0 1 1 *` +`@monthly` | Once a month (midnight, first day) | `0 0 0 1 * *` +`@weekly` | Once a week (midnight, Sunday) | `0 0 0 * * 0` +`@daily` | Once a day (midnight) | `0 0 0 * * *` +`@hourly` | Once an hour | `0 0 * * * *` +`@minutely` | Once a minute | `0 * * * * *` -## Predefined crontab +### Fixed Intervals -You may use one of several pre-defined crontab in place of a cron expression. +For simpler scheduling needs, use `@every` with a duration: -Entry | Description | Equivalent To ------ | ----------- | ------------- -@yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * -@monthly | Run once a month, midnight, first of month | 0 0 0 1 * * -@weekly | Run once a week, midnight between Sat/Sun | 0 0 0 * * 0 -@daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * -@hourly | Run once an hour, beginning of hour | 0 0 * * * * -@minutely | Run once an minute, beginning of minute | 0 * * * * * +```erlang +% Run every 30 minutes +"@every 30m" ->There are tools that help when constructing your cronjobs. ->You might find something like [https://crontab.guru/](https://crontab.guru/) or [https://cronjob.xyz/](https://cronjob.xyz/) helpful. ->But, note that these don't necessarily accept the exact same syntax as this library, ->for instance, it doesn't accept the seconds field, so keep that in mind. ->The best way to verify the spec format is `ecron:parse_spec("0 0 1 1 1-6 1", 10).`. +% Run every 1 hour and 30 minutes +"@every 1h30m" +``` -## Intervals +Duration units: `d`(days), `h`(hours), `m`(minutes), `s`(seconds) -Ecron also supports scheduling jobs at fixed intervals: +### Error Handling -```shell -@every -``` -For example, @every 1h30m10s schedules a job to run every 1 hour, 30 minutes, and 10 seconds. +- Failed jobs don't affect other jobs +- Execution results and timing are stored (last 16 runs) +- Jobs can be configured as singleton or parallel +- System time changes are handled gracefully + +## Best Practices + +1. **Job Names** + - Use descriptive, unique names + - Consider adding prefixes for different job types + +2. **Time Windows** + - Use start/end times for temporary jobs + - Consider time zone implications + +3. **Error Handling** + - Implement proper error handling in job functions + - Monitor job execution through telemetry + +4. **Resource Management** + - Group related jobs under same supervisor + - Use `singleton: false` only when needed + +5. **Testing** + - Validate cron expressions before deployment + - Test jobs with different time scenarios ->Note: The interval doesn't take the job runtime into account. ->For example, if a job takes 3 minutes to run, and it is scheduled to run every 5 minutes, ->it only has 2 minutes of idle time between each run. - ## Test This command will run property-based tests, common tests, and generate a coverage report with verbose output. diff --git a/changelog.md b/changelog.md index 46e115f..f1661a4 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,13 @@ + +### 1.0.2 +- Use ex_doc to update documentation. + +### 1.0.1 +- Fix concurrent cron job execution timing issue. + +### 1.0.0 +Nothing changed, just update version from 0.6.1 to 1.0.0. + ### 0.6.1 - fix some spec warning. - upgrade telemetry to 1.1.0 diff --git a/doc/global.md b/global.md similarity index 100% rename from doc/global.md rename to global.md diff --git a/rebar.config b/rebar.config index 9e35f2d..14f5f59 100644 --- a/rebar.config +++ b/rebar.config @@ -5,7 +5,7 @@ {profiles, [{test, [{deps, [proper]}]}]}. {project_plugins, [ - rebar3_proper, covertool, {rebar3_edoc_extensions, "1.5.0"}, rebar3_format, erlfmt + rebar3_proper, covertool, rebar3_ex_doc, rebar3_format, erlfmt ]}. {cover_enabled, true}. @@ -24,15 +24,6 @@ deprecated_functions ]}. -{edoc_opts, [ - {stylesheet, "stylesheet.css"}, - {preprocess, true}, - {includes, ["."]}, - {sort_functions, false}, - {doclet, edoc_doclet_chunks}, - {layout, edoc_layout_chunks} -]}. - {alias, [ {check, [ xref, @@ -55,3 +46,23 @@ %% ...or no options at all. {options, #{print_width => 120, ignore_pragma => true}} ]}. + +{hex, [ + {doc, #{provider => ex_doc}} +]}. + +{ex_doc, [ + {extras, [ + {"README.md", #{title => "Overview"}}, + + {"telemetry.md", #{title => "Telemetry"}}, + {"global.md", #{title => "Cluster Task"}}, + {"CHANGELOG.md", #{title => "Changelog"}}, + {"LICENSE", #{title => "License"}} + ]}, + {main, "README.md"}, + {homepage_url, "https://github.com/zhongwencool/ecron"}, + {source_url, "https://github.com/zhongwencool/ecron"}, + {api_reference, false}, + {with_mermaid, true} +]}. diff --git a/src/ecron.app.src b/src/ecron.app.src index 88452f7..1e9fb4f 100644 --- a/src/ecron.app.src +++ b/src/ecron.app.src @@ -1,6 +1,6 @@ {application, ecron, [ {description, "cron-like/crontab job scheduling library"}, - {vsn, "1.0.1"}, + {vsn, "1.0.2"}, {registered, [ecron_sup, ecron_local, ecron_monitor]}, {mod, {ecron_app, []}}, {applications, [kernel, stdlib, telemetry]}, @@ -31,6 +31,6 @@ ]} ]}, {modules, []}, - {licenses, ["Apache 2.0"]}, + {licenses, ["Apache-2.0"]}, {links, [{"Github", "https://github.com/zhongwencool/ecron"}]} ]}. diff --git a/src/ecron.erl b/src/ecron.erl index 1a5fa7e..938752d 100644 --- a/src/ecron.erl +++ b/src/ecron.erl @@ -130,18 +130,25 @@ add(JobName, Spec, MFA, Start, End, Opts) -> add(?LocalJob, JobName, Spec, MFA, Start, End, Opts). %% @doc -%% Add new crontab job. All jobs that exceed the limit will be automatically removed. +%% Adds a new crontab job with specified parameters. Jobs exceeding their limits are automatically removed. +%% +%% Parameters: %%
    -%%
  • `JobName': The unique name of job, return `{error, already_exist}' if JobName is already exist.
  • -%%
  • `Spec': A cron expression represents a set of times.
  • -%%
  • `MFA': Spawn a process to run MFA when crontab is triggered.
  • -%%
  • `Start': The job's next trigger time is Calculated from StartTime. Keeping `unlimited' if start from now on.
  • -%%
  • `End': The job will be remove at end time. Keeping `unlimited' if never end.
  • -%%
  • `Opts': The optional list of options. `{singleton, true}': Default job is singleton, Each task cannot be executed concurrently. -%% `{max_count, pos_integer()}': This task can be run up to `MaxCount' times, default is `unlimited'. -%%
  • +%%
  • `Register' - The process name where the job will be registered
  • +%%
  • `JobName' - Unique identifier for the job. Returns `{error, already_exist}' if duplicate
  • +%%
  • `Spec' - Crontab expression defining execution schedule
  • +%%
  • `MFA' - `{Module, Function, Args}' to execute when triggered
  • +%%
  • `Start' - Start time `{Hour,Min,Sec}' or `unlimited' for immediate start
  • +%%
  • `End' - End time `{Hour,Min,Sec}' or `unlimited' for no end
  • +%%
  • `Opts' - Options list: +%%
      +%%
    • `{singleton, boolean()}' - If true (default), prevents concurrent execution
    • +%%
    • `{max_count, pos_integer() | unlimited}' - Maximum executions allowed
    • +%%
    +%%
  • %%
%% +%% Returns: `{ok, JobName}' | `{error, already_exist}' | `{error, parse_error(), term()}' -spec add(register(), name(), crontab_spec(), mfargs(), start_at(), end_at(), options()) -> ecron_result(). add(Register, JobName, Spec, MFA, Start, End, Opts) -> @@ -175,15 +182,25 @@ add(Register, JobName, Spec, MFA, Start, End, Opts) -> end. %% @doc -%% Starts a timer which will send message to destination when crontab is triggered. +%% Creates a one-time timer that sends a message when the crontab spec is triggered. +%% +%% Parameters: +%%
    +%%
  • `Spec' - Crontab expression defining when to trigger
  • +%%
  • `Dest' - Destination pid() or registered name to receive message
  • +%%
  • `Message' - Term to send when timer triggers
  • +%%
+%% +%% Notes: %%
    -%%
  • Equivalent to `erlang:send_after/3' expect the `Time' format.
  • -%%
  • If Dest is a pid() it has to be a pid() of a local process, dead or alive.
  • -%%
  • The Time value can, in the current implementation, not be greater than 4294967295.
  • -%%
  • If Dest is an atom(), it is supposed to be the name of a registered process. The process referred to by the name is looked up at the time of delivery. No error is given if the name does not refer to a process.
  • -%%
  • If Dest is a pid(), the timer will be automatically canceled if the process referred to by the pid() is not alive, or when the process exits.
  • -%%
  • Warning: Cancels a timer by `erlang:cancel_timer(Ref)' not `ecron:delete/1'.
  • +%%
  • Similar to erlang:send_after/3 but uses crontab format
  • +%%
  • Dest pid must be local
  • +%%
  • Maximum time value is 4294967295 milliseconds
  • +%%
  • Timer auto-cancels if destination process dies
  • +%%
  • Use erlang:cancel_timer/1 to cancel, not ecron:delete/1
  • %%
+%% +%% Returns: `{ok, reference()}' | `{error, parse_error(), term()}' -spec send_after(crontab_spec(), pid() | atom(), term()) -> {ok, reference()} | {error, parse_error(), term()}. send_after(Spec, Pid, Message) -> @@ -214,20 +231,21 @@ send_interval(Register, Name, Spec, Message, Start, End, Option) -> send_interval(Register, Name, Spec, self(), Message, Start, End, Option). %% @doc -%% Evaluates Pid ! Message repeatedly when crontab is triggered. -%% (Pid can also be an atom of a registered name.) +%% Sends a message to a process repeatedly based on a crontab schedule. +%% +%% Parameters: %%
    -%%
  • `JobName': The unique name of job, return `{error, already_exist}' if the name is already exist.
  • -%%
  • `Spec': A cron expression represents a set of times.
  • -%%
  • `Pid': The target pid which receive message.
  • -%%
  • `Message': Any erlang term.
  • -%%
  • `Start': The job's next trigger time is Calculated from StartDatetime. Keeping `unlimited' if start from now on.
  • -%%
  • `End': The job will be remove at end time. Keeping `unlimited' if never end.
  • -%%
  • `Opts': The optional list of options. `{singleton, true}': Default job is singleton, Each task cannot be executed concurrently. -%% `{max_count, pos_integer()}': This task can be run up to `MaxCount' times, default is `unlimited'. -%%
  • +%%
  • `Register' - Process name where job will be registered
  • +%%
  • `JobName' - Unique identifier for the job
  • +%%
  • `Spec' - Crontab expression defining execution schedule
  • +%%
  • `Pid' - Destination process ID or registered name
  • +%%
  • `Message' - Term to send on each trigger
  • +%%
  • `Start' - Start time `{Hour,Min,Sec}' or `unlimited'
  • +%%
  • `End' - End time `{Hour,Min,Sec}' or `unlimited'
  • +%%
  • `Option' - Same options as add/7
  • %%
%% +%% Returns: Same as add/7 -spec send_interval( register(), name(), crontab_spec(), pid(), term(), start_at(), end_at(), options() ) -> @@ -235,23 +253,38 @@ send_interval(Register, Name, Spec, Message, Start, End, Option) -> send_interval(Register, JobName, Spec, Pid, Message, Start, End, Option) -> add(Register, JobName, Spec, {erlang, send, [Pid, Message]}, Start, End, Option). -%% @equiv delete(ecron_local, Name). +%% @equiv delete(ecron_local, Name) -spec delete(name()) -> ok. delete(JobName) -> delete(?LocalJob, JobName). %% @doc -%% Delete an exist job, if the job is nonexistent, nothing happened. +%% Deletes an existing job. If the job does not exist, it will be ignored. +%% +%% Parameters: +%%
    +%%
  • `Register' - Process name where job is registered
  • +%%
  • `JobName' - Name of job to delete
  • +%%
+%% +%% Returns: `ok' -spec delete(register(), name()) -> ok. delete(Register, JobName) -> ok = gen_server:call(Register, {delete, JobName}, infinity). -%% @equiv deactivate(ecron_local, Name). +%% @equiv deactivate(ecron_local, Name) -spec deactivate(name()) -> ok | {error, not_found}. deactivate(JobName) -> deactivate(?LocalJob, JobName). %% @doc -%% Deactivate an exist job, if the job is nonexistent, return `{error, not_found}'. -%% just freeze the job, use @see activate/2 to unfreeze job. +%% Deactivates an existing job temporarily. The job can be reactivated using activate/2. +%% +%% Parameters: +%%
    +%%
  • `Register' - Process name where job is registered
  • +%%
  • `JobName' - Name of job to deactivate
  • +%%
+%% +%% Returns: `ok' | `{error, not_found}' -spec deactivate(register(), name()) -> ok | {error, not_found}. deactivate(Register, JobName) -> case gen_server:call(Register, {deactivate, JobName}, infinity) of @@ -259,14 +292,20 @@ deactivate(Register, JobName) -> {error, not_found} -> {error, not_found} end. -%% @equiv activate(ecron_local, Name). +%% @equiv activate(ecron_local, Name) -spec activate(name()) -> ok | {error, not_found}. activate(JobName) -> activate(?LocalJob, JobName). %% @doc -%% Activate an exist job, if the job is nonexistent, return `{error, not_found}'. -%% if the job is already activate, nothing happened. -%% the same effect as reinstall the job from now on. +%% Activates a previously deactivated job. The job will resume from the current time. +%% +%% Parameters: +%%
    +%%
  • `Register' - Process name where job is registered
  • +%%
  • `JobName' - Name of job to activate
  • +%%
+%% +%% Returns: `ok' | `{error, not_found}' -spec activate(register(), name()) -> ok | {error, not_found}. activate(Register, JobName) -> case gen_server:call(Register, {activate, JobName}, infinity) of @@ -274,6 +313,11 @@ activate(Register, JobName) -> {error, not_found} -> {error, not_found} end. +%% @doc +%% Retrieves statistics for local registered jobs. +%% +%% Returns: List of statistics for all jobs in local registries. +%% Each statistic entry contains the same information as statistic/2. -spec statistic(register() | name()) -> [statistic()]. statistic(Register) -> case is_atom(Register) andalso undefined =/= erlang:whereis(Register) of @@ -284,8 +328,23 @@ statistic(Register) -> end. %% @doc -%% Statistic from an exist job. -%% if the job is nonexistent, return `{error, not_found}'. +%% Retrieves statistics for a specific job. +%% +%% Parameters: +%%
    +%%
  • `Register' - Process name where job is registered
  • +%%
  • `JobName' - Name of job to get statistics for
  • +%%
+%% +%% Returns: `{ok, statistic()}' | `{error, not_found}' +%% Where statistic() contains: +%%
    +%%
  • Job configuration
  • +%%
  • Execution counts (success/failure)
  • +%%
  • Latest results
  • +%%
  • Run times
  • +%%
  • Next scheduled runs
  • +%%
-spec statistic(register(), name()) -> {ok, statistic()} | {error, not_found}. statistic(Register, JobName) -> case ets:lookup(Register, JobName) of @@ -301,7 +360,10 @@ statistic(Register, JobName) -> end. %% @doc -%% Statistic for all jobs. +%% Retrieves statistics for both local and global registered jobs. +%% +%% Returns: List of statistics for all jobs in both local and global registries. +%% Each statistic entry contains the same information as statistic/2. -spec statistic() -> [statistic()]. statistic() -> Local = ets:foldl( @@ -319,14 +381,33 @@ statistic() -> lists:append(Local, Global). %% @doc -%% Reload task manually, such as you should reload manually when the system time has alter a lot. +%% Reloads all tasks manually. Useful when system time has changed significantly. +%% +%% This will: +%%
    +%%
  • Recalculate next execution times for all jobs
  • +%%
  • Reset internal timers
  • +%%
  • Apply to both local and global jobs
  • +%%
+%% +%% Returns: ok -spec reload() -> ok. reload() -> gen_server:cast(?LocalJob, reload), gen_server:cast({global, ?GlobalJob}, reload). %% @doc -%% Parse a crontab spec with next trigger time. For debug. +%% Parses a crontab specification and returns the next N trigger times. +%% Useful for debugging and validating crontab expressions. +%% +%% Parameters: +%%
    +%%
  • `Spec' - Crontab expression to parse
  • +%%
  • `Num' - Number of future trigger times to calculate
  • +%%
+%% +%% Returns: `{ok, #{type => cron|every, crontab => parsed_spec, next => [rfc3339_string()]}}' | +%% `{error, parse_error(), term()}' -spec parse_spec(crontab_spec(), pos_integer()) -> {ok, #{type => cron | every, crontab => crontab_spec(), next => [rfc3339_string()]}} | {error, atom(), term()}. @@ -342,6 +423,7 @@ parse_spec2({error, _Field, _Value} = Error, _Num) -> get_next_schedule_time(Name) -> gen_server:call(?LocalJob, {next_schedule_time, Name}, infinity). +%% @private clear() -> gen_server:call(?LocalJob, clear, infinity). predict_datetime(Job, Num) -> @@ -352,7 +434,7 @@ predict_datetime(Job, Num) -> %%%=================================================================== %%% CallBack %%%=================================================================== - +%% @private start_link({_, JobTab} = Name, JobSpec) -> case ecron_spec:parse_crontab(JobSpec, []) of {ok, Jobs} -> @@ -363,7 +445,7 @@ start_link({_, JobTab} = Name, JobSpec) -> end; start_link(JobTab, JobSpec) when is_atom(JobTab) -> start_link({local, JobTab}, JobSpec). - +%% @private init([JobTab, Jobs]) -> erlang:process_flag(trap_exit, true), TimerTab = ets:new(ecron_timer, [ordered_set, protected, {keypos, #timer.key}]), @@ -382,7 +464,7 @@ init([JobTab, Jobs]) -> time_zone = TimeZone }, {ok, State, next_timeout(State)}. - +%% @private handle_call({add, Job, Opts}, _From, State) -> #state{timer_tab = TimerTab, job_tab = JobTab, time_zone = TimeZone} = State, Reply = add_job(JobTab, TimerTab, Job, TimeZone, Opts, false), @@ -418,7 +500,7 @@ handle_call(clear, _From, State = #state{timer_tab = TimerTab, job_tab = JobTab} {reply, ok, State, next_timeout(State)}; handle_call(_Unknown, _From, State) -> {noreply, State, next_timeout(State)}. - +%% @private handle_info(timeout, State) -> {noreply, State, tick(State)}; handle_info({'EXIT', Pid, _Reason}, State = #state{timer_tab = TimerTab, job_tab = JobTab}) -> @@ -426,7 +508,7 @@ handle_info({'EXIT', Pid, _Reason}, State = #state{timer_tab = TimerTab, job_tab {noreply, State, next_timeout(State)}; handle_info(_Unknown, State) -> {noreply, State, next_timeout(State)}. - +%% @private handle_cast(_Unknown, State) -> {noreply, State, next_timeout(State)}. @@ -660,6 +742,7 @@ update_next_schedule(Count, _Max, Cron, Cur, Name, TZ, CurPid, Tab, _JobTab) -> NextTimer = Cron#timer{key = {Next, Name}, singleton = CurPid, cur_count = Count}, ets:insert(Tab, NextTimer). +%% @private spawn_mfa(JobTab, Name, MFA) -> Start = erlang:monotonic_time(), {Event, OkInc, FailedInc, NewRes} = diff --git a/doc/telemetry.md b/telemetry.md similarity index 99% rename from doc/telemetry.md rename to telemetry.md index 998b88a..f68d981 100644 --- a/doc/telemetry.md +++ b/telemetry.md @@ -1,4 +1,3 @@ - ## Telemetry Ecron uses [Telemetry](https://github.com/beam-telemetry/telemetry) for instrumentation and for having an extensible way of doing logging. Telemetry is a metrics and instrumentation library for Erlang and Elixir applications that is based on publishing events through a common interface and attaching handlers to handle those events.