diff --git a/lectures/05/dynamic_counters/.formatter.exs b/lectures/05/dynamic_counters/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/lectures/05/dynamic_counters/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/lectures/05/dynamic_counters/.gitignore b/lectures/05/dynamic_counters/.gitignore new file mode 100644 index 0000000..837b840 --- /dev/null +++ b/lectures/05/dynamic_counters/.gitignore @@ -0,0 +1,26 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +dynamic_counters-*.tar + +# Temporary files, for example, from tests. +/tmp/ diff --git a/lectures/05/dynamic_counters/README.md b/lectures/05/dynamic_counters/README.md new file mode 100644 index 0000000..8e27ad4 --- /dev/null +++ b/lectures/05/dynamic_counters/README.md @@ -0,0 +1,21 @@ +# DynamicCounters + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `dynamic_counters` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:dynamic_counters, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at . + diff --git a/lectures/05/dynamic_counters/lib/counter/application.ex b/lectures/05/dynamic_counters/lib/counter/application.ex new file mode 100644 index 0000000..e6eebe7 --- /dev/null +++ b/lectures/05/dynamic_counters/lib/counter/application.ex @@ -0,0 +1,26 @@ +defmodule Counter.Application do + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + @moduledoc false + + @registry Counter.Registry + @table Counter.Table + + use Application + + @impl true + def start(_type, _args) do + children = [ + {Registry, name: @registry, keys: :unique}, # recall same as {Registry, [name: @registry, keys: :unique]} + # Starts a worker by calling: Counter.Worker.start_link(arg) + # {DynamicSupervisor, name: Counter.DynamicSupervisor, strategy: :one_for_one} + Counter.DynamicSupervisor # no argument passed so we can just use this + ] + + :ets.new(@table, [:named_table, :public]) + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: Counter.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/lectures/05/dynamic_counters/lib/counter/dynamic_supervisor.ex b/lectures/05/dynamic_counters/lib/counter/dynamic_supervisor.ex new file mode 100644 index 0000000..b3f35ba --- /dev/null +++ b/lectures/05/dynamic_counters/lib/counter/dynamic_supervisor.ex @@ -0,0 +1,16 @@ +defmodule Counter.DynamicSupervisor do + use DynamicSupervisor + + def start_link(_) do + DynamicSupervisor.start_link(__MODULE__, nil, name: __MODULE__) + end + + def start_worker(name) do + DynamicSupervisor.start_child(__MODULE__, {Counter.Worker, name}) + end + + @impl true + def init(_) do + DynamicSupervisor.init(strategy: :one_for_one) + end +end diff --git a/lectures/05/dynamic_counters/lib/counter/worker.ex b/lectures/05/dynamic_counters/lib/counter/worker.ex new file mode 100644 index 0000000..325cb68 --- /dev/null +++ b/lectures/05/dynamic_counters/lib/counter/worker.ex @@ -0,0 +1,52 @@ +defmodule Counter.Worker do + use GenServer + + @registry Counter.Registry + @table Counter.Table + + ### Client API ### + def start_link(name) do + GenServer.start_link(__MODULE__, name, name: via(name)) + end + + def value(name) do + GenServer.call(via(name), :value) + end + + def inc(name, amt \\ 1) do + GenServer.cast(via(name), {:inc, amt}) + end + + # wrap the name in a triple to use the Registry to get the + # process ID by name + defp via(name) do + {:via, Registry, {@registry, {__MODULE__, name}}} + end + + ### Server Impl ### + @impl true + def init(arg) do + name = {__MODULE__, arg} + value = case :ets.lookup(@table, name) do + [] -> 0 + [{_, v}] -> v + end + {:ok, {name, value}} + end + + @impl true + def handle_call(:value, _from, {_, value} = state) do + {:reply, value, state} + end + + @impl true + def handle_cast({:inc, amt}, {name, value}) do + {:noreply, {name, value + amt}} + end + + # save state to term table when process terminates + @impl true + def terminate(_, state) do + :ets.insert(@table, state) + end +end diff --git a/lectures/05/dynamic_counters/mix.exs b/lectures/05/dynamic_counters/mix.exs new file mode 100644 index 0000000..8c6a165 --- /dev/null +++ b/lectures/05/dynamic_counters/mix.exs @@ -0,0 +1,29 @@ +defmodule Counter.MixProject do + use Mix.Project + + def project do + [ + app: :dynamic_counters, + version: "0.1.0", + elixir: "~> 1.17", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger], + mod: {Counter.Application, []} + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + ] + end +end diff --git a/lectures/05/dynamic_counters/test/dynamic_counters_test.exs b/lectures/05/dynamic_counters/test/dynamic_counters_test.exs new file mode 100644 index 0000000..5878099 --- /dev/null +++ b/lectures/05/dynamic_counters/test/dynamic_counters_test.exs @@ -0,0 +1,8 @@ +defmodule DynamicCountersTest do + use ExUnit.Case + doctest DynamicCounters + + test "greets the world" do + assert DynamicCounters.hello() == :world + end +end diff --git a/lectures/05/dynamic_counters/test/test_helper.exs b/lectures/05/dynamic_counters/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/lectures/05/dynamic_counters/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/lectures/05/notes.ex b/lectures/05/notes.ex new file mode 100644 index 0000000..a899b1c --- /dev/null +++ b/lectures/05/notes.ex @@ -0,0 +1,71 @@ +# register a process that can store key, value pairs +Registry.start_link(name: R, keys: :duplicate) + +# register(register_name, key, value) +Register.register(R, "hello", "world") +Register.lookup(R, "hello") + +# here we can associate a name, with a process ID +# can allow us to dynamically start any number of Workers +Registry.register(R, {Counter.Worker, "c1"}) + +f = fn (k, v) -> spawn(fn -> Registry.register(R, k, v); Process.sleep(30_000) end) end +pid = f.(:abc, 1) +Registry.keys(R, pid) # get all the keys for a PID -> [:abc] +Process.alive?(pid) # once dead i.e. returns -> false then: +Registry.keys(R, pid) # the process ID will be gone, -> [] not [:abc] + + +# Using our dynamic_counters implementation we have a counter server +# that is fault tolerant, i.e. if a worker dies, they will restart +DynamicSupervisor.start_child(Counter.DynamicSupervisor, {Counter.Worker, "c1"}) +# -> {:ok, #PID<0.147.0>} +{_, pid} = v +# -> {:ok, #PID<0.147.0>} +Counter.Worker.inc("c1") +# -> :ok +Counter.Worker.inc("c1") +# -> :ok +Counter.Worker.value("c1") +# -> 2 + +# we can crash and see it restarts +Counter.Worker.inc("c1", "hello") +# -> :ok + +# 10:04:25.932 [error] GenServer {Counter.Registry, {Counter.Worker, "c1"}} terminating +# ** (ArithmeticError) bad argument in arithmetic expression + # :erlang.+(2, "hello") + # (dynamic_counters 0.1.0) lib/dynamic_counters/worker.ex:37: Counter.Worker.handle_cast/2 + # (stdlib 6.2.2.5) gen_server.erl:2371: :gen_server.try_handle_cast/3 + # (stdlib 6.2.2.5) gen_server.erl:2433: :gen_server.handle_msg/6 + # (stdlib 6.2.2.5) proc_lib.erl:329: :proc_lib.init_p_do_apply/3 +# Last message: {:"$gen_cast", {:inc, "hello"}} +# State: 2 +Counter.Worker.value("c1") +# -> 0 has restarted without retaining state +Enum.each(1..100_000, fn n -> DynamicSupervisor.start_child(Counter.DynamicSupervisor, {Counter.Worker, "d#{n}"}) end ) +# -> :ok +Enum.each(1..100_000, fn n -> Counter.Worker.inc("d#{n}", n) end) +# -> :ok +Enum.map(1..10, fn n -> Counter.Worker.value("d#{n}") end) +# -> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + +### Erlang Term Store +t = :ets.new(T, []) +# you can insert tuples of any number of elements, but typically just a key, value pair +# by default you cannot have duplicate keys +:ets.insert(t, {:a, 1}) +:ets.lookup(t, :a) # -> [a: 1] lookup only matches the first element of a tuple +:ets.insert(t, {"hello", 2, :abc}) + +:ets.tab2list(t) # -> [{a: 2}, {"hello, 2, :abc}] +:ets.insert(t, {"world", 3, :xyz}) +:ets.match_object(t, {"hello", :_, :_}) # -> [{"hello", 2, :abc}] + +:ets.new(T, [:named_table, :public]) # can also use a named table and not need to assign to something like t +:ets.insert(T, {:abc, 123}) +:ets.lookup(T, :abc); + +System.schedulers_online() diff --git a/lectures/05/partition_counters/.formatter.exs b/lectures/05/partition_counters/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/lectures/05/partition_counters/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/lectures/05/partition_counters/.gitignore b/lectures/05/partition_counters/.gitignore new file mode 100644 index 0000000..1a1933d --- /dev/null +++ b/lectures/05/partition_counters/.gitignore @@ -0,0 +1,26 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +partition_counters-*.tar + +# Temporary files, for example, from tests. +/tmp/ diff --git a/lectures/05/partition_counters/README.md b/lectures/05/partition_counters/README.md new file mode 100644 index 0000000..bc261bf --- /dev/null +++ b/lectures/05/partition_counters/README.md @@ -0,0 +1,21 @@ +# Counter + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `partition_counters` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:partition_counters, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at . + diff --git a/lectures/05/partition_counters/lib/counter/application.ex b/lectures/05/partition_counters/lib/counter/application.ex new file mode 100644 index 0000000..62c7bc5 --- /dev/null +++ b/lectures/05/partition_counters/lib/counter/application.ex @@ -0,0 +1,26 @@ +defmodule Counter.Application do + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + @moduledoc false + + @registry Counter.Registry + @table Counter.Table + + use Application + + @impl true + def start(_type, _args) do + children = [ + {Registry, name: @registry, keys: :unique}, # recall same as {Registry, [name: @registry, keys: :unique]} + # Starts a worker by calling: Counter.Worker.start_link(arg) + # {DynamicSupervisor, name: Counter.DynamicSupervisor, strategy: :one_for_one} + {PartitionSupervisor, name: Counter.DynamicSupervisors, child_spec: Counter.DynamicSupervisor} + ] + + :ets.new(@table, [:named_table, :public]) + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: Counter.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/lectures/05/partition_counters/lib/counter/dynamic_supervisor.ex b/lectures/05/partition_counters/lib/counter/dynamic_supervisor.ex new file mode 100644 index 0000000..387be06 --- /dev/null +++ b/lectures/05/partition_counters/lib/counter/dynamic_supervisor.ex @@ -0,0 +1,18 @@ +defmodule Counter.DynamicSupervisor do + use DynamicSupervisor + + def start_link(_) do + DynamicSupervisor.start_link(__MODULE__, nil) + end + + def start_worker(name) do + DynamicSupervisor.start_child( + {:via, PartitionSupervisor, {Counter.DynamicSupervisors, self()}}, + {Counter.Worker, name}) + end + + @impl true + def init(_) do + DynamicSupervisor.init(strategy: :one_for_one) + end +end diff --git a/lectures/05/partition_counters/lib/counter/worker.ex b/lectures/05/partition_counters/lib/counter/worker.ex new file mode 100644 index 0000000..325cb68 --- /dev/null +++ b/lectures/05/partition_counters/lib/counter/worker.ex @@ -0,0 +1,52 @@ +defmodule Counter.Worker do + use GenServer + + @registry Counter.Registry + @table Counter.Table + + ### Client API ### + def start_link(name) do + GenServer.start_link(__MODULE__, name, name: via(name)) + end + + def value(name) do + GenServer.call(via(name), :value) + end + + def inc(name, amt \\ 1) do + GenServer.cast(via(name), {:inc, amt}) + end + + # wrap the name in a triple to use the Registry to get the + # process ID by name + defp via(name) do + {:via, Registry, {@registry, {__MODULE__, name}}} + end + + ### Server Impl ### + @impl true + def init(arg) do + name = {__MODULE__, arg} + value = case :ets.lookup(@table, name) do + [] -> 0 + [{_, v}] -> v + end + {:ok, {name, value}} + end + + @impl true + def handle_call(:value, _from, {_, value} = state) do + {:reply, value, state} + end + + @impl true + def handle_cast({:inc, amt}, {name, value}) do + {:noreply, {name, value + amt}} + end + + # save state to term table when process terminates + @impl true + def terminate(_, state) do + :ets.insert(@table, state) + end +end diff --git a/lectures/05/partition_counters/mix.exs b/lectures/05/partition_counters/mix.exs new file mode 100644 index 0000000..ed5bfca --- /dev/null +++ b/lectures/05/partition_counters/mix.exs @@ -0,0 +1,29 @@ +defmodule Counter.MixProject do + use Mix.Project + + def project do + [ + app: :partition_counters, + version: "0.1.0", + elixir: "~> 1.17", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger], + mod: {Counter.Application, []} + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + ] + end +end diff --git a/lectures/05/partition_counters/test/counter_test.exs b/lectures/05/partition_counters/test/counter_test.exs new file mode 100644 index 0000000..99884d6 --- /dev/null +++ b/lectures/05/partition_counters/test/counter_test.exs @@ -0,0 +1,8 @@ +defmodule CounterTest do + use ExUnit.Case + doctest Counter + + test "greets the world" do + assert Counter.hello() == :world + end +end diff --git a/lectures/05/partition_counters/test/test_helper.exs b/lectures/05/partition_counters/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/lectures/05/partition_counters/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()