# `Malla`
[🔗](https://github.com/netkubes/malla/blob/main/lib/malla.ex#L21)

`Malla` is a comprehensive framework that simplifies the development of distributed
services through a plugin-based architecture with compile-time callback chaining,
automatic service discovery across nodes, and minimal "magic" to keep systems
understandable.

See the [Introduction guide](../../guides/01-introduction.md) for a general overview.

## API Overview

### Service Management

- `get_service_id/0`, `get_service_id!/0` - Get current service from process dictionary.
- `put_service_id/1` - Set service context for current process.
- `get_service_name/1` - Get string name (cached for performance).
- `get_service_meta/1` - Get metadata (cluster, node, host, service).

### Callback Invocation

- `local/2` - Invoke local callback with service_id from process dictionary.
- `local/3` - Invoke local callback.
- `remote/3` - Invoke remote callback.
- `remote/4` - Invoke remote callback with options.
- `call/1` - Macro for syntactic sugar to invoke remote callbacks.
- `call/2` - Macro for syntactic sugar to invoke remote callbacks with options.

### Utilities

- `metric/4` - Record metric with auto-injected metadata.
- `event/2` - Generate event with service context.
- `authorize/3` - Authorization callback.

## See Also

- `Malla.Service` - Service behavior and lifecycle.
- `Malla.Plugin` - Plugin development guide.
- `Malla.Tracer` - Observability instrumentation.
- `Malla.Node` - Service discovery and RPC.

# `authorize_opt`

```elixir
@type authorize_opt() :: {:service_id, id()} | term()
```

# `class`

```elixir
@type class() :: atom()
```

# `config`

```elixir
@type config() :: list()
```

# `cont`

```elixir
@type cont() ::
  :cont | {:cont, list()} | {:cont, any(), any()} | {:cont, any(), any(), any()}
```

# `id`

```elixir
@type id() :: module()
```

# `metric_opt`

```elixir
@type metric_opt() :: {:service_id, id()}
```

# `remote_opt`

```elixir
@type remote_opt() ::
  {:timeout, pos_integer()}
  | {:sna_retries, pos_integer()}
  | {:excp_retries, pos_integer()}
  | {:retries_sleep_msec, pos_integer()}
```

# `service`

```elixir
@type service() :: Malla.Service.t()
```

# `vsn`

```elixir
@type vsn() :: String.t()
```

# `authorize`

```elixir
@spec authorize(term(), term(), [authorize_opt()]) ::
  boolean() | {boolean(), term()} | {:error, term()}
```

  Utility function to authorize a request.

  It will simply call `c:Malla.Plugins.Base.malla_authorize/3`.
  You must implement this callback in your service.
  By default it will return `{:error, :auth_not_implemented}`.

# `call`
*macro* 

Convenient macro to make remote calls more friendly.

It calls `remote/4` with the module, function name, and arguments extracted from the given expression.

## Examples

    call Module.fun(:a, :b), timeout: 5000
    # Translates to: remote(Module, :fun, [:a, :b], timeout: 5000)

# `call`
*macro* 

Convenient macro to make remote calls more friendly.

It calls `remote/3` with the module, function name, and arguments extracted from the given expression.

## Examples

    call Module.fun(:a, :b)
    # Translates to: remote(Module, :fun, [:a, :b])

# `get_service_id`

```elixir
@spec get_service_id() :: id() | nil
```

  Gets the current service ID from the process dictionary, or `nil` if not defined.

  On each callback call, called using `local/3` or `remote/4`, the service ID is always inserted in the process dictionary.

# `get_service_id`

```elixir
@spec get_service_id(list() | map()) :: id() | nil
```

Tries to extract the service ID from the `:service_id` key in a map or list,
or, if not found, from the process dictionary. See `get_service_id/0`.

# `get_service_id!`

```elixir
@spec get_service_id!() :: id()
```

Tries to get the service ID from the process dictionary, or raises `Malla.ServiceIdMissing`.
See `get_service_id/0`.

# `get_service_id!`

```elixir
@spec get_service_id!(Keyword.t() | map()) :: id()
```

Tries to extract the service ID from the `:service_id` key in a map or list,
or, if not found, from the process dictionary.
If not found, it raises `Malla.ServiceIdMissing`. See `get_service_id/0`.

# `get_service_meta`

```elixir
@spec get_service_meta(id()) :: %{
  cluster: String.t(),
  node: String.t(),
  host: String.t(),
  service: String.t()
}
```

Returns metadata about a service ID. Cached for fast access.
- `cluster` is extracted from `:malla` application's `:malla_cluster` environment variable.
- `node` is the current Erlang node.
- `host` is the first part of the node name.
- `service` uses `get_service_name/1`.

# `get_service_name`

```elixir
@spec get_service_name(nil | id() | String.t()) :: String.t()
```

Returns the string version of a service ID. Cached for fast access.

# `local`

```elixir
@spec local(atom(), list()) :: any()
```

  Invokes a service callback locally at this node.
  The service ID must be present in the process dictionary.
  See `local/3`.

# `local`

```elixir
@spec local(id(), atom(), list()) :: any()
```

  Invokes a service callback locally at this node.
  Service does not need to be running, since this simply:
  * puts service ID into process dictionary.
  * calls `c:Malla.Plugins.Base.service_cb_in/3`, which, if not overridden, will
  ultimately call the indicated function.
  * sets back previous value in process dictionary, if any.

# `metric`

```elixir
@spec metric(atom() | [atom()], number() | map() | keyword(), map() | keyword(), [
  metric_opt()
]) :: :ok
```

  Inserts a new metric value.

  It calls `:telemetry.execute/3` with the given `class`, `value` and `meta`.
  - `class`: Taken from the calling arg, but converted to a list if it is not already.
  - `value`: If it is a number, it is converted to `%{value: <number>}`.
    If it is a list, it is converted to a map.
  - `meta`: Merged with the service's metadata obtained from `get_service_meta/1`.
    The service ID must be in `opts` or the process dictionary.

# `put_service_id`

```elixir
@spec put_service_id(id() | nil) :: id()
```

  Puts a service ID into the process dictionary.

  This is used to mark the current process as belonging to this service.
  Callbacks called for a module will have it already.

  If id is `nil` or `:undefined`, the key is deleted.

# `remote`

```elixir
@spec remote(id(), atom(), [any()], [remote_opt()]) :: any()
```

  Calls a callback function defined at the home module of a local or remote service,
  using `Malla.Node.call_cb/4`. This could be a _normal_ function defined with `def` or
  a _callback function_ defined with `defcb`.

  On the remote side, the process leader will be changed to `:user` so that IO responses are not
  sent back to the caller. It will set the correct service ID in the process dictionary and call
  the callback function `c:Malla.Plugins.Base.service_cb_in/3`, which, if not overridden, will
  ultimately call the indicated function.

  * If the {:error, `malla_service_not_available`} is returned, it means `Malla.Node` could not find
    any service to process the request. The call will be retried if `sna_retries` is defined
    (default value is 5).
    The sleep time between retries can be set with `retries_sleep_msec`, and it is 1000 by default.

    This is very convenient in situations when the remote service is not yet available,
    because it may be starting or our node could not yet discover the service.

  * If an exception is produced during the call, the call is retried only if
    `excp_retries` is defined.
    Otherwise, the error `{:error, {:malla_rpc_error, <error_data>}}` is returned. Default `excp_retries` is 0.

    Be very careful when activating these retries, since the request could have been
    processed partially on the remote side, and you may re-execute it.

    The default timeout is 15000 ms.

    You can instrument the call by overriding `c:Malla.Plugins.Base.service_cb_in/3`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
