defmodule Inngest.FnOpts do
@moduledoc """
Function configuration options.
See the typespec for all available options.
"""
defstruct [
:id,
:name,
:debounce,
:priority,
:batch_events,
:rate_limit,
:idempotency,
:concurrency,
:cancel_on,
retries: 3
]
alias Inngest.Util
@type t() :: %__MODULE__{
id: id(),
name: name(),
retries: retries(),
debounce: debounce(),
priority: priority(),
batch_events: batch_events(),
rate_limit: rate_limit(),
idempotency: idempotency(),
concurrency: concurrency(),
cancel_on: cancel_on()
}
@typedoc """
A unique identifier for your function. This should not change between deploys.
"""
@type id() :: binary()
@typedoc """
A name for your function. If defined, this will be shown in the UI as a friendly display name instead of the ID.
"""
@type name() :: binary() | nil
@typedoc """
Configure the number of times the function will be retried from `0` to `20`. Default: `3`
"""
@type retries() :: number() | nil
@typedoc """
Options to configure function debounce ([reference](https://www.inngest.com/docs/reference/functions/debounce)).
**period** - `string` required
The time period of which to set the limit. The period begins when the first matching event is received. How long to wait before invoking the function with the batch even if it's not full. Current permitted values are from `1s` to `7d (168h)`.
**key** - `string` optional
A unique key expression to apply the debounce to. The expression is evaluated for each triggering event.
Expressions are defined using the [Common Expression Language (CEL)](https://github.com/google/cel-go) with the original event accessible using dot-notation. Examples:
* Debounce per customer id: `event.data.customer_id`
* Debounce per account and email address: `event.data.account_id + "-" + event.user.email`
"""
@type debounce() ::
%{
key: binary() | nil,
period: binary()
}
| nil
@typedoc """
Prioritize specific function runs ahead of others ([reference](https://www.inngest.com/docs/reference/functions/run-priority))
**run** - `string` optional
An expression which must return an integer between -600 and 600 (by default), with higher return values resulting in a higher priority.
See [reference](https://www.inngest.com/docs/reference/functions/run-priority) for more information.
"""
@type priority() ::
%{
run: binary() | nil
}
| nil
@typedoc """
Configure how the function should consume batches of events ([reference](https://www.inngest.com/docs/guides/batching))
**max_size** - `number` required
The maximum number of events a batch can have. Current limit is `100`.
**timeout** - `string` required
How long to wait before invoking the function with the batch even if it's not full.
Current permitted values are from `1s` to `60s`.
"""
@type batch_events() ::
%{
max_size: number(),
timeout: binary()
}
| nil
@typedoc """
Options to configure how to rate limit function execution ([reference](https://www.inngest.com/docs/reference/functions/rate-limit))
**limit** - `number` required
The maximum number of functions to run in the given time period.
**period** - `number` required
The time period of which to set the limit. The period begins when the first matching event is received.
How long to wait before invoking the function with the batch even if it's not full.
Current permitted values are from `1s` to `60s`.
**key** - `string` optional
A unique key expression to apply the limit to. The expression is evaluated for each triggering event.
Expressions are defined using the [Common Expression Language (CEL)](https://github.com/google/cel-go) with the original event accessible using dot-notation. Examples:
* Rate limit per customer id: `event.data.customer_id`
* Rate limit per account and email address: `event.data.account_id + "-" + event.user.email`
### Note
This option cannot be used with `cancel_on` and `rate_limit`.
"""
@type rate_limit() ::
%{
limit: number(),
period: binary(),
key: binary() | nil
}
| nil
@typedoc """
A key expression which is used to prevent duplicate events from triggering a function more than once in 24 hours.
This is equivalent to setting `rate_limit` with a `key`, a limit of `1` and period of `24hr`.
Expressions are defined using the [Common Expression Language (CEL)](https://github.com/google/cel-go) with the original event accessible using dot-notation. Examples:
* Only run once for each customer id: `event.data.customer_id`
* Only run once for each account and email address: `event.data.account_id + "-" + event.user.email`
"""
@type idempotency() :: binary() | nil
@typedoc """
Limit the number of concurrently running functions ([reference](https://www.inngest.com/docs/functions/concurrency))
**limit** - `string` required
The maximum number of concurrently running steps.
**key** - `string` optional
A unique key expression for which to restrict concurrently running steps to. The expression is evaluated for each triggering event and a unique key is generate.
"""
@type concurrency() :: number() | concurrency_option() | list(concurrency_option()) | nil
@type concurrency_option() ::
%{
limit: number(),
key: binary() | nil,
scope: binary() | nil
}
| nil
@concurrency_scopes ["fn", "env", "account"]
@typedoc """
Define an event that can be used to cancel a running or sleeping function ([reference](https://www.inngest.com/docs/functions/cancellation))
**event** - `string` required
The event name which will be used to cancel
**match** - `string` optional
The property to match the event trigger and the cancelling event, using dot-notation
e.g. `data.userId`
**if** - `string` optional
TODO
**timeout** - `string` optional
The amount of time to wait to receive the cancelling event.
e.g. `30m`, `3h`, or `2d`
"""
@type cancel_on() :: cancel_option() | list(cancel_option()) | nil
@type cancel_option() :: %{
event: binary(),
match: binary() | nil,
if: binary() | nil,
timeout: binary() | nil
}
@doc false
@spec validate_debounce(t(), map()) :: map()
def validate_debounce(fnopts, config) do
case fnopts |> Map.get(:debounce) do
nil ->
config
debounce ->
period = Map.get(debounce, :period)
if is_nil(period) do
raise Inngest.DebounceConfigError, message: "'period' must be set for debounce"
end
case Util.parse_duration(period) do
{:error, error} ->
raise Inngest.DebounceConfigError, message: error
{:ok, seconds} ->
# credo:disable-for-next-line
if seconds > 7 * Util.day_in_seconds() do
raise Inngest.DebounceConfigError,
message: "cannot specify period for more than 7 days"
end
end
Map.put(config, :debounce, debounce)
end
end
@doc false
@spec validate_priority(t(), map()) :: map()
def validate_priority(fnopts, config) do
case fnopts |> Map.get(:priority) do
nil ->
config
%{} = priority ->
run = Map.get(priority, :run)
if !is_nil(run) && !is_binary(run) do
raise Inngest.PriorityConfigError, message: "invalid priority run: '#{run}'"
end
Map.put(config, :priority, priority)
rest ->
raise Inngest.PriorityConfigError, message: "invalid priority config: '#{rest}'"
end
end
@doc false
@spec validate_batch_events(t(), map()) :: map()
# credo:disable-for-next-line
def validate_batch_events(fnopts, config) do
case fnopts |> Map.get(:batch_events) do
nil ->
config
batch ->
max_size = Map.get(batch, :max_size)
timeout = Map.get(batch, :timeout)
if is_nil(max_size) || is_nil(timeout) do
raise Inngest.BatchEventConfigError,
message: "'max_size' and 'timeout' must be set for batch_events"
end
rate_limit = Map.get(fnopts, :rate_limit)
cancel_on = Map.get(fnopts, :cancel_on)
if !is_nil(rate_limit) do
raise Inngest.BatchEventConfigError,
message: "'rate_limit' cannot be used with event_batches"
end
if !is_nil(cancel_on) do
raise Inngest.BatchEventConfigError,
message: "'cancel_on' cannot be used with event_batches"
end
case Util.parse_duration(timeout) do
{:error, error} ->
raise Inngest.BatchEventConfigError, message: error
{:ok, seconds} ->
# credo:disable-for-next-line
if seconds < 1 || seconds > 60 do
raise Inngest.BatchEventConfigError,
message: "'timeout' duration set to '#{timeout}', needs to be 1s - 60s"
end
end
batch = %{maxSize: max_size, timeout: timeout}
Map.put(config, :batchEvents, batch)
end
end
@doc false
@spec validate_rate_limit(t(), map()) :: map()
def validate_rate_limit(fnopts, config) do
case fnopts |> Map.get(:rate_limit) do
nil ->
config
rate_limit ->
limit = Map.get(rate_limit, :limit)
period = Map.get(rate_limit, :period)
if is_nil(limit) || is_nil(period) do
raise Inngest.RateLimitConfigError,
message: "'limit' and 'period' must be set for rate_limit"
end
case Util.parse_duration(period) do
{:error, error} ->
raise Inngest.RateLimitConfigError, message: error
{:ok, seconds} ->
# credo:disable-for-next-line
if seconds < 1 || seconds > 60 do
raise Inngest.RateLimitConfigError,
message: "'period' duration set to '#{period}', needs to be 1s - 60s"
end
end
Map.put(config, :rateLimit, rate_limit)
end
end
@doc false
@spec validate_idempotency(t(), map()) :: map()
def validate_idempotency(fnopts, config) do
# NOTE: nothing really to validate, just have this for the sake of consistency
case fnopts |> Map.get(:idempotency) do
nil ->
config
setting ->
if !is_binary(setting) do
raise Inngest.IdempotencyConfigError, message: "idempotency must be a CEL string"
end
Map.put(config, :idempotency, setting)
end
end
@doc false
@spec validate_concurrency(t(), map()) :: map()
def validate_concurrency(fnopts, config) do
case fnopts |> Map.get(:concurrency) do
nil ->
config
%{} = setting ->
validate_concurrency(setting)
Map.put(config, :concurrency, setting)
[_ | _] = settings ->
Enum.each(settings, &validate_concurrency/1)
Map.put(config, :concurrency, settings)
setting ->
if is_number(setting) do
Map.put(config, :concurrency, setting)
else
raise Inngest.ConcurrencyConfigError, message: "invalid concurrency setting"
end
end
end
defp validate_concurrency(%{} = setting) do
limit = Map.get(setting, :limit)
scope = Map.get(setting, :scope)
if is_nil(limit) do
raise Inngest.ConcurrencyConfigError, message: "'limit' must be set for concurrency"
end
if !is_nil(scope) && !Enum.member?(@concurrency_scopes, scope) do
raise Inngest.ConcurrencyConfigError,
message: "invalid scope '#{scope}', needs to be \"fn\"|\"env\"|\"account\""
end
end
@doc false
@spec validate_cancel_on(t(), map()) :: map()
def validate_cancel_on(fnopts, config) do
case fnopts |> Map.get(:cancel_on) do
nil ->
config
%{} = settings ->
validate_cancel_on(settings)
Map.put(config, :cancel, [settings])
[_ | _] = settings ->
if Enum.count(settings) > 5 do
raise Inngest.CancelConfigError,
message: "cannot have more than 5 cancellation triggers"
end
Enum.each(settings, &validate_cancel_on/1)
Map.put(config, :cancel, settings)
rest ->
raise Inngest.CancelConfigError, message: "invalid cancellation config: '#{rest}'"
end
end
defp validate_cancel_on(%{} = settings) do
event = Map.get(settings, :event)
timeout = Map.get(settings, :timeout)
if is_nil(event) do
raise Inngest.CancelConfigError, message: "'event' must be set for cancel_on"
end
if !is_nil(timeout) do
# credo:disable-for-next-line
case Util.parse_duration(timeout) do
{:error, error} ->
raise Inngest.CancelConfigError, message: error
{:ok, _} ->
nil
end
end
end
end