Skip to content

feat: New Redix and Caches lessons, refreshed existing storage lessons.#3056

Open
doomspork wants to merge 4 commits into
mainfrom
feat-redix-lesson
Open

feat: New Redix and Caches lessons, refreshed existing storage lessons.#3056
doomspork wants to merge 4 commits into
mainfrom
feat-redix-lesson

Conversation

@doomspork
Copy link
Copy Markdown
Member

@doomspork doomspork commented Jul 13, 2025

Summary

  • New Redix lesson covering basic usage, pipelining, data types, telemetry, transactions (MULTI/EXEC), and real-world examples (caching with TTL, rate limiting)
  • New Cachex lesson covering core operations, expiration/TTL, lazy loading with fetch, transactions, statistics, distributed caching, and custom hooks
  • Refreshed ETS lesson with updated terminology (objects), concurrency options, advanced querying, and a practical cache example

@doomspork doomspork force-pushed the feat-redix-lesson branch from a671023 to 8bed803 Compare July 13, 2025 17:59
@doomspork doomspork changed the title feat: New Redix lesson feat: New Redix and Caches lessons, refreshed existing storage lessons. Jul 13, 2025
@doomspork doomspork force-pushed the feat-redix-lesson branch from 8bed803 to 968cee3 Compare July 13, 2025 18:04
@doomspork doomspork force-pushed the feat-redix-lesson branch 2 times, most recently from e7a2e00 to e4ced0f Compare August 4, 2025 17:32
brain-geek
brain-geek previously approved these changes Nov 27, 2025
Copy link
Copy Markdown
Contributor

@brain-geek brain-geek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a massive and awesome update.

Added a few notes that might improve readability/consistency.

Comment thread lessons/en/storage/cachex.md Outdated
Comment thread lessons/en/storage/cachex.md Outdated
Comment thread lessons/en/storage/cachex.md Outdated
Comment thread lessons/en/storage/ets.md
Comment thread lessons/en/storage/ets.md Outdated
Comment thread lessons/en/storage/redix.md Outdated

### Building a Connection Pool

For high-traffic applications, we might want to use multiple Redis connections. Here's a simple connection pool implementation:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we maybe skip this section? High-traffic applications are definitely out of scope for basic technology lesson.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also thinking of removing the pub/sub section now too. I got carried away and wanted to cover too much to be comprehensive

