High-performance ETS-based state manager for multi-site configuration and lookup.
Manages runtime site data with optimized storage and retrieval using multiple ETS indexes for different access patterns. Designed for multi-tenant applications requiring fast site resolution, priority-based ordering, and efficient site management operations.
Architecture
Uses a multi-table ETS architecture with specialized indexes:
- Main Table: Primary storage with complete site data
- Host Index: O(1) lookups by hostname for request routing
- Name Index: O(1) lookups by site name for administration
- Origins Index: O(1) lookup of whether a site allows a CORS origin — see
allows_origin?/2 - Page Editions Index: the live editions of each page group, so a page names its other
languages without a query — see
page_editions/2 - Conflict Detection: Automatic hostname and name uniqueness validation
Data Structure
Each site record contains:
id: Unique site identifier (UUID or database ID)name: Human-readable site name for administrationhost: Hostname for HTTP request routingpriority: Site priority for ordering (higher = more important)active: Boolean flag for site availabilitymaster: Boolean flag indicating if this is the master site
Key Features
- O(1) Host Resolution: Instant site lookup by hostname for routing
- Conflict Prevention: Automatic hostname and name uniqueness validation
- Priority Ordering: Site ranking and display ordering
- Active Status Management: Site availability control
- Batch Operations: Efficient bulk site loading
- Flexible Pagination: Priority and name-based sorting options
- Memory Streaming: Process large site lists without memory overhead
- Concurrent Access: Read/write concurrency optimization
Performance Characteristics
- Host/name lookups: O(1)
- Pagination queries: O(n log n) where n = total sites
- Memory usage: ~150-200 bytes per site record
- Concurrent reads: Fully optimized
- Batch inserts: ~8x faster than individual saves
Usage Examples
# Register a new site
SiteState.save(%{
id: "site_123",
name: "My Blog",
host: "myblog.com",
priority: 90,
active: true,
master: false
})
# Fast host-based site resolution
{:ok, site} = SiteState.get_by_host("myblog.com")
# Administrative site lookup
{:ok, site} = SiteState.get_by_name("My Blog")
# Priority-ordered site listing
%{data: sites, total: count} = SiteState.paginate(%{
page: 1,
limit: 20,
active: true,
sort: :priority,
order: :desc
})
# Batch site loading for performance
SiteState.save_batch([
%{id: "site1", name: "Site One", host: "one.com", priority: 100, active: true, master: true},
%{id: "site2", name: "Site Two", host: "two.com", priority: 80, active: true, master: false}
])
# Memory-efficient site processing
SiteState.stream(%{active: true})
|> Stream.each(&process_site/1)
|> Stream.run()Request Routing Integration
# In a Phoenix plug or controller
def resolve_site(conn, _opts) do
host = get_req_header(conn, "host") |> List.first()
case SiteState.get_by_host(host) do
{:ok, site} when site.active ->
assign(conn, :current_site, site)
_ ->
send_resp(conn, 404, "Site not found") |> halt()
end
endIntegration Patterns
- Multi-tenant Routing: Host-based tenant resolution
- Site Administration: Management interface with priority ordering
- Load Balancing: Priority-based request distribution
- Feature Flags: Active/inactive site management
- Analytics: Site performance and usage tracking
- CDN Configuration: Host-based asset routing
Production Considerations
- Monitor site lookup performance and memory usage
- Implement proper error handling for inactive sites
- Use batch operations for initial site loading
- Consider hostname validation at application boundaries
- Implement audit logging for site configuration changes
- Use streaming for large-scale site operations
Summary
Functions
Whether the site site_id allows origin for CORS, an origin as
MishkaCmsCore.Runtime.OriginValidator.request_origin/1 reads a request's Origin header. One
ETS lookup: the origins a site allows are worked out by OriginValidator.site_origins/1 when the
site is saved, and dropped with it.
Returns a specification to start this module under a supervisor.
Clears all data from all tables.
Whether the site site_id — nil for the global scope — has an active component named name.
One ETS lookup, and nothing is made from a name it does not know.
Returns the number of sites.
Deletes a site by ID.
Forgets the component named name of the site site_id — nil for the global scope.
Takes the page row page out of the editions index — archived, deleted or switched off.
Fast check if a site exists.
Gets site by ID.
Gets site by ID, returns nil if not found.
Gets all active sites sorted by priority DESC. Note: For large datasets, consider using paginate/1 or stream/1 instead.
High-performance lookup by host (O(1)).
High-performance lookup by host, returns nil if not found.
Gets the master site. Returns {:ok, site} if master site exists, {:error, :not_found} otherwise.
Gets the master site, returns nil if not found.
High-performance lookup by name (O(1)).
Gets sites by priority range.
Fast check if a host exists.
The active, unarchived editions of the language group group on the site site_id, each as its
stored path, its language and its extra — what MishkaCmsCore.Runtime.PageEditions.editions/2
offers beside a page. One ETS range read, and no query: a group nothing is known of has no
editions.
High-performance paginated query with sorting by priority DESC.
Remembers that the site site_id — nil for the global scope — has an active component named
name.
Keeps the page row page in the editions index as it is now: under its site and language group
while it is active and unarchived, and out of it otherwise. A page moved to another group leaves
its old one in the same call, and no other row is touched.
Puts the tables back to a snapshot/0 (test helper). It reads no database, so it works from an
on_exit, once the test's rows have rolled back and only these entries are left of them.
Saves site data to the ETS tables. Returns :ok or {:error, :host_already_exists} / {:error, :name_already_exists}
Batch save multiple sites for better performance during boot. Skips conflict checking for speed - use only during initial load!
Puts every page of pages in the editions index at once — the boot's batch, from a read of the
active pages alone (see MishkaCmsCore.Runtime.Handlers.BootHandler.load_page_editions/0).
Sets a site as master, unsetting any existing master site.
Everything the tables hold now, for restore/1 (test helper).
Starts the SiteState GenServer and creates the ETS tables.
Stream all sites for memory-efficient processing.
Unsets the master site (finds the current master and sets its master flag to false). Returns {:ok, site} with the updated site, or {:error, :not_found} if no master exists.
Functions
@spec allows_origin?(term(), MishkaCmsCore.Runtime.OriginValidator.origin()) :: boolean()
Whether the site site_id allows origin for CORS, an origin as
MishkaCmsCore.Runtime.OriginValidator.request_origin/1 reads a request's Origin header. One
ETS lookup: the origins a site allows are worked out by OriginValidator.site_origins/1 when the
site is saved, and dropped with it.
{:ok, origin} = OriginValidator.request_origin("https://app.example.com")
SiteState.allows_origin?(site.id, origin) #=> true
Returns a specification to start this module under a supervisor.
See Supervisor.
Clears all data from all tables.
Whether the site site_id — nil for the global scope — has an active component named name.
One ETS lookup, and nothing is made from a name it does not know.
Returns the number of sites.
Deletes a site by ID.
Forgets the component named name of the site site_id — nil for the global scope.
@spec delete_page_edition(map()) :: :ok
Takes the page row page out of the editions index — archived, deleted or switched off.
Fast check if a site exists.
Gets site by ID.
Gets site by ID, returns nil if not found.
Gets all active sites sorted by priority DESC. Note: For large datasets, consider using paginate/1 or stream/1 instead.
High-performance lookup by host (O(1)).
High-performance lookup by host, returns nil if not found.
Gets the master site. Returns {:ok, site} if master site exists, {:error, :not_found} otherwise.
Gets the master site, returns nil if not found.
High-performance lookup by name (O(1)).
Gets sites by priority range.
Fast check if a host exists.
@spec page_editions(String.t(), String.t()) :: [ %{path: String.t(), language: String.t() | nil, extra: map() | nil} ]
The active, unarchived editions of the language group group on the site site_id, each as its
stored path, its language and its extra — what MishkaCmsCore.Runtime.PageEditions.editions/2
offers beside a page. One ETS range read, and no query: a group nothing is known of has no
editions.
SiteState.page_editions(site.id, about.language_group_id)
#=> [%{path: "/about", language: "en", extra: %{}},
# %{path: "/fa/about", language: "fa", extra: %{}}]
High-performance paginated query with sorting by priority DESC.
Options
:page- Page number (default: 1):limit- Items per page (default: 20):active- Filter by active status (default: nil, shows all):sort- Sort field, :priority or :name (default: :priority):order- Sort order, :asc or :desc (default: :desc for priority, :asc for name)
Examples
MishkaCmsCore.Runtime.States.SiteState.paginate(%{page: 1, limit: 20, active: true})
Remembers that the site site_id — nil for the global scope — has an active component named
name.
@spec put_page_edition(map()) :: :ok
Keeps the page row page in the editions index as it is now: under its site and language group
while it is active and unarchived, and out of it otherwise. A page moved to another group leaves
its old one in the same call, and no other row is touched.
MishkaCmsCore.Runtime.Resources.Notifiers.PageEditions calls it on the node that wrote the row,
and this process on every other one, from the row the write's broadcast carries.
Puts the tables back to a snapshot/0 (test helper). It reads no database, so it works from an
on_exit, once the test's rows have rolled back and only these entries are left of them.
Saves site data to the ETS tables. Returns :ok or {:error, :host_already_exists} / {:error, :name_already_exists}
Batch save multiple sites for better performance during boot. Skips conflict checking for speed - use only during initial load!
@spec save_page_editions([map()]) :: :ok
Puts every page of pages in the editions index at once — the boot's batch, from a read of the
active pages alone (see MishkaCmsCore.Runtime.Handlers.BootHandler.load_page_editions/0).
Sets a site as master, unsetting any existing master site.
Everything the tables hold now, for restore/1 (test helper).
snapshot = SiteState.snapshot()
on_exit(fn -> SiteState.restore(snapshot) end)
Starts the SiteState GenServer and creates the ETS tables.
Stream all sites for memory-efficient processing.
Options
:active- Filter by active status (default: nil, shows all):sort- Sort field, :priority or :name (default: :priority):order- Sort order, :asc or :desc (default: :desc for priority, :asc for name):chunk_size- Number of records per chunk (default: 100)
Examples
MishkaCmsCore.Runtime.States.SiteState.stream(%{active: true})
|> Stream.each(fn site -> IO.inspect(site) end)
|> Stream.run()
Unsets the master site (finds the current master and sets its master flag to false). Returns {:ok, site} with the updated site, or {:error, :not_found} if no master exists.