Code Monkey home page Code Monkey logo

local-cluster's Introduction

LocalCluster

Build Status Hex.pm Version Documentation

This library is designed to assist in testing distributed states in Elixir which require a number of local nodes.

The aim is to provide a small set of functions which hide the complexity of spawning local nodes, as well as providing the ease of cleaning up the started nodes. The entire library is simple shimming around the Erlang APIs for dealing with distributed nodes, As some of it is non-obvious, and as I need this code for several projects, I span it out as a smaller project.

Installation

To install it for your project, you can pull it directly from Hex. Rather than use the version shown below, you can use the the latest version from Hex (shown at the top of this README).

def deps do
  [{:local_cluster, "~> 1.2", only: [:test]}]
end

Documentation and examples can be found on Hexdocs as they're updated automatically alongside each release. Note that you should only use the :test flag in your dependency if you're not using it for other environments.

Setup

To configure your test suites for cluster testing, you need to run through a one-time setup to change some stuff in your test_helper.exs. This is required to avoid some potential issues with your node name changing after your application tree has already stated. This also reduces some bloat due to having LocalCluster.start/0 in most test cases. The snippet below can be used as a sample helper file. Make sure to change the application name to match your application name.

# start the current node as a manager
:ok = LocalCluster.start()

# start your application tree manually
Application.ensure_all_started(:my_app)

# run all tests!
ExUnit.start()

You will also need to pass the --no-start flag to mix test. Fortunately this is easy enough, as you can add an alias in your mix.exs to do this automatically:

def project do
  [
    # ...
    aliases: [
      test: "test --no-start"
    ]
    # ...
  ]
end

This library itself uses this setup, so you can copy/paste as needed or use as an example when integrating into your own codebase. Note that you must have ensured that epmd has been started before using this lib; typically with epmd -daemon.

Usage

As mentioned above, the API is deliberately tiny to make it easier to use this library when testing. Below is an example of using this library to spawn a set of child nodes for testing:

defmodule MyTest do
  use ExUnit.Case

  test "something with a required cluster" do
    nodes = LocalCluster.start_nodes("my-cluster", 3)

    [node1, node2, node3] = nodes

    assert Node.ping(node1) == :pong
    assert Node.ping(node2) == :pong
    assert Node.ping(node3) == :pong

    :ok = LocalCluster.stop_nodes([node1])

    assert Node.ping(node1) == :pang
    assert Node.ping(node2) == :pong
    assert Node.ping(node3) == :pong

    :ok = LocalCluster.stop()

    assert Node.ping(node1) == :pang
    assert Node.ping(node2) == :pang
    assert Node.ping(node3) == :pang
  end
end

After calling start_nodes/2, you will receive a list of node names you can then use to communicate with via RPC or however you'd like. Although they're automatically cleaned up when the calling process dies, you can manually stop nodes as well to test disconnection.

In the case you need to control application startup manually, you can make use of the :applications option. This option determines startup order of your applications, and allows you to exclude applications from the startup sequence. If this is not provided, the default behaviour will be to start the same applications as are running on the local node. These applications are loaded with all dependencies via Application.ensure_all_started/2.

LocalCluster.start_nodes(:spawn, 3, [
  applications: [
    :start_this_application,
    :and_then_this_one
  ]
])

If you need to load any additional files onto the remote nodes, you can make use of the :files option at startup time by providing an absolute file path to compile on the cluster. This is necessary if you wish to spawn tasks onto the cluster from inside your test code, as your test code is not loaded into the cluster automatically:

defmodule MyTest do
  use ExUnit.Case

  test "spawning tasks on a cluster" do
    nodes = LocalCluster.start_nodes(:spawn, 3, [
      files: [
        __ENV__.file
      ]
    ])

    [node1, node2, node3] = nodes

    assert Node.ping(node1) == :pong
    assert Node.ping(node2) == :pong
    assert Node.ping(node3) == :pong

    caller = self()

    Node.spawn(node1, fn ->
      send(caller, :from_node_1)
    end)

    Node.spawn(node2, fn ->
      send(caller, :from_node_2)
    end)

    Node.spawn(node3, fn ->
      send(caller, :from_node_3)
    end)

    assert_receive :from_node_1
    assert_receive :from_node_2
    assert_receive :from_node_3
  end
end

If you need to override the application environment inherrited by the remote nodes, you can use the :environment option at startup. This option set is merged over the environment inside the started nodes:

LocalCluster.start_nodes(:spawn, 1, [
  environment: [
    my_app: [
      port: 9999
    ]
  ]
])

local-cluster's People

Contributors