Comment thread lessons/en/storage/redix.md Outdated
# Clean up Redis before each test
ExUnit.after_suite(fn _results ->
{:ok, conn} = Redix.start_link(Application.get_env(:my_app, :redis_url))
Redix.command!(conn, ["FLUSHDB"])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will create weird concurrency test failures if you use it in async: true tests.

Also, ExUnit.after_suite runs it after full suite, not each test.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair, let me think about how to capture this best without going to off into the weeds.

Comment thread lessons/en/storage/redix.md Outdated
In our `config/test.exs`:

```elixir
config :my_app, :redis_url, "redis://localhost:6379/15" # Use a test database
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the first time we're using redis_url config variable.

Maybe use it in initial supervision tree setup example as well, instead of hardcoding the address?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good feedback!

Comment thread lessons/en/storage/redix.md Outdated
In this case, we are passing in the node associated with our IEx session.

## Nodes
This command creates a new schema on the current node. After running this, you'll notice a new directory in your current working directory named something like `Mnesia.nonode@nohost` - this is where Mnesia stores its data files.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is schema?

@doomspork
Copy link
Copy Markdown
Member Author

doomspork commented Nov 28, 2025

@brain-geek thank you, thank you! I've been slowly chipping away at these lessons locally as I get back into the flow of writing lessons and explore some potential changes. Let me work through your feedback and other changes, then I'll tag you for another look 😁

I also screwed up using 1 PR for all the lessons, I wasn't anticipating getting reviews but I love it 🫶

@doomspork doomspork marked this pull request as ready for review March 16, 2026 00:12
@doomspork doomspork requested a review from a team as a code owner March 16, 2026 00:12
Copilot AI review requested due to automatic review settings March 16, 2026 00:12
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds new storage lessons for Redis/Cache libraries and substantially refreshes existing in-runtime storage lessons to improve coverage of caching, concurrency, and distribution concepts.

Changes:

  • Added new lessons: Redix (Redis client) and Cachex (ETS-based cache library)
  • Major rewrite/expansion of the Mnesia lesson (schema, queries, distribution, examples)
  • Major refresh of the ETS lesson (table types, concurrency options, practical cache example)

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 13 comments.

File Description
lessons/en/storage/redix.md New Redix lesson with usage patterns, telemetry, caching, rate limiting, and transactions
lessons/en/storage/cachex.md New Cachex lesson covering core ops, TTL/expiration, fetch, transactions, stats, and distributed setup
lessons/en/storage/mnesia.md Expanded Mnesia lesson with updated structure, examples, querying, distribution/replication, and best practices
lessons/en/storage/ets.md Updated ETS lesson with table types, concurrency options, richer querying, and a cache example

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread lessons/en/storage/redix.md Outdated
end

defmodule MyApp.RateLimitPlug do
use Plug.Conn
Comment thread lessons/en/storage/redix.md Outdated
Comment on lines +367 to +374
# Use remote_ip if behind a proxy/load balancer
to_string(:inet_parse.ntoa(conn.remote_ip))
{:header, name} ->
get_req_header(conn, name) |> List.first() || "anonymous"
val when is_function(val, 1) ->
val.(conn)
nil ->
# Default fallback: IP
Comment thread lessons/en/storage/mnesia.md Outdated
Comment thread lessons/en/storage/ets.md
Comment thread lessons/en/storage/cachex.md Outdated
Comment thread lessons/en/storage/mnesia.md Outdated
Comment thread lessons/en/storage/ets.md Outdated
Comment thread lessons/en/storage/ets.md Outdated
Comment thread lessons/en/storage/cachex.md Outdated
Comment thread lessons/en/storage/cachex.md
@doomspork doomspork force-pushed the feat-redix-lesson branch 2 times, most recently from a76a031 to 7dc1df5 Compare March 16, 2026 00:31
@doomspork doomspork requested a review from Copilot March 20, 2026 00:59
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds new storage lessons for Redis (Redix) and caching (Cachex) while significantly refreshing the existing ETS and Mnesia lessons to include more advanced patterns and practical examples.

Changes:

  • Introduce new Redix lesson with basic usage, pipelining, telemetry, transactions, and example patterns (cache + rate limiting).
  • Introduce new Cachex lesson covering setup, TTL/expiration, fetch/lazy loading, transactions, stats/hooks, and distributed caching.
  • Refresh ETS and Mnesia lessons with updated terminology, concurrency/distribution guidance, and more “real application” examples.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 13 comments.

File Description
lessons/en/storage/redix.md New lesson covering Redix usage patterns, telemetry, caching, and rate limiting examples.
lessons/en/storage/cachex.md New Cachex lesson including setup, core/advanced operations, TTL, fetch, transactions, hooks, and distributed mode.
lessons/en/storage/ets.md Major rewrite expanding ETS table types, concurrency options, querying, and an ETS-backed cache example.
lessons/en/storage/mnesia.md Major rewrite expanding Mnesia setup, querying, distributed replication, and application-style examples.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lessons/en/storage/redix.md Outdated
redis_url = Application.get_env(:my_app, :redis_url, "redis://localhost:6379")

children = [
{Redix, name: :redix, host: redis_url}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the supervision tree example, redis_url is a full Redis URI but it's passed as host:. host: expects a hostname (e.g., "localhost"), so this configuration will fail. Use the URI form supported by Redix (e.g., pass the URI as the first argument / url: option, or use host: + port: parsed from the URI).

Suggested change
{Redix, name: :redix, host: redis_url}
{Redix, name: :redix, url: redis_url}

Copilot uses AI. Check for mistakes.
Comment thread lessons/en/storage/redix.md Outdated
def exists?(key) do
case Redix.command(@redix_name, ["EXISTS", key]) do
{:ok, 1} -> true
{:ok, 0} -> false
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exists?/1 only handles {:ok, 0 | 1}. If Redis/Redix returns {:error, reason} (or any other {:ok, _}), this will raise a CaseClauseError. Add a fallback clause (e.g., return false or {:error, reason}) so the example doesn't crash on connection/command errors.

Suggested change
{:ok, 0} -> false
{:ok, 0} -> false
_ -> false

Copilot uses AI. Check for mistakes.
Comment thread lessons/en/storage/redix.md Outdated
Comment on lines +317 to +329

pipeline = [
["INCR", key],
["EXPIRE", key, window_seconds]
]

case Redix.pipeline(@redix_name, pipeline) do
{:ok, [count, _expire_result]} when count <= limit ->
{:ok, %{allowed: true, count: count, limit: limit}}

{:ok, [count, _expire_result]} ->
{:ok, %{allowed: false, count: count, limit: limit}}

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rate limiting pipeline resets the key TTL on every request (EXPIRE always runs), which turns the window into an inactivity timeout rather than a fixed window as described. To implement a fixed window, only set the expiry when the counter is created (e.g., when INCR returns 1), or use a Lua script to INCR + set expiry atomically on first increment.

Suggested change
pipeline = [
["INCR", key],
["EXPIRE", key, window_seconds]
]
case Redix.pipeline(@redix_name, pipeline) do
{:ok, [count, _expire_result]} when count <= limit ->
{:ok, %{allowed: true, count: count, limit: limit}}
{:ok, [count, _expire_result]} ->
{:ok, %{allowed: false, count: count, limit: limit}}
script = """
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
return current
"""
case Redix.command(@redix_name, ["EVAL", script, "1", key, window_seconds]) do
{:ok, count} when count <= limit ->
{:ok, %{allowed: true, count: count, limit: limit}}
{:ok, count} ->
{:ok, %{allowed: false, count: count, limit: limit}}

Copilot uses AI. Check for mistakes.
Comment on lines +242 to +247
...> Mnesia.select(:users, [
...> {
...> {:users, :"$1", :"$2", :"$3", :"$4"}, # Match pattern
...> [{'=:=', {:hd, :"$2"}, ?A}], # Guard (name starts with 'A')
...> [:"$$"] # Return entire match
...> }
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this select/2 example, the guard uses {:hd, :"$2"} but :"$2" is bound to the name field which is a binary (e.g., "Alice"); hd/1 expects a list and will fail. Use a binary-safe approach (or adjust the example to store names as charlists) so the match spec is valid.

Copilot uses AI. Check for mistakes.
# Create schema if it doesn't exist
case Mnesia.create_schema([node()]) do
:ok -> :ok
{:error, {_, {:already_exists, _}}} -> :ok
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blog.Database.setup/0 pattern matches on only two create_schema/1 outcomes; any other error tuple will raise a CaseClauseError. Add a catch-all clause (e.g., return/raise a descriptive error) so the example handles failures like permissions/invalid directory cleanly.

Suggested change
{:error, {_, {:already_exists, _}}} -> :ok
{:error, {_, {:already_exists, _}}} -> :ok
{:error, reason} -> {:error, {:schema_creation_failed, reason}}

Copilot uses AI. Check for mistakes.
Comment thread lessons/en/storage/ets.md Outdated
[{:key, "value1"}, {:key, "value2"}]
```

#### Duplicating Bag
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heading typo: this should be "Duplicate Bag" (or "Duplicate Bag Example") to match the ETS table type name :duplicate_bag and avoid confusion with a different concept.

Suggested change
#### Duplicating Bag
#### Duplicate Bag

Copilot uses AI. Check for mistakes.
Comment thread lessons/en/storage/ets.md Outdated

@table_name :simple_cache

def start_link do
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SimpleCache.start_link/0 doesn't actually start or link a process; it creates an ETS table owned by the caller and returns {:ok, self()}. Under a supervisor this would be misleading (and the table would disappear if the owning process exits). Consider implementing this as a GenServer/Agent which owns the table, or rename the function to reflect that it just initializes the table in the current process.

Suggested change
def start_link do
def init do

Copilot uses AI. Check for mistakes.
Comment thread lessons/en/storage/ets.md Outdated
{:named_table, true},
{:type, :set},
{:keypos, 1},
{:protection, :protected}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The :ets.info/1 output shows {:protection, :protected}, but the cache table is created with :public in SimpleCache.start_link/0. Update either the table options or the sample :ets.info/1 output so they reflect the same configuration.

Suggested change
{:protection, :protected}
{:protection, :public}

Copilot uses AI. Check for mistakes.
Comment thread lessons/en/storage/cachex.md Outdated
To add Cachex to our project let's add it to our `mix.exs` dependencies:

```elixir
def deps do
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dependency examples in this repo consistently use defp deps do (see lessons/en/basics/mix.md). This lesson uses def deps do, which is inconsistent with the earlier convention and implies a public function unnecessarily. Update to defp deps do for consistency.

Suggested change
def deps do
defp deps do

Copilot uses AI. Check for mistakes.
Comment on lines +302 to +310
defmodule MyApp.CacheLogger do
use Cachex.Hook

def init(_), do: {:ok, nil}

def handle_notify({action, _args}, result, state) do
Logger.info("Cache #{action}: #{inspect(result)}")
{:ok, state}
end
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example uses Logger.info/1 inside MyApp.CacheLogger without require Logger. Since Logger calls are macros, the snippet won't compile as-is. Add require Logger in the module (or switch to IO.inspect/1/IO.puts/1 if you want to keep the example dependency-free).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants