├── 00_lang ├── 00_koans │ └── README.md ├── 04_bit_syntax │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ ├── charlist.ex │ │ └── utf8.ex │ ├── lib_sol │ │ ├── charlist.ex │ │ └── utf8.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ │ ├── charlist_test.exs │ │ ├── test_helper.exs │ │ └── utf8_test.exs └── 05_collections │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ ├── collectable.ex │ └── enumerable.ex │ ├── lib_sol │ ├── collectable_sol.ex │ └── enumerable_sol.ex │ ├── mix.exs │ └── test │ ├── collectable_test.exs │ ├── enumerable_test.exs │ └── test_helper.exs ├── 10_process ├── 12_my_task │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ └── my_task.ex │ ├── lib_sol │ │ └── my_task.ex │ ├── mix.exs │ └── test │ │ ├── my_task_test.exs │ │ └── test_helper.exs ├── 13_my_gen_server │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ └── my_gen_server.ex │ ├── lib_sol │ │ └── my_gen_server.ex │ ├── mix.exs │ └── test │ │ ├── my_gen_server_test.exs │ │ └── test_helper.exs ├── 14_my_agent │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ └── my_agent.ex │ ├── lib_sol │ │ └── my_agent.ex │ ├── mix.exs │ └── test │ │ ├── my_agent_test.exs │ │ └── test_helper.exs ├── 16_my_supervisor │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ ├── my_supervisor.ex │ │ └── timed_bomb.ex │ ├── lib_sol │ │ └── my_supervisor.ex │ ├── mix.exs │ └── test │ │ ├── my_supervisor_test.exs │ │ └── test_helper.exs └── 19_my_pool │ └── .gitkeep ├── 20_distribution ├── 20_learn_boot_server │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ └── distribution.ex │ ├── mix.exs │ └── test │ │ ├── distribution_test.exs │ │ └── test_helper.exs ├── 21_my_pg │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ └── rpg.ex │ ├── mix.exs │ └── test │ │ ├── rpg │ │ ├── multi_nodes_test.exs │ │ ├── nodedown_test.exs │ │ └── single_node_test.exs │ │ ├── support │ │ └── cluster.ex │ │ └── test_helper.exs ├── 22_pg2 │ └── README.md └── 29_learn_phx_pubsub │ ├── .formatter.exs │ ├── .gitignore │ ├── .iex.exs │ ├── README.md │ ├── lib │ ├── learn_phx_pubsub.ex │ └── my_tracker.ex │ ├── mix.exs │ ├── mix.lock │ ├── sys.config │ └── test │ ├── learn_phx_pubsub_test.exs │ └── test_helper.exs ├── 30_process_discovery ├── .gitkeep └── 15_my_registry │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ └── my_registry.ex │ ├── lib_sol │ └── my_registry.ex │ ├── mix.exs │ └── test │ ├── my_registry_test.exs │ └── test_helper.exs ├── 40_fp └── 40_learn_parser │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ ├── learn_parser.ex │ ├── my_parser.ex │ └── my_parser.ex.exs │ ├── mix.exs │ ├── mix.lock │ └── test │ ├── learn_parser_test.exs │ └── test_helper.exs ├── 50_code_quality ├── 50_use_gen_statem │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── img │ │ ├── code_lock.svg │ │ └── code_lock_2.svg │ ├── lib │ │ └── door_lock.ex │ ├── lib_sol │ │ └── door_lock.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ │ ├── door_lock_test.exs │ │ └── test_helper.exs └── 55_use_excoverage │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ └── learn_coverage.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ ├── learn_coverage_test.exs │ └── test_helper.exs ├── 60_monitoring └── 60_learn_observer │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ ├── observing.ex │ └── observing │ │ ├── application.ex │ │ ├── child_sup.ex │ │ ├── parent_worker.ex │ │ └── worker.ex │ ├── mix.exs │ └── test │ ├── observing_test.exs │ └── test_helper.exs ├── 90_performance └── 95_use_agent_ets │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── bench │ ├── cache_read_bench.exs │ └── cache_write_bench.exs │ ├── lib │ ├── agent_cache.ex │ ├── cache.ex │ └── ets_cache.ex │ ├── lib_sol │ ├── agent_cache.ex │ └── ets_cache.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ ├── cache_test.exs │ └── test_helper.exs ├── README.md └── renovate.json /00_lang/00_koans/README.md: -------------------------------------------------------------------------------- 1 | ## Elixir Koans 2 | 3 | syntax 를 제대로 익혔는지 https://github.com/elixirkoans/elixir-koans 를 clone 하여 모든 문제를 풀어보세요. 4 | -------------------------------------------------------------------------------- /00_lang/04_bit_syntax/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | locals_without_parens: [definject: 1, definject: 2] 5 | ] 6 | -------------------------------------------------------------------------------- /00_lang/04_bit_syntax/.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 third-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 | utf8-*.tar 24 | 25 | -------------------------------------------------------------------------------- /00_lang/04_bit_syntax/README.md: -------------------------------------------------------------------------------- 1 | ## charlist <-> string 변환을 직접 구현하고 bit syntax 를 제대로 익혀봅시다. 2 | 3 | Reference: 4 | 5 | 1. Wiki : https://en.wikipedia.org/wiki/UTF-8 6 | 2. Erlang : http://erlang.org/doc/programming_examples/bit_syntax.html 7 | 3. Elixir : https://elixir-lang.org/getting-started/binaries-strings-and-char-lists.html 8 | 9 | ## 1. Unicode integer <-> Utf8 binary 10 | 11 | lib/utf8.ex 내용을 채워봅시다. 12 | 13 | ## 2. Uncode charlist <-> Utf8 binary 14 | 15 | 1에서 만든 함수를 사용하여 16 | 17 | 1. Kernel.to_charlist/1 와 동일한 기능을 하는 Charlist.string_to_charlist 18 | 2. Kernel.to_string/1 와 동일한 기능을 하는 Charlist.charlist_to_string 19 | 20 | 을 lib/charlist.ex 에 구현합시다. 21 | -------------------------------------------------------------------------------- /00_lang/04_bit_syntax/lib/charlist.ex: -------------------------------------------------------------------------------- 1 | defmodule Charlist do 2 | def charlist_to_string(list) when is_list(list) do 3 | "" 4 | end 5 | 6 | def string_to_charlist(<>) do 7 | '' 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /00_lang/04_bit_syntax/lib/utf8.ex: -------------------------------------------------------------------------------- 1 | defmodule Utf8 do 2 | # +-------+---------+----------+----------+----------+----------+----------+ 3 | # | Bytes | First | Last | Byte 1 | Byte 2 | Byte 3 | Byte 4 | 4 | # +-------+---------+----------+----------+----------+----------+----------+ 5 | # | 1 | U+0000 | U+007F | 0xxxxxxx | | | | 6 | # +-------+---------+----------+----------+----------+----------+----------+ 7 | # | 2 | U+0080 | U+07FF | 110xxxxx | 10xxxxxx | | | 8 | # +-------+---------+----------+----------+----------+----------+----------+ 9 | # | 3 | U+0800 | U+FFFF | 1110xxxx | 10xxxxxx | 10xxxxxx | | 10 | # +-------+---------+----------+----------+----------+----------+----------+ 11 | # | 4 | U+10000 | U+10FFFF | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | 12 | # +-------+---------+----------+----------+----------+----------+----------+ 13 | 14 | # Encode 15 | 16 | def encode(n) when is_integer(n) and n >= 0x0000 and n <= 0x007F do 17 | <<>> 18 | end 19 | 20 | # Decode 21 | 22 | def decode(<<0::1, n::7>>) do 23 | 0 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /00_lang/04_bit_syntax/lib_sol/charlist.ex: -------------------------------------------------------------------------------- 1 | defmodule Charlist do 2 | def charlist_to_string(list) when is_list(list) do 3 | list 4 | |> Enum.map(&Utf8.encode/1) 5 | |> Enum.reduce(<<>>, fn bin, acc -> acc <> bin end) 6 | end 7 | 8 | def string_to_charlist(<>) do 9 | do_string_to_charlist(bin, []) 10 | end 11 | 12 | defp do_string_to_charlist(<<>>, acc) do 13 | acc 14 | end 15 | 16 | defp do_string_to_charlist(<<0b0::1, _::bits>> = bin, acc) do 17 | <> = bin 18 | do_string_to_charlist(tail, acc ++ [Utf8.decode(current)]) 19 | end 20 | 21 | defp do_string_to_charlist(<<0b110::3, _::bits>> = bin, acc) do 22 | <> = bin 23 | do_string_to_charlist(tail, acc ++ [Utf8.decode(current)]) 24 | end 25 | 26 | defp do_string_to_charlist(<<0b1110::4, _::bits>> = bin, acc) do 27 | <> = bin 28 | do_string_to_charlist(tail, acc ++ [Utf8.decode(current)]) 29 | end 30 | 31 | defp do_string_to_charlist(<<0b11110::5, _::bits>> = bin, acc) do 32 | <> = bin 33 | do_string_to_charlist(tail, acc ++ [Utf8.decode(current)]) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /00_lang/04_bit_syntax/lib_sol/utf8.ex: -------------------------------------------------------------------------------- 1 | defmodule Utf8 do 2 | # +-------+---------+----------+----------+----------+----------+----------+ 3 | # | Bytes | First | Last | Byte 1 | Byte 2 | Byte 3 | Byte 4 | 4 | # +-------+---------+----------+----------+----------+----------+----------+ 5 | # | 1 | U+0000 | U+007F | 0xxxxxxx | | | | 6 | # +-------+---------+----------+----------+----------+----------+----------+ 7 | # | 2 | U+0080 | U+07FF | 110xxxxx | 10xxxxxx | | | 8 | # +-------+---------+----------+----------+----------+----------+----------+ 9 | # | 3 | U+0800 | U+FFFF | 1110xxxx | 10xxxxxx | 10xxxxxx | | 10 | # +-------+---------+----------+----------+----------+----------+----------+ 11 | # | 4 | U+10000 | U+10FFFF | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | 12 | # +-------+---------+----------+----------+----------+----------+----------+ 13 | 14 | # Encode 15 | 16 | def encode(n) when is_integer(n) and n >= 0x0000 and n <= 0x007F do 17 | <<0::1, n::7>> 18 | end 19 | 20 | def encode(n) when is_integer(n) and n >= 0x0080 and n <= 0x07FF do 21 | <<0::5, first::5, second::6>> = <> 22 | <<0b110::3, first::5, 0b10::2, second::6>> 23 | end 24 | 25 | def encode(n) when is_integer(n) and n >= 0x0800 and n <= 0xFFFF do 26 | <> = <> 27 | <<0b1110::4, first::4, 0b10::2, second::6, 0b10::2, third::6>> 28 | end 29 | 30 | def encode(n) when is_integer(n) and n >= 0x10000 and n <= 0x10FFFF do 31 | <> = <> 32 | <<0b11110::5, first::3, 0b10::2, second::6, 0b10::2, third::6, 0b10::2, fourth::6>> 33 | end 34 | 35 | # Decode 36 | 37 | def decode(<<0::1, n::7>>) do 38 | n 39 | end 40 | 41 | def decode(<<0b110::3, first::5, 0b10::2, second::6>>) do 42 | <> = <<0::5, first::5, second::6>> 43 | n 44 | end 45 | 46 | def decode(<<0b1110::4, first::4, 0b10::2, second::6, 0b10::2, third::6>>) do 47 | <> = <> 48 | n 49 | end 50 | 51 | def decode(<<0b11110::5, first::3, 0b10::2, second::6, 0b10::2, third::6, 0b10::2, fourth::6>>) do 52 | <> = <> 53 | n 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /00_lang/04_bit_syntax/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Utf8Validator.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :utf8, 7 | version: "0.1.0", 8 | elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | # {:dep_from_hexpm, "~> 0.3.0"}, 25 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /00_lang/04_bit_syntax/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "definject": {:hex, :definject, "1.1.5", "de662d70aa2999384bb891d79d383c0e56edb0a2ca43c09a5c3ffababf775b73", [:mix], [], "hexpm", "ade0015c35d7fed70f640905c5a1de704962d1765cc1802038520c74fb596dc5"}, 3 | "gen_state_machine": {:hex, :gen_state_machine, "2.1.0", "a38b0e53fad812d29ec149f0d354da5d1bc0d7222c3711f3a0bd5aa608b42992", [:mix], [], "hexpm", "ae367038808db25cee2f2c4b8d0531522ea587c4995eb6f96ee73410a60fa06b"}, 4 | } 5 | -------------------------------------------------------------------------------- /00_lang/04_bit_syntax/test/charlist_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CharlistTest do 2 | use ExUnit.Case 3 | 4 | @moduletag :pending 5 | 6 | test "charlist_to_string" do 7 | assert Charlist.charlist_to_string('엘릭서 Elixir') == "엘릭서 Elixir" 8 | end 9 | 10 | test "string_to_charlist" do 11 | assert Charlist.string_to_charlist("엘릭서 Elixir") == '엘릭서 Elixir' 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /00_lang/04_bit_syntax/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.configure(exclude: [:pending]) 2 | ExUnit.start() 3 | -------------------------------------------------------------------------------- /00_lang/04_bit_syntax/test/utf8_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Utf8Test do 2 | use ExUnit.Case 3 | 4 | # Examples from https://en.wikipedia.org/wiki/UTF-8 5 | 6 | @unicode_to_utf8 [ 7 | {0x24, <<0x24::8>>}, 8 | {0xA2, <<0xC2A2::16>>}, 9 | {0x0939, <<0xE0A4B9::24>>}, 10 | {0x20AC, <<0xE282AC::24>>}, 11 | {0xD55C, <<0xED959C::24>>}, 12 | {0x10348, <<0xF0908D88::32>>} 13 | ] 14 | 15 | describe "Utf8" do 16 | @describetag :pending 17 | 18 | test "encode" do 19 | for {unicode, utf8} <- @unicode_to_utf8 do 20 | assert Utf8.encode(unicode) == utf8 21 | end 22 | end 23 | 24 | test "decode" do 25 | for {unicode, utf8} <- @unicode_to_utf8 do 26 | assert Utf8.decode(utf8) == unicode 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /00_lang/05_collections/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /00_lang/05_collections/.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 third-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 | collections-*.tar 24 | 25 | -------------------------------------------------------------------------------- /00_lang/05_collections/README.md: -------------------------------------------------------------------------------- 1 | # Collection 2 | 3 | ### Enumerable 4 | 5 | `Enumerable` protocol 을 구현하면, `Enum` 과 `Stream` 의 첫번째 아규먼트로 사용할 수 있게 됩니다. 6 | 7 | Binary 는 Enumerable 이 구현되어 있지 않은데, 이를 구현해보고 왜 List와 달리 Enumerable 이 기본적으로 구현되어 있지 않는지 생각해 봅시다. 8 | 9 | ### Collectable 10 | 11 | `Collectable` protocol 을 구현하면, `Enum.into` 함수의 2번째 아규먼트로 쓸수 있게 됩니다. 12 | 13 | Tuple 은 Collectable 이 구현되어 있지 않은데, 이를 구현해보고 왜 List와 달리 Collectable이 기본적으로 구현되어 있지 않는지 생각해 봅시다. 14 | -------------------------------------------------------------------------------- /00_lang/05_collections/lib/collectable.ex: -------------------------------------------------------------------------------- 1 | defimpl Collectable, for: Tuple do 2 | # https://github.com/elixir-lang/elixir/blob/v1.10.0/lib/elixir/lib/collectable.ex#L80 3 | end 4 | -------------------------------------------------------------------------------- /00_lang/05_collections/lib/enumerable.ex: -------------------------------------------------------------------------------- 1 | defimpl Enumerable, for: BitString do 2 | # https://github.com/elixir-lang/elixir/blob/v1.10.0/lib/elixir/lib/enum.ex#L3760 3 | end 4 | -------------------------------------------------------------------------------- /00_lang/05_collections/lib_sol/collectable_sol.ex: -------------------------------------------------------------------------------- 1 | defimpl Collectable, for: Tuple do 2 | def into(init) do 3 | fun = fn 4 | acc, {:cont, x} -> [x | acc] 5 | acc, :done -> acc |> Enum.reverse() |> List.to_tuple() 6 | end 7 | 8 | {init |> Tuple.to_list() |> Enum.reverse(), fun} 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /00_lang/05_collections/lib_sol/enumerable_sol.ex: -------------------------------------------------------------------------------- 1 | defimpl Enumerable, for: BitString do 2 | def count(bin), do: {:ok, byte_size(bin)} 3 | def member?(_bin, _element), do: {:error, __MODULE__} 4 | 5 | def reduce(_, {:halt, acc}, _fun), do: {:halted, acc} 6 | def reduce(bin, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(bin, &1, fun)} 7 | def reduce(<<>>, {:cont, acc}, _fun), do: {:done, acc} 8 | 9 | def reduce(<>, {:cont, acc}, fun), 10 | do: reduce(tail, fun.(head, acc), fun) 11 | 12 | def slice(bin), 13 | do: 14 | {:ok, byte_size(bin), 15 | fn start, length -> 16 | <<_::binary-size(start), x::binary-size(length), _::binary>> = bin 17 | x 18 | end} 19 | end 20 | -------------------------------------------------------------------------------- /00_lang/05_collections/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule MyEnum.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :collections, 7 | version: "0.1.0", 8 | elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | # {:dep_from_hexpm, "~> 0.3.0"}, 25 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /00_lang/05_collections/test/collectable_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CollectableTest do 2 | use ExUnit.Case 3 | 4 | test "Tuple" do 5 | assert [1, 2, 3] |> Enum.into({}) == {1, 2, 3} 6 | assert [1, 2, 3] |> Enum.into({:a, :b}) == {:a, :b, 1, 2, 3} 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /00_lang/05_collections/test/enumerable_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EnumerableTest do 2 | use ExUnit.Case 3 | 4 | test "BitString" do 5 | assert <<1, 2, 3>> |> Enum.to_list() == [1, 2, 3] 6 | assert <<1, 2, 3>> |> Enum.map(&(&1 * 10)) == [10, 20, 30] 7 | assert <<1, 2, 3>> |> Enum.filter(&(rem(&1, 2) == 1)) == [1, 3] 8 | assert <<1, 2, 3>> |> Enum.reduce(10, &(&1 + &2)) == 16 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /00_lang/05_collections/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /10_process/12_my_task/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /10_process/12_my_task/.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 third-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 | my_task-*.tar 24 | 25 | -------------------------------------------------------------------------------- /10_process/12_my_task/README.md: -------------------------------------------------------------------------------- 1 | ## MyTask 2 | 3 | 아래를 참고하여 spawn_link, make_ref, send, receive 를 사용하여 4 | Task.async, await 을 직접 구현해 보자. 5 | 6 | ``` 7 | iex(1)> Task.async(fn -> :foo end) 8 | %Task{ 9 | owner: #PID<0.165.0>, 10 | pid: #PID<0.167.0>, 11 | ref: #Reference<0.1470435912.721158150.77392> 12 | } 13 | iex(2)> flush 14 | {#Reference<0.1470435912.721158150.77392>, :foo} 15 | ``` 16 | 17 | ### 검증 18 | 19 | ``` 20 | mix test 21 | ``` 22 | 23 | -------------------------------------------------------------------------------- /10_process/12_my_task/lib/my_task.ex: -------------------------------------------------------------------------------- 1 | defmodule MyTask do 2 | defstruct [] 3 | 4 | def async(fun) do 5 | end 6 | 7 | def await(%MyTask{}) do 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /10_process/12_my_task/lib_sol/my_task.ex: -------------------------------------------------------------------------------- 1 | defmodule MyTask do 2 | defstruct owner: nil, pid: nil, ref: nil 3 | 4 | def async(fun) do 5 | from = self() 6 | ref = make_ref() 7 | pid = spawn_link(fn -> send(from, {ref, fun.()}) end) 8 | 9 | %MyTask{owner: from, pid: pid, ref: ref} 10 | end 11 | 12 | def await(%MyTask{ref: ref}) do 13 | receive do 14 | {^ref, value} -> value 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /10_process/12_my_task/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule MyTask.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :my_task, 7 | version: "0.1.0", 8 | elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | # {:dep_from_hexpm, "~> 0.3.0"}, 25 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /10_process/12_my_task/test/my_task_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MyTaskTest do 2 | use ExUnit.Case 3 | 4 | for task <- [Task, MyTask] do 5 | @task task 6 | 7 | test "#{task} success" do 8 | this = self() 9 | task = %@task{owner: ^this, pid: _pid, ref: _ref} = @task.async(fn -> :foo end) 10 | 11 | assert :foo == @task.await(task) 12 | end 13 | 14 | test "#{task} failure" do 15 | Process.flag(:trap_exit, true) 16 | 17 | @task.async(fn -> raise RuntimeError end) 18 | 19 | assert_receive {:EXIT, _, {%RuntimeError{}, _}} 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /10_process/12_my_task/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(capture_log: true) 2 | -------------------------------------------------------------------------------- /10_process/13_my_gen_server/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /10_process/13_my_gen_server/.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 third-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 | my_gen_server-*.tar 24 | 25 | -------------------------------------------------------------------------------- /10_process/13_my_gen_server/README.md: -------------------------------------------------------------------------------- 1 | # MyGenServer 2 | 3 | 아래와 같이 GenServer.call 이 보내는 메시지를 염탐하여 4 | 5 | ``` 6 | iex(3)> pid = spawn(fn -> receive do; x -> IO.inspect(x); end; end) 7 | #PID<0.158.0> 8 | iex(4)> GenServer.call(pid, :foo) 9 | {:"$gen_call", {#PID<0.152.0>, #Reference<0.2833509647.2609119234.221258>}, :foo} 10 | ``` 11 | 12 | 1. start_link/2 13 | 2. call/2 14 | 15 | 위 2가지 기능을 가진 GenServer 의 최소 버전을 구현해보자. (start_link 는 init/1 이 호출된 후에 리턴되어야 함을 유의.) 16 | 17 | ### 검증 18 | 19 | ``` 20 | mix test 21 | ``` 22 | -------------------------------------------------------------------------------- /10_process/13_my_gen_server/lib/my_gen_server.ex: -------------------------------------------------------------------------------- 1 | defmodule MyGenServer do 2 | def start_link(module, init_arg) do 3 | end 4 | 5 | def call(server, request) do 6 | end 7 | 8 | defp loop(module, state) do 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /10_process/13_my_gen_server/lib_sol/my_gen_server.ex: -------------------------------------------------------------------------------- 1 | defmodule MyGenServer do 2 | def start_link(module, init_arg) do 3 | this = self() 4 | ref = make_ref() 5 | 6 | pid = 7 | spawn_link(fn -> 8 | {:ok, state} = module.init(init_arg) 9 | send(this, ref) 10 | loop(module, state) 11 | end) 12 | 13 | receive do 14 | ^ref -> {:ok, pid} 15 | end 16 | end 17 | 18 | def call(server, request) do 19 | ref = make_ref() 20 | send(server, {:"$gen_call", request, self(), ref}) 21 | 22 | receive do 23 | {^ref, reply} -> reply 24 | end 25 | end 26 | 27 | defp loop(module, state) do 28 | receive do 29 | {:"$gen_call", request, from, ref} -> 30 | {:reply, reply, state} = module.handle_call(request, from, state) 31 | send(from, {ref, reply}) 32 | loop(module, state) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /10_process/13_my_gen_server/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule MyGenServer.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :my_gen_server, 7 | version: "0.1.0", 8 | elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | # {:dep_from_hexpm, "~> 0.3.0"}, 25 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /10_process/13_my_gen_server/test/my_gen_server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MyGenServerTest do 2 | use ExUnit.Case 3 | doctest MyGenServer 4 | 5 | defmodule Adder do 6 | def init(offset) do 7 | {:ok, offset} 8 | end 9 | 10 | def handle_call({:add, num}, _from, offset) do 11 | {:reply, num + offset, offset} 12 | end 13 | end 14 | 15 | for gen_server <- [GenServer, MyGenServer] do 16 | @gen_server gen_server 17 | 18 | test "#{@gen_server}.call" do 19 | {:ok, pid} = @gen_server.start_link(Adder, 10) 20 | 110 = @gen_server.call(pid, {:add, 100}) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /10_process/13_my_gen_server/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /10_process/14_my_agent/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /10_process/14_my_agent/.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 third-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 | my_agent-*.tar 24 | 25 | -------------------------------------------------------------------------------- /10_process/14_my_agent/README.md: -------------------------------------------------------------------------------- 1 | # MyAgent 2 | 3 | GenServer behaviour를 사용하여 Agent와 동일한 동작을 하는 MyAgent를 구현해보자. 구현해야 하는 함수는 4 | 5 | 1. start_link/1 6 | 1. get/2 7 | 1. update/2 8 | 9 | ### 검증 10 | 11 | ``` 12 | mix test 13 | ``` -------------------------------------------------------------------------------- /10_process/14_my_agent/lib/my_agent.ex: -------------------------------------------------------------------------------- 1 | defmodule MyAgent do 2 | use GenServer 3 | 4 | def start_link(init_fun) do 5 | # GenServer 를 사용하여 Agent 를 구현하세요. 6 | end 7 | 8 | def get(pid, get_fun) do 9 | end 10 | 11 | def update(pid, update_fun) do 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /10_process/14_my_agent/lib_sol/my_agent.ex: -------------------------------------------------------------------------------- 1 | defmodule MyAgent do 2 | use GenServer 3 | 4 | def start_link(init_fun) do 5 | GenServer.start_link(__MODULE__, init_fun) 6 | end 7 | 8 | def init(init_fun) do 9 | {:ok, init_fun.()} 10 | end 11 | 12 | def get(pid, get_fun) do 13 | GenServer.call(pid, {:get, get_fun}) 14 | end 15 | 16 | def update(pid, update_fun) do 17 | GenServer.call(pid, {:update, update_fun}) 18 | end 19 | 20 | # callback 21 | 22 | def handle_call({:get, get_fun}, _from, state) do 23 | {:reply, get_fun.(state), state} 24 | end 25 | 26 | def handle_call({:update, update_fun}, _from, state) do 27 | {:reply, :ok, update_fun.(state)} 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /10_process/14_my_agent/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule MyAgent.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :my_agent, 7 | version: "0.1.0", 8 | elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | # {:dep_from_hexpm, "~> 0.3.0"}, 25 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /10_process/14_my_agent/test/my_agent_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MyAgentTest do 2 | use ExUnit.Case 3 | 4 | for agent <- [Agent, MyAgent] do 5 | @agent agent 6 | 7 | test "#{@agent}" do 8 | this = self() 9 | ref = make_ref() 10 | 11 | {:ok, pid} = 12 | @agent.start_link(fn -> 13 | send(this, ref) 14 | %{} 15 | end) 16 | 17 | assert_receive ^ref 18 | 19 | assert @agent.get(pid, fn x -> map_size(x) end) == 0 20 | 21 | :ok = @agent.update(pid, fn map -> Map.put(map, :foo, :bar) end) 22 | assert @agent.get(pid, fn map -> Map.get(map, :foo) end) == :bar 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /10_process/14_my_agent/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /10_process/16_my_supervisor/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /10_process/16_my_supervisor/.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 third-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 | my_supervisor-*.tar 24 | 25 | -------------------------------------------------------------------------------- /10_process/16_my_supervisor/README.md: -------------------------------------------------------------------------------- 1 | # MySupervisor 2 | 3 | 목표: 기본적 기능만 하는 supervisor 를 만들어 보자. 4 | 5 | ### 요구사항 6 | 7 | - restart: :permanent 만 지원. (transient, temporary 지원 안함) 8 | - strategy: :one_for_one 만 지원. 9 | - max_restart 무시. (연속해서 terminate 하더라도 계속 restart) 10 | 11 | ### 도구 12 | 13 | - Process.flag(:trap_exit, true) 14 | - Process.monitor(child_pid) 15 | - receive 16 | 17 | ### 검증 18 | 19 | ``` 20 | mix test 21 | ``` 22 | -------------------------------------------------------------------------------- /10_process/16_my_supervisor/lib/my_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule MySupervisor do 2 | def start_link(children, strategy: :one_for_one) do 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /10_process/16_my_supervisor/lib/timed_bomb.ex: -------------------------------------------------------------------------------- 1 | defmodule TimedBomb do 2 | use GenServer 3 | require Logger 4 | 5 | def start_link(delay) do 6 | GenServer.start_link(__MODULE__, delay, name: __MODULE__) 7 | end 8 | 9 | def init(delay) do 10 | Process.send_after(self(), :bomb, delay) 11 | # Logger.info("Bomb scheduled after #{delay}ms.") 12 | {:ok, delay} 13 | end 14 | 15 | def handle_info(:bomb, delay) do 16 | # Logger.error("Bomb!!!!") 17 | {:stop, :normal, delay} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /10_process/16_my_supervisor/lib_sol/my_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule MySupervisor do 2 | def start_link(children, strategy: :one_for_one) do 3 | pid = spawn_link(fn -> supervise(children) end) 4 | {:ok, pid} 5 | end 6 | 7 | def supervise(children) do 8 | Process.flag(:trap_exit, true) 9 | 10 | children 11 | |> Enum.map(&start/1) 12 | |> Enum.zip(children) 13 | |> Enum.into(%{}) 14 | |> loop() 15 | end 16 | 17 | def start({mod, args}) do 18 | {:ok, pid} = Kernel.apply(mod, :start_link, [args]) 19 | ref = Process.monitor(pid) 20 | 21 | {pid, ref} 22 | end 23 | 24 | def loop(children) do 25 | receive do 26 | {:EXIT, _, _} -> 27 | loop(children) 28 | 29 | {:DOWN, ref, :process, pid, _reason} -> 30 | down = {pid, ref} 31 | mod_args = children |> Map.get(down) 32 | 33 | children 34 | |> Map.delete(down) 35 | |> Map.put(start(mod_args), mod_args) 36 | |> loop() 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /10_process/16_my_supervisor/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule MySupervisor.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :my_supervisor, 7 | version: "0.1.0", 8 | elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | # {:dep_from_hexpm, "~> 0.3.0"}, 25 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /10_process/16_my_supervisor/test/my_supervisor_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MySupervisorTest do 2 | use ExUnit.Case 3 | 4 | for sup_mod <- [Supervisor, MySupervisor] do 5 | @sup_mod sup_mod 6 | 7 | test "#{@sup_mod} for permanent restart" do 8 | Process.flag(:trap_exit, true) 9 | 10 | children = [ 11 | # Terminate every 200ms 12 | {TimedBomb, 200} 13 | ] 14 | 15 | {:ok, sup} = @sup_mod.start_link(children, strategy: :one_for_one) 16 | first = Process.whereis(TimedBomb) 17 | # Wait for first termination 18 | Process.sleep(300) 19 | second = Process.whereis(TimedBomb) 20 | 21 | assert first != second 22 | assert Process.alive?(sup) 23 | 24 | Process.sleep(100) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /10_process/16_my_supervisor/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /10_process/19_my_pool/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jechol/elixir-coding-club/0db2337524833288d82e1fbf7efac8a00bd16a2e/10_process/19_my_pool/.gitkeep -------------------------------------------------------------------------------- /20_distribution/20_learn_boot_server/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /20_distribution/20_learn_boot_server/.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 third-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 | distribution-*.tar 24 | 25 | -------------------------------------------------------------------------------- /20_distribution/20_learn_boot_server/README.md: -------------------------------------------------------------------------------- 1 | # Distribution 2 | 3 | ## Setup EPMD 4 | 5 | distributed node 에 필요한 API 들은 node discovery 를 위해 epmd 에 연결을 시도한다. 6 | epmd 는 iex 와 별개로 미리 실행되어 있어야 한다. 7 | (erlang release 는 distributed release 인 경우 자동으로 epmd를 실행함.) 8 | 9 | ```console 10 | $ epmd -daemon 11 | ``` 12 | 13 | epmd 가 응답하는지 확인하려면 14 | 15 | ```console 16 | $ epmd -names 17 | ``` 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /20_distribution/20_learn_boot_server/lib/distribution.ex: -------------------------------------------------------------------------------- 1 | defmodule Distribution do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | setup_siblings() 6 | 7 | Supervisor.start_link([], strategy: :one_for_one, name: __MODULE__) 8 | end 9 | 10 | def setup_siblings do 11 | # Turn node into a distributed node with the given name 12 | :net_kernel.start([:"self@127.0.0.1"]) 13 | # Allow spawned nodes to fetch all code from this node 14 | :erl_boot_server.start([]) 15 | :erl_boot_server.add_slave({127, 0, 0, 1}) 16 | 17 | for i <- [0, 1] do 18 | {:ok, node} = :slave.start('127.0.0.1', :"sibling#{i}", '-loader inet -hosts 127.0.0.1') 19 | :rpc.block_call(node, :code, :add_paths, [:code.get_path()]) 20 | 21 | # Transfer config 22 | for {app, _desc, _ver} <- Application.loaded_applications() do 23 | for {key, val} <- Application.get_all_env(app) do 24 | :rpc.block_call(node, Application, :put_env, [app, key, val]) 25 | end 26 | end 27 | 28 | :rpc.block_call(node, Application, :ensure_all_started, [:mix]) 29 | :rpc.block_call(node, Mix, :env, [Mix.env()]) 30 | 31 | # Start all apps 32 | for {app, _desc, _ver} <- Application.started_applications() do 33 | :rpc.block_call(node, Application, :ensure_all_started, [app]) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /20_distribution/20_learn_boot_server/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Distribution.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :distribution, 7 | version: "0.1.0", 8 | elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger], 18 | mod: {Distribution, []} 19 | ] 20 | end 21 | 22 | # Run "mix help deps" to learn about dependencies. 23 | defp deps do 24 | [ 25 | # {:dep_from_hexpm, "~> 0.3.0"}, 26 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 27 | ] 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /20_distribution/20_learn_boot_server/test/distribution_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DistributionTest do 2 | use ExUnit.Case 3 | doctest Distribution 4 | 5 | test "greets the world" do 6 | assert Distribution.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /20_distribution/20_learn_boot_server/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /20_distribution/21_my_pg/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /20_distribution/21_my_pg/.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 third-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 | rpg-*.tar 24 | 25 | 26 | # Temporary files for e.g. tests 27 | /tmp 28 | -------------------------------------------------------------------------------- /20_distribution/21_my_pg/README.md: -------------------------------------------------------------------------------- 1 | # Rpg (Re-impl of Erlang/OTP pg) 2 | 3 | (Copied from https://github.com/jechol/rpg) 4 | 5 | This is re-implementation of Erlang/OTP pg module in Elixir for learning purpose. 6 | 7 | Major differences are summarized below. 8 | 9 | | | pg | Rpg | 10 | |---------------------------- |----------------------------- |-------------------------- | 11 | | Language | Erlang | Elixir | 12 | | Component | gen_server + ets (scalable) | GenServer (not scalable) | 13 | | Code size (except comment) | 349 lines | 222 lines | 14 | | Production ready | Absolutely | Definitely NO | 15 | 16 | 17 | # 학습 주제 18 | 19 | * pg 사용법 (https://erlang.org/doc/man/pg.html) 20 | * start 21 | * join, leave 22 | * which_groups, get_members, get_local_members 23 | * 테스트 환경에서 분산 환경 시뮬레이션 24 | * test/support/cluster.ex (Phoenix.PubSub에서 복사후 수정함) 25 | * [epmd 를 자동으로 켜는 법](https://github.com/jechol/rpg/blob/db3ca9f661a6b478934168d8b8618a7b47eaf4a2/test/support/cluster.ex#L54) 26 | * [분산 노드를 켜는 법](https://github.com/jechol/rpg/blob/db3ca9f661a6b478934168d8b8618a7b47eaf4a2/test/support/cluster.ex#L65) 27 | * [분산 노드에 코드 실행을 지시하는 법](https://github.com/jechol/rpg/blob/db3ca9f661a6b478934168d8b8618a7b47eaf4a2/test/support/cluster.ex#L12) 28 | * [분산 노드를 끄는 법](https://github.com/jechol/rpg/blob/db3ca9f661a6b478934168d8b8618a7b47eaf4a2/test/support/cluster.ex#L21) 29 | * pg 프로토콜 이해 30 | * [scope을 기준으로 한 overlay network](https://github.com/jechol/rpg/blob/db3ca9f661a6b478934168d8b8618a7b47eaf4a2/lib/rpg.ex#L61) 31 | * discover -> sync 32 | * [send discover](https://github.com/jechol/rpg/blob/db3ca9f661a6b478934168d8b8618a7b47eaf4a2/lib/rpg.ex#L62) 33 | * [receive discover](https://github.com/jechol/rpg/blob/db3ca9f661a6b478934168d8b8618a7b47eaf4a2/lib/rpg.ex#L116) 34 | * [send sync](https://github.com/jechol/rpg/blob/db3ca9f661a6b478934168d8b8618a7b47eaf4a2/lib/rpg.ex#L128) 35 | * [receive sync](https://github.com/jechol/rpg/blob/db3ca9f661a6b478934168d8b8618a7b47eaf4a2/lib/rpg.ex#L144) 36 | * [overlay network down 감시](https://github.com/jechol/rpg/blob/db3ca9f661a6b478934168d8b8618a7b47eaf4a2/lib/rpg.ex#L138) 37 | * [local process down 감시](https://github.com/jechol/rpg/blob/db3ca9f661a6b478934168d8b8618a7b47eaf4a2/lib/rpg.ex#L280) -------------------------------------------------------------------------------- /20_distribution/21_my_pg/lib/rpg.ex: -------------------------------------------------------------------------------- 1 | defmodule Rpg do 2 | use GenServer 3 | 4 | def start_link(scope \\ __MODULE__) when is_atom(scope) do 5 | GenServer.start_link(__MODULE__, [scope], name: scope) 6 | end 7 | 8 | def start(scope) do 9 | GenServer.start(__MODULE__, [scope], name: scope) 10 | end 11 | 12 | # Write 13 | 14 | def join(scope \\ __MODULE__, group, pid) when is_pid(pid) do 15 | GenServer.call(scope, {:join_local, group, pid}) 16 | end 17 | 18 | def leave(scope \\ __MODULE__, group, pid) when is_pid(pid) do 19 | GenServer.call(scope, {:leave_local, group, pid}) 20 | end 21 | 22 | # Read 23 | 24 | def get_members(scope \\ __MODULE__, group) do 25 | GenServer.call(scope, {:get_members, group}) 26 | end 27 | 28 | def get_local_members(scope \\ __MODULE__, group) do 29 | GenServer.call(scope, {:get_local_members, group}) 30 | end 31 | 32 | def which_groups(scope \\ __MODULE__) do 33 | GenServer.call(scope, {:which_groups}) 34 | end 35 | 36 | # GenServer Impl. 37 | 38 | defmodule State do 39 | @type group :: any() 40 | @type t :: %__MODULE__{ 41 | # scope and also registered process name (self()) 42 | scope: atom(), 43 | # local processes and local groups they joined 44 | local_members: %{pid() => {m_ref :: reference(), groups :: [group()]}}, 45 | # groups to all and local processes 46 | groups: %{group() => {all :: [pid()], local :: [pid()]}}, 47 | # remote peers: scope process monitor and map of groups to pids for fast sync routine 48 | peers: %{pid() => {reference(), %{group() => [pid()]}}} 49 | } 50 | 51 | @enforce_keys [:scope] 52 | defstruct scope: nil, 53 | local_members: %{}, 54 | groups: %{}, 55 | peers: %{} 56 | end 57 | 58 | def init([scope]) do 59 | :ok = :net_kernel.monitor_nodes(true) 60 | # {registered_name, node} is used for send. 61 | peers = for(node <- :erlang.nodes(), do: {scope, node}) 62 | broadcast(peers, {:discover, self()}) 63 | {:ok, %State{scope: scope}} 64 | end 65 | 66 | # Call 67 | 68 | def handle_call( 69 | {:join_local, group, pid}, 70 | _from, 71 | %State{local_members: local_members, peers: peers, groups: groups} = state 72 | ) do 73 | new_local_members = local_members |> join_local_member(pid, group) 74 | new_groups = groups |> join_local_group(group, [pid]) 75 | broadcast(Map.keys(peers), {:join, self(), group, pid}) 76 | {:reply, :ok, %State{state | local_members: new_local_members, groups: new_groups}} 77 | end 78 | 79 | def handle_call( 80 | {:leave_local, group, pid}, 81 | _from, 82 | %State{local_members: local_members, peers: peers, groups: groups} = state 83 | ) do 84 | new_local_members = local_members |> leave_local_member(pid, group) 85 | new_groups = groups |> leave_local_group(group, [pid]) 86 | broadcast(Map.keys(peers), {:leave, self(), pid, [group]}) 87 | {:reply, :ok, %State{state | local_members: new_local_members, groups: new_groups}} 88 | end 89 | 90 | def handle_call({:get_members, group}, _from, %State{groups: groups} = state) do 91 | {all, _local} = groups |> Map.get(group, {[], []}) 92 | {:reply, all, state} 93 | end 94 | 95 | def handle_call({:get_local_members, group}, _from, %State{groups: groups} = state) do 96 | {_all, local} = groups |> Map.get(group, {[], []}) 97 | {:reply, local, state} 98 | end 99 | 100 | def handle_call({:which_groups}, _from, %State{groups: groups} = state) do 101 | {:reply, groups |> Map.keys(), state} 102 | end 103 | 104 | # Info 105 | 106 | def handle_info({:nodeup, node}, state) when node == :erlang.node() do 107 | {:noreply, state} 108 | end 109 | 110 | def handle_info({:nodeup, node}, %State{scope: scope} = state) do 111 | :erlang.send({scope, node}, {:discover, self()}) 112 | 113 | {:noreply, state} 114 | end 115 | 116 | def handle_info({:discover, peer}, %State{groups: groups, peers: peers} = state) do 117 | # Send local groups 118 | groups_with_local_procs = 119 | groups 120 | |> Enum.reduce(%{}, fn 121 | {_group, {_all, []}}, groups_with_local_procs -> 122 | groups_with_local_procs 123 | 124 | {group, {_all, local}}, groups_with_local_procs -> 125 | groups_with_local_procs |> Map.put(group, local) 126 | end) 127 | 128 | :erlang.send(peer, {:sync, self(), groups_with_local_procs}) 129 | 130 | # Request peer's data if necessary 131 | case peers |> Map.has_key?(peer) do 132 | true -> 133 | {:noreply, state} 134 | 135 | false -> 136 | :erlang.send(peer, {:discover, self()}) 137 | 138 | peer_info = {Process.monitor(peer), %{}} 139 | {:noreply, %State{state | peers: peers |> Map.put(peer, peer_info)}} 140 | end 141 | end 142 | 143 | def handle_info({:sync, peer, peer_groups}, %State{} = state) do 144 | new_state = apply_sync_data(state, peer, peer_groups) 145 | {:noreply, new_state} 146 | end 147 | 148 | def handle_info({:nodedown, _node}, state) do 149 | # We need only :nodeup. Peer's :DOWN signal will arrive soon. 150 | {:noreply, state} 151 | end 152 | 153 | # handle local process exit 154 | def handle_info( 155 | {:DOWN, m_ref, :process, pid, _info}, 156 | %State{local_members: local_members, groups: groups, peers: peers} = state 157 | ) 158 | when node(pid) == :erlang.node() do 159 | {member_info, new_local_members} = local_members |> Map.pop(pid) 160 | 161 | case member_info do 162 | nil -> 163 | # happens when pid exits before 'leave' is processed. 164 | {:noreply, state} 165 | 166 | {^m_ref, joined_groups} -> 167 | new_groups = 168 | joined_groups 169 | |> Enum.reduce(groups, fn joined_group, groups -> 170 | groups |> leave_local_group(joined_group, [pid]) 171 | end) 172 | 173 | broadcast(Map.keys(peers), {:leave, self(), pid, joined_groups}) 174 | 175 | {:noreply, %State{state | local_members: new_local_members, groups: new_groups}} 176 | end 177 | end 178 | 179 | # handle remote peer down or leaving overlay network 180 | def handle_info( 181 | {:DOWN, m_ref, :process, peer, _info}, 182 | %State{groups: groups, peers: peers} = state 183 | ) do 184 | {^m_ref, peer_groups} = peers |> Map.get(peer) 185 | new_peers = peers |> Map.delete(peer) 186 | 187 | new_groups = 188 | peer_groups 189 | |> Enum.reduce(groups, fn {group, pids}, groups -> 190 | groups |> leave_remote_group(group, pids) 191 | end) 192 | 193 | {:noreply, %State{state | groups: new_groups, peers: new_peers}} 194 | end 195 | 196 | # remote pid is joining the group 197 | def handle_info({:join, peer, group, pid}, %State{groups: groups, peers: peers} = state) do 198 | {m_ref, peer_groups} = Map.get(peers, peer) 199 | 200 | new_groups = groups |> join_remote_group(group, [pid]) 201 | new_peer_groups = peer_groups |> Map.update(group, [pid], fn pids -> [pid | pids] end) 202 | 203 | {:noreply, 204 | %State{state | groups: new_groups, peers: %{peers | peer => {m_ref, new_peer_groups}}}} 205 | end 206 | 207 | # remote pid is leaving multiple groups at once 208 | def handle_info( 209 | {:leave, peer, pid, joined_groups}, 210 | %State{groups: groups, peers: peers} = state 211 | ) do 212 | {m_ref, peer_groups} = Map.get(peers, peer) 213 | 214 | new_groups = 215 | joined_groups 216 | |> Enum.reduce(groups, fn joined_group, groups -> 217 | groups |> leave_remote_group(joined_group, [pid]) 218 | end) 219 | 220 | new_peer_groups = 221 | joined_groups 222 | |> Enum.reduce(peer_groups, fn joined_group, peer_groups -> 223 | peer_groups |> Map.update!(joined_group, fn pids -> pids |> List.delete(pid) end) 224 | end) 225 | 226 | new_peers = peers |> Map.put(peer, {m_ref, new_peer_groups}) 227 | 228 | {:noreply, %State{state | groups: new_groups, peers: new_peers}} 229 | end 230 | 231 | defp apply_sync_data(%State{peers: peers, groups: groups} = state, peer, new_peer_groups) 232 | when is_pid(peer) do 233 | {m_ref, old_peer_groups} = peers |> Map.get_lazy(peer, fn -> {Process.monitor(peer), %{}} end) 234 | 235 | new_groups = sync_groups(groups, old_peer_groups, new_peer_groups) 236 | new_peers = peers |> Map.put(peer, {m_ref, new_peer_groups}) 237 | 238 | %State{state | groups: new_groups, peers: new_peers} 239 | end 240 | 241 | defp sync_groups(groups, %{} = old_peer_groups, %{} = new_peer_groups) do 242 | (Map.keys(old_peer_groups) ++ Map.keys(new_peer_groups)) 243 | |> Enum.uniq() 244 | |> Enum.reduce(groups, fn group, groups -> 245 | new_pids = Map.get(new_peer_groups, group, []) 246 | old_pids = Map.get(old_peer_groups, group, []) 247 | 248 | groups = 249 | (new_pids -- old_pids) 250 | |> Enum.reduce(groups, fn pid, groups -> groups |> join_remote_group(group, [pid]) end) 251 | 252 | groups = 253 | (old_pids -- new_pids) 254 | |> Enum.reduce(groups, fn pid, groups -> groups |> leave_remote_group(group, [pid]) end) 255 | 256 | groups 257 | end) 258 | end 259 | 260 | # util for groups 261 | 262 | defp join_local_group(%{} = groups, group, pids) when is_list(pids) do 263 | {all, local} = groups |> Map.get(group, {[], []}) 264 | groups |> Map.put(group, {pids ++ all, pids ++ local}) 265 | end 266 | 267 | defp join_remote_group(%{} = groups, group, pids) when is_list(pids) do 268 | {all, local} = groups |> Map.get(group, {[], []}) 269 | groups |> Map.put(group, {pids ++ all, local}) 270 | end 271 | 272 | defp leave_local_group(%{} = groups, group, pids) when is_list(pids) do 273 | # don't handle exception. educational purpose only. 274 | {all, local} = groups |> Map.get(group) 275 | groups |> Map.put(group, {all -- pids, local -- pids}) 276 | end 277 | 278 | defp leave_remote_group(%{} = groups, group, pids) when is_list(pids) do 279 | # don't handle exception. educational purpose only. 280 | {all, local} = groups |> Map.get(group) 281 | groups |> Map.put(group, {all -- pids, local}) 282 | end 283 | 284 | # util for local_members (do not need to accept multiple groups) 285 | 286 | defp join_local_member(%{} = local_members, pid, group) do 287 | {m_ref, old_groups} = local_members |> Map.get_lazy(pid, fn -> {Process.monitor(pid), []} end) 288 | local_members |> Map.put(pid, {m_ref, [group | old_groups]}) 289 | end 290 | 291 | defp leave_local_member(%{} = local_members, pid, group) do 292 | # don't handle exception. educational purpose only. 293 | case local_members |> Map.get(pid) do 294 | {m_ref, [^group]} -> 295 | Process.demonitor(m_ref) 296 | local_members |> Map.delete(pid) 297 | 298 | {m_ref, groups} -> 299 | new_groups = groups |> List.delete(group) 300 | local_members |> Map.put(pid, {m_ref, new_groups}) 301 | end 302 | end 303 | 304 | # util for peers 305 | 306 | defp broadcast(peers, msg) do 307 | # No implicit connect because we handle :nodeup explicitly. 308 | peers 309 | |> Enum.each(fn peer -> Process.send(peer, msg, [:noconnect]) end) 310 | 311 | :ok 312 | end 313 | end 314 | -------------------------------------------------------------------------------- /20_distribution/21_my_pg/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Rpg.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :rpg, 7 | version: "0.1.0", 8 | elixir: "~> 1.11", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | start_permanent: Mix.env() == :prod, 11 | deps: deps() 12 | ] 13 | end 14 | 15 | # Run "mix help compile.app" to learn about applications. 16 | def application do 17 | [ 18 | extra_applications: [:logger] 19 | ] 20 | end 21 | 22 | # Run "mix help deps" to learn about dependencies. 23 | defp deps do 24 | [ 25 | # {:dep_from_hexpm, "~> 0.3.0"}, 26 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 27 | ] 28 | end 29 | 30 | # defp elixirc_paths(:test), do: ["lib", "test/support"] 31 | # defp elixirc_paths(_), do: ["lib"] 32 | defp elixirc_paths(_), do: ["lib", "test/support"] 33 | end 34 | -------------------------------------------------------------------------------- /20_distribution/21_my_pg/test/rpg/multi_nodes_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rpg.MultiNodesTest do 2 | use ExUnit.Case 3 | 4 | for pg <- [:pg, Rpg] do 5 | setup %{test: test} do 6 | pg = unquote(pg) 7 | scope = :"#{pg}-#{test}" 8 | Cluster.ensure_other_node_started() 9 | 10 | {:ok, _pid} = pg.start(scope) 11 | {:ok, _pid} = Cluster.rpc_other_node(pg, :start, [scope]) 12 | local = self() 13 | remote = Node.spawn(Cluster.other_node(), Process, :sleep, [:infinity]) 14 | 15 | {:ok, %{pg: pg, scope: scope, local: local, remote: remote}} 16 | end 17 | 18 | test "#{pg} join, leave, get_members, get_local_members", %{ 19 | pg: pg, 20 | scope: scope, 21 | local: local, 22 | remote: remote 23 | } do 24 | # Join from local 25 | :ok = pg.join(scope, :group1, local) 26 | # Join from remote 27 | :ok = Cluster.rpc_other_node(pg, :join, [scope, :group1, remote]) 28 | 29 | # Check members are synced 30 | Process.sleep(200) 31 | assert pg.get_members(scope, :group1) == [remote, local] 32 | assert pg.get_local_members(scope, :group1) == [local] 33 | assert Cluster.rpc_other_node(pg, :get_local_members, [scope, :group1]) == [remote] 34 | 35 | # Leave from local 36 | :ok = pg.leave(scope, :group1, local) 37 | # Leave from remote 38 | :ok = Cluster.rpc_other_node(pg, :leave, [scope, :group1, remote]) 39 | 40 | # Check members are synced 41 | Process.sleep(200) 42 | assert pg.get_members(scope, :group1) == [] 43 | assert pg.get_local_members(scope, :group1) == [] 44 | assert Cluster.rpc_other_node(pg, :get_local_members, [scope, :group1]) == [] 45 | end 46 | 47 | test "#{pg} restart remote", %{pg: pg, scope: scope, local: local} do 48 | # Join from local 49 | :ok = pg.join(scope, :group1, local) 50 | 51 | # Check remote is synced 52 | Process.sleep(200) 53 | assert Cluster.rpc_other_node(pg, :get_members, [scope, :group1]) == [local] 54 | 55 | # Kill remote scope 56 | # remote_scope_pid = Cluster.rpc_other_node(Process, :whereis, [scope]) 57 | # true = Cluster.rpc_other_node(Process, :exit, [remote_scope_pid, :kill]) 58 | 59 | scope_pid = Cluster.rpc_other_node(Process, :whereis, [scope]) 60 | ref = Process.monitor(scope_pid) 61 | 62 | true = Process.exit(scope_pid, :kill) 63 | 64 | receive do 65 | {:DOWN, ^ref, :process, ^scope_pid, :killed} -> :ok 66 | end 67 | 68 | # Restart remote scope and check remote is synced again 69 | {:ok, _new_scope_pid} = Cluster.rpc_other_node(pg, :start, [scope]) 70 | Process.sleep(200) 71 | assert Cluster.rpc_other_node(pg, :get_members, [scope, :group1]) == [local] 72 | end 73 | 74 | test "#{pg} restart local", %{pg: pg, scope: scope, remote: remote} do 75 | # Join from remote 76 | :ok = Cluster.rpc_other_node(pg, :join, [scope, :group1, remote]) 77 | assert Cluster.rpc_other_node(pg, :get_members, [scope, :group1]) == [remote] 78 | 79 | # Check local is synced 80 | Process.sleep(200) 81 | assert pg.get_members(scope, :group1) == [remote] 82 | 83 | # Kill local scope 84 | scope_pid = Process.whereis(scope) 85 | ref = Process.monitor(scope_pid) 86 | 87 | true = Process.exit(scope_pid, :kill) 88 | 89 | receive do 90 | {:DOWN, ^ref, :process, ^scope_pid, :killed} -> :ok 91 | end 92 | 93 | # Restart local scope and check local is synced again 94 | {:ok, _new_scope_pid} = pg.start(scope) 95 | Process.sleep(200) 96 | assert pg.get_members(scope, :group1) == [remote] 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /20_distribution/21_my_pg/test/rpg/nodedown_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rpg.NodeDownTest do 2 | use ExUnit.Case 3 | 4 | for pg <- [:pg, Rpg] do 5 | setup %{test: test} do 6 | pg = unquote(pg) 7 | scope = :"#{pg}-#{test}" 8 | Cluster.ensure_other_node_started() 9 | 10 | {:ok, _pid} = pg.start(scope) 11 | {:ok, _pid} = Cluster.rpc_other_node(pg, :start, [scope]) 12 | local = self() 13 | remote = Node.spawn(Cluster.other_node(), Process, :sleep, [:infinity]) 14 | 15 | {:ok, %{pg: pg, scope: scope, local: local, remote: remote}} 16 | end 17 | 18 | test "#{pg} remote members are removed when remote node is down", %{ 19 | pg: pg, 20 | scope: scope, 21 | local: local, 22 | remote: remote 23 | } do 24 | # Join from local 25 | :ok = pg.join(scope, :group1, local) 26 | # Join from remote 27 | :ok = Cluster.rpc_other_node(pg, :join, [scope, :group1, remote]) 28 | 29 | # Check members are synced 30 | Process.sleep(200) 31 | assert pg.get_members(scope, :group1) == [remote, local] 32 | 33 | # BOOM! Stop other node 34 | other_node = Cluster.other_node() 35 | Node.monitor(other_node, true) 36 | :ok = :slave.stop(other_node) 37 | 38 | receive do 39 | {:nodedown, ^other_node} -> 40 | :ok 41 | end 42 | 43 | # Check remote member is removed 44 | Process.sleep(200) 45 | assert pg.get_members(scope, :group1) == [local] 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /20_distribution/21_my_pg/test/rpg/single_node_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rpg.SingleNodeTest do 2 | use ExUnit.Case 3 | 4 | for pg <- [:pg, Rpg] do 5 | setup %{test: test} do 6 | pg = unquote(pg) 7 | scope = :"#{pg}-#{test}" 8 | {:ok, _pid} = pg.start_link(scope) 9 | {:ok, %{pg: pg, scope: scope}} 10 | end 11 | 12 | test "#{pg} which_groups", %{pg: pg, scope: scope} do 13 | assert pg.which_groups(scope) == [] 14 | 15 | pid1 = self() 16 | :ok = pg.join(scope, :group1, pid1) 17 | :ok = pg.join(scope, :group2, pid1) 18 | 19 | assert pg.which_groups(scope) |> Enum.member?(:group1) 20 | assert pg.which_groups(scope) |> Enum.member?(:group2) 21 | end 22 | 23 | test "#{pg} join group", %{pg: pg, scope: scope} do 24 | pid1 = self() 25 | 26 | :ok = pg.join(scope, :group1, pid1) 27 | assert pg.get_members(scope, :group1) == [pid1] 28 | assert pg.get_local_members(scope, :group1) == [pid1] 29 | 30 | # Join twice 31 | :ok = pg.join(scope, :group1, pid1) 32 | assert pg.get_members(scope, :group1) == [pid1, pid1] 33 | assert pg.get_local_members(scope, :group1) == [pid1, pid1] 34 | 35 | pid2 = spawn_link(Process, :sleep, [:infinity]) 36 | :ok = pg.join(scope, :group1, pid2) 37 | assert pg.get_members(scope, :group1) == [pid2, pid1, pid1] 38 | assert pg.get_local_members(scope, :group1) == [pid2, pid1, pid1] 39 | end 40 | 41 | test "#{pg} leave group", %{pg: pg, scope: scope} do 42 | pid1 = self() 43 | 44 | # Join twice 45 | :ok = pg.join(scope, :group1, pid1) 46 | :ok = pg.join(scope, :group1, pid1) 47 | 48 | # Leave once 49 | :ok = pg.leave(scope, :group1, pid1) 50 | assert pg.get_members(scope, :group1) == [pid1] 51 | 52 | # Leave once more 53 | :ok = pg.leave(scope, :group1, pid1) 54 | assert pg.get_members(scope, :group1) == [] 55 | assert pg.get_local_members(scope, :group1) == [] 56 | end 57 | 58 | test "#{pg} member dies", %{pg: pg, scope: scope} do 59 | pid1 = 60 | spawn_link(fn -> 61 | receive do 62 | :exit -> :ok 63 | end 64 | end) 65 | 66 | # Join twice 67 | :ok = pg.join(scope, :group1, pid1) 68 | :ok = pg.join(scope, :group1, pid1) 69 | assert pg.get_members(scope, :group1) == [pid1, pid1] 70 | 71 | # Kill pid1 72 | send(pid1, :exit) 73 | 74 | # Wait for pg to catch DOWN 75 | Process.sleep(100) 76 | assert pg.get_members(scope, :group1) == [] 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /20_distribution/21_my_pg/test/support/cluster.ex: -------------------------------------------------------------------------------- 1 | defmodule Cluster do 2 | @this_name :foo 3 | @other_name :bar 4 | 5 | def ensure_other_node_started() do 6 | if other_node() == nil do 7 | start_other_node() 8 | end 9 | end 10 | 11 | def rpc_other_node(m, f, a) do 12 | :rpc.block_call(other_node(), m, f, a) 13 | end 14 | 15 | def stop_other_node() do 16 | case other_node() do 17 | nil -> 18 | :ok 19 | 20 | other -> 21 | Node.monitor(other, true) 22 | :ok = :slave.stop(other) 23 | 24 | receive do 25 | {:nodedown, ^other} -> 26 | :ok 27 | end 28 | end 29 | end 30 | 31 | def other_node do 32 | case Node.list() do 33 | [] -> nil 34 | [other] -> other 35 | end 36 | end 37 | 38 | # Private 39 | 40 | defp start_other_node() do 41 | ensure_epmd_started() 42 | 43 | # Turn node into a distributed node 44 | Node.start(@this_name, :shortnames) 45 | 46 | # Allow other nodes to fetch code from this node 47 | allow_boot() 48 | 49 | # Spawn other node 50 | spawn_other_node() 51 | end 52 | 53 | defp ensure_epmd_started() do 54 | case :erl_epmd.names() do 55 | {:error, _} -> 56 | :os.cmd('epmd -daemon') 57 | :ok 58 | 59 | {:ok, _} -> 60 | :ok 61 | end 62 | end 63 | 64 | defp spawn_other_node() do 65 | {:ok, node} = :slave.start(:net_adm.localhost(), @other_name, inet_loader_args()) 66 | 67 | :ok = add_code_paths(node) 68 | :ok = transfer_configuration(node) 69 | :ok = ensure_applications_started(node) 70 | :ok = wait_for_other_node_up(node) 71 | node 72 | end 73 | 74 | defp inet_loader_args do 75 | '-loader inet -hosts 127.0.0.1 -setcookie #{:erlang.get_cookie()}' 76 | end 77 | 78 | defp allow_boot() do 79 | localhost = :net_adm.localhost() 80 | 81 | :erl_boot_server.start([localhost]) 82 | end 83 | 84 | defp add_code_paths(node) do 85 | :ok = :rpc.block_call(node, :code, :add_paths, [:code.get_path()]) 86 | end 87 | 88 | defp transfer_configuration(node) do 89 | for {app_name, _, _} <- Application.loaded_applications() do 90 | for {key, val} <- Application.get_all_env(app_name) do 91 | :ok = :rpc.block_call(node, Application, :put_env, [app_name, key, val]) 92 | end 93 | end 94 | 95 | :ok 96 | end 97 | 98 | defp ensure_applications_started(node) do 99 | {:ok, _} = :rpc.block_call(node, Application, :ensure_all_started, [:mix]) 100 | :ok = :rpc.block_call(node, Mix, :env, [Mix.env()]) 101 | 102 | for {app_name, _, _} <- Application.loaded_applications() do 103 | {:ok, _} = :rpc.block_call(node, Application, :ensure_all_started, [app_name]) 104 | end 105 | 106 | :ok 107 | end 108 | 109 | defp wait_for_other_node_up(node) do 110 | case :net_adm.ping(node) do 111 | :pong -> 112 | :ok 113 | 114 | :pang -> 115 | Process.sleep(100) 116 | wait_for_other_node_up(node) 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /20_distribution/21_my_pg/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /20_distribution/22_pg2/README.md: -------------------------------------------------------------------------------- 1 | What does pg2 let us do? (ref: https://youtu.be/_O-bLuVhcCA?t=291) 2 | 3 | * We can access a group of processes by a common name. For example, there can be a set of processes (which can be located on different nodes) that are all members of the group. :foobar 4 | 5 | * We can send a message to one, some, or all group members. 6 | 7 | * If a member process terminates, it is automatically removed from the group. -------------------------------------------------------------------------------- /20_distribution/29_learn_phx_pubsub/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /20_distribution/29_learn_phx_pubsub/.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 third-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 | learn_phx_pubsub-*.tar 24 | 25 | -------------------------------------------------------------------------------- /20_distribution/29_learn_phx_pubsub/.iex.exs: -------------------------------------------------------------------------------- 1 | alias Phoenix.PubSub 2 | alias Phoenix.Tracker 3 | -------------------------------------------------------------------------------- /20_distribution/29_learn_phx_pubsub/README.md: -------------------------------------------------------------------------------- 1 | # LearnPhxPubsub 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `learn_phx_pubsub` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:learn_phx_pubsub, "~> 0.1.0"} 14 | ] 15 | end 16 | ``` 17 | 18 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 19 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 20 | be found at [https://hexdocs.pm/learn_phx_pubsub](https://hexdocs.pm/learn_phx_pubsub). 21 | 22 | -------------------------------------------------------------------------------- /20_distribution/29_learn_phx_pubsub/lib/learn_phx_pubsub.ex: -------------------------------------------------------------------------------- 1 | defmodule LearnPhxPubsub do 2 | use Application 3 | alias Phoenix.PubSub 4 | 5 | def start(_type, _args) do 6 | children = [ 7 | {PubSub, name: :ps}, 8 | {MyTracker, [name: :tr, pubsub_server: :ps]} 9 | ] 10 | 11 | Supervisor.start_link(children, strategy: :rest_for_one) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /20_distribution/29_learn_phx_pubsub/lib/my_tracker.ex: -------------------------------------------------------------------------------- 1 | defmodule MyTracker do 2 | alias Phoenix.Tracker 3 | alias Phoenix.PubSub 4 | use Tracker 5 | 6 | def start_link(opts) do 7 | opts = Keyword.merge([name: __MODULE__], opts) 8 | Phoenix.Tracker.start_link(__MODULE__, opts, opts) 9 | end 10 | 11 | @impl true 12 | def init(opts) do 13 | server = Keyword.fetch!(opts, :pubsub_server) 14 | {:ok, %{pubsub_server: server}} 15 | end 16 | 17 | @impl true 18 | def handle_diff(diff, state = %{pubsub_server: pubsub_server}) do 19 | diff 20 | |> Enum.each(fn {topic, {joins, leaves}} -> 21 | joins 22 | |> Enum.each(fn {key, meta} -> 23 | IO.puts(~s(presence join: #{topic} "#{key}" with meta #{inspect(meta)})) 24 | msg = {:join, key, meta} 25 | PubSub.broadcast!(pubsub_server, topic, msg) 26 | end) 27 | 28 | leaves 29 | |> Enum.each(fn {key, meta} -> 30 | IO.puts(~s(presence leave: #{topic} "#{key}" with meta #{inspect(meta)})) 31 | msg = {:leave, key, meta} 32 | PubSub.broadcast!(pubsub_server, topic, msg) 33 | end) 34 | end) 35 | 36 | {:ok, state} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /20_distribution/29_learn_phx_pubsub/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule LearnPhxPubsub.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :learn_phx_pubsub, 7 | version: "0.1.0", 8 | elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger], 18 | mod: {LearnPhxPubsub, []} 19 | ] 20 | end 21 | 22 | # Run "mix help deps" to learn about dependencies. 23 | defp deps do 24 | [ 25 | {:phoenix_pubsub, "~> 2.0"} 26 | # {:dep_from_hexpm, "~> 0.3.0"}, 27 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 28 | ] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /20_distribution/29_learn_phx_pubsub/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, 3 | } 4 | -------------------------------------------------------------------------------- /20_distribution/29_learn_phx_pubsub/sys.config: -------------------------------------------------------------------------------- 1 | [{kernel, 2 | [ 3 | {sync_nodes_optional, ['n1@127.0.0.1', 'n2@127.0.0.1']}, 4 | {sync_nodes_timeout, 1000} 5 | ]} 6 | ]. -------------------------------------------------------------------------------- /20_distribution/29_learn_phx_pubsub/test/learn_phx_pubsub_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LearnPhxPubsubTest do 2 | use ExUnit.Case 3 | doctest LearnPhxPubsub 4 | end 5 | -------------------------------------------------------------------------------- /20_distribution/29_learn_phx_pubsub/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /30_process_discovery/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jechol/elixir-coding-club/0db2337524833288d82e1fbf7efac8a00bd16a2e/30_process_discovery/.gitkeep -------------------------------------------------------------------------------- /30_process_discovery/15_my_registry/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /30_process_discovery/15_my_registry/.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 third-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 | my_registry-*.tar 24 | 25 | -------------------------------------------------------------------------------- /30_process_discovery/15_my_registry/README.md: -------------------------------------------------------------------------------- 1 | # MyRegistry 2 | 3 | Registry 는 4 | 5 | - keys: :unique 인 경우 process registry 기능을 하고, (local node 에서 :global 와 유사한 기능) 6 | - keys: :duplicate 인 경우 process group 기능을 한다. (local node 에서 :pg 와 유사한 기능) 7 | 8 | ## Process registry 구현 9 | 10 | Registry의 축소 버전 MyRegistry 을 Agent를 사용하여 구현하자. 11 | 12 | - start_link(keys: :unique, name: name) 13 | - register/3 14 | - lookup/2 15 | - unregister/2 16 | 17 | ``` 18 | mix test test/my_registry_test.exs:10 19 | ``` 20 | 21 | ## :via 튜플 기능 구현 22 | 23 | Registry는 :via 튜플을 통한 등록, 조회 기능을 제공한다. 24 | 25 | ```elixir 26 | {:ok, reg} = Registry.start_link(keys: :unique, name: Foo) 27 | Agent.start_link(fn -> 0 end, name: {:via, Registry, {Foo, :some_key}}) 28 | 0 = Agent.get({:via, Registry, {Foo, :some_key}}, & &1) 29 | ``` 30 | 31 | Registry의 숨겨진 API 인 register_name, whereis_name 을 구현하여 같은 동작을 구현해보자. 32 | 33 | ### 검증 34 | 35 | ``` 36 | mix test test/my_registry_test.exs:24 37 | ``` -------------------------------------------------------------------------------- /30_process_discovery/15_my_registry/lib/my_registry.ex: -------------------------------------------------------------------------------- 1 | defmodule MyRegistry do 2 | def start_link(keys: :unique, name: reg_name) do 3 | end 4 | 5 | def register(reg_name, key, value) do 6 | end 7 | 8 | def lookup(reg_name, key) do 9 | end 10 | 11 | def unregister(reg_name, key) do 12 | end 13 | 14 | def register_name({reg_name, key}, pid) do 15 | # Hidden function. See Registry source at 16 | # https://github.com/elixir-lang/elixir/blob/1145dc01680aab7094f8a6dbd38b65185e14adb4/lib/elixir/lib/registry.ex#L244 17 | end 18 | 19 | def whereis_name({reg_name, key} = _name) do 20 | # Hidden function. See Registry source at 21 | # https://github.com/elixir-lang/elixir/blob/1145dc01680aab7094f8a6dbd38b65185e14adb4/lib/elixir/lib/registry.ex#L222 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /30_process_discovery/15_my_registry/lib_sol/my_registry.ex: -------------------------------------------------------------------------------- 1 | defmodule MyRegistry do 2 | def start_link(keys: :unique, name: reg_name) do 3 | {:ok, _reg} = Agent.start_link(fn -> %{} end, name: reg_name) 4 | end 5 | 6 | def register(reg_name, key, value) do 7 | this = self() 8 | 9 | Agent.get_and_update(reg_name, fn map -> 10 | case map |> Map.fetch(key) do 11 | {:ok, {existing_pid, _existing_value}} -> 12 | {{:error, {:already_registered, existing_pid}}, map} 13 | 14 | :error -> 15 | {{:ok, self()}, map |> Map.put_new(key, {this, value})} 16 | end 17 | end) 18 | end 19 | 20 | def lookup(reg_name, key) do 21 | Agent.get(reg_name, fn map -> map |> Map.get(key, []) |> List.wrap() end) 22 | end 23 | 24 | def unregister(reg_name, key) do 25 | Agent.update(reg_name, fn map -> map |> Map.delete(key) end) 26 | end 27 | 28 | def register_name({reg_name, key}, pid) do 29 | # Hidden function. See Registry source at 30 | # https://github.com/elixir-lang/elixir/blob/1145dc01680aab7094f8a6dbd38b65185e14adb4/lib/elixir/lib/registry.ex#L244 31 | 32 | register(reg_name, key, pid) 33 | :yes 34 | end 35 | 36 | def whereis_name({reg_name, key} = _name) do 37 | # Hidden function. See Registry source at 38 | # https://github.com/elixir-lang/elixir/blob/1145dc01680aab7094f8a6dbd38b65185e14adb4/lib/elixir/lib/registry.ex#L222 39 | 40 | Agent.get(reg_name, fn map -> map |> Map.get(key) end) 41 | |> case do 42 | {pid, _} -> pid 43 | nil -> :undefined 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /30_process_discovery/15_my_registry/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule MyRegistry.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :my_registry, 7 | version: "0.1.0", 8 | elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | # {:dep_from_hexpm, "~> 0.3.0"}, 25 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /30_process_discovery/15_my_registry/test/my_registry_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MyRegistryTest do 2 | use ExUnit.Case 3 | 4 | for registry <- [Registry, MyRegistry] do 5 | @registry registry 6 | 7 | test "#{@registry} register/lookup/unregister" do 8 | reg_name = [@registry, Test1Reg] |> Module.concat() 9 | this = self() 10 | 11 | {:ok, _} = @registry.start_link(keys: :unique, name: reg_name) 12 | {:ok, _registry_pid} = @registry.register(reg_name, "hello", "world") 13 | 14 | {:error, {:already_registered, ^this}} = @registry.register(reg_name, "hello", "world") 15 | 16 | [{^this, "world"}] = @registry.lookup(reg_name, "hello") 17 | :ok = @registry.unregister(reg_name, "hello") 18 | [] = @registry.lookup(reg_name, "hello") 19 | end 20 | 21 | test "{:via, #{@registry}, {registry, key}}" do 22 | reg_name = [@registry, Test2Reg] |> Module.concat() 23 | {:ok, _} = @registry.start_link(keys: :unique, name: reg_name) 24 | 25 | name = {:via, @registry, {reg_name, "agent"}} 26 | {:ok, _} = Agent.start_link(fn -> 0 end, name: name) 27 | 0 = Agent.get(name, & &1) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /30_process_discovery/15_my_registry/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /40_fp/40_learn_parser/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | locals_without_parens: [defparsec: 2, defparsecp: 2, defcombinator: 2, defcombinatorp: 2] 5 | ] 6 | -------------------------------------------------------------------------------- /40_fp/40_learn_parser/.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 third-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 | learn_parser-*.tar 24 | 25 | -------------------------------------------------------------------------------- /40_fp/40_learn_parser/README.md: -------------------------------------------------------------------------------- 1 | # LearnParser 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `learn_parser` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:learn_parser, "~> 0.1.0"} 14 | ] 15 | end 16 | ``` 17 | 18 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 19 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 20 | be found at [https://hexdocs.pm/learn_parser](https://hexdocs.pm/learn_parser). 21 | 22 | -------------------------------------------------------------------------------- /40_fp/40_learn_parser/lib/learn_parser.ex: -------------------------------------------------------------------------------- 1 | defmodule LearnParser do 2 | import NimbleParsec 3 | 4 | defparsec :digit_and_lowercase, 5 | empty() 6 | |> ascii_char([?0..?9]) 7 | |> ascii_char([?a..?z]) 8 | |> label("digit followed by lowercase letter") 9 | end 10 | -------------------------------------------------------------------------------- /40_fp/40_learn_parser/lib/my_parser.ex: -------------------------------------------------------------------------------- 1 | # Generated from lib/my_parser.ex.exs, do not edit. 2 | # Generated at 2020-10-04 21:52:12Z. 3 | 4 | # lib/my_parser.ex.exs 5 | defmodule MyParser do 6 | @moduledoc false 7 | 8 | @doc """ 9 | Parses the given `binary` as datetime. 10 | 11 | Returns `{:ok, [token], rest, context, position, byte_offset}` or 12 | `{:error, reason, rest, context, line, byte_offset}` where `position` 13 | describes the location of the datetime (start position) as `{line, column_on_line}`. 14 | 15 | ## Options 16 | 17 | * `:byte_offset` - the byte offset for the whole binary, defaults to 0 18 | * `:line` - the line and the byte offset into that line, defaults to `{1, byte_offset}` 19 | * `:context` - the initial context value. It will be converted to a map 20 | 21 | """ 22 | @spec datetime(binary, keyword) :: 23 | {:ok, [term], rest, context, line, byte_offset} 24 | | {:error, reason, rest, context, line, byte_offset} 25 | when line: {pos_integer, byte_offset}, 26 | byte_offset: pos_integer, 27 | rest: binary, 28 | reason: String.t(), 29 | context: map() 30 | def datetime(binary, opts \\ []) when is_binary(binary) do 31 | context = Map.new(Keyword.get(opts, :context, [])) 32 | byte_offset = Keyword.get(opts, :byte_offset, 0) 33 | 34 | line = 35 | case(Keyword.get(opts, :line, 1)) do 36 | {_, _} = line -> 37 | line 38 | 39 | line -> 40 | {line, byte_offset} 41 | end 42 | 43 | case(datetime__0(binary, [], [], context, line, byte_offset)) do 44 | {:ok, acc, rest, context, line, offset} -> 45 | {:ok, :lists.reverse(acc), rest, context, line, offset} 46 | 47 | {:error, _, _, _, _, _} = error -> 48 | error 49 | end 50 | end 51 | 52 | defp datetime__0( 53 | <>, 56 | acc, 57 | stack, 58 | context, 59 | comb__line, 60 | comb__offset 61 | ) 62 | when x0 >= 48 and x0 <= 57 and (x1 >= 48 and x1 <= 57) and (x2 >= 48 and x2 <= 57) and 63 | (x3 >= 48 and x3 <= 57) and (x4 >= 48 and x4 <= 57) and (x5 >= 48 and x5 <= 57) and 64 | (x6 >= 48 and x6 <= 57) and (x7 >= 48 and x7 <= 57) and (x8 >= 48 and x8 <= 57) and 65 | (x9 >= 48 and x9 <= 57) and (x10 >= 48 and x10 <= 57) and (x11 >= 48 and x11 <= 57) and 66 | (x12 >= 48 and x12 <= 57) and (x13 >= 48 and x13 <= 57) do 67 | datetime__1( 68 | rest, 69 | [ 70 | x13 - 48 + (x12 - 48) * 10, 71 | x11 - 48 + (x10 - 48) * 10, 72 | x9 - 48 + (x8 - 48) * 10, 73 | x7 - 48 + (x6 - 48) * 10, 74 | x5 - 48 + (x4 - 48) * 10, 75 | x3 - 48 + (x2 - 48) * 10 + (x1 - 48) * 100 + (x0 - 48) * 1000 76 | ] ++ acc, 77 | stack, 78 | context, 79 | comb__line, 80 | comb__offset + 19 81 | ) 82 | end 83 | 84 | defp datetime__0(rest, _acc, _stack, context, line, offset) do 85 | {:error, 86 | "expected ASCII character in the range '0' to '9', followed by ASCII character in the range '0' to '9', followed by ASCII character in the range '0' to '9', followed by ASCII character in the range '0' to '9', followed by string \"-\", followed by ASCII character in the range '0' to '9', followed by ASCII character in the range '0' to '9', followed by string \"-\", followed by ASCII character in the range '0' to '9', followed by ASCII character in the range '0' to '9', followed by string \"T\", followed by ASCII character in the range '0' to '9', followed by ASCII character in the range '0' to '9', followed by string \":\", followed by ASCII character in the range '0' to '9', followed by ASCII character in the range '0' to '9', followed by string \":\", followed by ASCII character in the range '0' to '9', followed by ASCII character in the range '0' to '9'", 87 | rest, context, line, offset} 88 | end 89 | 90 | defp datetime__1(<<"Z", rest::binary>>, acc, stack, context, comb__line, comb__offset) do 91 | datetime__2(rest, ["Z"] ++ acc, stack, context, comb__line, comb__offset + 1) 92 | end 93 | 94 | defp datetime__1(<>, acc, stack, context, comb__line, comb__offset) do 95 | datetime__2(rest, [] ++ acc, stack, context, comb__line, comb__offset) 96 | end 97 | 98 | defp datetime__2(rest, acc, _stack, context, line, offset) do 99 | {:ok, acc, rest, context, line, offset} 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /40_fp/40_learn_parser/lib/my_parser.ex.exs: -------------------------------------------------------------------------------- 1 | # lib/my_parser.ex.exs 2 | defmodule MyParser do 3 | @moduledoc false 4 | 5 | # parsec:MyParser 6 | import NimbleParsec 7 | 8 | date = 9 | integer(4) 10 | |> ignore(string("-")) 11 | |> integer(2) 12 | |> ignore(string("-")) 13 | |> integer(2) 14 | 15 | time = 16 | integer(2) 17 | |> ignore(string(":")) 18 | |> integer(2) 19 | |> ignore(string(":")) 20 | |> integer(2) 21 | |> optional(string("Z")) 22 | 23 | defparsec :datetime, date |> ignore(string("T")) |> concat(time) 24 | 25 | # parsec:MyParser 26 | end 27 | -------------------------------------------------------------------------------- /40_fp/40_learn_parser/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule LearnParser.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :learn_parser, 7 | version: "0.1.0", 8 | elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | {:nimble_parsec, "~> 1.0"} 25 | # {:dep_from_hexpm, "~> 0.3.0"}, 26 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 27 | ] 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /40_fp/40_learn_parser/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 3 | } 4 | -------------------------------------------------------------------------------- /40_fp/40_learn_parser/test/learn_parser_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LearnParserTest do 2 | use ExUnit.Case 3 | end 4 | -------------------------------------------------------------------------------- /40_fp/40_learn_parser/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /50_code_quality/50_use_gen_statem/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | locals_without_parens: [definject: 1, definject: 2] 5 | ] 6 | -------------------------------------------------------------------------------- /50_code_quality/50_use_gen_statem/.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 third-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 | door_lock-*.tar 24 | 25 | -------------------------------------------------------------------------------- /50_code_quality/50_use_gen_statem/README.md: -------------------------------------------------------------------------------- 1 | Reference : https://erlang.org/doc/design_principles/statem.html 2 | 3 | # State machine 이란 4 | 5 | 1. State machine 이란? 6 | 1. State + Input => (State', Output) 7 | 2. Erlang process 자체가 state machine임. 8 | 1. state = process dictionary + recursive call argument 9 | 2. input = receive 10 | 3. output = send 11 | 2. 그럼 gen_statem 은 무슨 용도? 12 | 1. State별로 input handler가 달라질 경우 13 | 2. State별로 output이 정해지는 경우 14 | 3. State/Event별로 Timeout이 필요할 경우 15 | 4. 위와 같은 경우 코드를 훨씬 가독성 있게 구조적으로 작성할수 있음. 16 | 3. state 와 data 구분? 17 | 1. state + data = process state 18 | 2. state : select input handler 19 | 4. GenStateMachine 20 | 1. Erlang의 gen_statem을 간단히 wrap한 Elixir 라이브러리. 21 | 2. GenStateMachine은 리턴값이나 동작 방식에 대한 설명이 부족하므로, gen_statem까지 읽어야 함. 22 | 1. https://hexdocs.pm/gen_state_machine/GenStateMachine.html 23 | 2. https://erlang.org/doc/design_principles/statem.html 24 | 3. https://erlang.org/doc/man/gen_statem.html 25 | 26 | ### 검증 27 | 28 | 아래 스테이지에 해당하는 테스트만 활성화 시킨 후, (즉 stage2 에서는 stage1 은 다시 비활성화 해야함.) 29 | ``` 30 | mix test 31 | ``` 32 | 33 | # Goal 1 : State transition 34 | 35 | 36 | 37 | 1. state : locked, open 38 | 2. init state : locked 39 | 3. init data : %{code :: [1, 2], input :: []} 40 | 4. event handler: 41 | 1. locked 에서 이벤트 {:button, 3} -> {:button, 1} -> {:button, 2} 를 받으면 open으로 전이하고 input clear. 42 | 2. open 상태에서는 {:button, \_} 무시. 43 | 44 | # Goal 2 : State timeout 45 | 46 | 1. locked -> open 으로 변할때 state_timeout 5000ms 걸고, 47 | 2. state_timeout 발생시 locked 로 transition. 48 | 49 | # Goal 3 : Event timeout 50 | 51 | 52 | 53 | 1. locked 상태에서 {:button, \_} 이벤트 발생마다 timeout 5000ms 를 걸고, 54 | 2. timeout 발생시 input clear. 55 | 56 | # Goal 4 : Moore machine 57 | 58 | 1. 현재 구현은 event handler 에서 side effect(=do_unlock)가 발생하는 Mealy machine임. 59 | 2. callback_mode 를 [:state_functions, :state_enter] 로 변경하고, state진입시 발생하는 enter event에서 side effect를 발생하도록 변경. 60 | 61 | # Goal 5 : Test w/o side effect 62 | 63 | 1. 문제점 : mix test 시마다 do_lock, do_unlock이 실행되어 Locked, Unlocked 메시지가 출력된다. 64 | 2. 해결법 : definject 라이브러리를 사용. 65 | -------------------------------------------------------------------------------- /50_code_quality/50_use_gen_statem/img/code_lock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 10 | 12 | 13 | 15 | 17 | 20 | locked 21 | 22 | 23 | 24 | 26 | 28 | 31 | open 32 | 33 | 34 | 35 | 36 | 38 | 41 | {button,Button} 42 | 43 | 44 | 45 | 46 | 48 | 51 | Correct Code? 52 | 53 | 54 | 55 | 56 | 58 | 61 | do_lock() 62 | 63 | 64 | 65 | 66 | 68 | 71 | state_timeout 72 | 73 | 74 | 75 | 76 | 77 | 80 | init 81 | 82 | 83 | 84 | 86 | 87 | 89 | 90 | 91 | 93 | 94 | 96 | 97 | 99 | 102 | Y 103 | 104 | 106 | N 107 | 108 | 109 | 111 | 112 | 114 | 115 | 116 | 117 | 119 | 122 | {button,Digit} 123 | 124 | 125 | 126 | 128 | 129 | 131 | 132 | 133 | 134 | 136 | 139 | do_lock() 140 | Clear input 141 | 142 | 143 | 144 | 145 | 147 | 150 | do_unlock() 151 | Clear input 152 | state_timeout 5s 153 | 154 | 155 | 156 | 158 | 159 | 161 | 162 | 163 | 165 | 166 | 168 | 169 | 171 | 172 | 173 | 175 | 178 | Append to input 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /50_code_quality/50_use_gen_statem/img/code_lock_2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 18 | 20 | 21 | 23 | 25 | 28 | locked 29 | 30 | 31 | 32 | 34 | 36 | 39 | open 40 | 41 | 42 | 43 | 44 | 46 | 49 | {button,Button} 50 | 51 | 52 | 53 | 54 | 56 | 59 | do_unlock() 60 | state_timeout 5s 61 | 62 | 63 | 64 | 65 | 67 | 70 | do_lock() 71 | Clear input 72 | 73 | 74 | 75 | 76 | 78 | 81 | state_timeout 82 | 83 | 84 | 85 | 86 | 87 | 90 | init 91 | 92 | 93 | 94 | 96 | 97 | 99 | 100 | 101 | 103 | 104 | 106 | 107 | 108 | 110 | 111 | 113 | 114 | 115 | 117 | 118 | 120 | 121 | 122 | 123 | 125 | 128 | timeout 5s 129 | 130 | 131 | 132 | 133 | 135 | 138 | timeout 139 | 140 | 141 | 142 | 143 | 145 | 148 | Append to input 149 | 150 | 151 | 152 | 153 | 155 | 156 | 158 | 159 | 160 | 161 | 163 | 166 | Clear input 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 176 | 179 | Correct Code? 180 | 181 | 182 | 185 | N 186 | 187 | 190 | Y 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /50_code_quality/50_use_gen_statem/lib/door_lock.ex: -------------------------------------------------------------------------------- 1 | defmodule DoorLock do 2 | use GenStateMachine, callback_mode: :state_functions 3 | 4 | defmodule Data do 5 | defstruct code: [], input: [] 6 | end 7 | 8 | # interface 9 | def start_link(code) when is_list(code) do 10 | GenStateMachine.start_link(__MODULE__, code, []) 11 | end 12 | 13 | def init(code) do 14 | {:ok, :locked, %Data{code: code}} 15 | end 16 | 17 | def button(pid, button) do 18 | GenStateMachine.cast(pid, {:button, button}) 19 | end 20 | 21 | ## state callback 22 | 23 | ## actions 24 | def do_lock() do 25 | IO.puts("Locked") 26 | end 27 | 28 | def do_unlock() do 29 | IO.puts("Unlocked") 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /50_code_quality/50_use_gen_statem/lib_sol/door_lock.ex: -------------------------------------------------------------------------------- 1 | defmodule DoorLock do 2 | use GenStateMachine, callback_mode: [:state_functions, :state_enter] 3 | import Definject 4 | 5 | defmodule Data do 6 | defstruct code: [], input: [] 7 | end 8 | 9 | # interface 10 | def start_link(code) when is_list(code) do 11 | GenStateMachine.start_link(__MODULE__, code, []) 12 | end 13 | 14 | definject init(code) do 15 | {:ok, :locked, %Data{code: code}} 16 | end 17 | 18 | def button(pid, button) do 19 | GenStateMachine.cast(pid, {:button, button}) 20 | end 21 | 22 | ## state callback 23 | definject locked(:enter, _old_state, %Data{}) do 24 | __MODULE__.do_lock() 25 | :keep_state_and_data 26 | end 27 | 28 | definject locked(:timeout, _, %Data{} = data) do 29 | {:keep_state, %Data{data | input: []}} 30 | end 31 | 32 | definject locked(:cast, {:button, button}, %Data{code: code, input: input} = data) do 33 | (input ++ [button]) 34 | |> Enum.take(-length(code)) 35 | |> case do 36 | ^code -> 37 | {:next_state, :open, %Data{data | input: []}, [{:state_timeout, 5000, :lock}]} 38 | 39 | new_input -> 40 | {:keep_state, %Data{data | input: new_input}, 5000} 41 | end 42 | end 43 | 44 | definject open(:enter, _old_state, %Data{}) do 45 | __MODULE__.do_unlock() 46 | :keep_state_and_data 47 | end 48 | 49 | definject open(:state_timeout, :lock, data) do 50 | {:next_state, :locked, data} 51 | end 52 | 53 | definject open(:cast, {:button, _}, _) do 54 | :keep_state_and_data 55 | end 56 | 57 | ## actions 58 | def do_lock() do 59 | IO.puts("Locked") 60 | end 61 | 62 | def do_unlock() do 63 | IO.puts("Unlocked") 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /50_code_quality/50_use_gen_statem/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule DoorLock.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :door_lock, 7 | version: "0.1.0", 8 | elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | {:gen_state_machine, "~> 3.0"}, 25 | {:definject, "~> 1.1"} 26 | # {:dep_from_hexpm, "~> 0.3.0"}, 27 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 28 | ] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /50_code_quality/50_use_gen_statem/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "definject": {:hex, :definject, "1.1.5", "de662d70aa2999384bb891d79d383c0e56edb0a2ca43c09a5c3ffababf775b73", [:mix], [], "hexpm", "ade0015c35d7fed70f640905c5a1de704962d1765cc1802038520c74fb596dc5"}, 3 | "gen_state_machine": {:hex, :gen_state_machine, "2.1.0", "a38b0e53fad812d29ec149f0d354da5d1bc0d7222c3711f3a0bd5aa608b42992", [:mix], [], "hexpm", "ae367038808db25cee2f2c4b8d0531522ea587c4995eb6f96ee73410a60fa06b"}, 4 | } 5 | -------------------------------------------------------------------------------- /50_code_quality/50_use_gen_statem/test/door_lock_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DoorLockTest do 2 | use ExUnit.Case 3 | 4 | alias DoorLock.Data 5 | 6 | @code [1, 2] 7 | 8 | describe "Goal 1: State transition" do 9 | @describetag :pending 10 | 11 | test "init" do 12 | assert DoorLock.init(@code) == {:ok, :locked, %Data{code: @code, input: []}} 13 | end 14 | 15 | test "locked" do 16 | assert DoorLock.locked(:cast, {:button, 2}, %Data{code: @code, input: []}) == 17 | {:keep_state, %Data{code: @code, input: [2]}} 18 | 19 | assert DoorLock.locked(:cast, {:button, 1}, %Data{code: @code, input: [2]}) == 20 | {:keep_state, %Data{code: @code, input: [2, 1]}} 21 | 22 | assert DoorLock.locked(:cast, {:button, 2}, %Data{code: @code, input: [2, 1]}) == 23 | {:next_state, :open, %Data{code: @code, input: []}} 24 | end 25 | 26 | test "open" do 27 | assert DoorLock.open(:cast, {:button, 1}, %Data{code: @code, input: @code}) == 28 | :keep_state_and_data 29 | end 30 | end 31 | 32 | describe "Goal 2: State timeout" do 33 | @describetag :pending 34 | 35 | test "locked" do 36 | assert DoorLock.locked(:cast, {:button, 2}, %Data{code: @code, input: [2, 1]}) == 37 | {:next_state, :open, %Data{code: @code, input: []}, 38 | [{:state_timeout, 5000, :lock}]} 39 | end 40 | 41 | test "open" do 42 | assert DoorLock.open(:state_timeout, :lock, %Data{code: @code, input: []}) == 43 | {:next_state, :locked, %Data{code: @code, input: []}} 44 | end 45 | end 46 | 47 | describe "Goal 3: Event timeout" do 48 | @describetag :pending 49 | 50 | test "locked" do 51 | assert DoorLock.locked(:cast, {:button, 1}, %Data{code: @code, input: []}) == 52 | {:keep_state, %Data{code: @code, input: [1]}, 5000} 53 | 54 | assert DoorLock.locked(:timeout, 5000, %Data{code: @code, input: [1]}) == 55 | {:keep_state, %Data{code: @code, input: []}} 56 | end 57 | end 58 | 59 | describe "Goal 4: Convert to Moore machine from Mealy machine" do 60 | @describetag :pending 61 | 62 | test "init" do 63 | assert DoorLock.init(@code) == {:ok, :locked, %Data{code: @code, input: []}} 64 | end 65 | 66 | test "locked" do 67 | assert DoorLock.locked(:enter, :open, %Data{code: @code, input: []}) == :keep_state_and_data 68 | end 69 | 70 | test "open" do 71 | assert DoorLock.open(:enter, :locked, %Data{code: @code, input: []}) == :keep_state_and_data 72 | end 73 | end 74 | 75 | describe "Goal 5: Replace side effect on test with definject" do 76 | @describetag :pending 77 | 78 | test "init" do 79 | assert DoorLock.init(@code, %{ 80 | &DoorLock.do_lock/0 => fn -> send(self(), :do_lock) end, 81 | strict: false 82 | }) == {:ok, :locked, %Data{code: @code, input: []}} 83 | 84 | refute_receive :do_lock 85 | end 86 | 87 | test "locked" do 88 | assert DoorLock.locked(:enter, :open, %Data{code: @code, input: []}, %{ 89 | &DoorLock.do_lock/0 => fn -> send(self(), :do_lock) end 90 | }) == :keep_state_and_data 91 | 92 | assert_receive :do_lock 93 | end 94 | 95 | test "open" do 96 | assert DoorLock.open(:enter, :locked, %Data{code: @code, input: []}, %{ 97 | &DoorLock.do_unlock/0 => fn -> send(self(), :do_unlock) end 98 | }) == :keep_state_and_data 99 | 100 | assert_receive :do_unlock 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /50_code_quality/50_use_gen_statem/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.configure(exclude: [:pending]) 2 | ExUnit.start() 3 | -------------------------------------------------------------------------------- /50_code_quality/55_use_excoverage/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /50_code_quality/55_use_excoverage/.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 third-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 | learn_coverage-*.tar 24 | 25 | -------------------------------------------------------------------------------- /50_code_quality/55_use_excoverage/README.md: -------------------------------------------------------------------------------- 1 | # LearnCoverage 2 | 3 | ### Usage 4 | 5 | ``` 6 | mix coveralls.html 7 | open cover/excoveralls.html 8 | ``` 9 | -------------------------------------------------------------------------------- /50_code_quality/55_use_excoverage/lib/learn_coverage.ex: -------------------------------------------------------------------------------- 1 | defmodule LearnCoverage do 2 | def target_called do 3 | "called" |> String.to_atom() 4 | end 5 | 6 | def target_not_called do 7 | "not_called" |> String.to_atom() 8 | end 9 | 10 | def not_target_called do 11 | :not_target 12 | end 13 | 14 | def not_target_not_called do 15 | :not_target 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /50_code_quality/55_use_excoverage/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule LearnCoverage.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :learn_coverage, 7 | version: "0.1.0", 8 | elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | test_coverage: [tool: ExCoveralls], 12 | preferred_cli_env: [ 13 | coveralls: :test, 14 | "coveralls.detail": :test, 15 | "coveralls.post": :test, 16 | "coveralls.html": :test 17 | ] 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 | {:excoveralls, "~> 0.13", only: :test} 32 | # {:dep_from_hexpm, "~> 0.3.0"}, 33 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 34 | ] 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /50_code_quality/55_use_excoverage/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, 3 | "coverex": {:hex, :coverex, "1.4.15", "60fadf825a6c0439b79d1f98cdb54b6733cdd5cb1b35d15d56026c44ed15a5a8", [:mix], [{:hackney, "~> 1.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "bfde7ad7ecb83e0199902d087caf570057047aa004cfd2eafef3b42a5428103c"}, 4 | "excoveralls": {:hex, :excoveralls, "0.13.2", "5ca05099750c086f144fcf75842c363fc15d7d9c6faa7ad323d010294ced685e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1e7ed75c158808a5a8f019d3ad63a5efe482994f2f8336c0a8c77d2f0ab152ce"}, 5 | "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, 6 | "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"}, 7 | "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, 8 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 9 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 10 | "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, 11 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, 12 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 13 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"}, 14 | } 15 | -------------------------------------------------------------------------------- /50_code_quality/55_use_excoverage/test/learn_coverage_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LearnCoverageTest do 2 | use ExUnit.Case 3 | 4 | test "call" do 5 | assert LearnCoverage.target_called() 6 | assert LearnCoverage.not_target_called() 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /50_code_quality/55_use_excoverage/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /60_monitoring/60_learn_observer/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /60_monitoring/60_learn_observer/.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 third-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 | observing-*.tar 24 | 25 | -------------------------------------------------------------------------------- /60_monitoring/60_learn_observer/README.md: -------------------------------------------------------------------------------- 1 | # Observing 2 | 3 | ### Observer 4 | 5 | ``` 6 | :observer.start 7 | ``` 8 | 9 | ### Process Tree 10 | 11 | observer 의 applications 에 나타나는 process tree 는 어떤 원리로 구성되는지 실험해 보자. 12 | 13 | ### ETS 14 | 15 | observer 의 ets 에서 parent ets 를 찾아보자. 16 | 17 | ### Remote observer 18 | 19 | 이번에는 release 를 실행한 후 remote iex 로 observer 를 실행해보자. 20 | 21 | (remote observer 실행을 위해 runtime_tools 를 extra_applications 에 추가했음.) 22 | 23 | ``` 24 | $ mix release 25 | $ _build/dev/rel/observing/bin/observing daemon 26 | 27 | $ iex --cookie (cat **/COOKIE) --sname dbg 28 | 29 | iex> Node.connect(:"observing@jechol-mbp15a") 30 | iex> :observer.start 31 | 32 | # 30 초 정도 기다리면, 메뉴의 Nodes 에서 observing 으로 전환 가능. 33 | 34 | _build/dev/rel/observing/bin/observing stop 35 | ``` 36 | -------------------------------------------------------------------------------- /60_monitoring/60_learn_observer/lib/observing.ex: -------------------------------------------------------------------------------- 1 | defmodule Observing do 2 | @moduledoc """ 3 | Documentation for `Observing`. 4 | """ 5 | 6 | @doc """ 7 | Hello world. 8 | 9 | ## Examples 10 | 11 | iex> Observing.hello() 12 | :world 13 | 14 | """ 15 | def hello do 16 | :world 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /60_monitoring/60_learn_observer/lib/observing/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Observing.Application do 2 | # See https://hexdocs.pm/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 | children = [ 10 | # Starts a worker by calling: Observing.Observing.Worker.start_link(arg) 11 | Supervisor.child_spec(Observing.Worker, id: Anonymous), 12 | Supervisor.child_spec({Observing.Worker, [name: Registered]}, id: Observing.Registered), 13 | {Observing.ParentWorker, [name: Parent]}, 14 | Observing.ChildSup 15 | ] 16 | 17 | # See https://hexdocs.pm/elixir/Supervisor.html 18 | # for other strategies and supported options 19 | opts = [strategy: :one_for_one, name: Observing.Supervisor] 20 | Supervisor.start_link(children, opts) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /60_monitoring/60_learn_observer/lib/observing/child_sup.ex: -------------------------------------------------------------------------------- 1 | defmodule Observing.ChildSup do 2 | use Supervisor 3 | 4 | def start_link([]) do 5 | Supervisor.start_link(__MODULE__, [], name: ChildSup) 6 | end 7 | 8 | @impl true 9 | def init([]) do 10 | children = [ 11 | Supervisor.child_spec(Observing.Worker, id: Observing.Worker1), 12 | Supervisor.child_spec(Observing.Worker, id: Observing.Worker2) 13 | ] 14 | 15 | Supervisor.init(children, strategy: :one_for_one) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /60_monitoring/60_learn_observer/lib/observing/parent_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Observing.ParentWorker do 2 | use GenServer 3 | 4 | def start_link(opts) do 5 | GenServer.start_link(__MODULE__, [], opts) 6 | end 7 | 8 | def init([]) do 9 | # ETS 10 | :ets.new(:parent, [:public, :named_table]) 11 | :ets.insert(:parent, {:message, "check this row in observer"}) 12 | 13 | # link 14 | # {:ok, pid} = Observing.Worker.start_link(name: LinkedChild) 15 | # send(pid, :check_this_message_in_observer) 16 | 17 | # link 18 | linked = spawn_link(&wait/0) 19 | Process.register(linked, Linked) 20 | 21 | # monitor 22 | {monitored, _ref} = spawn_monitor(&wait/0) 23 | Process.register(monitored, Monitored) 24 | 25 | {:ok, []} 26 | end 27 | 28 | def wait() do 29 | receive do 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /60_monitoring/60_learn_observer/lib/observing/worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Observing.Worker do 2 | use GenServer 3 | 4 | def start_link(opts) do 5 | GenServer.start_link(__MODULE__, [], opts) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /60_monitoring/60_learn_observer/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Observing.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :observing, 7 | version: "0.1.0", 8 | elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger, :runtime_tools], 18 | mod: {Observing.Application, []} 19 | ] 20 | end 21 | 22 | # Run "mix help deps" to learn about dependencies. 23 | defp deps do 24 | [ 25 | # {:dep_from_hexpm, "~> 0.3.0"}, 26 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 27 | ] 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /60_monitoring/60_learn_observer/test/observing_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ObservingTest do 2 | use ExUnit.Case 3 | doctest Observing 4 | 5 | test "greets the world" do 6 | assert Observing.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /60_monitoring/60_learn_observer/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /90_performance/95_use_agent_ets/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /90_performance/95_use_agent_ets/.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 third-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 | agent_ets-*.tar 24 | 25 | -------------------------------------------------------------------------------- /90_performance/95_use_agent_ets/README.md: -------------------------------------------------------------------------------- 1 | ## Agent vs ETS 2 | 3 | ### 구현 4 | 5 | Agent 와 ETS 를 사용하여 cache 를 구현해보자. 6 | 7 | (ETS 는 read_concurrency: true, write_concurrency: true 를 적용할 것.) 8 | 9 | ### 검증 10 | 11 | ``` 12 | mix test 13 | ``` 14 | 15 | ### 벤치마크 16 | 17 | (agent_cache.ex, ets_cache.ex 를 구현 후에만 benchmark 가능. lib_sol/ 참고) 18 | 19 | ``` 20 | mix run bench/cache_read_bench.exs 21 | mix run bench/cache_write_bench.exs 22 | ``` 23 | 24 | counter 예제에 대한 ETS와 Agent의 성능을 비교해보고, 어떤 솔루션이 더 적합한지 생각해보자. 25 | -------------------------------------------------------------------------------- /90_performance/95_use_agent_ets/bench/cache_read_bench.exs: -------------------------------------------------------------------------------- 1 | schedulers = :erlang.system_info(:schedulers) 2 | repeat = 10000 3 | keys = 1000 4 | 5 | bench_read = fn {cache_mod, cache, tasks} -> 6 | task_repeat = repeat |> div(tasks) 7 | 8 | 1..tasks 9 | |> Task.async_stream(fn _ -> 10 | 1..task_repeat 11 | |> Enum.each(fn _ -> cache_mod.get(cache, :rand.uniform(keys)) end) 12 | end) 13 | |> Stream.run() 14 | end 15 | 16 | setup_cache = fn cache_mod, tasks -> 17 | kv = Enum.zip(1..keys, StreamData.term() |> Enum.take(keys)) 18 | 19 | {:ok, cache} = cache_mod.start_link() 20 | 21 | kv 22 | |> Enum.each(fn {key, value} -> 23 | cache_mod.set(cache, key, value) 24 | end) 25 | 26 | {cache_mod, cache, tasks} 27 | end 28 | 29 | Benchee.run( 30 | [AgentCache, EtsCache] 31 | |> Enum.map(fn cache_mod -> 32 | {cache_mod, {bench_read, before_scenario: fn tasks -> setup_cache.(cache_mod, tasks) end}} 33 | end), 34 | inputs: [1, schedulers] |> Enum.map(fn tasks -> {%{tasks: tasks} |> inspect(), tasks} end) 35 | ) 36 | -------------------------------------------------------------------------------- /90_performance/95_use_agent_ets/bench/cache_write_bench.exs: -------------------------------------------------------------------------------- 1 | schedulers = :erlang.system_info(:schedulers) 2 | repeat = 10000 3 | 4 | bench_write = fn cache_mod, input -> 5 | {:ok, cache} = cache_mod.start_link() 6 | 7 | input 8 | |> Task.async_stream(fn task_input -> 9 | task_input 10 | |> Enum.each(fn {k, v} -> cache_mod.set(cache, k, v) end) 11 | end) 12 | |> Stream.run() 13 | end 14 | 15 | Benchee.run( 16 | [AgentCache, EtsCache] 17 | |> Enum.map(fn cache_mod -> 18 | {cache_mod, fn input -> bench_write.(cache_mod, input) end} 19 | end), 20 | inputs: 21 | [{1, 0.1}, {1, 0.5}, {schedulers, 0.1}, {schedulers, 0.5}] 22 | |> Enum.map(fn setup = {tasks, conflict} -> 23 | {%{tasks: tasks, conflict: conflict} |> inspect(), setup} 24 | end), 25 | before_scenario: fn {tasks, conflict} -> 26 | import StreamData 27 | 28 | task_repeat = repeat |> div(tasks) 29 | keys = ((repeat - 1) * (1 - conflict)) |> trunc() |> Kernel.+(1) 30 | 31 | 1..tasks 32 | |> Enum.map(fn _ -> 33 | {integer(1..keys), term() |> resize(10)} 34 | |> tuple() 35 | |> Enum.take(task_repeat) 36 | end) 37 | end 38 | ) 39 | -------------------------------------------------------------------------------- /90_performance/95_use_agent_ets/lib/agent_cache.ex: -------------------------------------------------------------------------------- 1 | defmodule AgentCache do 2 | @behaviour Cache 3 | 4 | def start_link() do 5 | end 6 | 7 | def get(cache, key) do 8 | end 9 | 10 | def set(cache, key, value) do 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /90_performance/95_use_agent_ets/lib/cache.ex: -------------------------------------------------------------------------------- 1 | defmodule Cache do 2 | @type cache :: reference() | pid() 3 | 4 | @callback start_link() :: {:ok, cache()} 5 | @callback get(cache(), any()) :: {:ok, any()} | :error 6 | @callback set(cache(), any(), any()) :: :ok 7 | end 8 | -------------------------------------------------------------------------------- /90_performance/95_use_agent_ets/lib/ets_cache.ex: -------------------------------------------------------------------------------- 1 | defmodule EtsCache do 2 | @behaviour Cache 3 | 4 | def start_link() do 5 | end 6 | 7 | def get(cache, key) do 8 | end 9 | 10 | def set(cache, key, value) do 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /90_performance/95_use_agent_ets/lib_sol/agent_cache.ex: -------------------------------------------------------------------------------- 1 | defmodule AgentCache do 2 | @behaviour Cache 3 | 4 | def start_link() do 5 | Agent.start_link(fn -> %{} end) 6 | end 7 | 8 | def get(cache, key) do 9 | Agent.get(cache, fn map -> map |> Map.fetch(key) end) 10 | end 11 | 12 | def set(cache, key, value) do 13 | Agent.update(cache, fn map -> map |> Map.put(key, value) end) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /90_performance/95_use_agent_ets/lib_sol/ets_cache.ex: -------------------------------------------------------------------------------- 1 | defmodule EtsCache do 2 | @behaviour Cache 3 | 4 | def start_link() do 5 | {:ok, :ets.new(:ets_cache, [:public, write_concurrency: true, read_concurrency: true])} 6 | end 7 | 8 | def get(cache, key) do 9 | cache 10 | |> :ets.lookup(key) 11 | |> case do 12 | [{^key, value}] -> {:ok, value} 13 | [] -> :error 14 | end 15 | end 16 | 17 | def set(cache, key, value) do 18 | true = cache |> :ets.insert({key, value}) 19 | :ok 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /90_performance/95_use_agent_ets/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule AgentEts.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :agent_ets, 7 | version: "0.1.0", 8 | elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | {:benchee, "~> 1.0", only: :dev}, 25 | {:stream_data, "~> 0.5", only: [:dev, :test]} 26 | # {:dep_from_hexpm, "~> 0.3.0"}, 27 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 28 | ] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /90_performance/95_use_agent_ets/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm", "3ad58ae787e9c7c94dd7ceda3b587ec2c64604563e049b2a0e8baafae832addb"}, 3 | "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, 4 | "stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"}, 5 | } 6 | -------------------------------------------------------------------------------- /90_performance/95_use_agent_ets/test/cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AgentCacheTest do 2 | use ExUnit.Case, async: false 3 | 4 | for cache <- [AgentCache, EtsCache] do 5 | @cache cache 6 | 7 | describe "#{@cache}" do 8 | test ":error for non-exist" do 9 | {:ok, cache} = @cache.start_link() 10 | assert @cache.get(cache, :never_cache) == :error 11 | end 12 | 13 | test "get-set" do 14 | {:ok, cache} = @cache.start_link() 15 | 16 | Task.async(fn -> 17 | @cache.set(cache, :a, :first) 18 | @cache.set(cache, :a, :second) 19 | @cache.set(cache, :b, :third) 20 | 21 | assert @cache.get(cache, :a) == {:ok, :second} 22 | assert @cache.get(cache, :b) == {:ok, :third} 23 | end) 24 | |> Task.await() 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /90_performance/95_use_agent_ets/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elixir 코딩 클럽! 2 | 3 | Elixir를 배우는데 알고리즘을 코딩해보는 것은 의미가 없습니다. 4 | 5 | Actor 모델과 같은 OTP만의 특징을 깊게 이해할수 있도록 6 | 1. 라이브러리 함수를 직접 구현해보거나, 7 | 2. 라이브러리 기능을 사용하여 toy 프로그램을 만들어 볼수 있도록 8 | 9 | 작은 문제를 만들어서 공유하는 repo입니다. 10 | 11 | 12 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | --------------------------------------------------------------------------------