├── test ├── test_helper.exs └── schism_test.exs ├── .formatter.exs ├── .github ├── dependabot.yml └── workflows │ └── elixir.yml ├── .gitignore ├── LICENSE ├── config └── config.exs ├── mix.exs ├── README.md ├── lib └── schism.ex └── mix.lock /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | :ok = LocalCluster.start() 2 | 3 | ExUnit.start() 4 | 5 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: mix 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: ex_doc 11 | versions: 12 | - 0.24.0 13 | - 0.24.1 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | schism-*.tar 24 | 25 | -------------------------------------------------------------------------------- /test/schism_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SchismTest do 2 | use ExUnit.Case 3 | doctest Schism 4 | 5 | @moduletag :capture_log 6 | 7 | test "paritions nodes" do 8 | nodes = LocalCluster.start_nodes("partition-nodes", 5) 9 | [n1, n2, n3, n4, n5] = nodes 10 | 11 | partitions = [[n1, n2], [n3], [n4, n5]] 12 | 13 | for partition <- partitions do 14 | Schism.partition(partition) 15 | end 16 | 17 | for partition <- partitions, node <- partition do 18 | for n <- partition do 19 | assert :pong = :rpc.call(node, Node, :ping, [n]) 20 | end 21 | 22 | for unreachable <- (nodes -- partition) do 23 | assert :pang == :rpc.call(node, Node, :ping, [unreachable]) 24 | end 25 | end 26 | 27 | for partition <- partitions do 28 | Schism.heal(partition) 29 | end 30 | 31 | for node <- nodes do 32 | for other_node <- nodes, node != other_node do 33 | assert :pong = :rpc.call(node, Node, :ping, [other_node]) 34 | end 35 | end 36 | end 37 | end 38 | 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Christopher Keathley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | config :logger, 6 | handle_otp_reports: true, 7 | level: :info 8 | 9 | # config :logger, :console, 10 | # colors: [enabled: true] 11 | 12 | # This configuration is loaded before any dependency and is restricted 13 | # to this project. If another project depends on this project, this 14 | # file won't be loaded nor affect the parent project. For this reason, 15 | # if you want to provide default values for your application for 16 | # 3rd-party users, it should be done in your "mix.exs" file. 17 | 18 | # You can configure your application as: 19 | # 20 | # config :schism, key: :value 21 | # 22 | # and access this configuration in your application as: 23 | # 24 | # Application.get_env(:schism, :key) 25 | # 26 | # You can also configure a 3rd-party app: 27 | # 28 | # config :logger, level: :info 29 | # 30 | 31 | # It is also possible to import configuration files, relative to this 32 | # directory. For example, you can emulate configuration per environment 33 | # by uncommenting the line below and defining dev.exs, test.exs and such. 34 | # Configuration from the imported file will override the ones defined 35 | # here (which is why it is important to import them last). 36 | # 37 | # import_config "#{Mix.env()}.exs" 38 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Schism.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :schism, 7 | version: "1.0.1", 8 | elixir: "~> 1.7", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | aliases: aliases(), 12 | 13 | description: description(), 14 | package: package(), 15 | name: "Schism", 16 | source_url: "https://github.com/keathley/schism", 17 | docs: docs(), 18 | ] 19 | end 20 | 21 | # Run "mix help compile.app" to learn about applications. 22 | def application do 23 | [ 24 | extra_applications: [:logger] 25 | ] 26 | end 27 | 28 | # Run "mix help deps" to learn about dependencies. 29 | defp deps do 30 | [ 31 | {:credo, "~> 1.5", only: [:dev, :test]}, 32 | {:local_cluster, "~> 1.0", only: [:dev, :test]}, 33 | {:ex_doc, "~> 0.19", only: [:dev, :test]}, 34 | ] 35 | end 36 | 37 | def aliases do 38 | [ 39 | test: "test --no-start", 40 | ] 41 | end 42 | 43 | def description do 44 | """ 45 | Schism provides a simple api for partitioning networked BEAM instances 46 | without having to leave elixir code. 47 | """ 48 | end 49 | 50 | def package do 51 | [ 52 | name: "schism", 53 | licenses: ["MIT"], 54 | links: %{"GitHub" => "https://github.com/keathley/schism"}, 55 | ] 56 | end 57 | 58 | def docs do 59 | [ 60 | main: "Schism", 61 | ] 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Schism 2 | 3 | [![CircleCI](https://circleci.com/gh/keathley/schism.svg?style=svg)](https://circleci.com/gh/keathley/schism)[![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://hexdocs.pm/schism/) 4 | 5 | Schism provides a simple api for testing partitions between BEAM nodes. 6 | 7 | Documentation: [https://hexdocs.pm/schism](https://hexdocs.pm/schism). 8 | 9 | ## Installation 10 | 11 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 12 | by adding `schism` to your list of dependencies in `mix.exs`: 13 | 14 | ```elixir 15 | def deps do 16 | [ 17 | {:schism, "~> 1.0", only: [:dev, :test]} 18 | ] 19 | end 20 | ``` 21 | 22 | ## Usage 23 | 24 | Let's say that we have 5 nodes and we want to test what happens when they 25 | disconnect from each other. We can use schism like so: 26 | 27 | ```elixir 28 | test "netsplits" do 29 | [n1, n2, n3, n4, n5] = nodes 30 | 31 | # Partition our nodes 32 | Schism.partition([n1, n3]) 33 | Schism.partition([n4]) 34 | Schism.partition([n2, n5]) 35 | 36 | # Test some stuff... 37 | 38 | # Heal our partitions 39 | Schism.heal([n1, n3]) 40 | Schism.heal([n2, n4, n5]) 41 | end 42 | ``` 43 | 44 | This api is useful for testing and development in conjunction with tools like 45 | [local cluster](https://github.com/whitfin/local-cluster) and [propcheck](https://github.com/alfert/propcheck). 46 | 47 | It is not recommended that you use this in production. 48 | 49 | ## Things Schism doesn't do 50 | 51 | Schism's reason for existing is to quickly and easily test netsplits between BEAMS 52 | and it uses a rudimentary trick with cookies to achieve this goal. This is great 53 | for quickly testing your system against faults. 54 | 55 | But because of Schism's simple nature it should not be considered a replacement 56 | for a more robust suite of tests. It *does not* test failed TCP connections, 57 | interleaving of messages, race-conditions, clock skew, corruption, or any of 58 | the other issues you will see in a distributed system. 59 | -------------------------------------------------------------------------------- /lib/schism.ex: -------------------------------------------------------------------------------- 1 | defmodule Schism do 2 | @moduledoc """ 3 | Schism allows you to create network partitions in erlang nodes without 4 | needing to leave elixir. 5 | 6 | Let's say that we have 5 nodes and we want to test what happens when they 7 | disconnect from each other. We can use Schism like so: 8 | 9 | ```elixir 10 | test "netsplits" do 11 | [n1, n2, n3, n4, n5] = nodes 12 | 13 | # Partition our nodes 14 | Schism.partition([n1, n3]) 15 | Schism.partition([n4]) 16 | Schism.partition([n2, n5]) 17 | 18 | # Test some stuff... 19 | 20 | # Heal our partitions 21 | Schism.heal([n1, n3]) 22 | Schism.heal([n2, n4, n5]) 23 | end 24 | ``` 25 | 26 | This api is useful for testing and development in conjunction with tools like 27 | [local cluster](https://github.com/whitfin/local-cluster) and 28 | [propcheck](https://github.com/alfert/propcheck). 29 | """ 30 | 31 | @doc """ 32 | Creates a partition amongst a set of nodes. Any nodes in the partition 33 | will be able to see each other but no other nodes in the network. The 34 | partitioned nodes will still be able to see the node that induced the 35 | partition. Otherwise we would not be able to heal the partition. 36 | """ 37 | @spec partition([Node.t], String.t) :: [Node.t] | none() 38 | def partition(nodes, id \\ random_string()) when is_binary(id) do 39 | manager = Node.self() 40 | 41 | for node <- nodes do 42 | # Force the node to disconnect from all nodes that aren't us 43 | all_except_us = :rpc.call(node, Node, :list, []) -- [manager] 44 | Enum.each(all_except_us, fn n -> :rpc.call(node, Node, :disconnect, [n]) end) 45 | 46 | # Set the remote nodes cookie to a different value 47 | true = :rpc.call(node, :erlang, :set_cookie, [node, String.to_atom(id)]) 48 | 49 | # Ensure we can still talk to the node 50 | :pong = Node.ping(node) 51 | end 52 | 53 | # Reconnect the nodes in partition now that the cookie is the same 54 | connect_nodes(nodes) 55 | 56 | nodes 57 | end 58 | 59 | @doc """ 60 | Re-connects the nodes to the cluster. 61 | """ 62 | @spec heal([Node.t]) :: [Node.t] | none() 63 | def heal(nodes) do 64 | # Restore the cookie 65 | partition(nodes, Atom.to_string(:erlang.get_cookie())) 66 | end 67 | 68 | defp connect_nodes([node | other_nodes]) do 69 | Enum.each(other_nodes, fn n -> :rpc.call(node, Node, :connect, [n]) end) 70 | connect_nodes(other_nodes) 71 | end 72 | 73 | defp connect_nodes([]), do: :ok 74 | 75 | defp random_string do 76 | :crypto.strong_rand_bytes(10) 77 | |> Base.url_encode64 78 | |> binary_part(0, 10) 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | MIX_ENV: test 11 | 12 | jobs: 13 | deps: 14 | name: Install Dependencies 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | elixir: [1.11] 19 | otp: [23.3] 20 | steps: 21 | - name: checkout 22 | uses: actions/checkout@v2 23 | with: 24 | fetch-depth: 0 25 | - name: setup 26 | uses: erlef/setup-beam@v1 27 | with: 28 | elixir-version: ${{ matrix.elixir }} 29 | otp-version: ${{ matrix.otp }} 30 | - name: Retrieve Cached Dependencies 31 | uses: actions/cache@v2 32 | id: mix-cache 33 | with: 34 | path: | 35 | deps 36 | _build 37 | priv/plts 38 | key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }} 39 | - name: Install deps 40 | if: steps.mix-cache.outputs.cache-hit != 'true' 41 | run: | 42 | mkdir -p priv/plts 43 | mix local.rebar --force 44 | mix local.hex --force 45 | mix deps.get 46 | mix deps.compile 47 | 48 | analyze: 49 | name: Analysis 50 | needs: deps 51 | runs-on: ubuntu-latest 52 | strategy: 53 | matrix: 54 | elixir: [1.11] 55 | otp: [23.3] 56 | steps: 57 | - uses: actions/checkout@v2 58 | with: 59 | fetch-depth: 0 60 | - name: Setup elixir 61 | uses: erlef/setup-beam@v1 62 | with: 63 | elixir-version: ${{ matrix.elixir }} 64 | otp-version: ${{ matrix.otp }} 65 | - name: Retrieve Cached Dependencies 66 | uses: actions/cache@v2 67 | id: mix-cache 68 | with: 69 | path: | 70 | deps 71 | _build 72 | priv/plts 73 | key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }} 74 | - name: Run Credo 75 | run: mix credo 76 | 77 | tests: 78 | name: Tests 79 | needs: deps 80 | runs-on: ubuntu-latest 81 | strategy: 82 | matrix: 83 | elixir: [1.11] 84 | otp: [23.3] 85 | steps: 86 | - uses: actions/checkout@v2 87 | with: 88 | fetch-depth: 0 89 | - uses: erlef/setup-beam@v1 90 | with: 91 | elixir-version: ${{ matrix.elixir }} 92 | otp-version: ${{ matrix.otp }} 93 | - name: Retrieve Cached Dependencies 94 | uses: actions/cache@v2 95 | id: mix-cache 96 | with: 97 | path: | 98 | deps 99 | _build 100 | priv/plts 101 | key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }} 102 | - name: epmd 103 | run: epmd -daemon 104 | - name: Run Tests 105 | run: mix test 106 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, 3 | "credo": {:hex, :credo, "1.6.7", "323f5734350fd23a456f2688b9430e7d517afb313fbd38671b8a4449798a7854", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "41e110bfb007f7eda7f897c10bf019ceab9a0b269ce79f015d54b0dcf4fc7dd3"}, 4 | "earmark": {:hex, :earmark, "1.4.4", "4821b8d05cda507189d51f2caeef370cf1e18ca5d7dfb7d31e9cafe6688106a4", [:mix], [], "hexpm", "1f93aba7340574847c0f609da787f0d79efcab51b044bb6e242cae5aca9d264d"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"}, 6 | "ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"}, 7 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 8 | "global_flags": {:hex, :global_flags, "1.0.0", "ee6b864979a1fb38d1fbc67838565644baf632212bce864adca21042df036433", [:rebar3], [], "hexpm", "85d944cecd0f8f96b20ce70b5b16ebccedfcd25e744376b131e89ce61ba93176"}, 9 | "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, 10 | "local_cluster": {:hex, :local_cluster, "1.2.1", "8eab3b8a387680f0872eacfb1a8bd5a91cb1d4d61256eec6a655b07ac7030c73", [:mix], [{:global_flags, "~> 1.0", [hex: :global_flags, repo: "hexpm", optional: false]}], "hexpm", "aae80c9bc92c911cb0be085fdeea2a9f5b88f81b6bec2ff1fec244bb0acc232c"}, 11 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 12 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, 13 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 14 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, 15 | } 16 | --------------------------------------------------------------------------------