When building web applications, managing shared state efficiently is often a challenge. For Elixir developers, ETS (Erlang Term Storage) provides an incredibly powerful tool to tackle this problem. ETS is a robust, in-memory data store built into the Erlang VM, offering fast lookups, concurrent access, and persistence during a process’s lifetime.
In this blog, we’ll explore how ETS works, why it’s an excellent choice for shared state in Phoenix applications, and demonstrate practical examples with code snippets.
What is ETS?
ETS (Erlang Term Storage) is a highly efficient in-memory storage system available in Elixir and Erlang. It supports storing key-value pairs, lists, and tuples and is designed to handle high-performance operations with concurrent reads and writes. ETS tables live as long as the process that owns them, making them ideal for managing temporary or shared state.
Why Use ETS in Phoenix?
ETS is a great fit for Phoenix applications in scenarios such as:
- Caching: Store frequently accessed data, such as API responses or computed results, for quick retrieval.
- Session Management: Maintain temporary user session data across requests.
- Rate Limiting: Track user activity to enforce API rate limits.
- Real-Time Data Sharing: Share data between processes efficiently.
ETS Basics
To get started, let’s create and manipulate a simple ETS table. You can create a table using :ets.new/2
and interact with it using functions like :ets.insert/2
and :ets.lookup/2
.
# Creating an ETS table
table = :ets.new(:example_table, [:set, :public])
# Inserting data
:ets.insert(table, {:key, "value"})
# Retrieving data
case :ets.lookup(table, :key) do
[{:key, value}] -> IO.puts("Found: #{value}")
[] -> IO.puts("Not found")
end
Using ETS in Phoenix
Let’s dive into a practical example: caching frequently accessed data in a Phoenix application.
1. ETS for Caching API Data
Suppose you have a Phoenix controller that fetches data from an external API. Instead of fetching the data on every request, you can cache the result in ETS.
ETS Cache Setup
Create a module to manage your ETS table:
defmodule MyApp.Cache do
@table :api_cache
def start_link do
:ets.new(@table, [:named_table, :set, :public, read_concurrency: true])
{:ok, %{}}
end
def put(key, value) do
:ets.insert(@table, {key, value})
end
def get(key) do
case :ets.lookup(@table, key) do
[{^key, value}] -> {:ok, value}
[] -> :error
end
end
end
Fetching and Caching API Data
Use this module in your controller:
defmodule MyAppWeb.PageController do
use MyAppWeb, :controller
alias MyApp.Cache
def index(conn, _params) do
data =
case Cache.get(:api_data) do
{:ok, cached_data} ->
cached_data
:error ->
# Fetch data from external API
api_data = fetch_data_from_api()
Cache.put(:api_data, api_data)
api_data
end
render(conn, "index.html", data: data)
end
defp fetch_data_from_api do
# Simulated API call
%{key: "value", timestamp: DateTime.utc_now()}
end
end
Now, subsequent requests to index/2
will retrieve data from the ETS cache instead of making repeated API calls.
2. Rate Limiting with ETS
Another practical use case is rate limiting. You can use ETS to track how many requests a user has made within a given time window.
Rate Limiter Module
defmodule MyApp.RateLimiter do
@table :rate_limiter
def start_link do
:ets.new(@table, [:named_table, :set, :public])
{:ok, %{}}
end
def allow?(user_id, limit \\ 10) do
case :ets.lookup(@table, user_id) do
[{^user_id, count}] when count >= limit ->
false
[{^user_id, count}] ->
:ets.insert(@table, {user_id, count + 1})
true
[] ->
:ets.insert(@table, {user_id, 1})
true
end
end
end
Using the Rate Limiter
Integrate it into a Phoenix controller to enforce limits:
defmodule MyAppWeb.ApiController do
use MyAppWeb, :controller
alias MyApp.RateLimiter
def index(conn, _params) do
user_id = get_user_id(conn)
if RateLimiter.allow?(user_id) do
json(conn, %{message: "Request allowed"})
else
conn
|> put_status(:too_many_requests)
|> json(%{error: "Rate limit exceeded"})
end
end
defp get_user_id(conn) do
# Example: extract user ID from session or token
conn.assigns[:current_user].id
end
end
This setup ensures that a user cannot exceed the specified request limit, and ETS handles the tracking efficiently.
Advanced ETS Tips
-
Named Tables: Use named tables (
[:named_table]
) for global access across processes. -
Concurrency Options: Use
:read_concurrency
and:write_concurrency
for performance optimization in read-heavy or write-heavy scenarios. -
Persistence: Combine ETS with
:dets
(Disk ETS) to persist data across application restarts.
Limitations of ETS
While ETS is powerful, it’s important to be aware of its limitations:
- Table Lifespan: Tables are tied to the lifecycle of their owning process. If the process crashes, the table is lost.
- Manual Garbage Collection: ETS doesn’t automatically manage expired data. You’ll need to implement a cleanup mechanism if necessary.
- Cluster Awareness: ETS is not distributed across nodes in a cluster. For distributed setups, consider using Mnesia or a database.
ETS is a fantastic tool for managing shared state in Phoenix applications. Its simplicity and speed make it ideal for caching, rate limiting, and real-time data sharing. However, understanding its limitations is key to using it effectively.
Have you used ETS in your Phoenix projects? Share your experiences in the comments or let me know what other ETS use cases you'd like to explore!
Feel free to reach out if you need help.
LinkedIn: https://www.linkedin.com/in/rushikesh-pandit
GitHub: https://github.com/rushikeshpandit
Portfolio: https://www.rushikeshpandit.in
#myelixirstatus , #elixir , #phoenixframework
Top comments (0)