├── .gitignore ├── 05_redis_pool ├── test │ ├── test_helper.exs │ └── redis_pool_test.exs ├── playground │ ├── test │ │ ├── test_helper.exs │ │ └── playground_test.exs │ ├── .formatter.exs │ ├── mix.lock │ ├── lib │ │ └── playground │ │ │ └── application.ex │ ├── mix.exs │ └── .gitignore ├── .formatter.exs ├── mix.lock ├── lib │ └── redis_pool.ex ├── mix.exs └── .gitignore ├── 07_app_env ├── test │ ├── test_helper.exs │ └── app_env_test.exs ├── .formatter.exs ├── config │ └── config.exs ├── lib │ ├── app_env.ex │ └── app_env │ │ ├── application.ex │ │ └── worker.ex ├── .gitignore └── mix.exs ├── 02_terms_cache ├── test │ ├── test_helper.exs │ ├── terms_cache_test.exs │ └── ets_terms_cache_test.exs ├── .formatter.exs ├── lib │ ├── terms_cache.ex │ └── ets_terms_cache.ex ├── .gitignore └── mix.exs ├── 03_redis_client ├── test │ ├── test_helper.exs │ ├── redis_client_test.exs │ └── redis_client_genstatem_test.exs ├── .formatter.exs ├── docker-compose.yml ├── lib │ ├── redis_client.ex │ ├── redis_client_genstatem.ex │ └── redis_client │ │ └── protocol.ex ├── .gitignore └── mix.exs ├── handrolled_genserver ├── test │ ├── test_helper.exs │ └── stack_test.exs ├── .formatter.exs ├── .gitignore ├── mix.exs └── lib │ ├── stack.ex │ └── handrolled_genserver.ex ├── handrolled_supervisor ├── test │ ├── test_helper.exs │ └── handrolled_supervisor_test.exs ├── .formatter.exs ├── .gitignore ├── mix.exs └── lib │ └── handrolled_supervisor.ex ├── 01_concurrency_foundations ├── test │ ├── test_helper.exs │ ├── parallel_enum_test.exs │ ├── shared_state_test.exs │ └── async_test.exs ├── lib │ ├── async.ex │ ├── parallel_enum.ex │ └── shared_state.ex ├── .formatter.exs ├── .gitignore └── mix.exs ├── 04_supervision_playground ├── test │ ├── test_helper.exs │ └── playground_test.exs ├── .formatter.exs ├── lib │ └── playground │ │ ├── application.ex │ │ └── worker.ex ├── .gitignore └── mix.exs ├── 06_phoenix_observer ├── priv │ ├── repo │ │ ├── migrations │ │ │ └── .formatter.exs │ │ └── seeds.exs │ └── gettext │ │ ├── en │ │ └── LC_MESSAGES │ │ │ └── errors.po │ │ └── errors.pot ├── test │ ├── test_helper.exs │ ├── phoenix_observer_web │ │ └── views │ │ │ └── error_view_test.exs │ └── support │ │ ├── channel_case.ex │ │ ├── conn_case.ex │ │ └── data_case.ex ├── docker-compose.yml ├── lib │ ├── phoenix_observer │ │ ├── repo.ex │ │ └── application.ex │ ├── phoenix_observer.ex │ ├── phoenix_observer_web │ │ ├── views │ │ │ ├── error_view.ex │ │ │ └── error_helpers.ex │ │ ├── gettext.ex │ │ ├── router.ex │ │ ├── channels │ │ │ └── user_socket.ex │ │ ├── endpoint.ex │ │ └── telemetry.ex │ └── phoenix_observer_web.ex ├── .formatter.exs ├── config │ ├── test.exs │ ├── config.exs │ ├── prod.secret.exs │ ├── dev.exs │ └── prod.exs ├── .gitignore ├── mix.exs └── mix.lock └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /05_redis_pool/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /07_app_env/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /02_terms_cache/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /03_redis_client/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /handrolled_genserver/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /handrolled_supervisor/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /01_concurrency_foundations/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /04_supervision_playground/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /05_redis_pool/playground/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /01_concurrency_foundations/lib/async.ex: -------------------------------------------------------------------------------- 1 | defmodule Async do 2 | end 3 | -------------------------------------------------------------------------------- /04_supervision_playground/test/playground_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PlaygroundTest do 2 | use ExUnit.Case 3 | end 4 | -------------------------------------------------------------------------------- /06_phoenix_observer/priv/repo/migrations/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto_sql], 3 | inputs: ["*.exs"] 4 | ] 5 | -------------------------------------------------------------------------------- /06_phoenix_observer/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Ecto.Adapters.SQL.Sandbox.mode(PhoenixObserver.Repo, :manual) 3 | -------------------------------------------------------------------------------- /07_app_env/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /02_terms_cache/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /03_redis_client/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /05_redis_pool/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /handrolled_genserver/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /04_supervision_playground/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /05_redis_pool/playground/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /handrolled_supervisor/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /01_concurrency_foundations/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /06_phoenix_observer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | postgres: 5 | image: "postgres" 6 | ports: 7 | - "5432:5432" 8 | -------------------------------------------------------------------------------- /07_app_env/config/config.exs: -------------------------------------------------------------------------------- 1 | import Mix.Config 2 | 3 | config :app_env, 4 | runtime_key: :runtime_value, 5 | compile_time_key: :compile_time_value 6 | -------------------------------------------------------------------------------- /06_phoenix_observer/lib/phoenix_observer/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixObserver.Repo do 2 | use Ecto.Repo, 3 | otp_app: :phoenix_observer, 4 | adapter: Ecto.Adapters.Postgres 5 | end 6 | -------------------------------------------------------------------------------- /07_app_env/test/app_env_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AppEnvTest do 2 | use ExUnit.Case 3 | doctest AppEnv 4 | 5 | test "greets the world" do 6 | assert AppEnv.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /06_phoenix_observer/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto, :phoenix], 3 | inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | subdirectories: ["priv/*/migrations"] 5 | ] 6 | -------------------------------------------------------------------------------- /03_redis_client/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | redis: 5 | image: "redis:alpine" 6 | ports: 7 | - "6379:6379" 8 | sysctls: 9 | net.core.somaxconn: 1024 10 | -------------------------------------------------------------------------------- /05_redis_pool/playground/test/playground_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PlaygroundTest do 2 | use ExUnit.Case 3 | doctest Playground 4 | 5 | test "greets the world" do 6 | assert Playground.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /01_concurrency_foundations/lib/parallel_enum.ex: -------------------------------------------------------------------------------- 1 | defmodule ParallelEnum do 2 | @compile {:no_warn_undefined, Async} 3 | 4 | def each(_enum, _fun) do 5 | raise "not implemented yet" 6 | end 7 | 8 | def map(_enum, _fun) do 9 | raise "not implemented yet" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /07_app_env/lib/app_env.ex: -------------------------------------------------------------------------------- 1 | defmodule AppEnv do 2 | @moduledoc """ 3 | Documentation for `AppEnv`. 4 | """ 5 | 6 | @doc """ 7 | Hello world. 8 | 9 | ## Examples 10 | 11 | iex> AppEnv.hello() 12 | :world 13 | 14 | """ 15 | def hello do 16 | :world 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /06_phoenix_observer/lib/phoenix_observer.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixObserver do 2 | @moduledoc """ 3 | PhoenixObserver keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /06_phoenix_observer/priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # PhoenixObserver.Repo.insert!(%PhoenixObserver.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /01_concurrency_foundations/lib/shared_state.ex: -------------------------------------------------------------------------------- 1 | defmodule SharedState do 2 | ## Public API 3 | 4 | @spec start(term()) :: pid() 5 | def start(_initial_state) do 6 | raise "not implemented yet" 7 | end 8 | 9 | @spec get(pid()) :: term() 10 | def get(_pid) do 11 | raise "not implemented yet" 12 | end 13 | 14 | @spec update(pid(), (term() -> term())) :: :ok 15 | def update(_pid, update_fun) when is_function(update_fun, 1) do 16 | raise "not implemented yet" 17 | end 18 | 19 | ## Process loop 20 | end 21 | -------------------------------------------------------------------------------- /03_redis_client/lib/redis_client.ex: -------------------------------------------------------------------------------- 1 | defmodule RedisClient do 2 | use GenServer 3 | 4 | defstruct [:socket] 5 | 6 | def start_link(opts) when is_list(opts) do 7 | GenServer.start_link(__MODULE__, opts) 8 | end 9 | 10 | def command(_pid, commands) when is_list(commands) do 11 | raise "not implemented yet" 12 | end 13 | 14 | @impl true 15 | def init(opts) do 16 | _host = Keyword.fetch!(opts, :host) 17 | _port = Keyword.fetch!(opts, :port) 18 | 19 | raise "not implemented yet" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /04_supervision_playground/lib/playground/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Playground.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 | 11 | # See https://hexdocs.pm/elixir/Supervisor.html 12 | # for other strategies and supported options 13 | opts = [strategy: :one_for_one, name: Playground.Supervisor] 14 | Supervisor.start_link(children, opts) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /02_terms_cache/lib/terms_cache.ex: -------------------------------------------------------------------------------- 1 | defmodule TermsCache do 2 | use GenServer 3 | 4 | @name __MODULE__ 5 | @evict_interval 5_000 6 | 7 | @spec start_link(keyword()) :: GenServer.on_start() 8 | def start_link([] = _opts) do 9 | raise "not implemented yet" 10 | end 11 | 12 | @spec put(term(), term(), non_neg_integer()) :: :ok 13 | def put(_key, _value, _ttl \\ :infinity) do 14 | raise "not implemented yet" 15 | end 16 | 17 | @spec get(term()) :: term() 18 | def get(_key) do 19 | raise "not implemented yet" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /05_redis_pool/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "redix": {:hex, :redix, "0.11.1", "cc3c8c938f9fcc98f31162d088131ffbebbe68e5b21c8a3dd4967f8812f8a97a", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9593ab0f6f39efbfb2d49634ee215297723ff614bba89fdfa7414f3cbb74a17e"}, 3 | "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, 4 | } 5 | -------------------------------------------------------------------------------- /05_redis_pool/playground/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "redix": {:hex, :redix, "0.11.1", "cc3c8c938f9fcc98f31162d088131ffbebbe68e5b21c8a3dd4967f8812f8a97a", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9593ab0f6f39efbfb2d49634ee215297723ff614bba89fdfa7414f3cbb74a17e"}, 3 | "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, 4 | } 5 | -------------------------------------------------------------------------------- /07_app_env/lib/app_env/application.ex: -------------------------------------------------------------------------------- 1 | defmodule AppEnv.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 | {AppEnv.Worker, name: :app_env_worker} 11 | ] 12 | 13 | # See https://hexdocs.pm/elixir/Supervisor.html 14 | # for other strategies and supported options 15 | opts = [strategy: :one_for_one, name: AppEnv.Supervisor] 16 | Supervisor.start_link(children, opts) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /05_redis_pool/lib/redis_pool.ex: -------------------------------------------------------------------------------- 1 | defmodule RedisPool do 2 | use Supervisor 3 | 4 | @spec start_link(keyword()) :: {:ok, tuple()} 5 | def start_link(opts) do 6 | pool_name = Keyword.fetch!(opts, :name) 7 | connections = Keyword.get(opts, :connections, 5) 8 | connection_options = Keyword.get(opts, :connection_options, []) 9 | 10 | raise "we need to start the supervisor" 11 | end 12 | 13 | @spec command(atom(), Redix.command()) :: {:ok, term()} | {:error, term()} 14 | def command(pool_name, command) when is_list(command) do 15 | raise "not implemented yet" 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /05_redis_pool/playground/lib/playground/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Playground.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 | {RedisPool, name: PlaygroundPool} 11 | ] 12 | 13 | # See https://hexdocs.pm/elixir/Supervisor.html 14 | # for other strategies and supported options 15 | opts = [strategy: :one_for_one, name: Playground.Supervisor] 16 | Supervisor.start_link(children, opts) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /05_redis_pool/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule RedisPool.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :redis_pool, 7 | version: "0.1.0", 8 | elixir: "~> 1.9", 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 | {:redix, "~> 0.11"} 25 | ] 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /06_phoenix_observer/test/phoenix_observer_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixObserverWeb.ErrorViewTest do 2 | use PhoenixObserverWeb.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.json" do 8 | assert render(PhoenixObserverWeb.ErrorView, "404.json", []) == %{errors: %{detail: "Not Found"}} 9 | end 10 | 11 | test "renders 500.json" do 12 | assert render(PhoenixObserverWeb.ErrorView, "500.json", []) == 13 | %{errors: %{detail: "Internal Server Error"}} 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /03_redis_client/test/redis_client_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RedisClientTest do 2 | use ExUnit.Case 3 | 4 | @tag :skip 5 | test "start connection and PING" do 6 | assert {:ok, conn} = RedisClient.start_link(host: "localhost", port: 6379) 7 | 8 | assert RedisClient.command(conn, ["PING"]) == "PONG" 9 | end 10 | 11 | @tag :skip 12 | test "GET + SET" do 13 | assert {:ok, conn} = RedisClient.start_link(host: "localhost", port: 6379) 14 | 15 | assert RedisClient.command(conn, ["SET", "mykey", "myvalue"]) == "OK" 16 | assert RedisClient.command(conn, ["GET", "mykey"]) == "myvalue" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /02_terms_cache/lib/ets_terms_cache.ex: -------------------------------------------------------------------------------- 1 | defmodule ETSTermsCache do 2 | use GenServer 3 | 4 | @name __MODULE__ 5 | @ets __MODULE__ 6 | @evict_interval 5_000 7 | 8 | # State 9 | defstruct [] 10 | 11 | @spec start_link(keyword()) :: GenServer.on_start() 12 | def start_link([] = _opts) do 13 | raise "not implemented yet" 14 | end 15 | 16 | @spec put(term(), term(), non_neg_integer()) :: :ok 17 | def put(key, value, ttl \\ :infinity) do 18 | raise "not implemented yet" 19 | end 20 | 21 | @spec get(term()) :: term() 22 | def get(key) do 23 | raise "not implemented yet" 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /05_redis_pool/playground/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Playground.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :playground, 7 | version: "0.1.0", 8 | elixir: "~> 1.9", 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: {Playground.Application, []} 19 | ] 20 | end 21 | 22 | # Run "mix help deps" to learn about dependencies. 23 | defp deps do 24 | [ 25 | {:redis_pool, path: ".."} 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /06_phoenix_observer/lib/phoenix_observer_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixObserverWeb.ErrorView do 2 | use PhoenixObserverWeb, :view 3 | 4 | # If you want to customize a particular status code 5 | # for a certain format, you may uncomment below. 6 | # def render("500.json", _assigns) do 7 | # %{errors: %{detail: "Internal Server Error"}} 8 | # end 9 | 10 | # By default, Phoenix returns the status message from 11 | # the template name. For example, "404.json" becomes 12 | # "Not Found". 13 | def template_not_found(template, _assigns) do 14 | %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}} 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /04_supervision_playground/lib/playground/worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Playground.Worker do 2 | use GenServer 3 | 4 | require Logger 5 | 6 | @spec start_link(keyword()) :: GenServer.on_start() 7 | def start_link(opts) do 8 | name = Keyword.fetch!(opts, :name) 9 | GenServer.start_link(__MODULE__, opts, name: name) 10 | end 11 | 12 | @impl true 13 | def init(opts) do 14 | _ = Logger.info("Starting worker #{inspect(opts[:name])}") 15 | Process.flag(:trap_exit, true) 16 | {:ok, opts[:name]} 17 | end 18 | 19 | @impl true 20 | def terminate(reason, name) do 21 | _ = Logger.info("Terminating worker #{inspect(name)} with reason: #{inspect(reason)}") 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /07_app_env/.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 | app_env-*.tar 24 | 25 | -------------------------------------------------------------------------------- /03_redis_client/test/redis_client_genstatem_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RedisClientGenStatemTest do 2 | use ExUnit.Case 3 | 4 | @tag :skip 5 | test "start connection and PING" do 6 | assert {:ok, conn} = RedisClientGenStatem.start_link(host: "localhost", port: 6379) 7 | 8 | assert RedisClientGenStatem.command(conn, ["PING"]) == {:ok, "PONG"} 9 | end 10 | 11 | @tag :skip 12 | test "GET + SET" do 13 | assert {:ok, conn} = RedisClientGenStatem.start_link(host: "localhost", port: 6379) 14 | 15 | assert RedisClientGenStatem.command(conn, ["SET", "mykey", "myvalue"]) == {:ok, "OK"} 16 | assert RedisClientGenStatem.command(conn, ["GET", "mykey"]) == {:ok, "myvalue"} 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /05_redis_pool/.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 | redis_pool-*.tar 24 | 25 | -------------------------------------------------------------------------------- /02_terms_cache/.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 | terms_cache-*.tar 24 | 25 | -------------------------------------------------------------------------------- /03_redis_client/.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 | redis_client-*.tar 24 | 25 | -------------------------------------------------------------------------------- /05_redis_pool/playground/.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 | playground-*.tar 24 | 25 | -------------------------------------------------------------------------------- /02_terms_cache/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule TermsCache.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :terms_cache, 7 | version: "0.1.0", 8 | elixir: "~> 1.9", 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 | -------------------------------------------------------------------------------- /04_supervision_playground/.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 | playground-*.tar 24 | 25 | -------------------------------------------------------------------------------- /03_redis_client/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule RedisClient.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :redis_client, 7 | version: "0.1.0", 8 | elixir: "~> 1.9", 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 | -------------------------------------------------------------------------------- /handrolled_genserver/.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 | handrolled_genserver-*.tar 24 | 25 | -------------------------------------------------------------------------------- /handrolled_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 | handrolled_supervisor-*.tar 24 | 25 | -------------------------------------------------------------------------------- /01_concurrency_foundations/.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 | concurrency_foundations-*.tar 24 | 25 | -------------------------------------------------------------------------------- /handrolled_genserver/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule HandrolledGenserver.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :handrolled_genserver, 7 | version: "0.1.0", 8 | elixir: "~> 1.11-dev", 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 | -------------------------------------------------------------------------------- /handrolled_supervisor/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule HandrolledSupervisor.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :handrolled_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 | -------------------------------------------------------------------------------- /01_concurrency_foundations/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ConcurrencyFoundations.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :concurrency_foundations, 7 | version: "0.1.0", 8 | elixir: "~> 1.9", 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 | -------------------------------------------------------------------------------- /03_redis_client/lib/redis_client_genstatem.ex: -------------------------------------------------------------------------------- 1 | defmodule RedisClientGenStatem do 2 | @behaviour :gen_statem 3 | 4 | require Logger 5 | 6 | defstruct [:host, :port, :socket, :requests] 7 | 8 | @spec start_link(keyword()) :: :gen_statem.start_ret() 9 | def start_link(opts) when is_list(opts) do 10 | :gen_statem.start_link(__MODULE__, opts, []) 11 | end 12 | 13 | @spec command(pid(), [String.t()]) :: {:ok, term()} | {:error, term()} 14 | def command(_pid, commands) when is_list(commands) do 15 | raise "not implemented yet" 16 | end 17 | 18 | @impl true 19 | def callback_mode, do: :state_functions 20 | 21 | ## States 22 | 23 | @impl true 24 | def init(_opts) do 25 | raise "not implemented yet" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /07_app_env/lib/app_env/worker.ex: -------------------------------------------------------------------------------- 1 | defmodule AppEnv.Worker do 2 | use GenServer 3 | 4 | require Logger 5 | 6 | @compile_time_env Application.get_all_env(:app_env) 7 | IO.inspect(@compile_time_env, label: "@compile_time_env") 8 | 9 | @spec start_link(keyword()) :: GenServer.on_start() 10 | def start_link(opts) do 11 | Application.put_env(:app_env, :runtime_key, :new_runtime_value) 12 | name = Keyword.fetch!(opts, :name) 13 | GenServer.start_link(__MODULE__, opts, name: name) 14 | end 15 | 16 | @impl true 17 | def init(opts) do 18 | _ = Logger.info("Starting worker: #{inspect(opts[:name])}") 19 | IO.inspect(Application.get_all_env(:app_env), label: "Env when starting worker") 20 | {:ok, :no_state} 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /04_supervision_playground/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Playground.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :playground, 7 | version: "0.1.0", 8 | elixir: "~> 1.9", 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: {Playground.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 | -------------------------------------------------------------------------------- /handrolled_genserver/test/stack_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HandrolledGenserverTest do 2 | use ExUnit.Case 3 | 4 | import ExUnit.CaptureLog 5 | 6 | test "start_link + push + pop" do 7 | {:ok, pid} = Stack.start_link([]) 8 | 9 | assert Stack.push(pid, 1) == :ok 10 | assert Stack.push(pid, 2) == :ok 11 | assert Stack.push(pid, 3) == :ok 12 | 13 | assert Stack.pop(pid) == 3 14 | assert Stack.pop(pid) == 2 15 | end 16 | 17 | test "sending an unknown message to the stack process logs an errors" do 18 | {:ok, pid} = Stack.start_link([]) 19 | 20 | log = 21 | capture_log(fn -> 22 | send(pid, :unknown_message) 23 | Process.sleep(100) 24 | end) 25 | 26 | assert log =~ "Received unknown message: :unknown_message" 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /07_app_env/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule AppEnv.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :app_env, 7 | version: "0.1.0", 8 | elixir: "~> 1.11-dev", 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: {AppEnv.Application, []}, 19 | env: [mix_exs_key: :mix_exs_value] 20 | ] 21 | end 22 | 23 | # Run "mix help deps" to learn about dependencies. 24 | defp deps do 25 | [ 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 | -------------------------------------------------------------------------------- /02_terms_cache/test/terms_cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TermsCacheTest do 2 | use ExUnit.Case 3 | 4 | @tag :skip 5 | test "put + get" do 6 | {:ok, _pid} = TermsCache.start_link(_opts = []) 7 | 8 | assert :ok = TermsCache.put(:andrea, "Andrea Leopardi") 9 | 10 | assert TermsCache.get(:andrea) == "Andrea Leopardi" 11 | assert TermsCache.get(:jose) == nil 12 | end 13 | 14 | @tag :skip 15 | test "put with TTL" do 16 | {:ok, _pid} = TermsCache.start_link(_opts = []) 17 | key = :erlang.md5("https://elixir-lang.org") 18 | value = :crypto.strong_rand_bytes(100) 19 | 20 | assert :ok = TermsCache.put(key, value, 3000) 21 | 22 | assert TermsCache.get(key) == value 23 | 24 | Process.sleep(5_500) 25 | 26 | assert TermsCache.get(key) == nil 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /06_phoenix_observer/config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Configure your database 4 | # 5 | # The MIX_TEST_PARTITION environment variable can be used 6 | # to provide built-in test partitioning in CI environment. 7 | # Run `mix help test` for more information. 8 | config :phoenix_observer, PhoenixObserver.Repo, 9 | username: "postgres", 10 | password: "postgres", 11 | database: "phoenix_observer_test#{System.get_env("MIX_TEST_PARTITION")}", 12 | hostname: "localhost", 13 | pool: Ecto.Adapters.SQL.Sandbox 14 | 15 | # We don't run a server during test. If one is required, 16 | # you can enable the server option below. 17 | config :phoenix_observer, PhoenixObserverWeb.Endpoint, 18 | http: [port: 4002], 19 | server: false 20 | 21 | # Print only warnings and errors during test 22 | config :logger, level: :warn 23 | -------------------------------------------------------------------------------- /02_terms_cache/test/ets_terms_cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ETSTermsCacheTest do 2 | use ExUnit.Case 3 | 4 | @tag :skip 5 | test "put + get" do 6 | {:ok, _pid} = ETSTermsCache.start_link(_opts = []) 7 | 8 | assert :ok = ETSTermsCache.put(:andrea, "Andrea Leopardi") 9 | 10 | assert ETSTermsCache.get(:andrea) == "Andrea Leopardi" 11 | assert ETSTermsCache.get(:jose) == nil 12 | end 13 | 14 | @tag :skip 15 | test "put with TTL" do 16 | {:ok, _pid} = ETSTermsCache.start_link(_opts = []) 17 | key = :erlang.md5("https://elixir-lang.org") 18 | value = :crypto.strong_rand_bytes(100) 19 | 20 | assert :ok = ETSTermsCache.put(key, value, 3000) 21 | 22 | assert ETSTermsCache.get(key) == value 23 | 24 | Process.sleep(5_500) 25 | 26 | assert ETSTermsCache.get(key) == nil 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /06_phoenix_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 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | phoenix_observer-*.tar 24 | 25 | # Since we are building assets from assets/, 26 | # we ignore priv/static. You may want to comment 27 | # this depending on your deployment strategy. 28 | /priv/static/ 29 | -------------------------------------------------------------------------------- /06_phoenix_observer/lib/phoenix_observer_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixObserverWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import PhoenixObserverWeb.Gettext 9 | 10 | # Simple translation 11 | gettext("Here is the string to translate") 12 | 13 | # Plural translation 14 | ngettext("Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3) 17 | 18 | # Domain-based translation 19 | dgettext("errors", "Here is the error message to translate") 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :phoenix_observer 24 | end 25 | -------------------------------------------------------------------------------- /handrolled_genserver/lib/stack.ex: -------------------------------------------------------------------------------- 1 | defmodule Stack do 2 | @behaviour HandrolledGenServer 3 | 4 | require Logger 5 | 6 | ## Public API 7 | 8 | def start_link(initial_stack) do 9 | HandrolledGenServer.start_link(__MODULE__, initial_stack) 10 | end 11 | 12 | def push(pid, elem) do 13 | HandrolledGenServer.cast(pid, {:push, elem}) 14 | end 15 | 16 | def pop(pid) do 17 | HandrolledGenServer.call(pid, :pop) 18 | end 19 | 20 | @impl true 21 | def init(stack) do 22 | stack 23 | end 24 | 25 | @impl true 26 | def handle_cast({:push, elem}, stack) do 27 | [elem | stack] 28 | end 29 | 30 | @impl true 31 | def handle_call(:pop, [elem | stack]) do 32 | {elem, stack} 33 | end 34 | 35 | @impl true 36 | def handle_info(message, stack) do 37 | _ = Logger.error("Received unknown message: #{inspect(message)}") 38 | stack 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /01_concurrency_foundations/test/parallel_enum_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ParallelEnumTest do 2 | use ExUnit.Case 3 | 4 | @tag :skip 5 | test "each/2" do 6 | test_pid = self() 7 | ref = make_ref() 8 | 9 | ParallelEnum.each(1..5, fn item -> 10 | send(test_pid, {ref, self(), item}) 11 | end) 12 | 13 | pids = 14 | for expected_item <- 1..5, into: MapSet.new() do 15 | assert_receive {^ref, spawned_pid, ^expected_item} 16 | spawned_pid 17 | end 18 | 19 | assert MapSet.size(pids) == 5 20 | end 21 | 22 | @tag :skip 23 | test "map/2" do 24 | pids_and_items = 25 | ParallelEnum.map(1..5, fn item -> 26 | {self(), item} 27 | end) 28 | 29 | {pids, items} = Enum.unzip(pids_and_items) 30 | 31 | assert pids |> MapSet.new() |> MapSet.size() == 5 32 | 33 | assert Enum.sort(items) == Enum.sort(1..5) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /06_phoenix_observer/lib/phoenix_observer_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixObserverWeb.Router do 2 | use PhoenixObserverWeb, :router 3 | 4 | pipeline :api do 5 | plug :accepts, ["json"] 6 | end 7 | 8 | scope "/api", PhoenixObserverWeb do 9 | pipe_through :api 10 | end 11 | 12 | # Enables LiveDashboard only for development 13 | # 14 | # If you want to use the LiveDashboard in production, you should put 15 | # it behind authentication and allow only admins to access it. 16 | # If your application does not have an admins-only section yet, 17 | # you can use Plug.BasicAuth to set up some basic authentication 18 | # as long as you are also using SSL (which you should anyway). 19 | if Mix.env() in [:dev, :test] do 20 | import Phoenix.LiveDashboard.Router 21 | 22 | scope "/" do 23 | pipe_through [:fetch_session, :protect_from_forgery] 24 | live_dashboard "/dashboard", metrics: PhoenixObserverWeb.Telemetry 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /05_redis_pool/test/redis_pool_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RedisPoolTest do 2 | use ExUnit.Case 3 | 4 | @tag :skip 5 | test "starting and using the pool", context do 6 | assert {:ok, sup} = RedisPool.start_link(name: context.test) 7 | assert is_pid(sup) 8 | 9 | assert RedisPool.command(context.test, ["PING"]) == {:ok, "PONG"} 10 | end 11 | 12 | @tag :skip 13 | test "with two connections, requests are routed at random", context do 14 | assert {:ok, sup} = RedisPool.start_link(name: context.test, connections: 2) 15 | assert is_pid(sup) 16 | 17 | # If we request the client ID of the connected client 200 times, it's *very* unlikely 18 | # that we'll only go through one connection so it's Good Enough™ for this test. 19 | client_ids = 20 | for _ <- 1..200, into: MapSet.new() do 21 | assert {:ok, client_id} = RedisPool.command(context.test, ["CLIENT", "ID"]) 22 | client_id 23 | end 24 | 25 | assert MapSet.size(client_ids) == 2 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /01_concurrency_foundations/test/shared_state_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SharedStateTest do 2 | use ExUnit.Case 3 | 4 | @tag :skip 5 | test "start/1 + get/1" do 6 | pid = SharedState.start(_initial_state = 1) 7 | assert is_pid(pid) 8 | 9 | assert SharedState.get(pid) == 1 10 | end 11 | 12 | @tag :skip 13 | test "start/1 + update/2 + get/1" do 14 | pid = SharedState.start(_initial_state = 1) 15 | assert is_pid(pid) 16 | 17 | assert :ok = SharedState.update(pid, &(&1 + 1)) 18 | 19 | assert SharedState.get(pid) == 2 20 | end 21 | 22 | @tag :skip 23 | test "update/2 executes the function in the state process" do 24 | test_pid = self() 25 | ref = make_ref() 26 | 27 | pid = SharedState.start(_initial_state = 1) 28 | 29 | assert :ok = 30 | SharedState.update(pid, fn state -> 31 | send(test_pid, {:update_called, ref, self()}) 32 | state 33 | end) 34 | 35 | assert_receive {:update_called, ^ref, ^pid} 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /06_phoenix_observer/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | 7 | # General application configuration 8 | use Mix.Config 9 | 10 | config :phoenix_observer, 11 | ecto_repos: [PhoenixObserver.Repo] 12 | 13 | # Configures the endpoint 14 | config :phoenix_observer, PhoenixObserverWeb.Endpoint, 15 | url: [host: "localhost"], 16 | secret_key_base: "YSaxr5zoC4fRV7p5EmwPXp9kF2eZdY9jTYdPXILrOkz/LmeIx0YvkAJgZlktYrbx", 17 | render_errors: [view: PhoenixObserverWeb.ErrorView, accepts: ~w(json), layout: false], 18 | pubsub_server: PhoenixObserver.PubSub, 19 | live_view: [signing_salt: "QWSXy9oT"] 20 | 21 | # Configures Elixir's Logger 22 | config :logger, :console, 23 | format: "$time $metadata[$level] $message\n", 24 | metadata: [:request_id] 25 | 26 | # Use Jason for JSON parsing in Phoenix 27 | config :phoenix, :json_library, Jason 28 | 29 | # Import environment specific config. This must remain at the bottom 30 | # of this file so it overrides the configuration defined above. 31 | import_config "#{Mix.env()}.exs" 32 | -------------------------------------------------------------------------------- /06_phoenix_observer/lib/phoenix_observer/application.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixObserver.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 | # Start the Ecto repository 11 | PhoenixObserver.Repo, 12 | # Start the Telemetry supervisor 13 | PhoenixObserverWeb.Telemetry, 14 | # Start the PubSub system 15 | {Phoenix.PubSub, name: PhoenixObserver.PubSub}, 16 | # Start the Endpoint (http/https) 17 | PhoenixObserverWeb.Endpoint 18 | # Start a worker by calling: PhoenixObserver.Worker.start_link(arg) 19 | # {PhoenixObserver.Worker, arg} 20 | ] 21 | 22 | # See https://hexdocs.pm/elixir/Supervisor.html 23 | # for other strategies and supported options 24 | opts = [strategy: :one_for_one, name: PhoenixObserver.Supervisor] 25 | Supervisor.start_link(children, opts) 26 | end 27 | 28 | # Tell Phoenix to update the endpoint configuration 29 | # whenever the application is updated. 30 | def config_change(changed, _new, removed) do 31 | PhoenixObserverWeb.Endpoint.config_change(changed, removed) 32 | :ok 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /06_phoenix_observer/lib/phoenix_observer_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixObserverWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", PhoenixObserverWeb.RoomChannel 6 | 7 | # Socket params are passed from the client and can 8 | # be used to verify and authenticate a user. After 9 | # verification, you can put default assigns into 10 | # the socket that will be set for all channels, ie 11 | # 12 | # {:ok, assign(socket, :user_id, verified_user_id)} 13 | # 14 | # To deny connection, return `:error`. 15 | # 16 | # See `Phoenix.Token` documentation for examples in 17 | # performing token verification on connect. 18 | @impl true 19 | def connect(_params, socket, _connect_info) do 20 | {:ok, socket} 21 | end 22 | 23 | # Socket id's are topics that allow you to identify all sockets for a given user: 24 | # 25 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 26 | # 27 | # Would allow you to broadcast a "disconnect" event and terminate 28 | # all active sockets and channels for a given user: 29 | # 30 | # PhoenixObserverWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 31 | # 32 | # Returning `nil` makes this socket anonymous. 33 | @impl true 34 | def id(_socket), do: nil 35 | end 36 | -------------------------------------------------------------------------------- /handrolled_supervisor/test/handrolled_supervisor_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HandrolledSupervisorTest do 2 | use ExUnit.Case 3 | 4 | import ExUnit.CaptureLog 5 | 6 | defmodule Counter do 7 | use Agent 8 | 9 | def start_link(opts) do 10 | {initial_value, opts} = Keyword.pop!(opts, :initial_value) 11 | Agent.start_link(fn -> initial_value end, opts) 12 | end 13 | end 14 | 15 | test "starts a few children" do 16 | children = [ 17 | {Counter, initial_value: 1, name: :small_counter}, 18 | {Counter, initial_value: 10, name: :medium_counter}, 19 | {Counter, initial_value: 100, name: :large_counter} 20 | ] 21 | 22 | assert {:ok, sup} = HandrolledSupervisor.start_link(children) 23 | assert is_pid(sup) 24 | 25 | assert Agent.get(:small_counter, & &1) == 1 26 | assert Agent.get(:medium_counter, & &1) == 10 27 | assert Agent.get(:large_counter, & &1) == 100 28 | 29 | assert :ok = Agent.update(:medium_counter, &(&1 + 1)) 30 | 31 | assert Agent.get(:medium_counter, & &1) == 11 32 | 33 | capture_log(fn -> 34 | assert :ok = Agent.cast(:medium_counter, fn _state -> exit(:oops) end) 35 | Process.sleep(100) 36 | end) 37 | 38 | assert :medium_counter |> Process.whereis() |> Process.alive?() 39 | 40 | assert Agent.get(:medium_counter, & &1) == 10 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /06_phoenix_observer/test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixObserverWeb.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use PhoenixObserverWeb.ChannelCase, async: true`, although 15 | this option is not recommended for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # Import conveniences for testing with channels 23 | import Phoenix.ChannelTest 24 | import PhoenixObserverWeb.ChannelCase 25 | 26 | # The default endpoint for testing 27 | @endpoint PhoenixObserverWeb.Endpoint 28 | end 29 | end 30 | 31 | setup tags do 32 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(PhoenixObserver.Repo) 33 | 34 | unless tags[:async] do 35 | Ecto.Adapters.SQL.Sandbox.mode(PhoenixObserver.Repo, {:shared, self()}) 36 | end 37 | 38 | :ok 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /06_phoenix_observer/lib/phoenix_observer_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixObserverWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | @doc """ 7 | Translates an error message using gettext. 8 | """ 9 | def translate_error({msg, opts}) do 10 | # When using gettext, we typically pass the strings we want 11 | # to translate as a static argument: 12 | # 13 | # # Translate "is invalid" in the "errors" domain 14 | # dgettext("errors", "is invalid") 15 | # 16 | # # Translate the number of files with plural rules 17 | # dngettext("errors", "1 file", "%{count} files", count) 18 | # 19 | # Because the error messages we show in our forms and APIs 20 | # are defined inside Ecto, we need to translate them dynamically. 21 | # This requires us to call the Gettext module passing our gettext 22 | # backend as first argument. 23 | # 24 | # Note we use the "errors" domain, which means translations 25 | # should be written to the errors.po file. The :count option is 26 | # set by Ecto and indicates we should also apply plural rules. 27 | if count = opts[:count] do 28 | Gettext.dngettext(PhoenixObserverWeb.Gettext, "errors", msg, msg, count, opts) 29 | else 30 | Gettext.dgettext(PhoenixObserverWeb.Gettext, "errors", msg, opts) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /06_phoenix_observer/config/prod.secret.exs: -------------------------------------------------------------------------------- 1 | # In this file, we load production configuration and secrets 2 | # from environment variables. You can also hardcode secrets, 3 | # although such is generally not recommended and you have to 4 | # remember to add this file to your .gitignore. 5 | use Mix.Config 6 | 7 | database_url = 8 | System.get_env("DATABASE_URL") || 9 | raise """ 10 | environment variable DATABASE_URL is missing. 11 | For example: ecto://USER:PASS@HOST/DATABASE 12 | """ 13 | 14 | config :phoenix_observer, PhoenixObserver.Repo, 15 | # ssl: true, 16 | url: database_url, 17 | pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10") 18 | 19 | secret_key_base = 20 | System.get_env("SECRET_KEY_BASE") || 21 | raise """ 22 | environment variable SECRET_KEY_BASE is missing. 23 | You can generate one by calling: mix phx.gen.secret 24 | """ 25 | 26 | config :phoenix_observer, PhoenixObserverWeb.Endpoint, 27 | http: [ 28 | port: String.to_integer(System.get_env("PORT") || "4000"), 29 | transport_options: [socket_opts: [:inet6]] 30 | ], 31 | secret_key_base: secret_key_base 32 | 33 | # ## Using releases (Elixir v1.9+) 34 | # 35 | # If you are doing OTP releases, you need to instruct Phoenix 36 | # to start each relevant endpoint: 37 | # 38 | # config :phoenix_observer, PhoenixObserverWeb.Endpoint, server: true 39 | # 40 | # Then you can assemble a release by calling `mix release`. 41 | # See `mix help release` for more information. 42 | -------------------------------------------------------------------------------- /06_phoenix_observer/test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixObserverWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use PhoenixObserverWeb.ConnCase, async: true`, although 15 | this option is not recommended for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # Import conveniences for testing with connections 23 | import Plug.Conn 24 | import Phoenix.ConnTest 25 | import PhoenixObserverWeb.ConnCase 26 | 27 | alias PhoenixObserverWeb.Router.Helpers, as: Routes 28 | 29 | # The default endpoint for testing 30 | @endpoint PhoenixObserverWeb.Endpoint 31 | end 32 | end 33 | 34 | setup tags do 35 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(PhoenixObserver.Repo) 36 | 37 | unless tags[:async] do 38 | Ecto.Adapters.SQL.Sandbox.mode(PhoenixObserver.Repo, {:shared, self()}) 39 | end 40 | 41 | {:ok, conn: Phoenix.ConnTest.build_conn()} 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /06_phoenix_observer/lib/phoenix_observer_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixObserverWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :phoenix_observer 3 | 4 | # The session will be stored in the cookie and signed, 5 | # this means its contents can be read but not tampered with. 6 | # Set :encryption_salt if you would also like to encrypt it. 7 | @session_options [ 8 | store: :cookie, 9 | key: "_phoenix_observer_key", 10 | signing_salt: "SVutZVSS" 11 | ] 12 | 13 | socket "/socket", PhoenixObserverWeb.UserSocket, 14 | websocket: true, 15 | longpoll: false 16 | 17 | socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] 18 | 19 | # Serve at "/" the static files from "priv/static" directory. 20 | # 21 | # You should set gzip to true if you are running phx.digest 22 | # when deploying your static files in production. 23 | plug Plug.Static, 24 | at: "/", 25 | from: :phoenix_observer, 26 | gzip: false, 27 | only: ~w(css fonts images js favicon.ico robots.txt) 28 | 29 | # Code reloading can be explicitly enabled under the 30 | # :code_reloader configuration of your endpoint. 31 | if code_reloading? do 32 | plug Phoenix.CodeReloader 33 | plug Phoenix.Ecto.CheckRepoStatus, otp_app: :phoenix_observer 34 | end 35 | 36 | plug Phoenix.LiveDashboard.RequestLogger, 37 | param_key: "request_logger", 38 | cookie_key: "request_logger" 39 | 40 | plug Plug.RequestId 41 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 42 | 43 | plug Plug.Parsers, 44 | parsers: [:urlencoded, :multipart, :json], 45 | pass: ["*/*"], 46 | json_decoder: Phoenix.json_library() 47 | 48 | plug Plug.MethodOverride 49 | plug Plug.Head 50 | plug Plug.Session, @session_options 51 | plug PhoenixObserverWeb.Router 52 | end 53 | -------------------------------------------------------------------------------- /06_phoenix_observer/test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixObserver.DataCase do 2 | @moduledoc """ 3 | This module defines the setup for tests requiring 4 | access to the application's data layer. 5 | 6 | You may define functions here to be used as helpers in 7 | your tests. 8 | 9 | Finally, if the test case interacts with the database, 10 | we enable the SQL sandbox, so changes done to the database 11 | are reverted at the end of every test. If you are using 12 | PostgreSQL, you can even run database tests asynchronously 13 | by setting `use PhoenixObserver.DataCase, async: true`, although 14 | this option is not recommended for other databases. 15 | """ 16 | 17 | use ExUnit.CaseTemplate 18 | 19 | using do 20 | quote do 21 | alias PhoenixObserver.Repo 22 | 23 | import Ecto 24 | import Ecto.Changeset 25 | import Ecto.Query 26 | import PhoenixObserver.DataCase 27 | end 28 | end 29 | 30 | setup tags do 31 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(PhoenixObserver.Repo) 32 | 33 | unless tags[:async] do 34 | Ecto.Adapters.SQL.Sandbox.mode(PhoenixObserver.Repo, {:shared, self()}) 35 | end 36 | 37 | :ok 38 | end 39 | 40 | @doc """ 41 | A helper that transforms changeset errors into a map of messages. 42 | 43 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 44 | assert "password is too short" in errors_on(changeset).password 45 | assert %{password: ["password is too short"]} = errors_on(changeset) 46 | 47 | """ 48 | def errors_on(changeset) do 49 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 50 | Regex.replace(~r"%{(\w+)}", message, fn _, key -> 51 | opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() 52 | end) 53 | end) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /01_concurrency_foundations/test/async_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AsyncTest do 2 | use ExUnit.Case 3 | 4 | @compile {:no_warn_undefined, Async} 5 | 6 | @tag :skip 7 | test "execute_async/1 + await_result/1" do 8 | start_time = System.monotonic_time(:millisecond) 9 | 10 | task = 11 | Async.execute_async(fn -> 12 | Process.sleep(500) 13 | :i_slept_for_half_second 14 | end) 15 | 16 | assert Async.await_result(task) == :i_slept_for_half_second 17 | 18 | assert System.monotonic_time(:millisecond) - start_time >= 500 19 | end 20 | 21 | @tag :skip 22 | test "execute_async/1 + await_result/2 (with timeout)" do 23 | task = 24 | Async.execute_async(fn -> 25 | Process.sleep(500) 26 | :i_slept_for_half_second 27 | end) 28 | 29 | assert Async.await_result(task, 100) == :timeout 30 | end 31 | 32 | @tag :skip 33 | test "execute_async/1 + await_or_kill/2" do 34 | task = 35 | Async.execute_async(fn -> 36 | Process.sleep(500) 37 | :i_slept_for_half_second 38 | end) 39 | 40 | assert Async.await_or_kill(task, 100) == :killed 41 | end 42 | 43 | @tag :skip 44 | test "execute_async_with_monitor/1 + await_or_kill_with_monitor/2" do 45 | successful_task = 46 | Async.execute_async_with_monitor(fn -> 47 | :i_returned 48 | end) 49 | 50 | assert Async.await_or_kill_with_monitor(successful_task, 100) == {:ok, :i_returned} 51 | 52 | crashing_task = 53 | Async.execute_async_with_monitor(fn -> 54 | exit(:crashed) 55 | end) 56 | 57 | assert {:error, :crashed} = Async.await_or_kill_with_monitor(crashing_task, 100) 58 | 59 | timeout_task = 60 | Async.execute_async_with_monitor(fn -> 61 | Process.sleep(500) 62 | :i_slept_for_half_second 63 | end) 64 | 65 | assert Async.await_or_kill_with_monitor(timeout_task, 100) == :killed 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /06_phoenix_observer/config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Configure your database 4 | config :phoenix_observer, PhoenixObserver.Repo, 5 | username: "postgres", 6 | password: "postgres", 7 | database: "phoenix_observer_dev", 8 | hostname: "localhost", 9 | show_sensitive_data_on_connection_error: true, 10 | pool_size: 10 11 | 12 | # For development, we disable any cache and enable 13 | # debugging and code reloading. 14 | # 15 | # The watchers configuration can be used to run external 16 | # watchers to your application. For example, we use it 17 | # with webpack to recompile .js and .css sources. 18 | config :phoenix_observer, PhoenixObserverWeb.Endpoint, 19 | http: [port: 4000], 20 | debug_errors: true, 21 | code_reloader: true, 22 | check_origin: false, 23 | watchers: [] 24 | 25 | # ## SSL Support 26 | # 27 | # In order to use HTTPS in development, a self-signed 28 | # certificate can be generated by running the following 29 | # Mix task: 30 | # 31 | # mix phx.gen.cert 32 | # 33 | # Note that this task requires Erlang/OTP 20 or later. 34 | # Run `mix help phx.gen.cert` for more information. 35 | # 36 | # The `http:` config above can be replaced with: 37 | # 38 | # https: [ 39 | # port: 4001, 40 | # cipher_suite: :strong, 41 | # keyfile: "priv/cert/selfsigned_key.pem", 42 | # certfile: "priv/cert/selfsigned.pem" 43 | # ], 44 | # 45 | # If desired, both `http:` and `https:` keys can be 46 | # configured to run both http and https servers on 47 | # different ports. 48 | 49 | # Do not include metadata nor timestamps in development logs 50 | config :logger, :console, format: "[$level] $message\n" 51 | 52 | # Set a higher stacktrace during development. Avoid configuring such 53 | # in production as building large stacktraces may be expensive. 54 | config :phoenix, :stacktrace_depth, 20 55 | 56 | # Initialize plugs at runtime for faster development compilation 57 | config :phoenix, :plug_init_mode, :runtime 58 | -------------------------------------------------------------------------------- /06_phoenix_observer/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixObserver.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :phoenix_observer, 7 | version: "0.1.0", 8 | elixir: "~> 1.7", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | compilers: [:phoenix, :gettext] ++ Mix.compilers(), 11 | start_permanent: Mix.env() == :prod, 12 | aliases: aliases(), 13 | deps: deps() 14 | ] 15 | end 16 | 17 | # Configuration for the OTP application. 18 | # 19 | # Type `mix help compile.app` for more information. 20 | def application do 21 | [ 22 | mod: {PhoenixObserver.Application, []}, 23 | extra_applications: [:logger, :runtime_tools] 24 | ] 25 | end 26 | 27 | # Specifies which paths to compile per environment. 28 | defp elixirc_paths(:test), do: ["lib", "test/support"] 29 | defp elixirc_paths(_), do: ["lib"] 30 | 31 | # Specifies your project dependencies. 32 | # 33 | # Type `mix help deps` for examples and options. 34 | defp deps do 35 | [ 36 | {:phoenix, "~> 1.5.3"}, 37 | {:phoenix_ecto, "~> 4.1"}, 38 | {:ecto_sql, "~> 3.4"}, 39 | {:postgrex, ">= 0.0.0"}, 40 | {:phoenix_live_dashboard, "~> 0.2.0"}, 41 | {:telemetry_metrics, "~> 0.4"}, 42 | {:telemetry_poller, "~> 0.4"}, 43 | {:gettext, "~> 0.11"}, 44 | {:jason, "~> 1.0"}, 45 | {:plug_cowboy, "~> 2.0"} 46 | ] 47 | end 48 | 49 | # Aliases are shortcuts or tasks specific to the current project. 50 | # For example, to install project dependencies and perform other setup tasks, run: 51 | # 52 | # $ mix setup 53 | # 54 | # See the documentation for `Mix` for more info on aliases. 55 | defp aliases do 56 | [ 57 | setup: ["deps.get", "ecto.setup"], 58 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 59 | "ecto.reset": ["ecto.drop", "ecto.setup"], 60 | test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"] 61 | ] 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /06_phoenix_observer/lib/phoenix_observer_web/telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixObserverWeb.Telemetry do 2 | use Supervisor 3 | import Telemetry.Metrics 4 | 5 | def start_link(arg) do 6 | Supervisor.start_link(__MODULE__, arg, name: __MODULE__) 7 | end 8 | 9 | @impl true 10 | def init(_arg) do 11 | children = [ 12 | # Telemetry poller will execute the given period measurements 13 | # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics 14 | {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} 15 | # Add reporters as children of your supervision tree. 16 | # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} 17 | ] 18 | 19 | Supervisor.init(children, strategy: :one_for_one) 20 | end 21 | 22 | def metrics do 23 | [ 24 | # Phoenix Metrics 25 | summary("phoenix.endpoint.stop.duration", 26 | unit: {:native, :millisecond} 27 | ), 28 | summary("phoenix.router_dispatch.stop.duration", 29 | tags: [:route], 30 | unit: {:native, :millisecond} 31 | ), 32 | 33 | # Database Metrics 34 | summary("phoenix_observer.repo.query.total_time", unit: {:native, :millisecond}), 35 | summary("phoenix_observer.repo.query.decode_time", unit: {:native, :millisecond}), 36 | summary("phoenix_observer.repo.query.query_time", unit: {:native, :millisecond}), 37 | summary("phoenix_observer.repo.query.queue_time", unit: {:native, :millisecond}), 38 | summary("phoenix_observer.repo.query.idle_time", unit: {:native, :millisecond}), 39 | 40 | # VM Metrics 41 | summary("vm.memory.total", unit: {:byte, :kilobyte}), 42 | summary("vm.total_run_queue_lengths.total"), 43 | summary("vm.total_run_queue_lengths.cpu"), 44 | summary("vm.total_run_queue_lengths.io") 45 | ] 46 | end 47 | 48 | defp periodic_measurements do 49 | [ 50 | # A module, function and arguments to be invoked periodically. 51 | # This function must call :telemetry.execute/3 and a metric must be added above. 52 | # {PhoenixObserverWeb, :count_users, []} 53 | ] 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /06_phoenix_observer/lib/phoenix_observer_web.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixObserverWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use PhoenixObserverWeb, :controller 9 | use PhoenixObserverWeb, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: PhoenixObserverWeb 23 | 24 | import Plug.Conn 25 | import PhoenixObserverWeb.Gettext 26 | alias PhoenixObserverWeb.Router.Helpers, as: Routes 27 | end 28 | end 29 | 30 | def view do 31 | quote do 32 | use Phoenix.View, 33 | root: "lib/phoenix_observer_web/templates", 34 | namespace: PhoenixObserverWeb 35 | 36 | # Import convenience functions from controllers 37 | import Phoenix.Controller, 38 | only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] 39 | 40 | # Include shared imports and aliases for views 41 | unquote(view_helpers()) 42 | end 43 | end 44 | 45 | def router do 46 | quote do 47 | use Phoenix.Router 48 | 49 | import Plug.Conn 50 | import Phoenix.Controller 51 | end 52 | end 53 | 54 | def channel do 55 | quote do 56 | use Phoenix.Channel 57 | import PhoenixObserverWeb.Gettext 58 | end 59 | end 60 | 61 | defp view_helpers do 62 | quote do 63 | # Import basic rendering functionality (render, render_layout, etc) 64 | import Phoenix.View 65 | 66 | import PhoenixObserverWeb.ErrorHelpers 67 | import PhoenixObserverWeb.Gettext 68 | alias PhoenixObserverWeb.Router.Helpers, as: Routes 69 | end 70 | end 71 | 72 | @doc """ 73 | When used, dispatch to the appropriate controller/view/etc. 74 | """ 75 | defmacro __using__(which) when is_atom(which) do 76 | apply(__MODULE__, which, []) 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /06_phoenix_observer/config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, don't forget to configure the url host 4 | # to something meaningful, Phoenix uses this information 5 | # when generating URLs. 6 | # 7 | # Note we also include the path to a cache manifest 8 | # containing the digested version of static files. This 9 | # manifest is generated by the `mix phx.digest` task, 10 | # which you should run after static files are built and 11 | # before starting your production server. 12 | config :phoenix_observer, PhoenixObserverWeb.Endpoint, 13 | url: [host: "example.com", port: 80], 14 | cache_static_manifest: "priv/static/cache_manifest.json" 15 | 16 | # Do not print debug messages in production 17 | config :logger, level: :info 18 | 19 | # ## SSL Support 20 | # 21 | # To get SSL working, you will need to add the `https` key 22 | # to the previous section and set your `:url` port to 443: 23 | # 24 | # config :phoenix_observer, PhoenixObserverWeb.Endpoint, 25 | # ... 26 | # url: [host: "example.com", port: 443], 27 | # https: [ 28 | # port: 443, 29 | # cipher_suite: :strong, 30 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 31 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH"), 32 | # transport_options: [socket_opts: [:inet6]] 33 | # ] 34 | # 35 | # The `cipher_suite` is set to `:strong` to support only the 36 | # latest and more secure SSL ciphers. This means old browsers 37 | # and clients may not be supported. You can set it to 38 | # `:compatible` for wider support. 39 | # 40 | # `:keyfile` and `:certfile` expect an absolute path to the key 41 | # and cert in disk or a relative path inside priv, for example 42 | # "priv/ssl/server.key". For all supported SSL configuration 43 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 44 | # 45 | # We also recommend setting `force_ssl` in your endpoint, ensuring 46 | # no data is ever sent via http, always redirecting to https: 47 | # 48 | # config :phoenix_observer, PhoenixObserverWeb.Endpoint, 49 | # force_ssl: [hsts: true] 50 | # 51 | # Check `Plug.SSL` for all available options in `force_ssl`. 52 | 53 | # Finally import the config/prod.secret.exs which loads secrets 54 | # and configuration from environment variables. 55 | import_config "prod.secret.exs" 56 | -------------------------------------------------------------------------------- /06_phoenix_observer/priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | 13 | ## From Ecto.Changeset.cast/4 14 | msgid "can't be blank" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.unique_constraint/3 18 | msgid "has already been taken" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.put_change/3 22 | msgid "is invalid" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_acceptance/3 26 | msgid "must be accepted" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_format/3 30 | msgid "has invalid format" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_subset/3 34 | msgid "has an invalid entry" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.validate_exclusion/3 38 | msgid "is reserved" 39 | msgstr "" 40 | 41 | ## From Ecto.Changeset.validate_confirmation/3 42 | msgid "does not match confirmation" 43 | msgstr "" 44 | 45 | ## From Ecto.Changeset.no_assoc_constraint/3 46 | msgid "is still associated with this entry" 47 | msgstr "" 48 | 49 | msgid "are still associated with this entry" 50 | msgstr "" 51 | 52 | ## From Ecto.Changeset.validate_length/3 53 | msgid "should be %{count} character(s)" 54 | msgid_plural "should be %{count} character(s)" 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | msgid "should have %{count} item(s)" 59 | msgid_plural "should have %{count} item(s)" 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | 63 | msgid "should be at least %{count} character(s)" 64 | msgid_plural "should be at least %{count} character(s)" 65 | msgstr[0] "" 66 | msgstr[1] "" 67 | 68 | msgid "should have at least %{count} item(s)" 69 | msgid_plural "should have at least %{count} item(s)" 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | msgid "should be at most %{count} character(s)" 74 | msgid_plural "should be at most %{count} character(s)" 75 | msgstr[0] "" 76 | msgstr[1] "" 77 | 78 | msgid "should have at most %{count} item(s)" 79 | msgid_plural "should have at most %{count} item(s)" 80 | msgstr[0] "" 81 | msgstr[1] "" 82 | 83 | ## From Ecto.Changeset.validate_number/3 84 | msgid "must be less than %{number}" 85 | msgstr "" 86 | 87 | msgid "must be greater than %{number}" 88 | msgstr "" 89 | 90 | msgid "must be less than or equal to %{number}" 91 | msgstr "" 92 | 93 | msgid "must be greater than or equal to %{number}" 94 | msgstr "" 95 | 96 | msgid "must be equal to %{number}" 97 | msgstr "" 98 | -------------------------------------------------------------------------------- /06_phoenix_observer/priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here has no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | ## From Ecto.Changeset.cast/4 12 | msgid "can't be blank" 13 | msgstr "" 14 | 15 | ## From Ecto.Changeset.unique_constraint/3 16 | msgid "has already been taken" 17 | msgstr "" 18 | 19 | ## From Ecto.Changeset.put_change/3 20 | msgid "is invalid" 21 | msgstr "" 22 | 23 | ## From Ecto.Changeset.validate_acceptance/3 24 | msgid "must be accepted" 25 | msgstr "" 26 | 27 | ## From Ecto.Changeset.validate_format/3 28 | msgid "has invalid format" 29 | msgstr "" 30 | 31 | ## From Ecto.Changeset.validate_subset/3 32 | msgid "has an invalid entry" 33 | msgstr "" 34 | 35 | ## From Ecto.Changeset.validate_exclusion/3 36 | msgid "is reserved" 37 | msgstr "" 38 | 39 | ## From Ecto.Changeset.validate_confirmation/3 40 | msgid "does not match confirmation" 41 | msgstr "" 42 | 43 | ## From Ecto.Changeset.no_assoc_constraint/3 44 | msgid "is still associated with this entry" 45 | msgstr "" 46 | 47 | msgid "are still associated with this entry" 48 | msgstr "" 49 | 50 | ## From Ecto.Changeset.validate_length/3 51 | msgid "should be %{count} character(s)" 52 | msgid_plural "should be %{count} character(s)" 53 | msgstr[0] "" 54 | msgstr[1] "" 55 | 56 | msgid "should have %{count} item(s)" 57 | msgid_plural "should have %{count} item(s)" 58 | msgstr[0] "" 59 | msgstr[1] "" 60 | 61 | msgid "should be at least %{count} character(s)" 62 | msgid_plural "should be at least %{count} character(s)" 63 | msgstr[0] "" 64 | msgstr[1] "" 65 | 66 | msgid "should have at least %{count} item(s)" 67 | msgid_plural "should have at least %{count} item(s)" 68 | msgstr[0] "" 69 | msgstr[1] "" 70 | 71 | msgid "should be at most %{count} character(s)" 72 | msgid_plural "should be at most %{count} character(s)" 73 | msgstr[0] "" 74 | msgstr[1] "" 75 | 76 | msgid "should have at most %{count} item(s)" 77 | msgid_plural "should have at most %{count} item(s)" 78 | msgstr[0] "" 79 | msgstr[1] "" 80 | 81 | ## From Ecto.Changeset.validate_number/3 82 | msgid "must be less than %{number}" 83 | msgstr "" 84 | 85 | msgid "must be greater than %{number}" 86 | msgstr "" 87 | 88 | msgid "must be less than or equal to %{number}" 89 | msgstr "" 90 | 91 | msgid "must be greater than or equal to %{number}" 92 | msgstr "" 93 | 94 | msgid "must be equal to %{number}" 95 | msgstr "" 96 | -------------------------------------------------------------------------------- /handrolled_supervisor/lib/handrolled_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule HandrolledSupervisor do 2 | use GenServer 3 | 4 | require Logger 5 | 6 | defstruct [:children] 7 | 8 | @doc """ 9 | Starts a supervisor with the given list of `children`. 10 | 11 | `children` has to be a list of `{module, init_arg}` tuple, which is common for 12 | Elixir modules. Each `module` must implement `child_spec/1` that returns a 13 | `t:Supervisor.child_spec/1`. 14 | """ 15 | @spec start_link([child, ...]) :: GenServer.on_start() 16 | when child: {module(), init_arg :: term()} 17 | def start_link(children) when is_list(children) and children != [] do 18 | child_specs = Enum.map(children, fn {mod, args} -> mod.child_spec(args) end) 19 | GenServer.start_link(__MODULE__, child_specs) 20 | end 21 | 22 | @impl true 23 | def init(child_specs) do 24 | # The first thing we need to do is trap exits. This ensures that if the supervisor 25 | # shuts down (or crashes), then all of its children *need* to also exit to avoid 26 | # "zombie" processes that will never be shut down. However, we don't want the supervisor 27 | # to crash if one of its children crashes, so we need to trap exits in the supervisor 28 | # so that children exiting are translated into :EXIT messages. The supervisor will use 29 | # those to detect children going down and restart them. 30 | Process.flag(:trap_exit, true) 31 | 32 | case start_children(child_specs, _acc = []) do 33 | {:ok, children} -> {:ok, %__MODULE__{children: children}} 34 | {:error, reason} -> {:stop, reason} 35 | end 36 | end 37 | 38 | # This is the juice of the restarting: one of our children went down. 39 | @impl true 40 | def handle_info({:EXIT, pid, reason}, %__MODULE__{children: children} = state) do 41 | case List.keyfind(children, pid, 1) do 42 | {child_spec, ^pid} -> 43 | # If we have this child, now it's the time to restart it. 44 | case start_child(child_spec) do 45 | {:ok, new_pid} -> 46 | # We replace the child's old PID with its new PID in the list of children. 47 | children = List.keyreplace(children, pid, 1, {child_spec, new_pid}) 48 | state = %__MODULE__{state | children: children} 49 | {:noreply, state} 50 | 51 | # We're being dramatic here, but if any child that crashes then fails to 52 | # start up again, we're going to bring the whole circus down and shut down 53 | # the supervisor. All or nothing. 54 | {:error, reason} -> 55 | {:stop, reason} 56 | end 57 | 58 | # This is really just a fail safe in case we get linked to another process that then 59 | # exits (which results in us getting an :EXIT message since we're trapping exits). 60 | nil -> 61 | _ = Logger.error("Received EXIT signal from process that is not a child") 62 | {:noreply, state} 63 | end 64 | end 65 | 66 | defp start_children([child_spec | child_specs], acc) do 67 | case start_child(child_spec) do 68 | {:ok, pid} -> 69 | # We store {child_spec, pid} tuples in the list of children. We could store 70 | # children as a map instead but that would prevent us from implementing strategies 71 | # like :one_for_all and :rest_for_one, where we need to know the original child order 72 | # when a child crashes in order to restart other children correctly. 73 | start_children(child_specs, [{child_spec, pid} | acc]) 74 | 75 | # If any of the children fail to start, we stop the whole show. This is a simplification 76 | # as we might want to give a chance to children to start a few times when erroring out. 77 | # Moreover, we have some children that might have been started successfully at this point 78 | # that it would be nice to shut down gracefully, but we can avoid caring too much 79 | # since they are linked to this supervisor. That means that when this supervisor will 80 | # die, they'll also die and not be left behind as zombie processes. 81 | {:error, reason} -> 82 | {:error, reason} 83 | end 84 | end 85 | 86 | defp start_children([], acc) do 87 | {:ok, Enum.reverse(acc)} 88 | end 89 | 90 | # The child_spec is a map that contains the :start key with value {mod, fun, args} used 91 | # to start the process (by calling mod.fun(args...)). 92 | defp start_child(child_spec) when is_map(child_spec) do 93 | %{start: {mod, fun, args}} = child_spec 94 | apply(mod, fun, args) 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OTP in Elixir 2 | 3 | > Material for a training done at [Elixir Club Kyiv](https://www.meetup.com/Elixir-Club/) in ~2020. 👩‍🏫 4 | 5 | ## Requirements 6 | 7 | * Elixir 1.10 8 | * Erlang/OTP 21 or later 9 | * [Docker](https://www.docker.com/get-started) 10 | * [Docker Compose](https://docs.docker.com/compose/install/) 11 | 12 | ## How to use this repository 13 | 14 | This repo contains the source code for the [*OTP in Elixir* 15 | training](http://www.elixirkyiv.club). 16 | 17 | During the training, we'll live-code a lot of the source code available here, 18 | but attendees will also be handed the full implementations for their reference. 19 | 20 | During the training, we'll explore the following projects: 21 | 22 | * `01_concurrency_foundations` - covers solid foundations of concurrency in 23 | the BEAM. Spawning processes, sending and receiving messages, monitors, 24 | state loops. 25 | 26 | * `02_terms_cache` - covers a few ways of writing a *terms cache*, which is a 27 | process that can hold a key-value store of terms which are fast to store and 28 | retrieve. Starts with a single GenServer process, then adds TTL and cache 29 | eviction, and finally moves up to using an ETS table. 30 | 31 | * `03_redis_client` - covers a barebones Redis client that can connect to 32 | Redis and execute commands. Starts with a *blocking* client which can handle 33 | one request at a time and then moves to a non-blocking client. These are 34 | implemented as GenServers: the last step here is rewriting them as state 35 | machines by using `gen_statem`. 36 | 37 | * `04_supervision_playground` - an almost-empty app that we'll use to play 38 | around with supervisors and supervision trees. 39 | 40 | * `05_redis_pool` - a small implementation of a pool built on top of 41 | [Redix](https://github.com/whatyouhide/redix), Elixir's Redis client. We'll 42 | use this to illustrate a supervisor used in a real-world scenario plus some 43 | nifty concepts like `:persistent_term`. 44 | 45 | * `06_phoenix_observer` - a boilerplate Phoenix app that we'll only use to run 46 | `:observer` and visually look at a complex supervision tree. 47 | 48 | Then, there is some content that fits outside of our schedule and is provided as 49 | additional resources to attendees: 50 | 51 | * `handrolled_genserver` - contains a hand-rolled and 52 | significantly-but-not-unbelievably simplified version of how the `GenServer` 53 | behaviour is implemented under the hood (in Erlang/OTP). Also contains a 54 | "stack" process (that you can push to and pop from) implemented on top of 55 | the hand-rolled GenServer-like behaviour. 56 | 57 | * `handrolled_supervisor` - similar to the GenServer idea, contains a *very* 58 | simplified version of a supervisor that can start a list of children and 59 | restart them (mimicking a `:one_for_one` strategy). 60 | 61 | ## Branches 62 | 63 | The `main` branch contains the code we'll work on. It's mostly skeletons of 64 | modules. The code that is in there is boilerplate or helpers that will save us 65 | time during the training. 66 | 67 | Should you want to peek at the "finished" code, the `complete` branch contains 68 | the complete code. What we come up with during the training might slightly 69 | differ from what's in the `complete` branch depending on how we go about 70 | implementing stuff. 71 | 72 | ## Resources 73 | 74 | * Documentation for [`Task`](https://hexdocs.pm/elixir/Task.html), which we 75 | reimplemented parts of 76 | * Documentation for [`GenServer`](https://hexdocs.pm/elixir/GenServer.html), 77 | which is full of nice tips and things to learn 78 | * Documentation for [`ets`](http://erlang.org/doc/man/ets.html), equally dense 79 | of interesting stuff to read 80 | * [Erlang efficiency guide on 81 | processes](http://erlang.org/doc/efficiency_guide/processes.html#creating-an-erlang-process) 82 | * Two of my blog posts about connection processes in OTP: 83 | * 84 | * 85 | * Insightful post from Fred Hebert about what supervisors and restarts are 86 | meant for: [*It's about the 87 | guarantees*](https://ferd.ca/it-s-about-the-guarantees.html) 88 | * One of the best books to learn in depth about OTP and its design patterns: 89 | [*Designing for Scalability with 90 | Erlang/OTP](https://www.oreilly.com/library/view/designing-for-scalability/9781449361556/) 91 | -------------------------------------------------------------------------------- /handrolled_genserver/lib/handrolled_genserver.ex: -------------------------------------------------------------------------------- 1 | defmodule HandrolledGenServer do 2 | @type state() :: term() 3 | @type command() :: term() 4 | 5 | ## Callbacks 6 | 7 | @doc """ 8 | Simplified initialization. 9 | 10 | Must return the initial state. 11 | """ 12 | @callback init(term()) :: initial_state :: state() 13 | 14 | @doc """ 15 | Simplified cast. 16 | 17 | Must return an updated state. 18 | """ 19 | @callback handle_cast(command(), state()) :: new_state :: state() 20 | 21 | @doc """ 22 | Simplified call. 23 | 24 | Can only return a tuple with `{reply, new_state}`, so can only reply synchronously 25 | to the caller. This is in contrast with the `{:noreply, state}` tuple provided by GenServer 26 | alongside `GenServer.reply/2`. 27 | """ 28 | @callback handle_call(command(), state()) :: {reply :: term(), new_state :: state()} 29 | 30 | @doc """ 31 | Simplified message handling. 32 | 33 | Must return an updated state. 34 | """ 35 | @callback handle_info(message :: term(), state()) :: new_state :: state() 36 | 37 | ## Public API 38 | 39 | @doc """ 40 | Starts a "handrolled GenServer" process using `callback_module` as the module that 41 | implements this behaviour and `init_arg` as the initial argument. 42 | 43 | ## Example 44 | 45 | {:ok, pid} = HandrolledGenServer.start_link(Stack, _init_arg = []) 46 | 47 | """ 48 | @spec start_link(module(), state()) :: {:ok, pid()} 49 | def start_link(callback_module, init_arg) do 50 | caller = self() 51 | ref = make_ref() 52 | 53 | # Here we first call the init/1 callback to initialize the process. Then we send a :ready 54 | # message to the caller and start the process loop. This way, the caller process can 55 | # wait until init/1 returns before considering the start_link successful. Note that linking 56 | # makes sure that whichever process goes down brings the other one down too so there's 57 | # no need for monitoring. 58 | pid = 59 | spawn_link(fn -> 60 | initial_state = callback_module.init(init_arg) 61 | send(caller, {:ready, ref}) 62 | loop(callback_module, initial_state) 63 | end) 64 | 65 | receive do 66 | {:ready, ^ref} -> {:ok, pid} 67 | after 68 | # The timeout here is hardcoded but it would be simple enough to pass it as an argument. 69 | 5000 -> exit({:timeout, 5000}) 70 | end 71 | end 72 | 73 | @doc """ 74 | Sends a cast to the given process. 75 | 76 | ## Examples 77 | 78 | HandrolledGenServer.cast(pid, {:push, 1}) 79 | #=> :ok 80 | 81 | """ 82 | @spec cast(pid(), command()) :: :ok 83 | def cast(pid, command) do 84 | send(pid, {:"$genserver_cast", command}) 85 | :ok 86 | end 87 | 88 | @doc """ 89 | Sends a call to the given process and waits for a reply. 90 | 91 | ## Examples 92 | 93 | HandrolledGenServer.call(pid, :pop) 94 | #=> 1 95 | 96 | """ 97 | @spec call(pid(), command(), timeout()) :: term() 98 | def call(pid, command, timeout \\ 5000) do 99 | # We monitor the GenServer so that if it goes down while the current process is waiting 100 | # for a reply, then we can exit the current process. We also use the returned monitor ref 101 | # to "tag" the call and reply. 102 | ref = Process.monitor(pid) 103 | 104 | send(pid, {:"$genserver_call", ref, self(), command}) 105 | 106 | receive do 107 | {^ref, reply} -> 108 | # It's important to *demonitor* a process if we're done with it because otheriwse 109 | # the monitor will stay on and if the GenServer will eventually go down, we'll find 110 | # ourselves with a :DOWN message sitting in this process' mailbox. The :flush option 111 | # ensures that if the monitored process goes down while we call Process.demonitor/2, 112 | # then it will "eat" the :DOWN message. Essentially, calling Process.demonitor/2 113 | # with the :flush option makes sure there's no monitor and no leaked :DOWN messages. 114 | Process.demonitor(ref, [:flush]) 115 | reply 116 | 117 | {:DOWN, ^ref, _, _, reason} -> 118 | exit(reason) 119 | after 120 | timeout -> exit({:timeout, timeout}) 121 | end 122 | end 123 | 124 | ## Process loop 125 | 126 | defp loop(callback_module, state) do 127 | receive do 128 | {:"$genserver_cast", command} -> 129 | new_state = callback_module.handle_cast(command, state) 130 | loop(callback_module, new_state) 131 | 132 | {:"$genserver_call", ref, caller_pid, command} -> 133 | {reply, new_state} = callback_module.handle_call(command, state) 134 | send(caller_pid, {ref, reply}) 135 | loop(callback_module, new_state) 136 | 137 | other -> 138 | new_state = callback_module.handle_info(other, state) 139 | loop(callback_module, new_state) 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /06_phoenix_observer/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"}, 3 | "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"}, 4 | "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"}, 5 | "db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"}, 6 | "decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"}, 7 | "ecto": {:hex, :ecto, "3.4.5", "2bcd262f57b2c888b0bd7f7a28c8a48aa11dc1a2c6a858e45dd8f8426d504265", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8c6d1d4d524559e9b7a062f0498e2c206122552d63eacff0a6567ffe7a8e8691"}, 8 | "ecto_sql": {:hex, :ecto_sql, "3.4.4", "d28bac2d420f708993baed522054870086fd45016a9d09bb2cd521b9c48d32ea", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "edb49af715dd72f213b66adfd0f668a43c17ed510b5d9ac7528569b23af57fe8"}, 9 | "gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"}, 10 | "jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"}, 11 | "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"}, 12 | "phoenix": {:hex, :phoenix, "1.5.3", "bfe0404e48ea03dfe17f141eff34e1e058a23f15f109885bbdcf62be303b49ff", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8e16febeb9640d8b33895a691a56481464b82836d338bb3a23125cd7b6157c25"}, 13 | "phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"}, 14 | "phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"}, 15 | "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.2.6", "1b4e1b7d797386b7f9d70d2af931dc9843a5f2f2423609d22cef1eec4e4dba7d", [:mix], [{:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.13.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "b20dcad98c4ca63d38a7f5e7a40936e1e8e9da983d3d722b88ae33afb866c9ca"}, 16 | "phoenix_live_view": {:hex, :phoenix_live_view, "0.13.3", "2186c55cc7c54ca45b97c6f28cfd267d1c61b5f205f3c83533704cd991bdfdec", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.4.17 or ~> 1.5.2", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "c6309a7da2e779cb9cdf2fb603d75f38f49ef324bedc7a81825998bd1744ff8a"}, 17 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, 18 | "plug": {:hex, :plug, "1.10.3", "c9cebe917637d8db0e759039cc106adca069874e1a9034fd6e3fdd427fd3c283", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "01f9037a2a1de1d633b5a881101e6a444bcabb1d386ca1e00bb273a1f1d9d939"}, 19 | "plug_cowboy": {:hex, :plug_cowboy, "2.3.0", "149a50e05cb73c12aad6506a371cd75750c0b19a32f81866e1a323dda9e0e99d", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bc595a1870cef13f9c1e03df56d96804db7f702175e4ccacdb8fc75c02a7b97e"}, 20 | "plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"}, 21 | "postgrex": {:hex, :postgrex, "0.15.5", "aec40306a622d459b01bff890fa42f1430dac61593b122754144ad9033a2152f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ed90c81e1525f65a2ba2279dbcebf030d6d13328daa2f8088b9661eb9143af7f"}, 22 | "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, 23 | "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, 24 | "telemetry_metrics": {:hex, :telemetry_metrics, "0.5.0", "1b796e74add83abf844e808564275dfb342bcc930b04c7577ab780e262b0d998", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31225e6ce7a37a421a0a96ec55244386aec1c190b22578bd245188a4a33298fd"}, 25 | "telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"}, 26 | } 27 | -------------------------------------------------------------------------------- /03_redis_client/lib/redis_client/protocol.ex: -------------------------------------------------------------------------------- 1 | defmodule RedisClient.Protocol do 2 | @moduledoc """ 3 | This module provides functions to work with the [Redis binary 4 | protocol](http://redis.io/topics/protocol). 5 | """ 6 | 7 | defmodule Error do 8 | @moduledoc """ 9 | Error returned by Redis. 10 | 11 | This exception represents semantic errors returned by Redis: for example, 12 | non-existing commands or operations on keys with the wrong type (`INCR 13 | not_an_integer`). 14 | """ 15 | 16 | defexception [:message] 17 | 18 | @type t :: %__MODULE__{message: binary} 19 | end 20 | 21 | defmodule ParseError do 22 | @moduledoc """ 23 | Error in parsing data according to the 24 | [RESP](http://redis.io/topics/protocol) protocol. 25 | """ 26 | 27 | defexception [:message] 28 | end 29 | 30 | @type redis_value :: binary | integer | nil | Error.t() | [redis_value] 31 | @type on_parse(value) :: {:ok, value, binary} | {:continuation, (binary -> on_parse(value))} 32 | 33 | @crlf "\r\n" 34 | @crlf_iodata [?\r, ?\n] 35 | 36 | @doc ~S""" 37 | Packs a list of Elixir terms to a Redis (RESP) array. 38 | 39 | This function returns an iodata (instead of a binary) because the packed 40 | result is usually sent to Redis through `:gen_tcp.send/2` or similar. It can 41 | be converted to a binary with `IO.iodata_to_binary/1`. 42 | 43 | All elements of `elems` are converted to strings with `to_string/1`, hence 44 | this function supports encoding everything that implements `String.Chars`. 45 | 46 | ## Examples 47 | 48 | iex> iodata = Redix.Protocol.pack(["SET", "mykey", 1]) 49 | iex> IO.iodata_to_binary(iodata) 50 | "*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$1\r\n1\r\n" 51 | 52 | """ 53 | @spec pack([binary]) :: iodata 54 | def pack(items) when is_list(items) do 55 | pack(items, [], 0) 56 | end 57 | 58 | defp pack([item | rest], acc, count) do 59 | item = to_string(item) 60 | new_acc = [acc, [?$, Integer.to_string(byte_size(item)), @crlf_iodata, item, @crlf_iodata]] 61 | pack(rest, new_acc, count + 1) 62 | end 63 | 64 | defp pack([], acc, count) do 65 | [?*, Integer.to_string(count), @crlf_iodata, acc] 66 | end 67 | 68 | @doc ~S""" 69 | Parses a RESP-encoded value from the given `data`. 70 | 71 | Returns `{:ok, value, rest}` if a value is parsed successfully, or a 72 | continuation in the form `{:continuation, fun}` if the data is incomplete. 73 | 74 | ## Examples 75 | 76 | iex> Redix.Protocol.parse("+OK\r\ncruft") 77 | {:ok, "OK", "cruft"} 78 | 79 | iex> Redix.Protocol.parse("-ERR wrong type\r\n") 80 | {:ok, %Redix.Error{message: "ERR wrong type"}, ""} 81 | 82 | iex> {:continuation, fun} = Redix.Protocol.parse("+OK") 83 | iex> fun.("\r\n") 84 | {:ok, "OK", ""} 85 | 86 | """ 87 | @spec parse(binary) :: on_parse(redis_value) 88 | def parse(data) 89 | 90 | def parse("+" <> rest), do: parse_simple_string(rest) 91 | def parse("-" <> rest), do: parse_error(rest) 92 | def parse(":" <> rest), do: parse_integer(rest) 93 | def parse("$" <> rest), do: parse_bulk_string(rest) 94 | def parse("*" <> rest), do: parse_array(rest) 95 | def parse(""), do: {:continuation, &parse/1} 96 | 97 | def parse(<> <> _), 98 | do: raise(ParseError, message: "invalid type specifier (#{inspect(<>)})") 99 | 100 | @doc ~S""" 101 | Parses `n` RESP-encoded values from the given `data`. 102 | 103 | Each element is parsed as described in `parse/1`. If an element can't be fully 104 | parsed or there are less than `n` elements encoded in `data`, then a 105 | continuation in the form of `{:continuation, fun}` is returned. Otherwise, 106 | `{:ok, values, rest}` is returned. If there's an error in decoding, a 107 | `Redix.Protocol.ParseError` exception is raised. 108 | 109 | ## Examples 110 | 111 | iex> Redix.Protocol.parse_multi("+OK\r\n+COOL\r\n", 2) 112 | {:ok, ["OK", "COOL"], ""} 113 | 114 | iex> {:continuation, fun} = Redix.Protocol.parse_multi("+OK\r\n", 2) 115 | iex> fun.("+OK\r\n") 116 | {:ok, ["OK", "OK"], ""} 117 | 118 | """ 119 | @spec parse_multi(binary, non_neg_integer) :: on_parse([redis_value]) 120 | def parse_multi(data, nelems) 121 | 122 | # We treat the case when we have just one element to parse differently as it's 123 | # a very common case since single commands are treated as pipelines with just 124 | # one command in them. 125 | def parse_multi(data, 1) do 126 | resolve_cont(parse(data), &{:ok, [&1], &2}) 127 | end 128 | 129 | def parse_multi(data, n) do 130 | take_elems(data, n, []) 131 | end 132 | 133 | # Type parsers 134 | 135 | defp parse_simple_string(data) do 136 | until_crlf(data) 137 | end 138 | 139 | defp parse_error(data) do 140 | data 141 | |> until_crlf() 142 | |> resolve_cont(&{:ok, %Error{message: &1}, &2}) 143 | end 144 | 145 | defp parse_integer(""), do: {:continuation, &parse_integer/1} 146 | 147 | defp parse_integer("-" <> rest), 148 | do: resolve_cont(parse_integer_without_sign(rest), &{:ok, -&1, &2}) 149 | 150 | defp parse_integer(bin), do: parse_integer_without_sign(bin) 151 | 152 | defp parse_integer_without_sign("") do 153 | {:continuation, &parse_integer_without_sign/1} 154 | end 155 | 156 | defp parse_integer_without_sign(<> = bin) when digit in ?0..?9 do 157 | resolve_cont(parse_integer_digits(bin, 0), fn i, rest -> 158 | resolve_cont(until_crlf(rest), fn 159 | "", rest -> 160 | {:ok, i, rest} 161 | 162 | <>, _rest -> 163 | raise ParseError, message: "expected CRLF, found: #{inspect(<>)}" 164 | end) 165 | end) 166 | end 167 | 168 | defp parse_integer_without_sign(<>) do 169 | raise ParseError, message: "expected integer, found: #{inspect(<>)}" 170 | end 171 | 172 | defp parse_integer_digits(<>, acc) when digit in ?0..?9, 173 | do: parse_integer_digits(rest, acc * 10 + (digit - ?0)) 174 | 175 | defp parse_integer_digits(<<_non_digit, _::binary>> = rest, acc), do: {:ok, acc, rest} 176 | defp parse_integer_digits(<<>>, acc), do: {:continuation, &parse_integer_digits(&1, acc)} 177 | 178 | defp parse_bulk_string(rest) do 179 | resolve_cont(parse_integer(rest), fn 180 | -1, rest -> 181 | {:ok, nil, rest} 182 | 183 | size, rest -> 184 | parse_string_of_known_size(rest, size) 185 | end) 186 | end 187 | 188 | defp parse_string_of_known_size(data, size) do 189 | case data do 190 | <> -> 191 | {:ok, str, rest} 192 | 193 | _ -> 194 | {:continuation, &parse_string_of_known_size(data <> &1, size)} 195 | end 196 | end 197 | 198 | defp parse_array(rest) do 199 | resolve_cont(parse_integer(rest), fn 200 | -1, rest -> 201 | {:ok, nil, rest} 202 | 203 | size, rest -> 204 | take_elems(rest, size, []) 205 | end) 206 | end 207 | 208 | defp until_crlf(data, acc \\ "") 209 | 210 | defp until_crlf(<<@crlf, rest::binary>>, acc), do: {:ok, acc, rest} 211 | defp until_crlf(<<>>, acc), do: {:continuation, &until_crlf(&1, acc)} 212 | defp until_crlf(<>, acc), do: {:continuation, &until_crlf(<>, acc)} 213 | defp until_crlf(<>, acc), do: until_crlf(rest, <>) 214 | 215 | defp take_elems(data, 0, acc) do 216 | {:ok, Enum.reverse(acc), data} 217 | end 218 | 219 | defp take_elems(<<_, _::binary>> = data, n, acc) when n > 0 do 220 | resolve_cont(parse(data), fn elem, rest -> 221 | take_elems(rest, n - 1, [elem | acc]) 222 | end) 223 | end 224 | 225 | defp take_elems(<<>>, n, acc) do 226 | {:continuation, &take_elems(&1, n, acc)} 227 | end 228 | 229 | defp resolve_cont({:ok, val, rest}, ok) when is_function(ok, 2), do: ok.(val, rest) 230 | 231 | defp resolve_cont({:continuation, cont}, ok), 232 | do: {:continuation, fn new_data -> resolve_cont(cont.(new_data), ok) end} 233 | end 234 | --------------------------------------------------------------------------------