Skip to content

Commit

Permalink
Version 2.0
Browse files Browse the repository at this point in the history
Please see the changelog for more detailed changes.
  • Loading branch information
bitwalker committed Nov 8, 2016
1 parent 01ca2ba commit 1b4dfe6
Show file tree
Hide file tree
Showing 12 changed files with 306 additions and 298 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## 2.0.0

### Added

- Configurable `connect` and `disconnect` options for implementing strategies
on top of custom topologies
- The ability to start libcluster for more than a single topology
- Added `polling_interval` option to Kubernetes strategy
- Added ability to specify a list of hosts for the Epmd strategy to connect to on start

### Removed

- Cluster.Events module, as it was redundant and unused

### Changed

- Configuration format has changed significantly, please review the docs
81 changes: 53 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,55 @@ View the docs [here](https://hexdocs.pm/libcluster).
- multicast UDP gossip, using a configurable port/multicast address,
- the Kubernetes API, via a configurable pod selector and node basename.
- provide your own clustering strategies (e.g. an EC2 strategy, etc.)
- easy pubsub for cluster events
- provide your own topology plumbing (e.g. something other than standard Erlang distribution)

## An example configuration

The following will help you understand the more descriptive text below. The configuration
for libcluster can also be described as a spec for the clustering topologies and strategies
which will be used.

```elixir
config :libcluster,
topologies: [
example: [
# The selected clustering strategy. Required.
strategy: Cluster.Strategy.Epmd,
# Configuration for the provided strategy. Optional.
config: [hosts: [:"a@127.0.0.1", :"b@127.0.0.1"]],
# The function to use for connecting nodes. The node
# name will be appended to the argument list. Optional
connect: {:net_kernel, :connect, []},
# The function to use for disconnecting nodes. The node
# name will be appended to the argument list. Optional
disconnect: {:net_kernel, :disconnect, []},
# A list of options for the supervisor child spec
# of the selected strategy. Optional
opts: [restart: :transient]

This comment has been minimized.

Copy link
@josevalim

josevalim Nov 9, 2016

Maybe it would be better to call this child_spec or child_optsinstead ofoptsto avoid confusion withconfig? You can keep it backwards compatible by doing topology[:child_spec] || topology[:opts]`?

This comment has been minimized.

Copy link
@bitwalker

bitwalker Nov 9, 2016

Author Owner

I like this idea, I'll change that.

]
]
```


## Clustering

You have three choices with regards to cluster management. You can use the built-in Erlang tooling for connecting
You have three choices with regards to cluster management out of the box. You can use the built-in Erlang tooling for connecting
nodes, by setting `strategy: Cluster.Strategy.Epmd` in the config. If set to `Cluster.Strategy.Gossip` it will make use of
the multicast gossip protocol to dynamically form a cluster. If set to `Cluster.Strategy.Kubernetes`, it will use the
Kubernetes API to query endpoints based on a basename and label selector, using the token and namespace injected into
every pod; once it has a list of endpoints, it uses that list to form a cluster, and keep it up to date.

You can provide your own clustering strategy by setting `strategy: MyApp.Strategy` where `MyApp.Strategy` implements the
`Cluster.Strategy` behaviour, which currently consists of exporting a `start_link/0` callback. You don't necessarily have
`Cluster.Strategy` behaviour, which currently consists of exporting a `start_link/1` callback. You don't necessarily have
to start a process as part of your strategy, but since it's very likely you will need to maintain some state, designing your
strategy as an OTP process (i.e. `GenServer`) is the ideal method, however any valid OTP process will work. `libcluster` starts
the strategy process as part of it's supervision tree.

Currently it's required that strategies connect nodes via the Erlang distribution protocol, i.e. with `Node.connect/1`,
`:net_kernel.connect_node/1`, etc. In the future I plan on supporting alternate methods of clustering, but it's still unclear
on how to properly do so.
If you do not wish to use the default Erlang distribution protocol, you may provide an alternative means of connecting/
disconnecting nodes via the `connect` and `disconnect` configuration options. They take a `{module, fun, args}` tuple,
and append the node name being targeted to the `args` list. How to implement distribution in this way is left as an
exercise for the reader, but I recommend taking a look at the [Firenest](https://github.com/phoenixframework/firenest) project
currently under development.

### Clustering Strategies

Expand All @@ -46,14 +76,17 @@ following config settings:

```elixir
config :libcluster,
strategy: Cluster.Strategy.Gossip,
port: 45892,
if_addr: {0,0,0,0},
multicast_addr: {230,1,1,251},
# a TTL of 1 remains on the local network,
# use this to change the number of jumps the
# multicast packets will make
multicast_ttl: 1
topologies: [
gossip_example: [
strategy: Cluster.Strategy.Gossip,
config: [
port: 45892,
if_addr: {0,0,0,0},
multicast_addr: {230,1,1,251},
# a TTL of 1 remains on the local network,
# use this to change the number of jumps the
# multicast packets will make
multicast_ttl: 1]]]
```

The Kubernetes strategy works by querying the Kubernetes API for all endpoints in the same namespace which match the provided
Expand All @@ -64,9 +97,12 @@ IP address. Configuration might look like so:

```elixir
config :libcluster,
strategy: Cluster.Strategy.Kubernetes,
kubernetes_selector: "app=myapp",
kubernetes_node_basename: "myapp"
topologies: [
k8s_example: [
strategy: Cluster.Strategy.Kubernetes,
config: [
kubernetes_selector: "app=myapp",
kubernetes_node_basename: "myapp"]]]
```

And in vm.args:
Expand All @@ -76,17 +112,6 @@ And in vm.args:
-setcookie test
```

