# Wormhole
![wormhole](wormhole.jpg)
## Description
Wormhole captures anything that is emitted out of the callback
(return value or error reason) and transfers it to the calling process
in the form `{:ok, state}` or `{:error, reason}`.
Wormhole invokes `callback` in separate process and
waits for message from callback process containing callback return value
if finished successfully or
error reason if callback process failed for any reason.
If `callback` execution is not finished within specified timeout,
`callback` process is killed and error returned.
Default timeout value is specified in `@timeout_ms`.
User can specify `timeout_ms` in `options` keyword list.
By default if callback fails stacktrace will **not** be returned.
User can set `stacktrace` option to `true` and in that case stacktrace will
be returned in response.
Note: `stacktrace` option works only if `crush_report` is not enabled.
By default there is no retry, but user can specify
`retry_count` and `backoff_ms` in `options`.
Default back-off time value is specified in `@backoff_ms`.
Note: `retry_count` specifies maximum number of times `callback` can be invoked.
More accurate name would be `try_count` but I think it would bring
more confusion than clarity, hence the name remains.
By default exceptions in callback-process are handled so that
supervisor does not generate CRUSH REPORT (when released - Exrm/Distillery).
This behavior can be overridden by setting `crush_report` to `true`.
Note:
- Crush report is not generated in Elixir by default.
- Letting exceptions propagate might be useful for
some other applications too (e.g sentry client).
## Installation
Add to the list of dependencies:
```elixir
def deps do
[
{:wormhole, "~> 1.4"}
]
end
```
Add to the list of applications:
```elixir
def application do
[applications: [:wormhole]]
end
```
## Examples
### Successful execution - returning callback return value
Unnamed function:
```elixir
iex> Wormhole.capture(fn-> :a end)
{:ok, :a}
```
Named function without arguments:
```elixir
iex> Wormhole.capture(&Process.list/0)
{:ok, [#PID<0.0.0>, #PID<0.3.0>, #PID<0.6.0>, #PID<0.7.0>, ...]}
```
Named function with arguments:
```elixir
iex> Wormhole.capture(Enum, :count, [[1,2,3]])
{:ok, 3}
```
Both versions with timeout explicitly set to 2 seconds:
```elixir
iex> Wormhole.capture(&Process.list/0, timeout_ms: 2_000)
{:ok, [#PID<0.0.0>, #PID<0.3.0>, #PID<0.6.0>, #PID<0.7.0>, ...]}
iex> Wormhole.capture(Enum, :count, [[1,2,3]], timeout_ms: 2_000)
{:ok, 3}
```
### Failed execution - returning failure reason
```elixir
defmodule Test do
def f do
raise "Hello"
end
end
iex> Wormhole.capture(&Test.f/0)
{:error,
{%RuntimeError{message: "Hello"},
[{Test, :f, 0, [file: 'iex', line: 23]},
{Wormhole, :"-send_return_value/1-fun-0-", 2,
[file: 'lib/wormhole.ex', line: 75]}]}}
iex> Wormhole.capture(fn-> throw :foo end)
{:error,
{{:nocatch, :foo},
[{Wormhole, :"-send_return_value/1-fun-0-", 2,
[file: 'lib/wormhole.ex', line: 75]}]}}
iex> Wormhole.capture(fn-> exit :foo end)
{:error, :foo}
```
### Retry
```elixir
iex> Wormhole.capture(&foo/0, [timeout_ms: 2_000, retry_count: 3, backoff_ms: 300])
```
### Usage pattern
```elixir
def ... do
...
(&some_function/0) |> Wormhole.capture |> some_function_response_handler
...
end
def some_function_response_handler({:ok, response}) do
...
end
def some_function_response_handler({:error, error}) do
...
end
```