├── test ├── test_helper.exs └── voting_test.exs ├── README.md ├── mix.lock ├── lib ├── voting.ex └── voting │ ├── vote_recorder.ex │ ├── supervisors │ ├── application.ex │ ├── aggregator_supervisor.ex │ └── result_presenter_supervisor.ex │ ├── result_presenter.ex │ └── aggregator.ex ├── mix.exs ├── .gitignore └── config └── config.exs /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.configure(exclude: [pending: true, skip: true], trace: true) 2 | ExUnit.start() 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Voting 2 | 3 | This repository holds the example from the blog post Object Oriented Programming in Elixir and OTP. 4 | 5 | Run tests with 'mix test' 6 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"gen_stage": {:hex, :gen_stage, "0.11.0", "943bdfa85c75fa624e0a36a9d135baad20a523be040178f5a215444b45c66ea4", [:mix], []}, 2 | "gproc": {:hex, :gproc, "0.6.1", "4579663e5677970758a05d8f65d13c3e9814ec707ad51d8dcef7294eda1a730c", [:rebar3], []}} 3 | -------------------------------------------------------------------------------- /lib/voting.ex: -------------------------------------------------------------------------------- 1 | defmodule Voting do 2 | @moduledoc """ 3 | Documentation for Voting. 4 | """ 5 | 6 | @doc """ 7 | Hello world. 8 | 9 | ## Examples 10 | 11 | iex> Voting.hello 12 | :world 13 | 14 | """ 15 | def hello do 16 | :world 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Voting.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :voting, 6 | version: "0.1.0", 7 | elixir: "~> 1.4", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps()] 11 | end 12 | 13 | def application do 14 | # Specify extra applications you'll use from Erlang/Elixir 15 | [extra_applications: [:logger], 16 | mod: {Voting.Application, []}] 17 | end 18 | 19 | defp deps do 20 | [ 21 | {:gproc,"~> 0.6.1"}, 22 | {:gen_stage, "~> 0.11.0"} 23 | ] 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /.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 | # OS/X files 23 | .DS_Store 24 | -------------------------------------------------------------------------------- /lib/voting/vote_recorder.ex: -------------------------------------------------------------------------------- 1 | defmodule Voting.VoteRecorder do 2 | @moduledoc """ 3 | This module receives votes and sends them to the proper 4 | aggregator. This module uses supervised tasks to ensure 5 | that any failure is recovered from and the vote is not 6 | lost. 7 | """ 8 | 9 | @doc """ 10 | Start a task to track the submittal of a vote to an 11 | aggregator. This is a supervised task to ensure 12 | completion. 13 | """ 14 | def cast_vote where, who do 15 | Task.Supervisor.async_nolink(Voting.VoteTaskSupervisor, 16 | fn -> 17 | Voting.Aggregator.submit_vote where, who 18 | end) 19 | |> Task.await 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/voting/supervisors/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Voting.Application do 2 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | def start(_type, _args) do 9 | import Supervisor.Spec, warn: false 10 | 11 | # Define workers and child supervisors to be supervised 12 | children = [ 13 | # Starts a worker by calling: Voting.Worker.start_link(arg1, arg2, arg3) 14 | supervisor(Task.Supervisor, 15 | [[name: Voting.VoteTaskSupervisor]]), 16 | supervisor(Voting.AggregatorSupervisor, []), 17 | supervisor(Voting.ResultPresenterSupervisor, []) 18 | ] 19 | 20 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 21 | # for other strategies and supported options 22 | opts = [strategy: :one_for_one, name: Voting.Supervisor] 23 | Supervisor.start_link(children, opts) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /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 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :voting, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:voting, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /lib/voting/supervisors/aggregator_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Voting.AggregatorSupervisor do 2 | use Supervisor 3 | import Logger, warn: false 4 | 5 | @moduledoc """ 6 | This supervisor is the root of the dynamic TemplateType supervisison 7 | tree. It has one child for each template type defined. 8 | """ 9 | 10 | def start_link do 11 | Supervisor.start_link __MODULE__, [], name: __MODULE__ 12 | end 13 | 14 | def init [] do 15 | children = [ 16 | worker(Voting.Aggregator, []) 17 | ] 18 | supervise children, strategy: :simple_one_for_one 19 | end 20 | 21 | @doc """ 22 | Start an aggregator server 23 | """ 24 | def start_aggregator id, children do 25 | {:ok, pid} = Supervisor.start_child __MODULE__, [id, children] 26 | {:ok, pid} 27 | end 28 | 29 | @doc """ 30 | Stop an aggregator server 31 | """ 32 | def stop_aggregator id do 33 | {:via, :gproc, name} = Voting.Aggregator.via_tuple(id) 34 | pid = :gproc.lookup_pid(name) 35 | Supervisor.terminate_child __MODULE__, pid 36 | end 37 | 38 | @doc """ 39 | Termnate all aggregator servers 40 | """ 41 | def stop_all_aggregators do 42 | # Logger.debug "@@@@ Stopping all aggregators" 43 | Supervisor.which_children(__MODULE__) 44 | |> Enum.each(fn child -> 45 | {_id, pid, _type, _mod} = child 46 | Supervisor.terminate_child __MODULE__, pid 47 | end) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/voting/supervisors/result_presenter_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Voting.ResultPresenterSupervisor do 2 | use Supervisor 3 | import Logger, warn: false 4 | 5 | @moduledoc """ 6 | This supervisor is the root of the dynamic TemplateType supervisison 7 | tree. It has one child for each template type defined. 8 | """ 9 | 10 | def start_link do 11 | Supervisor.start_link __MODULE__, [], name: __MODULE__ 12 | end 13 | 14 | def init [] do 15 | children = [ 16 | worker(Voting.ResultPresenter, []) 17 | ] 18 | supervise children, strategy: :simple_one_for_one 19 | end 20 | 21 | @doc """ 22 | Start a result presenter server 23 | """ 24 | def start_presenter id, aggregator_id do 25 | {:ok, pid} = Supervisor.start_child __MODULE__, [id, aggregator_id] 26 | {:ok, pid} 27 | end 28 | 29 | @doc """ 30 | Stop a result presenter server 31 | """ 32 | def stop_presenter id do 33 | {:via, :gproc, name} = Voting.ResultPresenter.via_tuple(id) 34 | pid = :gproc.lookup_pid(name) 35 | Supervisor.terminate_child __MODULE__, pid 36 | end 37 | 38 | @doc """ 39 | Termnate all aggregator servers 40 | """ 41 | def stop_all_presenters do 42 | # Logger.debug "@@@@ Stopping all aggregators" 43 | Supervisor.which_children(__MODULE__) 44 | |> Enum.each(fn child -> 45 | {_id, pid, _type, _mod} = child 46 | Supervisor.terminate_child __MODULE__, pid 47 | end) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/voting/result_presenter.ex: -------------------------------------------------------------------------------- 1 | defmodule Voting.ResultPresenter do 2 | use GenStage 3 | import Logger, warn: false 4 | @moduledoc """ 5 | This module is a GenStage consumer collecting 6 | vote results from an aggregator 7 | """ 8 | 9 | @doc """ 10 | Start the server process. 11 | """ 12 | def start_link id, aggregator_id do 13 | GenStage.start_link __MODULE__, {id, aggregator_id}, 14 | name: via_tuple(id) 15 | end 16 | 17 | @doc """ 18 | Initialize the server and persistent state. 19 | """ 20 | def init {id, aggregator_id} do 21 | agg_pid = Voting.Aggregator.via_tuple(aggregator_id) 22 | {:consumer, %{id: id, votes: %{}}, 23 | subscribe_to: [agg_pid]} 24 | end 25 | 26 | @doc """ 27 | Generate the process registry tuple for a process given the 28 | id. This uses the gproc registry. 29 | """ 30 | def via_tuple id do 31 | {:via, :gproc, {:n, :l, {:voting, :presenter, id}}} 32 | end 33 | 34 | @doc """ 35 | Handle requests for results 36 | """ 37 | def handle_call :get_votes, _from, state do 38 | {:reply, {:ok, state.votes}, [], state} 39 | end 40 | 41 | @doc """ 42 | Obtain the results from this presenter 43 | """ 44 | def get_votes id do 45 | pid = Voting.ResultPresenter.via_tuple(id) 46 | {:ok, votes} = GenStage.call pid, :get_votes 47 | votes 48 | end 49 | 50 | @doc """ 51 | Receive votes from aggregator 52 | """ 53 | def handle_events events, _from, state do 54 | Logger.debug "@@@@ Presenter received: #{inspect events}" 55 | votes = Enum.reduce events, state.votes, fn v, votes -> 56 | Enum.reduce v, votes, fn {k,v}, votes -> 57 | Map.put(votes, k, v) 58 | end 59 | end 60 | {:noreply, [], %{state | votes: votes}} 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/voting/aggregator.ex: -------------------------------------------------------------------------------- 1 | defmodule Voting.Aggregator do 2 | use GenStage 3 | import Logger, warn: false 4 | @moduledoc """ 5 | The GenStage Server module for vote aggregation. 6 | Collect vote input and aggregate. 7 | """ 8 | 9 | @doc """ 10 | Start the server process. Args: district name, and type 11 | """ 12 | def start_link id, children do 13 | GenStage.start_link __MODULE__, {id, children}, 14 | name: via_tuple(id) 15 | end 16 | 17 | @doc """ 18 | Initialize the server and persistent state. 19 | """ 20 | def init {id, nil} do 21 | {:producer_consumer, %{id: id, votes: %{}}} 22 | end 23 | def init {id, children} do 24 | pids = Enum.map children, & __MODULE__.via_tuple(&1) 25 | {:producer_consumer, %{id: id, votes: %{}}, 26 | subscribe_to: pids} 27 | end 28 | 29 | @doc """ 30 | Generate the process registry tuple for a process given the 31 | district name. This uses the gproc registry. 32 | """ 33 | def via_tuple id do 34 | {:via, :gproc, {:n, :l, {:voting, :aggregator, id}}} 35 | end 36 | 37 | @doc """ 38 | Submit a single vote to an aggregator 39 | """ 40 | def submit_vote id, candidate do 41 | pid = __MODULE__.via_tuple(id) 42 | :ok = GenStage.call pid, {:submit_vote, candidate} 43 | end 44 | 45 | @doc """ 46 | Respond to requests 47 | """ 48 | def handle_call {:submit_vote, candidate}, _from, state do 49 | n = state.votes[candidate] || 0 50 | state = %{state | votes: Map.put(state.votes, candidate, n+1)} 51 | {:reply, :ok, [%{state.id => state.votes}], state} 52 | end 53 | 54 | @doc """ 55 | Handle events from subordinate aggregators 56 | """ 57 | def handle_events events, _from, state do 58 | votes = Enum.reduce events, state.votes, fn e, votes -> 59 | Enum.reduce e, votes, fn {k,v}, votes -> 60 | Map.put(votes, k, v) # replace any entries for subordinates 61 | end 62 | end 63 | # Any jurisdiction specific policy would go here 64 | 65 | # sum the votes by candidate for the published event 66 | merged = Enum.reduce votes, %{}, fn {j, jv}, votes -> 67 | # Each jourisdiction is summed for each candidate 68 | Enum.reduce jv, votes, fn {candidate, tot}, votes -> 69 | Logger.debug "@@@@ Votes in #{inspect j} for #{inspect candidate}: #{inspect tot}" 70 | n = votes[candidate] || 0 71 | Map.put(votes, candidate, n + tot) 72 | end 73 | end 74 | # return the published event and the state which retains 75 | # votes by jourisdiction 76 | {:noreply, [%{state.id => merged}], %{state | votes: votes}} 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/voting_test.exs: -------------------------------------------------------------------------------- 1 | defmodule VotingTest do 2 | use ExUnit.Case 3 | 4 | # setup _context do 5 | # Voting.AggregatorSupervisor.stop_all_aggregators 6 | # Voting.ResultPresenterSupervisor.stop_all_presenters 7 | # :ok 8 | # end 9 | 10 | test "Basic vote collection" do 11 | # Create 1 aggregator for state of CA 12 | {:ok, _} = Voting.AggregatorSupervisor.start_aggregator "CA", nil 13 | # Create a presenter to receive the results 14 | {:ok, _} = Voting.ResultPresenterSupervisor.start_presenter "CA", "CA" 15 | try do 16 | # cast a vote for candidate Tom 17 | Voting.VoteRecorder.cast_vote "CA", "Tom" 18 | # Verify total in presenter 19 | Process.sleep(1000) 20 | votes = Voting.ResultPresenter.get_votes "CA" 21 | assert votes == %{"CA" => %{"Tom" => 1}} 22 | after 23 | Voting.AggregatorSupervisor.stop_all_aggregators 24 | Voting.ResultPresenterSupervisor.stop_all_presenters 25 | end 26 | end 27 | 28 | test "Vote Aggregation for one candidate" do 29 | # Create 1 aggregator for state of CA 30 | {:ok, _} = Voting.AggregatorSupervisor.start_aggregator "CA", nil 31 | {:ok, _} = Voting.AggregatorSupervisor.start_aggregator "NV", nil 32 | {:ok, _} = Voting.AggregatorSupervisor.start_aggregator "US", ["CA", "NV"] 33 | # Create a presenter to receive the results 34 | {:ok, _} = Voting.ResultPresenterSupervisor.start_presenter "US", "US" 35 | try do 36 | # cast a vote for candidate Tom 37 | Voting.VoteRecorder.cast_vote "CA", "Tom" 38 | Voting.VoteRecorder.cast_vote "NV", "Tom" 39 | # Verify total in presenter 40 | Process.sleep(1000) 41 | votes = Voting.ResultPresenter.get_votes "US" 42 | assert votes == %{"US" => %{"Tom" => 2}} 43 | after 44 | Voting.AggregatorSupervisor.stop_all_aggregators 45 | Voting.ResultPresenterSupervisor.stop_all_presenters 46 | end 47 | end 48 | 49 | test "Vote Aggregation for two candidates" do 50 | # Create 1 aggregator for state of CA 51 | {:ok, _} = Voting.AggregatorSupervisor.start_aggregator "CA", nil 52 | {:ok, _} = Voting.AggregatorSupervisor.start_aggregator "NV", nil 53 | {:ok, _} = Voting.AggregatorSupervisor.start_aggregator "US", ["CA", "NV"] 54 | # Create a presenter to receive the results 55 | {:ok, _} = Voting.ResultPresenterSupervisor.start_presenter "US", "US" 56 | try do 57 | # cast a vote for candidate Tom 58 | Voting.VoteRecorder.cast_vote "CA", "Tom" 59 | Voting.VoteRecorder.cast_vote "NV", "Fred" 60 | # Verify total in presenter 61 | Process.sleep(1000) 62 | votes = Voting.ResultPresenter.get_votes "US" 63 | assert votes == %{"US" => %{"Tom" => 1, "Fred" => 1}} 64 | after 65 | Voting.AggregatorSupervisor.stop_all_aggregators 66 | Voting.ResultPresenterSupervisor.stop_all_presenters 67 | end 68 | end 69 | end 70 | --------------------------------------------------------------------------------