Nova Integration
View SourceSeki integrates with Nova through the plugin system. Rate limiting, deadline propagation, and circuit breaking fit naturally into Nova's pre/post request pipeline.
Application Setup
Initialize seki primitives in your application startup, before Nova starts serving requests:
-module(my_app).
-behaviour(application).
-export([start/2, stop/1]).
start(_Type, _Args) ->
setup_rate_limiters(),
setup_breakers(),
setup_bulkheads(),
seki_otel:setup(),
my_sup:start_link().
stop(_State) -> ok.
setup_rate_limiters() ->
%% General API: 1000 req/min per IP
ok = seki:new_limiter(api_limit, #{
algorithm => sliding_window,
limit => 1000,
window => timer:minutes(1)
}),
%% Auth endpoints: 5 req/min per IP (strict)
ok = seki:new_limiter(auth_limit, #{
algorithm => token_bucket,
limit => 5,
window => timer:minutes(1),
burst => 5
}),
%% Webhook delivery: smooth outbound rate
ok = seki:new_limiter(webhook_limit, #{
algorithm => leaky_bucket,
limit => 50,
window => timer:seconds(1)
}).
setup_breakers() ->
{ok, _} = seki:new_breaker(database, #{
failure_threshold => 50,
wait_duration => 30000
}),
{ok, _} = seki:new_breaker(external_api, #{
failure_threshold => 30,
slow_call_duration => 5000,
wait_duration => 60000
}).
setup_bulkheads() ->
{ok, _} = seki_bulkhead:start_link(payment_service, #{
max_concurrent => 20
}).Rate Limiting Plugin
Nova plugins intercept requests before they reach your controllers. A rate limiting plugin checks seki before allowing the request through:
-module(seki_rate_limit_plugin).
-behaviour(nova_plugin).
-export([pre_request/4, post_request/4, plugin_info/0]).
pre_request(Req, _Env, Opts, State) ->
Limiter = maps:get(limiter, Opts, api_limit),
Key = rate_limit_key(Req, Opts),
case seki:check(Limiter, Key) of
{allow, #{remaining := Remaining}} ->
Req2 = cowboy_req:set_resp_header(
<<"x-ratelimit-remaining">>,
integer_to_binary(Remaining), Req),
{ok, Req2, State};
{deny, #{retry_after := Ms}} ->
Req2 = cowboy_req:set_resp_header(
<<"retry-after">>,
integer_to_binary(Ms div 1000), Req),
Body = jsx:encode(#{error => <<"Rate limited">>,
retry_after_ms => Ms}),
{stop, {reply, 429, #{<<"content-type">> => <<"application/json">>},
Body}, Req2, State}
end.
post_request(Req, _Env, _Opts, State) ->
{ok, Req, State}.
plugin_info() ->
#{title => <<"Seki Rate Limit">>,
version => <<"0.1.0">>,
description => <<"Rate limiting plugin for Nova using Seki">>}.
%% Internal
rate_limit_key(Req, Opts) ->
case maps:get(key, Opts, ip) of
ip ->
{IP, _Port} = cowboy_req:peer(Req),
IP;
{header, Name} ->
cowboy_req:header(Name, Req, <<"anonymous">>);
Fun when is_function(Fun, 1) ->
Fun(Req)
end.Deadline Plugin
Propagates deadlines from upstream services. If a request arrives with an
x-deadline-remaining header, seki picks up the upstream deadline. Otherwise,
it sets a default timeout. This integrates with seki_retry to stop retrying
when time is running out:
-module(seki_deadline_plugin).
-behaviour(nova_plugin).
-export([pre_request/4, post_request/4, plugin_info/0]).
pre_request(Req, _Env, Opts, State) ->
DefaultTimeout = maps:get(timeout, Opts, 30000),
case cowboy_req:header(<<"x-deadline-remaining">>, Req) of
undefined ->
seki_deadline:set(DefaultTimeout);
Value ->
case seki_deadline:from_header(Value) of
ok -> ok;
{error, _} -> seki_deadline:set(DefaultTimeout)
end
end,
{ok, Req, State}.
post_request(Req, _Env, _Opts, State) ->
seki_deadline:clear(),
{ok, Req, State}.
plugin_info() ->
#{title => <<"Seki Deadline">>,
version => <<"0.1.0">>,
description => <<"Deadline propagation plugin for Nova">>}.Route Configuration
Apply plugins to route groups with different settings per group:
#{prefix => "/api",
plugins => [
{pre_request, seki_deadline_plugin, #{timeout => 30000}},
{pre_request, seki_rate_limit_plugin, #{limiter => api_limit}}
],
routes => [
{"/users", {user_controller, index}, #{methods => [get]}},
{"/users/:id", {user_controller, show}, #{methods => [get]}}
]
}.
%% Stricter limits for auth endpoints
#{prefix => "/auth",
plugins => [
{pre_request, seki_rate_limit_plugin, #{
limiter => auth_limit,
key => ip
}}
],
routes => [
{"/login", {auth_controller, login}, #{methods => [post]}},
{"/register", {auth_controller, register}, #{methods => [post]}}
]
}.
%% Internal/admin endpoints with per-API-key limiting
#{prefix => "/admin",
security => #{module => admin_security},
plugins => [
{pre_request, seki_rate_limit_plugin, #{
limiter => admin_limit,
key => {header, <<"x-api-key">>}
}}
],
routes => [
{"/stats", {admin_controller, stats}, #{methods => [get]}}
]
}.Circuit Breaker in Controllers
Use circuit breakers in your controllers to protect calls to external services:
-module(user_controller).
-export([index/1, show/1]).
index(#{req := _Req}) ->
case seki:call(database, fun() -> db:query("SELECT * FROM users") end) of
{ok, Users} ->
{json, 200, #{}, #{users => Users}};
{error, circuit_open} ->
{json, 503, #{}, #{error => <<"Database temporarily unavailable">>}};
{error, _Reason} ->
{json, 500, #{}, #{error => <<"Internal error">>}}
end.
show(#{req := _Req, params := #{<<"id">> := Id}}) ->
case seki:call(external_api, fun() -> fetch_user_details(Id) end) of
{ok, Details} ->
{json, 200, #{}, Details};
{error, circuit_open} ->
%% Return cached/partial data when the API is down
case cache:get({user, Id}) of
{ok, Cached} -> {json, 200, #{}, Cached};
miss -> {json, 503, #{}, #{error => <<"Service unavailable">>}}
end;
{error, Reason} ->
logger:error("Failed to fetch user ~p: ~p", [Id, Reason]),
{json, 502, #{}, #{error => <<"Upstream error">>}}
end.Bulkhead in Controllers
Limit concurrent calls to expensive operations:
-module(report_controller).
-export([generate/1]).
generate(#{req := _Req, params := Params}) ->
case seki_bulkhead:call(report_gen, fun() ->
reports:generate(Params)
end) of
{ok, Report} ->
{json, 200, #{}, Report};
{error, bulkhead_full} ->
{json, 503, #{}, #{
error => <<"Too many reports generating, try again later">>
}}
end.Retry with Deadline in Controllers
Combine retry and deadline for resilient external calls:
-module(payment_controller).
-export([charge/1]).
charge(#{req := _Req, body := Body}) ->
%% Deadline was set by the plugin — retry respects it automatically
case seki_retry:run(payment_retry, fun() ->
seki:call(payment_api, fun() ->
payment_gateway:charge(Body)
end)
end, #{
max_attempts => 3,
backoff => exponential,
base_delay => 200,
retry_on => fun
({error, {http, 503, _}}) -> true;
({error, timeout}) -> true;
(_) -> false
end
}) of
{ok, {ok, Receipt}} ->
{json, 200, #{}, Receipt};
{ok, {error, circuit_open}} ->
{json, 503, #{}, #{error => <<"Payment service unavailable">>}};
{error, _} ->
{json, 502, #{}, #{error => <<"Payment failed">>}}
end.Health Check Endpoint
Expose seki health checks via a Nova controller for Kubernetes probes:
-module(health_controller).
-export([liveness/1, readiness/1]).
liveness(#{req := _Req}) ->
case seki_health:liveness(app_health) of
ok -> {json, 200, #{}, #{status => <<"ok">>}};
{error, _} -> {json, 503, #{}, #{status => <<"unhealthy">>}}
end.
readiness(#{req := _Req}) ->
case seki_health:readiness(app_health) of
ok -> {json, 200, #{}, #{status => <<"ready">>}};
{error, _} -> {json, 503, #{}, #{status => <<"not ready">>}}
end.%% Routes
#{prefix => "/health",
routes => [
{"/live", {health_controller, liveness}, #{methods => [get]}},
{"/ready", {health_controller, readiness}, #{methods => [get]}}
]
}.