## Cluster Events

You can subscribe/unsubscribe a process to cluster events with `Cluster.Events.subscribe(pid)` and
`Cluster.Events.unsubscribe(pid)`. Currently, only two events are published to subscribers:

- `{:nodeup, node}` - when a node is connected, the node name is an atom
- `{:nodedown, node}` - same as above, but occurs when a node is disconnected

Events are sent to subscribers with `Kernel.send/2`, so if subscribing a `gen_*` process, you'll receive
them via the `handle_info/2` callback.

## License

MIT
22 changes: 19 additions & 3 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
# This file is provided as an example of how to configure libcluster in your own apps
use Mix.Config

config :libcluster,
strategy: Cluster.Strategy.Epmd
# You can start clustering for one or more topologies.
topologies: [
example: [
# The selected clustering strategy. Required.
strategy: Cluster.Strategy.Epmd,
# Options for the provided strategy. Optional.
strategy_opts: [],

This comment has been minimized.

Copy link
@josevalim

josevalim Nov 9, 2016

I think this should be config:.

This comment has been minimized.

Copy link
@bitwalker

bitwalker Nov 9, 2016

Author Owner

You are correct, I knew changing that would bite me in the ass haha

# The function to use for connecting nodes. The node
# name will be appended to the argument list. Optional
connect: {:net_kernel, :connect, []},
# The function to use for disconnecting nodes. The node
# name will be appended to the argument list. Optional
disconnect: {:net_kernel, :disconnect, []},
# A list of options for the supervisor child spec
# of the selected strategy. Optional
opts: []
]
]
25 changes: 20 additions & 5 deletions lib/app.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,27 @@ defmodule Cluster.App do
def start(_type, _args) do
import Supervisor.Spec, warn: false

children = [
worker(Cluster.Events, []),
Cluster.Strategy.spec()
]

children = get_child_specs()
opts = [strategy: :one_for_one, name: Cluster.Supervisor]
Supervisor.start_link(children, opts)
end

defp get_child_specs() do
import Supervisor.Spec, warn: false
specs = Application.get_env(:libcluster, :topologies, [])
for {topology, spec} <- specs do
strategy = Keyword.fetch!(spec, :strategy)
config = Keyword.get(spec, :config, [])
connect_mfa = Keyword.get(spec, :connect, {:net_kernel, :connect, []})
disconnect_mfa = Keyword.get(spec, :disconnect, {:net_kernel, :disconnect, []})
opts = Keyword.get(spec, :opts, [])
worker_args = [[
topology: topology,
connect: connect_mfa,
disconnect: disconnect_mfa,
config: config
]]
worker(strategy, worker_args, opts)
end
end
end
174 changes: 0 additions & 174 deletions lib/events.ex

This file was deleted.

10 changes: 5 additions & 5 deletions lib/logger.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ defmodule Cluster.Logger do
@moduledoc false
require Logger

def debug(msg), do: log(:debug, msg)
def info(msg), do: log(:info, msg)
def warn(msg), do: log(:warn, msg)
def error(msg), do: log(:error, msg)
def debug(t, msg), do: log(:debug, t, msg)
def info(t, msg), do: log(:info, t, msg)
def warn(t, msg), do: log(:warn, t, msg)
def error(t, msg), do: log(:error, t, msg)

defp log(level, msg), do: Logger.log(level, "[libcluster] #{msg}")
defp log(t, level, msg), do: Logger.log(level, "[libcluster:#{t}] #{msg}")
end

0 comments on commit 1b4dfe6

Please sign in to comment.