README.md

# Barnacle

[![Package Version](https://img.shields.io/hexpm/v/barnacle)](https://hex.pm/packages/barnacle)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/barnacle/)

Self-healing clusters for Gleam applications on the BEAM!

Connect to other BEAM nodes, and automatically reconnect when they go down.

## Features

- Discover nodes using different strategies. Built-in strategies:
  - Local EPMD
  - Remote EPMD
  - DNS
- Automatically reconnect to nodes when they come back online
- Supply your own strategies
- Listen to events
- Trigger refreshes manually

## Getting Started

```sh
gleam add barnacle
```

You'll also need to start your Gleam process with a node name, and set the cookie.
You can do this using the `ERL_FLAGS` environment variable.

> [!NOTE]
> There's a Fly.io example coming soon.

```sh
ERL_FLAGS="-name my_app@127.0.0.1 -setcookie my_cookie" gleam run
```

### Start Barnacle under a Supervisor (recommended)

```gleam
import barnacle
import gleam/erlang/process
import gleam/otp/supervisor

pub fn main() {
  // Configure your Barnacle
  let barnacle =
    barnacle.local_epmd()
    |> barnacle.with_poll_interval(15_000)
    |> barnacle.with_name("my_barnacle")

  // Create a process to receive the child process later
  let self = process.new_subject()

  // Start the child process under a supervisor
  let barnacle_worker = barnacle.child_spec(barnacle, None)
  let assert Ok(_) = supervisor.start(supervisor.add(_, barnacle_worker))

  // Get a subject to send messages to the child process
  let assert Ok(barnacle_subject) = process.receive(self, 10_000)

  // Continue your setup...

  process.sleep_forever()
}
```

### Start Barnacle as a standalone actor

This is **not** recommended as your barnacle won't be restarted if it crashes for any
reason.

```gleam
import barnacle
import gleam/erlang/process

pub fn main() {
  // Configure your Barnacle
  let barnacle =
    barnacle.local_epmd()
    |> barnacle.with_poll_interval(15_000)
    |> barnacle.with_name("my_barnacle")

  // Start the actor
  let barnacle_subject = barnacle.start(barnacle)

  // Continue your setup...

  process.sleep_forever()
}
```

### Run the refresh function once

If you don't need any sort of polling, or want to manage your the refresh lifecycle
yourself, you can call the `run_once` function to run the refresh function a single
time.

In this case, most of the configuration options will be ignored.

```gleam
import barnacle

pub fn main() {
  // Configure your Barnacle
  let barnacle =
    barnacle.local_epmd()

  // Run the refresh function once
  barnacle.run_once(barnacle)

  // Continue your program...
}
```

## Strategies

Barnacle ships with a few built-in strategies, but you can also supply your own by
providing a single callback function.

### Local EPMD

Discover nodes using the local EPMD. This will automatically attempt to connect to
nodes that are running on the same machine.

```gleam
import barnacle

pub fn main() {
  barnacle.local_epmd()
  |> barnacle.start
}
```

### Remote EPMD

Connect to nodes using a known list.

```gleam
import barnacle
import gleam/erlang/atom

pub fn main() {
  barnacle.epmd(
    ["node1@192.168.1.1", "node2@192.168.1.2"]
    |> list.map(atom.create_from_string),
  )
  |> barnacle.start
}
```

### DNS

Discover nodes using DNS.

The first argument is the basename of the node. Currently, Barnacle only supports
connecting to nodes with the same basename.

Barnacle provides a helper function to get the basename of the current node.

```gleam
import barnacle

pub fn main() {
  let assert Ok(basename) = barnacle.get_node_basename(node.self())
  barnacle.dns(
    basename,
    // The hostname to query against
    "my_app.example.com",
  )
  |> barnacle.start
}
```

### Creating a custom strategy

You can create your own strategy using the `new_strategy` function.

```gleam
import barnacle

pub fn main() {
  barnacle.new_strategy(
    // Discover nodes
    fn() {
      Ok([])
    },
  )
  |> barnacle.custom
  |> barnacle.start
}
```

You can also supply your own functions for connecting, disconnecting, and listing nodes.
If these are not supplied, the built-in functions will be used.

This may be useful if you want to use an alternative to Distributed Erlang, such as
[Partisan](https://github.com/lasp-lang/partisan).

```gleam
import barnacle

pub fn main() {
  barnacle.new_strategy(my_discover_function)
  |> barnacle.with_connect_nodes_function(my_connect_function)
  |> barnacle.with_disconnect_nodes_function(my_disconnect_function)
  |> barnacle.with_list_nodes_function(my_list_function)
  |> barnacle.custom
  |> barnacle.start
}
```

You can find more information about creating custom strategies in the
[docs](/barnacle/barnacle.html#custom).

## Events

You can provide a custom subject to receive events from your barnacle.
This will be notified whenever the barnacle refreshes, is paused, or is shutdown.

```gleam
import barnacle
import gleam/erlang/process

pub fn main() {
  let self = process.new_subject()

  // Configure your Barnacle
  let barnacle =
    barnacle.local_epmd()
      |> barnacle.with_poll_interval(15_000)
      |> barnacle.with_name("my_barnacle")
      |> barnacle.with_listener(self)

  // Start the actor
  barnacle.start(barnacle)

  // Wait for the barnacle to refresh
  process.sleep(10_000)
  let assert Ok(barnacle.RefreshResponse(Ok(new_nodes))) = process.receive(self, 10_000)
}
```

## Manual interactions

You can interact with your barnacle manually by sending messages to it.

```gleam
import barnacle
import gleam/erlang/process
import gleam/otp/actor

pub fn main() {
  // Start a barnacle
  let barnacle_subject =
    barnacle.local_epmd()
    |> barnacle.start

  let assert Ok(_) = barnacle.pause(barnacle_subject, 1000)
  let assert Ok(_) = barnacle.refresh(barnacle_subject, 10_000)
  let assert Ok(_) = barnacle.shutdown(barnacle_subject, 1000)
}
```

Further documentation can be found at <https://hexdocs.pm/barnacle>.

## Development

```sh
gleam run   # Run the project
gleam test  # Run the tests
```

If you would like to contribute, please open an issue or a PR. New strategies are
welcome, though try to keep dependencies to a minimum.

## TODO

- [ ] Tests!
- [ ] Add new strategies
  - [ ] Kubernetes
  - [ ] Kubernetes with DNS
  - [ ] Multicast UDP gossip
  - [ ] `.hosts.erlang` file
- [ ] Add a Fly.io example

## With thanks

A lot of inspiration came from the following projects:

- [libcluster](https://github.com/bitwalker/libcluster)
- [nessie_cluster](https://github.com/ckreiling/nessie_cluster)

Thanks!