diff --git a/README.MD b/README.MD index eb4fd82..f0be2cf 100644 --- a/README.MD +++ b/README.MD @@ -30,6 +30,7 @@ You can find language-specific implementations in their corresponding folder: - [Python](/docs/source/build-with-python.md) - [Rust](/docs/source/build-with-rust.md) - [Ruby](/docs/source/build-with-ruby.md) +- [Elixir](/docs/source/build-with-elixir.md) ## Sample Projects @@ -38,6 +39,8 @@ We also built the full project with some extra features for you use as study gui - [Rust Project](/rust) - [Python Project](/python) +- [Ruby Project](/ruby) +- [Elixir Project](/elixir) ## Contributing diff --git a/docs/source/_static/img/programming_languages/elixir.png b/docs/source/_static/img/programming_languages/elixir.png new file mode 100644 index 0000000..a17e49f Binary files /dev/null and b/docs/source/_static/img/programming_languages/elixir.png differ diff --git a/docs/source/build-with-elixir.md b/docs/source/build-with-elixir.md new file mode 100644 index 0000000..9bb8b4e --- /dev/null +++ b/docs/source/build-with-elixir.md @@ -0,0 +1,459 @@ +# Quick Start: Elixir + +## 1. Setup the Environment + +### 1.1 Downloading Elixir and dependencies: + +If you don't have Elixir and Erlang installed already on your machine, you can install from two possible sources: + +1. [Elixir main website](https://elixir-lang.org/install.html) +2. [asdf](https://asdf-vm.com/guide/getting-started.html) +> NOTE: After installing asdf correctly make sure to install [Erlang](https://github.com/asdf-vm/asdf-erlang) first and then [Elixir](https://github.com/asdf-vm/asdf-elixir). + +### 1.2 Starting the project + +Now with Elixir properly installed you can create a project using [mix](https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html). To create our project just run: + +```sh +mix new media_player +``` + +### 1.3 Setting the project dependencies + +Let's do a quick change into our `mix.exs` and add our project dependencies. + +```exs +defp deps do + [ + {:decimal, "~> 1.0"}, + {:xandra, "~> 0.14"} + ] +end +``` + +- [Decimal](https://hexdocs.pm/decimal/readme.html): Arbitrary precision decimal arithmetic +- [Xandra](https://github.com/lexhide/xandra): Fast, simple, and robust Cassandra/ScyllaDB driver for Elixir + +To carry out modifications, use the module already created in `lib/media_player.ex`, as this is where we are going to carry out some modifications. + +> NOTE: In this tutorial we are going to prepare an interactive project so that you can perform tests using Elixir's interactive shell. On every code update, don't forget to restart Elixir's interactive shell to perform the recompilation. + +## 2. Connecting to the Cluster + +Make sure to get the right credentials on your [ScyllaDB Cloud Dashboard](https://cloud.scylladb.com/clusters) in the tab `Connect`. + +```ex +def start_link do + options = [username: "scylla", password: "a-very-secure-password"] + + {:ok, cluster} = + Xandra.Cluster.start_link( + sync_connect: :infinity, + authentication: {Xandra.Authenticator.Password, options}, + nodes: [ + "node-0.aws-sa-east-1.xxx.clusters.scylla.cloud", + "node-1.aws-sa-east-1.xxx.clusters.scylla.cloud", + "node-2.aws-sa-east-1.xxx.clusters.scylla.cloud" + ], + pool_size: 10 + ) + + cluster +end +``` + +When starting a cluster notice that there is an option with the name `sync_connect` being informed. This option is used to inform that we are dealing with an asynchronous connection, saying to wait the necessary time (`:infinity`) to make the complete connection of the cluster. + +To test our connection let's initialize Elixir's interactive shell: + +```sh +iex -S mix +``` + +Then you will see a screen waiting for some input, with in the bottom left corner a message that looks like `iex(1)>` (this means we are ready to test our first module). To test it now, let's run: + +```ex +MediaPlayer.start_link +``` + +A process will be started, so the return should be nothing other than something like `#PID<0.230.0>`. Don't worry, in the next topic we'll run our first query and see a real result. This function will be used every time we need to start a connection link with our cluster. + +> If the connection got refused, check if your IP Address is added into allowed IPs. + +## 3. Handling Queries + +With `Xandra` you can run queries and save their returns in maps, making it possible to parse this information and manipulate it as you decide. First of all, let's create a function that will simply execute queries, receiving information from the cluster and the query to be executed as parameters. If the return is `:ok`, it means that the query executed successfully, so we return it. If the return is `:error`, it means that we had an error, so let's inspect it. An important detail is for the address, which instead of bringing a simple text brings a tuple with four integers. + +```ex +def run_query(cluster, query) do + case Xandra.Cluster.execute(cluster, query) do + {:ok, result} -> + result + + {:error, error} -> + IO.inspect(error) + end +end + +def handling_queries do + statement = "SELECT address, port, connection_stage FROM system.clients LIMIT 5;" + + run_query(start_link(), statement) + |> Enum.each(fn %{ + "address" => address, + "connection_stage" => connection_stage, + "port" => port + } -> + # `address` is a tuple of 4 integers + address_formated = + address + |> Tuple.to_list() + |> Enum.map(&Integer.to_string/1) + |> Enum.join(".") + + IO.puts("IP -> #{address_formated}, Port -> #{port}, CS -> #{connection_stage}") + end) +end +``` + +The output should look something like: + +``` +IP -> 123.123.123.69, Port -> 61667, CS -> READY +IP -> 123.123.123.69, Port -> 62377, CS -> AUTHENTICATING +IP -> 123.123.123.69, Port -> 63221, CS -> AUTHENTICATING +IP -> 123.123.123.69, Port -> 65225, CS -> READY +``` + +### 3.1 Creating a Keyspace + +The `keyspace` inside the ScyllaDB ecossystem can be interpreted as your `database` or `collection`. + +On your connection boot, you don't need to provide it but you will use it later and also is able to create when you need. + +```ex +def keyspace_exists?(keyspace_name) do + cluster = start_link() + + # In this case I won't use the `run_query` function because I want to + # show the possibility of using maps to bind its parameters. + %Xandra.Page{} = + page = + Xandra.Cluster.run(cluster, fn conn -> + prepared = + Xandra.prepare!( + conn, + "SELECT * FROM system_schema.keyspaces WHERE keyspace_name = :keyspace_name;" + ) + + Xandra.execute!(conn, prepared, %{"keyspace_name" => keyspace_name}) + end) + + Enum.to_list(page) != [] +end + +def create_keyspace(keyspace_name) do + case keyspace_exists?(keyspace_name) do + true -> + IO.puts("Keyspace already exists") + + false -> + cluster = start_link() + + query = "CREATE KEYSPACE IF NOT EXISTS #{keyspace_name} + WITH REPLICATION = { + 'class': 'NetworkTopologyStrategy', + 'replication_factor': '3' + } + AND durable_writes = true;" + + run_query(cluster, query) + + IO.puts("Keyspace created") + end +end +``` + +To test run your interactive shell again with `iex -S mix` and then run: + +```ex +iex(1)> MediaPlayer.create_keyspace("media_player") +``` + +Done! Now your keyspace is officially created! + +### 3.2 Creating a table + +A table is used to store part or all the data of your app (depends on how you will build it). Remember to add your `keyspace` into your connection and let's create a table to store our liked songs. + +```ex +def table_exists?(keyspace_name, table_name) do + cluster = start_link() + + %Xandra.Page{} = + page = + Xandra.Cluster.run(cluster, fn conn -> + prepared = + Xandra.prepare!( + conn, + "SELECT keyspace_name, table_name FROM system_schema.tables WHERE keyspace_name = :keyspace_name AND table_name = :table_name;" + ) + + Xandra.execute!(conn, prepared, %{ + "keyspace_name" => keyspace_name, + "table_name" => table_name + }) + end) + + Enum.to_list(page) != [] +end + +def create_table(keyspace_name, table_name) do + case table_exists?(keyspace_name, table_name) do + true -> + IO.puts("Table already exists") + + false -> + cluster = start_link() + + query = "CREATE TABLE IF NOT EXISTS #{keyspace_name}.#{table_name} ( + id uuid, + title text, + album text, + artist text, + created_at timestamp, + PRIMARY KEY (id, created_at) + );" + + run_query(cluster, query) + + IO.puts("Table created") + end +end +``` + +We added a check if the table exists, passing the keyspace name and the table name as parameters, working in the same way as the keyspace check. To test your table creation, open the interactive shell again and run: + +```ex +iex(1)> MediaPlayer.create_table("media_player", "playlist") +``` + +Done! Now your playlist table is officially created! + +### 3.3 Inserting data + +Now that we have the keyspace and a table inside of it, we need to bring some good songs and populate it. + +First of all, let's add a dependency to our `mix.exs` to work with UUID, then just run `mix deps.get` to update the dependencies! + +```exs +{:elixir_uuid, "~> 1.2"} +``` + +To execute our query, let's create another `run_query` function that will receive three parameters (the cluster, the query and the other parameters) to prepare our execution. In Elixir, we can have functions with the same name but different number of parameters, in which it will be understood that they are different functions. + +```ex +def run_query(cluster, query, params) do + prepared = Xandra.Cluster.prepare!(cluster, query) + + case Xandra.Cluster.execute(cluster, prepared, params) do + {:ok, result} -> + result + + {:error, error} -> + IO.inspect(error) + end +end + +def insert_songs(keyspace, table) do + cluster = start_link() + + song_list = [ + %{ + id: UUID.uuid4(), + title: "Getaway Car", + album: "Reputation", + artist: "Taylor Swift", + created_at: DateTime.utc_now() + }, + %{ + id: UUID.uuid4(), + title: "Still Into You", + album: "Paramore", + artist: "Paramore", + created_at: DateTime.utc_now() + }, + %{ + id: UUID.uuid4(), + title: "Stolen Dance", + album: "Sadnecessary", + artist: "Milky Chance", + created_at: DateTime.utc_now() + } + ] + + Enum.each(song_list, fn %{ + id: id, + title: title, + album: album, + artist: artist, + created_at: created_at + } -> + query = + "INSERT INTO #{keyspace}.#{table} (id, title, album, artist, created_at) VALUES (?, ?, ?, ?, ?);" + + run_query(cluster, query, [id, title, album, artist, created_at]) + end) +end +``` + +The need to create a function was to properly prepare our query with our arguments. To test just run the interactive shell again with `iex -S mix` and run: + +```ex +iex(1)> MediaPlayer.insert_songs("media_player", "playlist") +``` + +### 3.4 Reading data + +Since probably we added more than 3 songs into our database, let's list it into our terminal. + +```ex +def read_data(keyspace, table) do + cluster = start_link() + + query = "SELECT id, title, album, artist, created_at FROM #{keyspace}.#{table};" + + run_query(cluster, query) + |> Enum.each(fn %{ + "id" => id, + "title" => title, + "album" => album, + "artist" => artist, + "created_at" => created_at + } -> + IO.puts( + "ID: #{id} | Title: #{title} | Album: #{album} | Artist: #{artist} | Created At: #{created_at}" + ) + end) +end +``` + +To test just run the interactive shell again with `iex -S mix` and run: + +```ex +iex(1)> MediaPlayer.read_data("media_player", "playlist") +``` + +The result will look like: + +``` +ID: 09679e47-0898-40fd-b114-52b27de5a21c | Title: Stolen Dance | Album: Sadnecessary | Artist: Milky Chance | Created At: 2023-09-07 22:26:56.798Z +ID: 56fac772-dc54-4518-86df-2a628a2a45f6 | Title: Still Into You | Album: Paramore | Artist: Paramore | Created At: 2023-09-07 22:26:56.798Z +ID: 11bbeed9-c9a8-45cc-9842-c60483b4cb67 | Title: Getaway Car | Album: Reputation | Artist: Taylor Swift | Created At: 2023-09-07 22:26:56.798Z +``` + +### 3.5 Updating data + +Ok, almost there! Now we're going to learn about update but here's a disclaimer: +> INSERT and UPDATES are not equals! + +There's a myth in Scylla/Cassandra community that it's the same for the fact that you just need the `Partition Key` and `Clustering Key` (if you have one) and query it. + +If you want to read more about it, [click here.](https://docs.scylladb.com/stable/using-scylla/cdc/cdc-basic-operations.html) + +As we can see, the `UPDATE QUERY` takes two fields on `WHERE` (PK and CK). Check the snippet below: + +```ex +def update_data(keyspace, table) do + cluster = start_link() + + query = + "UPDATE #{keyspace}.#{table} SET title = ?, album = ?, artist = ? WHERE id = ? AND created_at = ?;" + + {:ok, date_formated, _} = DateTime.from_iso8601("2023-09-07 22:26:56.798Z") + + run_query(cluster, query, [ + "Getaway Car UPDATED", + "Reputation", + "Taylor Swift", + "11bbeed9-c9a8-45cc-9842-c60483b4cb67", + date_formated + ]) +end +``` + +Note that we had to convert the saved date format to iso8601. To test just run the interactive shell again with `iex -S mix` and run: + +```ex +iex(1)> MediaPlayer.update_data("media_player", "playlist") +``` + +So to check if it's been updated: + +```ex +iex(2)> MediaPlayer.read_data("media_player", "playlist") +``` + +The result will look like: + +``` +ID: 09679e47-0898-40fd-b114-52b27de5a21c | Title: Stolen Dance | Album: Sadnecessary | Artist: Milky Chance | Created At: 2023-09-07 22:26:56.798Z +ID: 56fac772-dc54-4518-86df-2a628a2a45f6 | Title: Still Into You | Album: Paramore | Artist: Paramore | Created At: 2023-09-07 22:26:56.798Z +ID: 11bbeed9-c9a8-45cc-9842-c60483b4cb67 | Title: Getaway Car UPDATED | Album: Reputation | Artist: Taylor Swift | Created At: 2023-09-07 22:26:56.798Z +``` + +### 3.5 Deleting data + +Let's understand what we can DELETE with this statement. There's the normal `DELETE` statement that focus on `ROWS` and other one that delete data only from `COLUMNS` and the syntax is very similar. + +```sql +-- Deletes a single row +DELETE FROM songs WHERE id = d754f8d5-e037-4898-af75-44587b9cc424; + +-- Deletes a whole column +DELETE artist FROM songs WHERE id = d754f8d5-e037-4898-af75-44587b9cc424; +``` + +If you want to erase a specific column, you also should pass as parameter the `Clustering Key` and be very specific in which register you want to delete something. On the other hand, the "normal delete" just need the `Partition Key` to handle it. Just remember: if you use the statement "DELETE FROM keyspace.table_name" it will delete ALL the rows that you stored with that ID. + +```ex +def delete_data(keyspace, table) do + cluster = start_link() + + query = "DELETE FROM #{keyspace}.#{table} WHERE id = ? AND created_at = ?;" + + {:ok, date_formated, _} = DateTime.from_iso8601("2023-09-07 22:26:56.798Z") + + run_query(cluster, query, [ + "11bbeed9-c9a8-45cc-9842-c60483b4cb67", + date_formated + ]) +end +``` + +To test just run the interactive shell again with `iex -S mix` and run: + +```ex +iex(1)> MediaPlayer.delete_data("media_player", "playlist") +``` + +So to check if it's been updated: + +```ex +iex(2)> MediaPlayer.read_data("media_player", "playlist") +``` + +The result will look like: + +``` +ID: 09679e47-0898-40fd-b114-52b27de5a21c | Title: Stolen Dance | Album: Sadnecessary | Artist: Milky Chance | Created At: 2023-09-07 22:26:56.798Z +ID: 56fac772-dc54-4518-86df-2a628a2a45f6 | Title: Still Into You | Album: Paramore | Artist: Paramore | Created At: 2023-09-07 22:26:56.798Z +``` + +## Conclusion + +Yay! You now have the knowledge to use the basics of ScyllaDB with Elixir. + +If you thinks that something can be improved, please open an issue and let's make it happen! + +Did you like the content? Dont forget to star the repo and follow us on socials. \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 362f8d2..82003b9 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -86,6 +86,12 @@ :link: build-with-ruby :class: large-4 +.. topic-box:: + :title: Build with Elixir + :image: /_static/img/programming_languages/elixir.png + :link: build-with-elixir + :class: large-4 + .. raw:: html diff --git a/elixir/.env.example b/elixir/.env.example new file mode 100644 index 0000000..c385850 --- /dev/null +++ b/elixir/.env.example @@ -0,0 +1,5 @@ +SCYLLADB_USERNAME= +SCYLLADB_PASSWORD= +SCYLLADB_NODE= +SCYLLADB_KEYSPACE= +SCYLLADB_TABLE= diff --git a/elixir/README.md b/elixir/README.md new file mode 100644 index 0000000..574e457 --- /dev/null +++ b/elixir/README.md @@ -0,0 +1,69 @@ +# ScyllaDB Cloud Media Player Metrics - Elixir + +Project to store songs that you like to listen daily and keep track of them in a shape of a CLI! + +## Prerequisites + +* [Elixir](https://elixir-lang.org/) + +## Running the project + +Clone the repository into your machine: + +```sh +git clone https://github.com/scylladb/scylla-cloud-getting-started.git +cd scylla-cloud-getting-started/elixir +``` + +Install the project dependencies and run the project: + +```sh +mix deps.get && mix run +``` + +> Replace the environment variables with your cluster information + +## Available Commands + +Check which commands are currently available on this sample: + +| Command | Description | +|---|---| +| !add | Add a new song to your liked songs list | +| !delete | Delete a specific song from your liked songs list | +| !list | Creates a register of which song and when you listened to it | +| !stress | Retrieve all songs and create a 'stressing' loop to test a ScyllaDB Cloud Cluster | + +## CQL Queries + +All the CQL queries used on the project + +```sql +CREATE KEYSPACE prod_media_player + WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '3'} + AND durable_writes = true; + +CREATE TABLE prod_media_player.songs ( + id uuid, + title text, + album text, + artist text, + created_at timestamp, + PRIMARY KEY (id, created_at) +); + +CREATE TABLE prod_media_player.song_counter ( + song_id uuid, + times_played counter, + PRIMARY KEY (song_id) +); + +SELECT keyspace_name FROM system_schema.keyspaces WHERE keyspace_name = ? +SELECT keyspace_name,table_name FROM system_schema.tables WHERE keyspace_name = ? AND table_name = ? + +SELECT * FROM songs +INSERT INTO recently_played_songs (song_id, listened_at) VALUES (?, ?) +UPDATE played_songs_counter SET times_played = times_played + 1 WHERE song_id = ? +DELETE FROM songs WHERE id = ? + +``` \ No newline at end of file diff --git a/elixir/lib/media_player.ex b/elixir/lib/media_player.ex new file mode 100644 index 0000000..6b29376 --- /dev/null +++ b/elixir/lib/media_player.ex @@ -0,0 +1,82 @@ +defmodule MediaPlayer do + @moduledoc """ + Documentation for `MediaPlayer`. + """ + + @doc """ + Hello from ScyllaDB! + + This is a simple application example using Elixir with ScyllaDB! + The project consists of using `Xandra`. + + To run the project, you need to have a ScyllaDB cluster running. + You can use ScyllaDB Cloud, ScyllaDB on Docker or ScyllaDB on Kubernetes. + You can find more information about ScyllaDB on https://www.scylladb.com/ + + To run the project, you need to have Elixir installed. + You can find more information about Elixir on https://elixir-lang.org/ + + To run the project, you need to have the following environment variables: + - SCYLLADB_USERNAME + - SCYLLADB_PASSWORD + - SCYLLADB_NODE_1 + - SCYLLADB_NODE_2 + - SCYLLADB_NODE_3 + + You can find more information about environment variables on + https://elixir-lang.org/getting-started/mix-otp/config-and-releases.html#environment-configuration + """ + alias MediaPlayer.Commands, as: Commands + + def loop do + IO.puts("--------------------------------------") + IO.puts("Type any command: ") + command = IO.gets("") |> String.trim() + + case command do + "!add" -> + Commands.add() + loop() + + "!list" -> + Commands.list() + loop() + + "!delete" -> + Commands.delete() + loop() + + "!stress" -> + Commands.stress() + loop() + + "exit" -> + IO.puts("Bye bye!") + :ok + + _ -> + IO.puts("Command not found!") + loop() + end + end + + def start(_, _) do + run() + {:ok, self()} + end + + def run do + IO.puts("--------------------------------------") + IO.puts("- ScyllaDB Cloud Elixir Media Player -") + IO.puts("- Leave a star on the repo -") + IO.puts("--------------------------------------") + IO.puts("Here some possibilities") + IO.puts(" !add - add new song") + IO.puts(" !list - list all songs") + IO.puts(" !delete - delete a specific song") + IO.puts(" !stress - stress testing with mocked data") + IO.puts("--------------------------------------") + + loop() + end +end diff --git a/elixir/lib/media_player/actions.ex b/elixir/lib/media_player/actions.ex new file mode 100644 index 0000000..760fce5 --- /dev/null +++ b/elixir/lib/media_player/actions.ex @@ -0,0 +1,25 @@ +defmodule MediaPlayer.Actions do + def cluster, do: MediaPlayer.Config.Database.start_link() + + def run_query(query) do + case Xandra.Cluster.execute(cluster(), query) do + {:ok, result} -> + result + + {:error, error} -> + IO.inspect(error) + end + end + + def run_query(query, params) do + prepared = Xandra.Cluster.prepare!(cluster(), query) + + case Xandra.Cluster.execute(cluster(), prepared, params) do + {:ok, result} -> + result + + {:error, error} -> + IO.inspect(error) + end + end +end diff --git a/elixir/lib/media_player/commands.ex b/elixir/lib/media_player/commands.ex new file mode 100644 index 0000000..f19ddcb --- /dev/null +++ b/elixir/lib/media_player/commands.ex @@ -0,0 +1,116 @@ +defmodule MediaPlayer.Commands do + use Task + + alias MediaPlayer.Actions, as: Actions + alias MediaPlayer.Config.Connection, as: Connection + + defp keyspace, do: Connection.keyspace() + defp table, do: Connection.table() + + def add_from(title, album, artist, created) do + query = + "INSERT INTO #{keyspace()}.#{table()} (id, title, album, artist, created_at) VALUES (?, ?, ?, ?, ?);" + + {:ok, created, _} = DateTime.from_iso8601(created <> "T00:00:00Z") + + Actions.run_query(query, [UUID.uuid4(), title, album, artist, created]) + + IO.puts("Song added!") + end + + def add() do + title = IO.gets("Enter the title of the song: ") |> String.trim() + album = IO.gets("Enter the album of the song: ") |> String.trim() + artist = IO.gets("Enter the artist of the song: ") |> String.trim() + + created = + IO.gets("Enter the date the song was created (YYYY-MM-DD): ") + |> String.trim() + + add_from(title, album, artist, created) + end + + def list do + query = "SELECT id, title, album, artist, created_at FROM #{keyspace()}.#{table()};" + + Actions.run_query(query) + |> Enum.each(fn %{ + "id" => id, + "title" => title, + "album" => album, + "artist" => artist, + "created_at" => created_at + } -> + IO.puts( + "ID: #{id} | Title: #{title} | Album: #{album} | Artist: #{artist} | Created At: #{created_at}" + ) + end) + end + + def delete() do + query = "SELECT id, title, album, artist, created_at FROM #{keyspace()}.#{table()};" + + songs = + Actions.run_query(query) + |> Enum.with_index(fn %{ + "id" => id, + "title" => title, + "album" => album, + "artist" => artist, + "created_at" => created_at + }, + index -> + IO.puts( + "Index: #{index + 1} | Title: #{title} | Album: #{album} | Artist: #{artist} | Created At: #{created_at}" + ) + + %{id: id, title: title, album: album, artist: artist, created_at: created_at} + end) + + {input, _} = IO.gets("Enter the index of the song you want to delete: ") |> Integer.parse() + + case Enum.at(songs, input - 1) do + %{} = song -> + query = "DELETE FROM #{keyspace()}.#{table()} WHERE id = ? AND created_at = ?;" + + Actions.run_query(query, [song.id, song.created_at]) + + IO.puts("Song deleted!") + + nil -> + IO.puts("Invalid index.") + end + end + + defp generate_stress_query(some_id) do + current_date = Date.to_string(Date.utc_today()) + + "INSERT INTO #{keyspace()}.#{table()} ( + id, title, album, artist, created_at + ) VALUES ( + #{UUID.uuid4()}, + 'Test Song #{some_id}', + 'Test Artist #{some_id}', + 'Test Album #{some_id}', + '#{current_date}' + );" + end + + def stress do + start = Time.utc_now() + cluster = MediaPlayer.Config.Database.start_link() + + # Simple stress test + 1..100_000 + |> Task.async_stream( + fn id -> + IO.puts("[#{id}] Adding seed") + Xandra.Cluster.execute(cluster, generate_stress_query(id)) + end, + max_concurrency: 500 + ) + |> Enum.to_list() + + IO.puts("Time taken: #{Time.diff(Time.utc_now(), start, :second)} seconds") + end +end diff --git a/elixir/lib/media_player/config/connection.ex b/elixir/lib/media_player/config/connection.ex new file mode 100644 index 0000000..74e6e53 --- /dev/null +++ b/elixir/lib/media_player/config/connection.ex @@ -0,0 +1,13 @@ +defmodule MediaPlayer.Config.Connection do + import Dotenv + + load() + + def keyspace() do + System.get_env("SCYLLADB_KEYSPACE") + end + + def table() do + System.get_env("SCYLLADB_TABLE") + end +end diff --git a/elixir/lib/media_player/config/database.ex b/elixir/lib/media_player/config/database.ex new file mode 100644 index 0000000..aa76353 --- /dev/null +++ b/elixir/lib/media_player/config/database.ex @@ -0,0 +1,25 @@ +defmodule MediaPlayer.Config.Database do + import Dotenv + + load() + + def start_link do + options = [ + username: System.get_env("SCYLLADB_USERNAME"), + password: System.get_env("SCYLLADB_PASSWORD") + ] + + {:ok, cluster} = + Xandra.Cluster.start_link( + sync_connect: :infinity, + authentication: {Xandra.Authenticator.Password, options}, + nodes: + # Add the cluster connection urls separated by commas without spaces + # Example: scylladb-node1,scylladb-node2,scylladb-node3 + System.get_env("SCYLLADB_NODE") + |> String.split(",") + ) + + cluster + end +end diff --git a/elixir/mix.exs b/elixir/mix.exs new file mode 100644 index 0000000..59313c2 --- /dev/null +++ b/elixir/mix.exs @@ -0,0 +1,33 @@ +defmodule MediaPlayer.MixProject do + use Mix.Project + + def project do + [ + app: :media_player, + version: "0.1.0", + elixir: "~> 1.13", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger], + mod: {MediaPlayer, []} + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:dotenv, "~> 3.0"}, + {:decimal, "~> 1.0"}, + {:xandra, "~> 0.14"}, + {:elixir_uuid, "~> 1.2"} + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + ] + end +end diff --git a/elixir/mix.lock b/elixir/mix.lock new file mode 100644 index 0000000..e238023 --- /dev/null +++ b/elixir/mix.lock @@ -0,0 +1,9 @@ +%{ + "db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"}, + "decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"}, + "dotenv": {:hex, :dotenv, "3.1.0", "d5a76bb17dc28acfb5236655bbe5776a1ffbdc8d3589fc992de0882b3ae4bc10", [:mix], [], "hexpm", "01bed84d21bedd8739aebad16489a3ce12d19c2d59af87377da65ebb361980d3"}, + "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, + "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "xandra": {:hex, :xandra, "0.17.0", "c1291a6ade16d19ddf4ebb5e3e947b5e3177e3a0791913a2c4a947b34aa5d400", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.7 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "65937898bbfe5eba692a5ce2937cba792bef01deae866cecdd0f0f59b327c88a"}, +} diff --git a/elixir/test/media_player_test.exs b/elixir/test/media_player_test.exs new file mode 100644 index 0000000..53744af --- /dev/null +++ b/elixir/test/media_player_test.exs @@ -0,0 +1,8 @@ +defmodule MediaPlayerTest do + use ExUnit.Case + doctest MediaPlayer + + test "greets the world" do + assert MediaPlayer.hello() == :world + end +end diff --git a/elixir/test/test_helper.exs b/elixir/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/elixir/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()