Initial storage implementation
This commit is contained in:
commit
8c4377499c
4
.formatter.exs
Normal file
4
.formatter.exs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Used by "mix format"
|
||||||
|
[
|
||||||
|
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||||
|
]
|
||||||
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# The directory Mix will write compiled artifacts to.
|
||||||
|
/_build/
|
||||||
|
|
||||||
|
# If you run "mix test --cover", coverage assets end up here.
|
||||||
|
/cover/
|
||||||
|
|
||||||
|
# The directory Mix downloads your dependencies sources to.
|
||||||
|
/deps/
|
||||||
|
|
||||||
|
# Where third-party dependencies like ExDoc output generated docs.
|
||||||
|
/doc/
|
||||||
|
|
||||||
|
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||||
|
erl_crash.dump
|
||||||
|
|
||||||
|
# Also ignore archive artifacts (built via "mix archive.build").
|
||||||
|
*.ez
|
||||||
|
|
||||||
|
# Ignore package tarball (built via "mix hex.build").
|
||||||
|
message_server-*.tar
|
||||||
|
|
||||||
|
# Temporary files, for example, from tests.
|
||||||
|
/tmp/
|
||||||
21
README.md
Normal file
21
README.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# MessageServer
|
||||||
|
|
||||||
|
**TODO: Add description**
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
|
||||||
|
by adding `message_server` to your list of dependencies in `mix.exs`:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
def deps do
|
||||||
|
[
|
||||||
|
{:message_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/message_server>.
|
||||||
|
|
||||||
10
lib/message_server.ex
Normal file
10
lib/message_server.ex
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
defmodule MessageServer do
|
||||||
|
@moduledoc """
|
||||||
|
MessageServer - a small system of two connected servers
|
||||||
|
|
||||||
|
The servers each own some users from which they accept requests.
|
||||||
|
If a request requires updating state of a user that is located on
|
||||||
|
another server, then the servers must communicate these changes
|
||||||
|
between themselves. Users only communicate with their “home” server.
|
||||||
|
"""
|
||||||
|
end
|
||||||
20
lib/message_server/application.ex
Normal file
20
lib/message_server/application.ex
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
defmodule MessageServer.Application do
|
||||||
|
# See https://hexdocs.pm/elixir/Application.html
|
||||||
|
# for more information on OTP Applications
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
use Application
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def start(_type, _args) do
|
||||||
|
children = [
|
||||||
|
# Starts a worker by calling: MessageServer.Worker.start_link(arg)
|
||||||
|
# {MessageServer.Worker, arg}
|
||||||
|
]
|
||||||
|
|
||||||
|
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||||
|
# for other strategies and supported options
|
||||||
|
opts = [strategy: :one_for_one, name: MessageServer.Supervisor]
|
||||||
|
Supervisor.start_link(children, opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
41
lib/message_server/storage.ex
Normal file
41
lib/message_server/storage.ex
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
defmodule MessageServer.Storage do
|
||||||
|
use GenServer
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
def start_link(server_id) do
|
||||||
|
GenServer.start_link(__MODULE__, server_id, name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec append_message(String.t(), String.t(), String.t()) :: :ok | {:error, String.t()}
|
||||||
|
def append_message(user_id, from_user, message) do
|
||||||
|
GenServer.call(__MODULE__, {:append_message, user_id, from_user, message})
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(server_id) do
|
||||||
|
storage_dir = "storage/server_#{server_id}"
|
||||||
|
File.mkdir_p!(storage_dir)
|
||||||
|
{:ok, %{storage_dir: storage_dir, server_id: server_id}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call({:append_message, user_id, from_user, message}, _from, state) do
|
||||||
|
case write_message_to_file(state, user_id, from_user, message) do
|
||||||
|
:ok ->
|
||||||
|
Logger.info("Message stored for user #{user_id}")
|
||||||
|
{:reply, :ok, state}
|
||||||
|
|
||||||
|
{:error, reason} = error ->
|
||||||
|
Logger.error("Failed to store message for user #{user_id}: #{reason}")
|
||||||
|
{:reply, error, state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec write_message_to_file(map(), String.t(), String.t(), String.t()) :: :ok | {:error, atom()}
|
||||||
|
defp write_message_to_file(%{storage_dir: storage_dir}, user_id, from_user, message) do
|
||||||
|
file_path = Path.join(storage_dir, "#{user_id}.txt")
|
||||||
|
content = "#{from_user}: #{message}\n"
|
||||||
|
|
||||||
|
File.write(file_path, content, [:append])
|
||||||
|
end
|
||||||
|
end
|
||||||
29
mix.exs
Normal file
29
mix.exs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
defmodule MessageServer.MixProject do
|
||||||
|
use Mix.Project
|
||||||
|
|
||||||
|
def project do
|
||||||
|
[
|
||||||
|
app: :message_server,
|
||||||
|
version: "0.1.0",
|
||||||
|
elixir: "~> 1.18",
|
||||||
|
start_permanent: Mix.env() == :prod,
|
||||||
|
deps: deps()
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def application do
|
||||||
|
[
|
||||||
|
extra_applications: [:logger],
|
||||||
|
mod: {MessageServer.Application, []}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp deps do
|
||||||
|
[
|
||||||
|
{:plug, "~> 1.16"},
|
||||||
|
{:bandit, "~> 1.5"},
|
||||||
|
{:req, "~> 0.5"},
|
||||||
|
{:jason, "~> 1.4"}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
16
mix.lock
Normal file
16
mix.lock
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
%{
|
||||||
|
"bandit": {:hex, :bandit, "1.7.0", "d1564f30553c97d3e25f9623144bb8df11f3787a26733f00b21699a128105c0c", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "3e2f7a98c7a11f48d9d8c037f7177cd39778e74d55c7af06fe6227c742a8168a"},
|
||||||
|
"finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"},
|
||||||
|
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
|
||||||
|
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||||
|
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||||
|
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
||||||
|
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||||
|
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||||
|
"plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"},
|
||||||
|
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
||||||
|
"req": {:hex, :req, "0.5.15", "662020efb6ea60b9f0e0fac9be88cd7558b53fe51155a2d9899de594f9906ba9", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a6513a35fad65467893ced9785457e91693352c70b58bbc045b47e5eb2ef0c53"},
|
||||||
|
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
||||||
|
"thousand_island": {:hex, :thousand_island, "1.3.14", "ad45ebed2577b5437582bcc79c5eccd1e2a8c326abf6a3464ab6c06e2055a34a", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d0d24a929d31cdd1d7903a4fe7f2409afeedff092d277be604966cd6aa4307ef"},
|
||||||
|
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||||
|
}
|
||||||
7
storage/server_test_server/test_server-alice.txt
Normal file
7
storage/server_test_server/test_server-alice.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
test_server-bob: Hello Alice!
|
||||||
|
test_server-bob: Hi Alice
|
||||||
|
test_server-bob: Hello! 🎉 Special chars: @#$%^&*()
|
||||||
|
test_server-bob: Test message
|
||||||
|
test_server-bob: First message
|
||||||
|
test_server-charlie: Second message
|
||||||
|
test_server-bob: Hello Alice!
|
||||||
1
storage/server_test_server/test_server-charlie.txt
Normal file
1
storage/server_test_server/test_server-charlie.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
test_server-bob: Hi Charlie
|
||||||
116
test/message_server/storage_test.exs
Normal file
116
test/message_server/storage_test.exs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
defmodule MessageServer.StorageTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
import ExUnit.CaptureLog
|
||||||
|
|
||||||
|
@test_server_id "test_server"
|
||||||
|
@test_storage_dir "storage/server_#{@test_server_id}"
|
||||||
|
|
||||||
|
setup do
|
||||||
|
# clean up any existing test storage
|
||||||
|
File.rm_rf!(@test_storage_dir)
|
||||||
|
|
||||||
|
# start the Storage GenServer for testing
|
||||||
|
{:ok, pid} = MessageServer.Storage.start_link(@test_server_id)
|
||||||
|
|
||||||
|
on_exit(fn ->
|
||||||
|
# clean up after test
|
||||||
|
if Process.alive?(pid), do: GenServer.stop(pid)
|
||||||
|
File.rm_rf!(@test_storage_dir)
|
||||||
|
end)
|
||||||
|
|
||||||
|
%{storage_pid: pid}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "creates storage directory on startup" do
|
||||||
|
assert File.exists?(@test_storage_dir)
|
||||||
|
assert File.dir?(@test_storage_dir)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "appends message to user file" do
|
||||||
|
user_id = "test_server-alice"
|
||||||
|
from_user = "test_server-bob"
|
||||||
|
message = "Hello Alice!"
|
||||||
|
|
||||||
|
assert :ok = MessageServer.Storage.append_message(user_id, from_user, message)
|
||||||
|
|
||||||
|
file_path = Path.join(@test_storage_dir, "#{user_id}.txt")
|
||||||
|
assert File.exists?(file_path)
|
||||||
|
|
||||||
|
content = File.read!(file_path)
|
||||||
|
assert content == "#{from_user}: #{message}\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "appends multiple messages to same user file" do
|
||||||
|
user_id = "test_server-alice"
|
||||||
|
|
||||||
|
assert :ok = MessageServer.Storage.append_message(user_id, "test_server-bob", "First message")
|
||||||
|
|
||||||
|
assert :ok =
|
||||||
|
MessageServer.Storage.append_message(
|
||||||
|
user_id,
|
||||||
|
"test_server-charlie",
|
||||||
|
"Second message"
|
||||||
|
)
|
||||||
|
|
||||||
|
file_path = Path.join(@test_storage_dir, "#{user_id}.txt")
|
||||||
|
content = File.read!(file_path)
|
||||||
|
|
||||||
|
expected_content = """
|
||||||
|
test_server-bob: First message
|
||||||
|
test_server-charlie: Second message
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert content == expected_content
|
||||||
|
end
|
||||||
|
|
||||||
|
test "creates separate files for different users" do
|
||||||
|
assert :ok =
|
||||||
|
MessageServer.Storage.append_message(
|
||||||
|
"test_server-alice",
|
||||||
|
"test_server-bob",
|
||||||
|
"Hi Alice"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert :ok =
|
||||||
|
MessageServer.Storage.append_message(
|
||||||
|
"test_server-charlie",
|
||||||
|
"test_server-bob",
|
||||||
|
"Hi Charlie"
|
||||||
|
)
|
||||||
|
|
||||||
|
alice_file = Path.join(@test_storage_dir, "test_server-alice.txt")
|
||||||
|
charlie_file = Path.join(@test_storage_dir, "test_server-charlie.txt")
|
||||||
|
|
||||||
|
assert File.exists?(alice_file)
|
||||||
|
assert File.exists?(charlie_file)
|
||||||
|
|
||||||
|
assert File.read!(alice_file) == "test_server-bob: Hi Alice\n"
|
||||||
|
assert File.read!(charlie_file) == "test_server-bob: Hi Charlie\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles messages with special characters" do
|
||||||
|
user_id = "test_server-alice"
|
||||||
|
from_user = "test_server-bob"
|
||||||
|
message = "Hello! 🎉 Special chars: @#$%^&*()"
|
||||||
|
|
||||||
|
assert :ok = MessageServer.Storage.append_message(user_id, from_user, message)
|
||||||
|
|
||||||
|
file_path = Path.join(@test_storage_dir, "#{user_id}.txt")
|
||||||
|
content = File.read!(file_path)
|
||||||
|
|
||||||
|
assert content == "#{from_user}: #{message}\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logs successful message storage" do
|
||||||
|
user_id = "test_server-alice"
|
||||||
|
from_user = "test_server-bob"
|
||||||
|
message = "Test message"
|
||||||
|
|
||||||
|
log_output =
|
||||||
|
capture_log(fn ->
|
||||||
|
assert :ok = MessageServer.Storage.append_message(user_id, from_user, message)
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert log_output =~ "Message stored for user #{user_id}"
|
||||||
|
end
|
||||||
|
end
|
||||||
1
test/test_helper.exs
Normal file
1
test/test_helper.exs
Normal file
@ -0,0 +1 @@
|
|||||||
|
ExUnit.start()
|
||||||
Loading…
x
Reference in New Issue
Block a user