-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3182b0c
commit baae7d6
Showing
20 changed files
with
610 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# Memore | ||
|
||
Memory-Cache service for `Kingdom-System` project. | ||
|
||
|
||
## Usage | ||
### Manually start server | ||
|
||
```sh | ||
user@machine %: iex -S mix | ||
``` | ||
|
||
![alt text](https://github.com/0xBLCKLPTN/Memore/blob/master/docs/screenshots/code.png) | ||
|
||
### Connect to server and manipulate with him | ||
```sh | ||
user@machine %:telnet 127.0.0.1 4040 | ||
``` | ||
![alt text](https://github.com/0xBLCKLPTN/Memore/blob/master/docs/screenshots/telnet_output.png) | ||
|
||
### Commands: | ||
```sh | ||
GET, PUT, DELETE, CREATE | ||
``` | ||
|
||
### Example: | ||
```sh | ||
user@machine %: telnet 127.0.0.1 4040 | ||
Trying 127.0.0.1... | ||
Connected to localhost. | ||
Escape character is '^]'. | ||
|
||
CREATE shopping_list | ||
|
||
PUT shopping_list milk 1 | ||
PUT shopping_list name 'Andrey' | ||
|
||
GET shopping_list milk | ||
GET shopping_list name | ||
|
||
DELETE shopping_list milk | ||
DELETE shopping_list name | ||
``` | ||
|
||
# Tests | ||
All checks have now been passed | ||
```sh | ||
Finished in 0.1 seconds (0.06s async, 0.06s sync) | ||
8 doctests, 2 tests, 1 failure | ||
``` | ||
|
||
### TODO: | ||
- [ ] ci/cd | ||
- [ ] docker image | ||
- [x] gen_tcp server | ||
- [x] write docmodules | ||
- [x] write normal README.md |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# Memore | ||
|
||
**TODO: Add description** | ||
|
||
## Installation | ||
|
||
If [available in Hex](https://hex.pm/docs/publish), the package can be installed | ||
by adding `memore` to your list of dependencies in `mix.exs`: | ||
|
||
```elixir | ||
def deps do | ||
[ | ||
{:memore, "~> 0.1.0"} | ||
] | ||
end | ||
``` | ||
|
||
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) | ||
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can | ||
be found at <https://hexdocs.pm/memore>. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
defmodule Memore do | ||
use Agent, restart: :temporary | ||
use Application | ||
@moduledoc """ | ||
Documentation for Memore. | ||
""" | ||
|
||
@impl true | ||
def start(_type, _args) do | ||
Superwizard.start_link(name: Superwizard) | ||
end | ||
|
||
@doc """ | ||
Starts a new memore instance. | ||
""" | ||
def start_link(_opts) do | ||
Agent.start_link(fn -> %{} end) | ||
end | ||
|
||
@doc """ | ||
Get a value from the 'memore' by `key` | ||
""" | ||
def get(memore, key) do | ||
Agent.get(memore, &Map.get(&1, key)) | ||
end | ||
|
||
@doc """ | ||
Pus the `value` for the giving `key` in the `memore`. | ||
""" | ||
def put(memore, key, value) do | ||
Agent.update(memore, &Map.put(&1, key, value)) | ||
end | ||
|
||
@doc """ | ||
Deletes `key` from `bucket`. | ||
Returns the current value of `key`, if `key` exists. | ||
""" | ||
def delete(memore, key) do | ||
Agent.get_and_update(memore, fn dict -> Map.pop(dict, key) end) | ||
end | ||
|
||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
defmodule Registry do | ||
use GenServer | ||
|
||
## Client API | ||
|
||
@doc """ | ||
Starts the registry | ||
""" | ||
|
||
def start_link(opts) do | ||
|
||
# 1. pass the name to GenServer's init | ||
server = Keyword.fetch!(opts, :name) | ||
GenServer.start_link(__MODULE__, server, opts) | ||
end | ||
|
||
@doc """ | ||
Looks up the memore pid for `name` stored in `server` | ||
Returns `{:ok, pid}` if the bucket exists, `:error` otherwise. | ||
""" | ||
def lookup(server, name) do | ||
|
||
# 2. Lookup os now done directrly in ETS, without accessing the server. | ||
case :ets.lookup(server, name) do | ||
[{^name, pid}] -> {:ok, pid} | ||
[] -> :error | ||
end | ||
end | ||
|
||
@doc """ | ||
Ensures there is a memore associated with given `name` in `server`. | ||
""" | ||
def create(server, name) do | ||
GenServer.call(server, {:create, name}) | ||
end | ||
|
||
## Server callbacks | ||
|
||
@impl true | ||
def init(table) do | ||
# 3. We have replaced the names map by the ETS table. | ||
names = :ets.new(table, [:named_table, read_concurrency: true]) | ||
refs = %{} | ||
{:ok, {names, refs}} | ||
end | ||
|
||
# 4. The previous handle_call callback for lookup was removed | ||
|
||
@impl true | ||
def handle_call({:create, name}, _from, {names, refs}) do | ||
|
||
# 5. Read and write to the ETS table insead of the map | ||
case lookup(names, name) do | ||
{:ok, pid} -> | ||
{:reply, pid, {names,refs}} | ||
:error -> | ||
{:ok, pid} = DynamicSupervisor.start_child(MemoreSupervisor, Memore) | ||
ref = Process.monitor(pid) | ||
refs = Map.put(refs, ref, name) | ||
:ets.insert(names, {name, pid}) | ||
{:reply, pid, {names, refs}} | ||
end | ||
end | ||
|
||
@impl true | ||
def handle_info({:DOWN, ref, :process, _pid, _reason}, {names,refs}) do | ||
# 6. Delete from the ETS table instead of the map | ||
{name, refs} = Map.pop(refs, ref) | ||
:ets.delete(names, name) | ||
{:noreply, {names, refs}} | ||
end | ||
|
||
@impl true | ||
def handle_info(_msg, state) do | ||
{:noreply, state} | ||
end | ||
|
||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
defmodule Superwizard do | ||
use Supervisor | ||
|
||
def start_link(opts) do | ||
Supervisor.start_link(__MODULE__, :ok, opts) | ||
end | ||
|
||
@impl true | ||
def init(:ok) do | ||
children = [ | ||
{DynamicSupervisor, name: MemoreSupervisor, stategy: :one_for_one}, | ||
{Registry, name: Registry}, | ||
] | ||
|
||
Supervisor.init(children, strategy: :one_for_all) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
defmodule Memore.MixProject do | ||
use Mix.Project | ||
|
||
def project do | ||
[ | ||
app: :memore, | ||
build_path: "../../_build", | ||
config_path: "../../config/config.exs", | ||
deps_path: "../../deps", | ||
lockfile: "../../mix.lock", | ||
version: "0.1.0", | ||
elixir: "~> 1.14", | ||
start_permanent: Mix.env() == :prod, | ||
deps: deps() | ||
] | ||
end | ||
|
||
# Run "mix help compile.app" to learn about applications. | ||
def application do | ||
[ | ||
extra_applications: [:logger], | ||
mod: {Memore, []} | ||
] | ||
end | ||
|
||
# Run "mix help deps" to learn about dependencies. | ||
defp deps do | ||
[ | ||
] | ||
|
||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
defmodule MemoreTest do | ||
use ExUnit.Case, async: true | ||
doctest Memore | ||
|
||
setup context do | ||
_ = start_supervised!({Registry, name: context.test}) | ||
%{registry: context.test} | ||
end | ||
|
||
test "spawn memores", %{registry: registry} do | ||
assert Registry.lookup(registry, "shopping") == :error | ||
|
||
Registry.create(registry, "shopping") | ||
assert {:ok, memore} = Registry.lookup(registry, "shopping") | ||
|
||
Memore.put(memore, "milk", 1) | ||
assert Memore.get(memore, "milk") == 1 | ||
end | ||
|
||
test "remove memores on exit", %{registry: registry} do | ||
Registry.create(registry, "shopping") | ||
{:ok, memore} = Registry.lookup(registry, "shopping") | ||
Agent.stop(memore) | ||
|
||
_ = Registry.create(registry, "bogus") | ||
assert Registry.lookup(registry, "shopping") == :error | ||
end | ||
|
||
test "removes bucket on crash", %{registry: registry} do | ||
Registry.create(registry, "shopping") | ||
{:ok, memore} = Registry.lookup(registry, "shopping") | ||
|
||
# Stop the memore with non-normal reason | ||
Agent.stop(memore, :shutdown) | ||
|
||
_ = Registry.create(registry, "bogus") | ||
assert Registry.lookup(registry, "shopping") == :error | ||
end | ||
|
||
test "are temporary workers" do | ||
assert Supervisor.child_spec(Memore, []).restart == :temporary | ||
end | ||
|
||
test "memore can crash at any time", %{registry: registry} do | ||
Registry.create(registry, "shopping") | ||
{:ok, memore} = Registry.lookup(registry, "shopping") | ||
|
||
# Simulate a memore crash by explicitly and synchronously shuttibg it down | ||
Agent.stop(memore, :shutdown) | ||
catch_exit Memore.put(memore, "milk", 3) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
ExUnit.start() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# MemoreServer | ||
|
||
**TODO: Add description** | ||
|
||
## Installation | ||
|
||
If [available in Hex](https://hex.pm/docs/publish), the package can be installed | ||
by adding `memore_server` to your list of dependencies in `mix.exs`: | ||
|
||
```elixir | ||
def deps do | ||
[ | ||
{:memore_server, "~> 0.1.0"} | ||
] | ||
end | ||
``` | ||
|
||
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) | ||
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can | ||
be found at <https://hexdocs.pm/memore_server>. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
defmodule MemoreServer do | ||
require Logger | ||
|
||
def accept(port) do | ||
{:ok, socket} = | ||
:gen_tcp.listen(port, [:binary, packet: :line, active: false, reuseaddr: true]) | ||
Logger.info("Accepting connections on port #{port}") | ||
loop_acceptor(socket) | ||
end | ||
|
||
defp loop_acceptor(socket) do | ||
{:ok, client} = :gen_tcp.accept(socket) | ||
{:ok, pid} = Task.Supervisor.start_child(MemoreServer.TaskSupervisor, fn -> serve(client) end) | ||
:ok = :gen_tcp.controlling_process(client, pid) | ||
loop_acceptor(socket) | ||
end | ||
|
||
# socket |> read_line() |> write_line(socket) == write_line(read_line(socket), socket) | ||
defp serve(socket) do | ||
msg = | ||
with {:ok, data} <- read_line(socket), | ||
{:ok, command} <- MemoreServer.Command.parse(data), | ||
do: MemoreServer.Command.run(command) | ||
|
||
write_line(socket,msg) | ||
serve(socket) | ||
end | ||
|
||
defp read_line(socket) do | ||
:gen_tcp.recv(socket, 0) | ||
end | ||
|
||
defp write_line(socket, {:ok, text}) do | ||
:gen_tcp.send(socket, text) | ||
end | ||
|
||
defp write_line(socket, {:error, :not_found}) do | ||
# Known error; write to the client | ||
:gen_tcp.send(socket, "NOT FOUND\r\n") | ||
end | ||
|
||
defp write_line(_socket, {:error, :closed}) do | ||
# The connection was closed, exit politely | ||
exit(:shutdown) | ||
end | ||
|
||
defp write_line(socket, {:error, error}) do | ||
:gen_tcp.send(socket, "ERROR\r\n") | ||
exit(error) | ||
end | ||
|
||
defp write_line(socket, {:error, :unknown_command}) do | ||
:gen_tcp.send(socket, "UNKNOWN COMMAND\r\n") | ||
end | ||
end |
Oops, something went wrong.