Legion.RateLimiter.Postgres (Legion v0.5.0)

View Source

A Postgres-backed Legion.RateLimiter extending Legion.Store.Postgres.

This adapter depends on an existing Legion.Store.Postgres configuration. It stores rate-limit metadata in that store's table and reads persisted usage from it. Configure the store first, then define a rate limiter using the same Ecto repo and table:

defmodule MyApp.AgentStore do
  use Legion.Store.Postgres, repo: MyApp.Repo
end

defmodule MyApp.RateLimiter do
  use Legion.RateLimiter.Postgres, repo: MyApp.Repo
end

The Store migration creates the rate-limit metadata column and its GIN index. No separate rate-limiter migration is required:

defmodule MyApp.Repo.Migrations.AddLegionAgents do
  use Ecto.Migration

  def up, do: Legion.Store.Postgres.Migration.up()
  def down, do: Legion.Store.Postgres.Migration.down()
end

Call the generated limiter before running work, passing every rule that applies to the agent:

policy = %Legion.RateLimiter.Policy{
  window_ms: :timer.minutes(1),
  max_agents: 10,
  max_tokens: 100_000
}

:ok =
  MyApp.RateLimiter.enforce!(agent_id, [
    %Legion.RateLimiter.Rule{identity: %{"ip" => "203.0.113.42"}, policy: policy}
  ])

All rules are evaluated inside one transaction: the adapter locks every distinct identity (in a global order, so agents naming the same identities in different orders cannot deadlock), records the agent, then checks the rules in the order given. The first violated rule raises and rolls the whole call back, leaving metadata only for allowed calls.

Limits are evaluated when a turn starts; a turn that is already running is never interrupted, so one turn can carry the recorded total past :max_tokens before the next call is denied.

Identity matching

An agent's rule identities are merged into one JSON metadata document and matched with Postgres JSON containment. Each rule is evaluated against the agents whose metadata contains its identity, so an agent recorded under %{"ip" => "203.0.113.42"} and %{"email" => "someone@example.com"} counts towards both groups, and a broader identity such as %{"ip" => "203.0.113.42"} also matches an agent recorded with a "tenant" field. Rules passed directly to enforce!/2 must agree on shared fields; Legion.start_link/2 validates this, and the adapter does not. Calling enforce!/2 again for the same agent replaces its stored metadata while retaining its original start time.

Limit evaluation

The adapter includes the agent being checked when it evaluates :max_agents, so it raises only when that call would make the matching count exceed the configured maximum. Agents are counted by started_at.

:max_tokens is evaluated from recorded "total_tokens" usage whose "at" timestamp falls inside the policy's window; once that total reaches the configured maximum, later calls raise.

Token limits require Legion usage tracking, which is enabled by default. If your application configures config :legion, :track_usage, false, leave :max_tokens as nil; agent limits do not require usage tracking.

Options

  • :repo (required) - the Ecto repo used by the configured store.
  • :table - the shared Store table name, defaulting to "legion_agents". It must also be passed to the Store migration.