├── apps ├── kv_server │ ├── test │ │ ├── test_helper.exs │ │ ├── kv_server │ │ │ └── command_test.exs │ │ └── kv_server_test.exs │ ├── .formatter.exs │ ├── README.md │ ├── .gitignore │ ├── lib │ │ ├── kv_server │ │ │ ├── application.ex │ │ │ └── command.ex │ │ └── kv_server.ex │ ├── mix.exs │ └── config │ │ └── config.exs └── kv │ ├── test │ ├── kv_test.exs │ ├── test_helper.exs │ └── kv │ │ ├── router_test.exs │ │ ├── bucket_test.exs │ │ └── registry_test.exs │ ├── .formatter.exs │ ├── lib │ ├── kv.ex │ └── kv │ │ ├── supervisor.ex │ │ ├── bucket.ex │ │ ├── router.ex │ │ └── registry.ex │ ├── README.md │ ├── .gitignore │ ├── mix.exs │ └── config │ └── config.exs ├── README.md ├── .formatter.exs ├── mix.exs ├── .gitignore ├── config └── config.exs └── .vscode └── launch.json /apps/kv_server/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KvUmbrella 2 | 3 | **TODO: Add description** 4 | 5 | -------------------------------------------------------------------------------- /apps/kv/test/kv_test.exs: -------------------------------------------------------------------------------- 1 | defmodule KVTest do 2 | use ExUnit.Case 3 | doctest KV 4 | end 5 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "config/*.exs"], 4 | subdirectories: ["apps/*"] 5 | ] 6 | -------------------------------------------------------------------------------- /apps/kv/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /apps/kv/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | exclude = if Node.alive?(), do: [], else: [distributed: true] 2 | 3 | ExUnit.start(exclude: exclude) 4 | -------------------------------------------------------------------------------- /apps/kv_server/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /apps/kv_server/test/kv_server/command_test.exs: -------------------------------------------------------------------------------- 1 | defmodule KVServer.CommandTest do 2 | use ExUnit.Case, async: true 3 | doctest KVServer.Command 4 | end 5 | -------------------------------------------------------------------------------- /apps/kv/lib/kv.ex: -------------------------------------------------------------------------------- 1 | defmodule KV do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | KV.Supervisor.start_link(name: KV.Supervisor) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /apps/kv/lib/kv/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule KV.Supervisor do 2 | use Supervisor 3 | 4 | def start_link(opts) do 5 | Supervisor.start_link(__MODULE__, :ok, opts) 6 | end 7 | 8 | def init(:ok) do 9 | children = [ 10 | {DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one}, 11 | {KV.Registry, name: KV.Registry}, 12 | {Task.Supervisor, name: KV.RouterTasks} 13 | ] 14 | 15 | Supervisor.init(children, strategy: :one_for_all) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule KvUmbrella.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | apps_path: "apps", 7 | start_permanent: Mix.env() == :prod, 8 | deps: deps() 9 | ] 10 | end 11 | 12 | # Dependencies listed here are available only for this 13 | # project and cannot be accessed from applications inside 14 | # the apps folder. 15 | # 16 | # Run "mix help deps" for examples and options. 17 | defp deps do 18 | [] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /apps/kv/README.md: -------------------------------------------------------------------------------- 1 | # KV 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `kv` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:kv, "~> 0.1.0"} 14 | ] 15 | end 16 | ``` 17 | 18 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 19 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 20 | be found at [https://hexdocs.pm/kv](https://hexdocs.pm/kv). 21 | 22 | -------------------------------------------------------------------------------- /apps/kv/test/kv/router_test.exs: -------------------------------------------------------------------------------- 1 | defmodule KV.RouterTest do 2 | use ExUnit.Case, async: true 3 | 4 | @tag :distributed 5 | test "route requests across nodes" do 6 | assert KV.Router.route("hello", Kernel, :node, []) == 7 | :"foo@DESKTOP-QIGQPFK" 8 | 9 | assert KV.Router.route("world", Kernel, :node, []) == 10 | :"bar@DESKTOP-QIGQPFK" 11 | end 12 | 13 | test "raises on unknown entries" do 14 | assert_raise RuntimeError, ~r/could not find entry/, fn -> 15 | KV.Router.route(<<0>>, Kernel, :node, []) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /apps/kv_server/README.md: -------------------------------------------------------------------------------- 1 | # KVServer 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `kv_server` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:kv_server, "~> 0.1.0"} 14 | ] 15 | end 16 | ``` 17 | 18 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 19 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 20 | be found at [https://hexdocs.pm/kv_server](https://hexdocs.pm/kv_server). 21 | 22 | -------------------------------------------------------------------------------- /.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 | /.elixir_ls/ 23 | -------------------------------------------------------------------------------- /apps/kv/test/kv/bucket_test.exs: -------------------------------------------------------------------------------- 1 | defmodule KV.BucketTest do 2 | use ExUnit.Case, async: true 3 | 4 | setup do 5 | bucket = start_supervised!(KV.Bucket) 6 | %{bucket: bucket} 7 | end 8 | 9 | test "stores values by key", %{bucket: bucket} do 10 | assert KV.Bucket.get(bucket, "milk") == nil 11 | 12 | KV.Bucket.put(bucket, "milk", 3) 13 | assert KV.Bucket.get(bucket, "milk") == 3 14 | assert KV.Bucket.delete(bucket, "milk") == 3 15 | assert KV.Bucket.get(bucket, "milk") == nil 16 | end 17 | 18 | test "are temporary workers" do 19 | assert Supervisor.child_spec(KV.Bucket, []).restart == :temporary 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /apps/kv/.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 | kv-*.tar 24 | 25 | -------------------------------------------------------------------------------- /apps/kv_server/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | kv_server-*.tar 24 | 25 | -------------------------------------------------------------------------------- /apps/kv/lib/kv/bucket.ex: -------------------------------------------------------------------------------- 1 | defmodule KV.Bucket do 2 | use Agent, restart: :temporary 3 | 4 | @doc """ 5 | Starts a new bucket. 6 | """ 7 | def start_link(_opts) do 8 | Agent.start_link(fn -> %{} end) 9 | end 10 | 11 | @doc """ 12 | Gets a value from the `bucket` by `key`. 13 | """ 14 | def get(bucket, key) do 15 | Agent.get(bucket, &Map.get(&1, key)) 16 | end 17 | 18 | @doc """ 19 | Puts the `value` for the given `key` in the `bucket`. 20 | """ 21 | def put(bucket, key, value) do 22 | Agent.update(bucket, &Map.put(&1, key, value)) 23 | end 24 | 25 | @doc """ 26 | Deletes `key` from `bucket`. 27 | 28 | Returns the current value of `key`, if `key` exists. 29 | """ 30 | def delete(bucket, key) do 31 | Agent.get_and_update(bucket, &Map.pop(&1, key)) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /apps/kv/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule KV.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :kv, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.8", 13 | start_permanent: Mix.env() == :prod, 14 | deps: deps() 15 | ] 16 | end 17 | 18 | # Run "mix help compile.app" to learn about applications. 19 | def application do 20 | [ 21 | extra_applications: [:logger], 22 | env: [routing_table: []], 23 | mod: {KV, {}} 24 | ] 25 | end 26 | 27 | # Run "mix help deps" to learn about dependencies. 28 | defp deps do 29 | [ 30 | # {:dep_from_hexpm, "~> 0.3.0"}, 31 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 32 | ] 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /apps/kv_server/lib/kv_server/application.ex: -------------------------------------------------------------------------------- 1 | defmodule KVServer.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 | port = String.to_integer(System.get_env("PORT") || "4040") 10 | 11 | # List all child processes to be supervised 12 | children = [ 13 | # Starts a worker by calling: KVServer.Worker.start_link(arg) 14 | # {KVServer.Worker, arg} 15 | {Task.Supervisor, name: KVServer.TaskSupervisor}, 16 | Supervisor.child_spec({Task, fn -> KVServer.accept(port) end}, restart: :permanent) 17 | ] 18 | 19 | # See https://hexdocs.pm/elixir/Supervisor.html 20 | # for other strategies and supported options 21 | opts = [strategy: :one_for_one, name: KVServer.Supervisor] 22 | Supervisor.start_link(children, opts) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # By default, the umbrella project as well as each child 6 | # application will require this configuration file, as 7 | # configuration and dependencies are shared in an umbrella 8 | # project. While one could configure all applications here, 9 | # we prefer to keep the configuration of each individual 10 | # child application in their own app, but all other 11 | # dependencies, regardless if they belong to one or multiple 12 | # apps, should be configured in the umbrella to avoid confusion. 13 | import_config "../apps/*/config/config.exs" 14 | 15 | # Sample configuration (overrides the imported configuration above): 16 | # 17 | # config :logger, :console, 18 | # level: :info, 19 | # format: "$date $time [$level] $metadata$message\n", 20 | # metadata: [:user_id] 21 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "mix_task", 9 | "name": "mix (Default task)", 10 | "request": "launch", 11 | "projectDir": "${workspaceRoot}" 12 | }, 13 | { 14 | "type": "mix_task", 15 | "name": "mix test", 16 | "request": "launch", 17 | "task": "test", 18 | "taskArgs": [ 19 | "--trace" 20 | ], 21 | "startApps": true, 22 | "projectDir": "${workspaceRoot}", 23 | "requireFiles": [ 24 | "test/**/test_helper.exs", 25 | "test/**/*_test.exs" 26 | ] 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /apps/kv_server/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule KVServer.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :kv_server, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.8", 13 | start_permanent: Mix.env() == :prod, 14 | deps: deps() 15 | ] 16 | end 17 | 18 | # Run "mix help compile.app" to learn about applications. 19 | def application do 20 | [ 21 | extra_applications: [:logger], 22 | mod: {KVServer.Application, []} 23 | ] 24 | end 25 | 26 | # Run "mix help deps" to learn about dependencies. 27 | defp deps do 28 | [ 29 | # {:dep_from_hexpm, "~> 0.3.0"}, 30 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}, 31 | # {:sibling_app_in_umbrella, in_umbrella: true} 32 | {:kv, in_umbrella: true} 33 | ] 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /apps/kv/lib/kv/router.ex: -------------------------------------------------------------------------------- 1 | defmodule KV.Router do 2 | @doc """ 3 | Dispatch the given `mod`, `fun`, `args` request 4 | to the appropriate node based on the `bucket`. 5 | """ 6 | def route(bucket, mod, fun, args) do 7 | # Get the first byte of the binary 8 | first = :binary.first(bucket) 9 | 10 | # Try to find an entry in the table() or raise 11 | entry = 12 | Enum.find(table(), fn {enum, _node} -> 13 | first in enum 14 | end) || no_entry_error(bucket) 15 | 16 | # If the entry node is the current node 17 | if elem(entry, 1) == node() do 18 | apply(mod, fun, args) 19 | else 20 | {KV.RouterTasks, elem(entry, 1)} 21 | |> Task.Supervisor.async(KV.Router, :route, [bucket, mod, fun, args]) 22 | |> Task.await() 23 | end 24 | end 25 | 26 | defp no_entry_error(bucket) do 27 | raise "could not find entry for #{inspect(bucket)} in table #{inspect(table())}" 28 | end 29 | 30 | @doc """ 31 | The routing table. 32 | """ 33 | def table do 34 | Application.fetch_env!(:kv, :routing_table) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /apps/kv_server/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # third-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :kv_server, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:kv_server, :key) 18 | # 19 | # You can also configure a third-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env()}.exs" 31 | -------------------------------------------------------------------------------- /apps/kv/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # third-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :kv, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:kv, :key) 18 | # 19 | # You can also configure a third-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env()}.exs" 31 | 32 | # Replace computer-name with your local machine nodes 33 | config :kv, :routing_table, [ 34 | {?a..?m, :"foo@computer-name"}, 35 | {?n..?z, :"bar@computer-name"} 36 | ] 37 | -------------------------------------------------------------------------------- /apps/kv_server/test/kv_server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule KVServerTest do 2 | use ExUnit.Case 3 | doctest KVServer 4 | 5 | @moduletag :capture_log 6 | 7 | setup do 8 | Application.stop(:kv) 9 | :ok = Application.start(:kv) 10 | end 11 | 12 | setup do 13 | opts = [:binary, packet: :line, active: false] 14 | {:ok, socket} = :gen_tcp.connect('localhost', 4040, opts) 15 | %{socket: socket} 16 | end 17 | 18 | test "server interaction", %{socket: socket} do 19 | assert send_and_recv(socket, "UNKNOWN shopping\r\n") == 20 | "UNKNOWN COMMAND\r\n" 21 | 22 | assert send_and_recv(socket, "GET shopping eggs\r\n") == 23 | "NOT FOUND\r\n" 24 | 25 | assert send_and_recv(socket, "CREATE shopping\r\n") == 26 | "OK\r\n" 27 | 28 | assert send_and_recv(socket, "PUT shopping eggs 3\r\n") == 29 | "OK\r\n" 30 | 31 | # GET returns two lines 32 | assert send_and_recv(socket, "GET shopping eggs\r\n") == "3\r\n" 33 | assert send_and_recv(socket, "") == "OK\r\n" 34 | 35 | assert send_and_recv(socket, "DELETE shopping eggs\r\n") == 36 | "OK\r\n" 37 | 38 | # GET returns two lines 39 | assert send_and_recv(socket, "GET shopping eggs\r\n") == "\r\n" 40 | assert send_and_recv(socket, "") == "OK\r\n" 41 | end 42 | 43 | defp send_and_recv(socket, command) do 44 | :ok = :gen_tcp.send(socket, command) 45 | {:ok, data} = :gen_tcp.recv(socket, 0, 1000) 46 | data 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /apps/kv/test/kv/registry_test.exs: -------------------------------------------------------------------------------- 1 | defmodule KV.RegistryTest do 2 | use ExUnit.Case, async: true 3 | 4 | setup context do 5 | _ = start_supervised!({KV.Registry, name: context.test}) 6 | %{registry: context.test} 7 | end 8 | 9 | test "spawns buckets", %{registry: registry} do 10 | assert KV.Registry.lookup(registry, "shopping") == :error 11 | 12 | KV.Registry.create(registry, "shopping") 13 | assert {:ok, bucket} = KV.Registry.lookup(registry, "shopping") 14 | 15 | KV.Bucket.put(bucket, "milk", 1) 16 | assert KV.Bucket.get(bucket, "milk") == 1 17 | end 18 | 19 | test "removes buckets on exit", %{registry: registry} do 20 | KV.Registry.create(registry, "shopping") 21 | {:ok, bucket} = KV.Registry.lookup(registry, "shopping") 22 | Agent.stop(bucket) 23 | 24 | # Do a call to ensure the registry processed the DOWN message 25 | _ = KV.Registry.create(registry, "bogus") 26 | assert KV.Registry.lookup(registry, "shopping") == :error 27 | end 28 | 29 | test "removes bucket on crash", %{registry: registry} do 30 | KV.Registry.create(registry, "shopping") 31 | {:ok, bucket} = KV.Registry.lookup(registry, "shopping") 32 | 33 | # Stop the bucket with non-normal reason 34 | Agent.stop(bucket, :shutdown) 35 | 36 | # Do a call to ensure the registry processed the DOWN message 37 | _ = KV.Registry.create(registry, "bogus") 38 | assert KV.Registry.lookup(registry, "shopping") == :error 39 | end 40 | 41 | test "bucket can crash at any time", %{registry: registry} do 42 | KV.Registry.create(registry, "shopping") 43 | {:ok, bucket} = KV.Registry.lookup(registry, "shopping") 44 | 45 | # Simulate a bucket crash by explicitly and synchronously shutting it down 46 | Agent.stop(bucket, :shutdown) 47 | 48 | # Now trying to call the dead process causes a :noproc exit 49 | catch_exit(KV.Bucket.put(bucket, "milk", 3)) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /apps/kv/lib/kv/registry.ex: -------------------------------------------------------------------------------- 1 | defmodule KV.Registry do 2 | use GenServer 3 | 4 | ## Client API 5 | 6 | @doc """ 7 | Starts the registry with the given options. 8 | 9 | `:name` is always required. 10 | """ 11 | def start_link(opts) do 12 | server = Keyword.fetch!(opts, :name) 13 | GenServer.start_link(__MODULE__, server, opts) 14 | end 15 | 16 | @doc """ 17 | Looks up the bucket pid for `name` stored in `server`. 18 | 19 | Returns `{:ok, pid}` if the bucket exists, `:error` otherwise. 20 | """ 21 | def lookup(server, name) do 22 | case :ets.lookup(server, name) do 23 | [{^name, pid}] -> {:ok, pid} 24 | [] -> :error 25 | end 26 | end 27 | 28 | @doc """ 29 | Ensures there is a bucket associated with the given `name` in `server`. 30 | """ 31 | def create(server, name) do 32 | GenServer.call(server, {:create, name}) 33 | end 34 | 35 | @doc """ 36 | Stops the registry. 37 | """ 38 | def stop(server) do 39 | GenServer.stop(server) 40 | end 41 | 42 | ## Server Callbacks 43 | 44 | def init(table) do 45 | names = :ets.new(table, [:named_table, read_concurrency: true]) 46 | refs = %{} 47 | {:ok, {names, refs}} 48 | end 49 | 50 | def handle_call({:create, name}, _from, {names, refs}) do 51 | case lookup(names, name) do 52 | {:ok, pid} -> 53 | {:reply, pid, {names, refs}} 54 | 55 | :error -> 56 | {:ok, pid} = DynamicSupervisor.start_child(KV.BucketSupervisor, KV.Bucket) 57 | ref = Process.monitor(pid) 58 | refs = Map.put(refs, ref, name) 59 | :ets.insert(names, {name, pid}) 60 | {:reply, pid, {names, refs}} 61 | end 62 | end 63 | 64 | def handle_info({:DOWN, ref, :process, _pid, _reason}, {names, refs}) do 65 | {name, refs} = Map.pop(refs, ref) 66 | :ets.delete(names, name) 67 | {:noreply, {names, refs}} 68 | end 69 | 70 | def handle_info(_msg, state) do 71 | {:noreply, state} 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /apps/kv_server/lib/kv_server.ex: -------------------------------------------------------------------------------- 1 | defmodule KVServer do 2 | require Logger 3 | 4 | def accept(port) do 5 | # The options below mean: 6 | # 7 | # 1. `:binary` - receives data as binaries (instead of lists) 8 | # 2. `packet: :line` - receives data line by line 9 | # 3. `active: false` - blocks on `:gen_tcp.recv/2` until data is available 10 | # 4. `reuseaddr: true` - allows us to reuse the address if the listener crashes 11 | # 12 | {:ok, socket} = 13 | :gen_tcp.listen(port, [:binary, packet: :line, active: false, reuseaddr: true]) 14 | 15 | Logger.info("Accepting connections on port #{port}") 16 | loop_acceptor(socket) 17 | end 18 | 19 | defp loop_acceptor(socket) do 20 | {:ok, client} = :gen_tcp.accept(socket) 21 | {:ok, pid} = Task.Supervisor.start_child(KVServer.TaskSupervisor, fn -> serve(client) end) 22 | :ok = :gen_tcp.controlling_process(client, pid) 23 | loop_acceptor(socket) 24 | end 25 | 26 | defp serve(socket) do 27 | msg = 28 | with {:ok, data} <- read_line(socket), 29 | {:ok, command} <- KVServer.Command.parse(data), 30 | do: KVServer.Command.run(command) 31 | 32 | write_line(socket, msg) 33 | serve(socket) 34 | end 35 | 36 | defp read_line(socket) do 37 | :gen_tcp.recv(socket, 0) 38 | end 39 | 40 | defp write_line(socket, {:ok, text}) do 41 | :gen_tcp.send(socket, text) 42 | end 43 | 44 | defp write_line(socket, {:error, :unknown_command}) do 45 | # Known error; write to the client 46 | :gen_tcp.send(socket, "UNKNOWN COMMAND\r\n") 47 | end 48 | 49 | defp write_line(_socket, {:error, :closed}) do 50 | # The connection was closed, exit politely 51 | exit(:shutdown) 52 | end 53 | 54 | defp write_line(socket, {:error, :not_found}) do 55 | :gen_tcp.send(socket, "NOT FOUND\r\n") 56 | end 57 | 58 | defp write_line(socket, {:error, error}) do 59 | # Unknown error; write to the client and exit 60 | :gen_tcp.send(socket, "ERROR\r\n") 61 | exit(error) 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /apps/kv_server/lib/kv_server/command.ex: -------------------------------------------------------------------------------- 1 | defmodule KVServer.Command do 2 | @doc ~S""" 3 | Parses the given `line` into a command. 4 | 5 | ## Examples 6 | 7 | iex> KVServer.Command.parse "CREATE shopping\r\n" 8 | {:ok, {:create, "shopping"}} 9 | 10 | iex> KVServer.Command.parse "CREATE shopping \r\n" 11 | {:ok, {:create, "shopping"}} 12 | 13 | iex> KVServer.Command.parse "PUT shopping milk 1\r\n" 14 | {:ok, {:put, "shopping", "milk", "1"}} 15 | 16 | iex> KVServer.Command.parse "GET shopping milk\r\n" 17 | {:ok, {:get, "shopping", "milk"}} 18 | 19 | iex> KVServer.Command.parse "DELETE shopping eggs\r\n" 20 | {:ok, {:delete, "shopping", "eggs"}} 21 | 22 | Unknown commands or commands with the wrong number of 23 | arguments return an error: 24 | 25 | iex> KVServer.Command.parse "UNKNOWN shopping eggs\r\n" 26 | {:error, :unknown_command} 27 | 28 | iex> KVServer.Command.parse "GET shopping\r\n" 29 | {:error, :unknown_command} 30 | 31 | """ 32 | def parse(line) do 33 | case String.split(line) do 34 | ["CREATE", bucket] -> {:ok, {:create, bucket}} 35 | ["GET", bucket, key] -> {:ok, {:get, bucket, key}} 36 | ["PUT", bucket, key, value] -> {:ok, {:put, bucket, key, value}} 37 | ["DELETE", bucket, key] -> {:ok, {:delete, bucket, key}} 38 | _ -> {:error, :unknown_command} 39 | end 40 | end 41 | 42 | @doc """ 43 | Runs the given command. 44 | """ 45 | def run(command) 46 | 47 | def run({:create, bucket}) do 48 | KV.Registry.create(KV.Registry, bucket) 49 | {:ok, "OK\r\n"} 50 | end 51 | 52 | def run({:get, bucket, key}) do 53 | lookup(bucket, fn pid -> 54 | value = KV.Bucket.get(pid, key) 55 | {:ok, "#{value}\r\nOK\r\n"} 56 | end) 57 | end 58 | 59 | def run({:put, bucket, key, value}) do 60 | lookup(bucket, fn pid -> 61 | KV.Bucket.put(pid, key, value) 62 | {:ok, "OK\r\n"} 63 | end) 64 | end 65 | 66 | def run({:delete, bucket, key}) do 67 | lookup(bucket, fn pid -> 68 | KV.Bucket.delete(pid, key) 69 | {:ok, "OK\r\n"} 70 | end) 71 | end 72 | 73 | defp lookup(bucket, callback) do 74 | case KV.Registry.lookup(KV.Registry, bucket) do 75 | {:ok, pid} -> callback.(pid) 76 | :error -> {:error, :not_found} 77 | end 78 | end 79 | end 80 | --------------------------------------------------------------------------------