bernardd avatar eldritchideen avatar hissssst avatar pggalaviz avatar pmenhart avatar whitfin avatar wojtekmach avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

local-cluster's Issues

Can we start nodes with test env (MIX_ENV=test)?

Is there a way to start the nodes with test env (MIX_ENV=test)?
I'm using Swarm as a dependency, and by default it logs a lot of data when running in dev mode (info and debug).
While using Local Cluster in a test, I can see all the logs Swarm produces from all nodes on the test cluster as they're running with dev environment.

Is there a way to prevent all these logs to appear during tests but keep them for dev?

Some clarifications

Hello!

Tried this library and got permanent :not_alive error on LocalCluster.start_nodes
Also your library test returned this

00:02:37.767 [info]  Protocol 'inet_tcp': register/listen error: econnrefused                                                                                                                                                   
                                                                                                                                                                                                                                
** (MatchError) no match of right hand side value: {:error, {{:shutdown, {:failed_to_start_child, :net_kernel, {:EXIT, :nodistribution}}}, {:child, :undefined, :net_sup_dynamic, {:erl_distribution, :start_link, [[:"manager@1
27.0.0.1"], false]}, :permanent, 1000, :supervisor, [:erl_distribution]}}}                                                                                                                                                      
    test/test_helper.exs:2: (file)                                                                                                                                                                                              
    (elixir) lib/code.ex:813: Code.require_file/2                                                                                                                                                                               
    (elixir) lib/enum.ex:783: Enum."-each/2-lists^foreach/1-0-"/2    

I am not sure - may be this issue is trivial and self obvious for true erlang developers, but for noobs like me it was kind of pain.

Finally I got the reason - there must be epmd daemon running.

So may be reflect this requirements in documentation? It seems mandatory condition so looks logical for me to write couple of lines about it.

Feature Request: Simulating slow or unstable connections

I'd like to use this library not inside unit-tests, but inside some integration tests that ensure that the core functionality of our distributed application (https://planga.io) keeps functioning within expected bounds, even if the connection between nodes is slow, or if from time to time requests are being dropped.

What do you think? ๐Ÿ™‚

Starting local cluster with the main node as a hidden node?

The title here is probably more of a potential solution then the underlying problem.

In a lot of my tests I need to ensure that nodes discover and connect to each other. But when local cluster the primary node connects to the other nodes and creates a full mesh between all of the existing nodes. I'm not even sure if what I want to do is possible but I'm wondering if there's a programatic way to start the main node as a hidden node or something similar so that the cluster behaves as though the main node isn't truly a part of the cluster. I know that you can pass an erl flag to set a node as hidden but I'm not sure if you can do that once the node is already booted.

I might be going in a totally wrong direction here (and if so please tell me so) but I wanted to get your thoughts on it.

Crashes with certain cookies

Some systems generate cookies that have special characters in them, this trips the shell when supplying that cookie with the -setcookie option.

For example, I have the cookie 0mwHxf/~5h{?|pm<ST1PCtean_Be~bT9386.5O9!RL2s5=[YkPLCew%^@[A&CO^x, and shell outputs these errors:

sh: ST1PCtean_Be~bT9386.5O9!RL2s5=[YkPLCew%^@[A: No such file or directory
sh: CO^x: command not found

This is easily fixed by wrapping the cookie with quotes.

umbrella apps config

I see that application configs are passed to spawned nodes here

rpc.(Application, :put_env, [ app_name, key, val ])

And looks like it works, but sometimes in my umbrella project some applications are terminating because of missing configs. I have 2 suggestions

is it possible?

Can't load libcluster configuration

Hi, I'm using https://github.com/bitwalker/libcluster to setup, well, the cluster ๐Ÿ˜„
But, for some reason, creating nodes from test via LocalCluster.start_nodes("my_cluster", 2) doesn't pick up libcluster configuration.

In config/config.exs I have:

config :my_app, MyAppWeb.Endpoint,
  ...

config :libcluster,
  topologies: [
    local: [
      strategy: Cluster.Strategy.Gossip,
      connect: {:net_kernel, :connect_node, []},
      disconnect: {:erlang, :disconnect_node, []},
      list_nodes: {:erlang, :nodes, [:connected]}
    ]
  ]

And in application.ex I use topologies = Application.fetch_env!(:libcluster, :topologies) to fetch that configuration and supply it to libcluster. However, :libcluster configuration is empty for nodes created via LocalCluster. At the same time, it is properly picked up when project is started in dev/prod mode and manager node started with tests also picks it up correctly.

In test helper I have:

:ok = LocalCluster.start()
Application.ensure_all_started(:my_app)
ExUnit.start()

Note that if I change the configuration to config :my_app instead of config :libcluster, it all works correctly (configuration is picked up). So there is a workaround, but I'd still like to be able to set the config key properly. Funny thing is, e.g. config :logger is correctly picked up.

Getting 'undefined function' error on simple test

I wrote a simplified version of a test based on the documentation, over here: https://github.com/whitfin/local-cluster#usage

However, when I run the test it fails (message not received), and I see the following error in stdout:

$ mix test
.
10:03:57.708 [error] Process #PID<20934.220.0> on node :"[email protected]" raised an exception
** (UndefinedFunctionError) undefined function
    #Function<0.60679234/0 in ChatTest."test test node things"/1>()

10:03:57.708 [error] Process #PID<0.220.0> on node :"[email protected]" raised an exception
** (UndefinedFunctionError) undefined function
    #Function<0.60679234/0 in ChatTest>()


  1) test test node things (ChatTest)
     test/chat_test.exs:5
     Assertion failed, no matching message after 100ms
     The process mailbox is empty.
     code: assert_receive :a_message_from_the_node
     stacktrace:
       test/chat_test.exs:14: (test)


Finished in 3.1 seconds (0.00s async, 3.1s sync)
1 doctest, 1 test, 1 failure

Randomized with seed 660329

My test looks as follows:

defmodule ChatTest do
  use ExUnit.Case
  doctest Chat

  test "test node things" do
    [n1, _n2, _n3] = LocalCluster.start_nodes("my-cluster", 3)

    caller = self()

    _n1_pid = Node.spawn(n1, fn ->
      send(caller, :a_message_from_the_node)
    end)

    assert_receive :a_message_from_the_node
  end
end

My test_helper.exs:

# start the current node as a manager
:ok = LocalCluster.start()

# start your application tree manually
Application.ensure_all_started(:my_app)

# run all tests!
ExUnit.start()

My mix.exs:

defmodule Chat.MixProject do
  use Mix.Project

  def project do
    [
      app: :chat,
      version: "0.1.0",
      elixir: "~> 1.14",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      aliases: [
        test: "test --no-start"
      ]
    ]
  end

  def application do
    [
      extra_applications: [:logger],
      mod: {Chat.Application, []}
    ]
  end

  defp deps do
    [
      {:local_cluster, "~> 1.2", only: [:test]}
    ]
  end
end

My elixir version:

$ elixir --version
Erlang/OTP 25 [erts-13.0.4] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit]

Elixir 1.14.0 (compiled with Erlang/OTP 25)

Anything obvious that I'm doing wrong?
I get the same behavior when I use receive do ... instead of assert_receive
When I delete the assert_receive line, I don't get the 'undefined function` error anymore.

Error when running `LocalCluster.start/0`

When running LocalCluster.start/0 I get the following error:

{:error, 
  {{:shutdown, {:failed_to_start_child, :net_kernel, {:EXIT, :nodistribution}}},  
   {:child, :undefined, :net_sup_dynamic,   
    {:erl_distribution, :start_link, [[:"[email protected]"], false]},   
    :permanent, 1000, :supervisor, [:erl_distribution]}}}

I followed the getting started guide, other then that I have a pretty simple phoenix app with some other deps.

This is my Elixir and Erlang/OTP version:

Erlang/OTP 21 [erts-10.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] [dtrace]
Elixir 1.8.1 (compiled with Erlang/OTP 21)

Any clue?

Update the readme to show ExUnit.start can run before LocalCluster.start ?

I was following the readme to the letter recently by running LocalCluster.start before my ExUnit.start in the test_helper (Phoenix) but ... strangely enough it turns out you can run the LocalCluster.start in a single setup for just 1 test meaning the ExUnit.start can remain in the test_helper as you would see in any default application layout.

Any reason we can't update the readme to reflect this^ world view?

note: working example app w/ my results to prove its fine running after ExUnit.start

toranb/elixir-budget@b657a18

How to start a supervisor in the slave nodes?

I am working on a Spawnfest idea and I am using LocalCluster to test the distribution. If I start my supervision tree implicitly (with the :mod key), then Application.ensure_started works and LocalCluster's doc document as much (started applications are replicated to slave nodes).

But how does it work if I don't want to start my app up implicitly? Instead of starting it implicitly, I am requiring users to put it in their supervision tree. In my test, I can manually call start_link to start it which is pretty neat, but when LocalCluster is involved, I am not able to get these supervision tree started there.

I tried to do a :rpc.call(node1, MySupervisor, :start_link, []) but that doesn't work either, is it not possible?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.