Application boot handler for MishkaCms runtime system initialization and asset compilation.
The BootHandler orchestrates the complete startup sequence for the MishkaCms runtime system,
managing the loading of sites, compilation of templates and modules, and initialization of
various runtime states. It ensures all essential components are properly loaded and compiled
before the application becomes available to serve requests.
Features
- Parallel Boot Process: Executes multiple initialization tasks concurrently for optimal startup time
- Site Management: Loads and caches site configurations in optimized batches
- Template Compilation: Compiles pages, layouts, components, and error pages into runtime modules
- Asset Pipeline Integration: Creates compilation jobs for CSS and JavaScript assets
- Error Recovery: Provides detailed error reporting and graceful failure handling
- Performance Optimization: Uses adaptive batch sizing based on dataset size
- Resource Cleanup: Manages temporary files and cleans up stale Oban jobs
Boot Process Overview
The boot process executes the following tasks in parallel:
- Site Loading: Loads all active sites into
SiteStatefor efficient lookups - Component Compilation: Compiles reusable UI components into runtime modules
- Error Page Compilation: Builds error handling pages for each site
- Layout Compilation: Compiles page layouts and templates
- JavaScript Hooks: Processes JS hooks for Phoenix LiveView integration
- Page Compilation: Compiles individual content pages
- Asset Jobs: Schedules CSS and JavaScript compilation jobs
Once they are done, every open site's sitemap is built on this node (build_sitemaps/0).
A node that does not serve the site
Every node that starts the application boots, and only one whose endpoint serves the site
(serving?/0) touches the compile jobs the nodes share: it cancels the ones a previous run left
(take_over_compile_jobs/1) and queues its sites' CSS/JS bundles (queue_boot_jobs/0). A
standalone node — a mix run script, a release's eval, iex -S mix — compiles its own sites,
pages and sitemaps as every node does, cancels no job and queues no bundle. It runs no job queue
either when the host starts Oban with oban_config/1, so every job it inserts is the serving
node's to run.
Architecture Integration
The BootHandler integrates with several MishkaCms subsystems:
- SiteState: Caches site configurations for runtime lookups
- WebRunTimeMetaDataState: Stores page metadata and routing information
- ModuleCompilerHandler: Handles dynamic module compilation
- RuntimeCompilerWorker: Manages background asset compilation via Oban
- TailwindCompiler: Generates CSS assets for each site
Performance Configuration
The boot process automatically adapts to dataset sizes:
# Batch configuration scales with data volume
# <= 1,000 items: batch_size: 1,000, chunk_size: 250
# <= 5,000 items: batch_size: 2,000, chunk_size: 500
# <= 20,000 items: batch_size: 5,000, chunk_size: 1,000
# <= 100,000 items: batch_size: 10,000, chunk_size: 2,000
# > 100,000 items: batch_size: 20,000, chunk_size: 5,000Template and Asset Management
The BootHandler creates template files for Tailwind CSS compilation:
# Template files are organized by site and type:
# priv/static/temp/runtime/
# ├── site1/
# │ ├── page/
# │ ├── layout/
# │ ├── component/
# │ └── error/
# ├── site2/
# │ └── ...
# └── __global__/
# └── ...Production Considerations
- Startup Time: Boot time scales with data volume; typical startup: 1-5 seconds
- Memory Usage: Sites and metadata are cached in memory for performance
- Concurrency: Uses all available CPU cores for parallel compilation
- Resource Cleanup: Automatically cleans up stale Oban jobs and temporary files
- Error Recovery: Failed individual items don't prevent overall boot success
- Asset Pipeline: Integrates with background job processing for asset compilation
Summary
Functions
This running instance's own id, made when :mishka_cms_core starts (new_boot_id/0) and nil
before then.
When this node started — the moment every compile job created before it stopped describing
anything: a serving node's boot cancels those still pending (take_over_compile_jobs/1) and
compiles from the database itself, without a job.
Aggregates every class-bearing field of a Component into one blob for
Tailwind to scan: template, body, helper code, the templates
Builds comprehensive template content for error pages by aggregating all related templates.
Aggregate template + helper code for Page/Layout resources. Tailwind scans the result, so unparsed Elixir snippets are fine.
Builds every open site's sitemap into THIS node's AssetsState — the boot counterpart of the
site's "sitemap" job (see MishkaCmsCore.Runtime.Compilers.SitemapCompiler).
Cancels every runtime compile job a previous run left pending or executing, in whichever queue it
was put — the boot rebuilds what they would have. Other workers' jobs (mail, notifications) are
left to Oban.Lifeline. Takes the Oban instance, Oban by default.
Returns a specification to start this module under a supervisor.
Maps one Task.async_stream element from a boot compile pass to a compile outcome.
Recompiles every active dependent part of a single site — the per-site counterpart to the boot passes. Writes templates + routing for active records, compiles the precompiled modules, and rebuilds the site's CSS/JS bundle and its sitemap. Used by the Site runtime worker on activate / unarchive / rename.
Creates background jobs for asset compilation across all active sites.
Processes JavaScript hooks and creates template files for CSS class extraction.
Creates a template file in the appropriate directory structure for CSS compilation.
Deletes a template file from the organized directory structure.
Determines optimal batch and chunk sizes based on dataset size for performance tuning.
Retrieves the current boot status of the application.
Whether the running Oban rescues jobs a stopped node left executing (lifeline: in the app's
Oban config). Without it, a mail or notification job interrupted by a restart stays executing.
Streams the language editions of every site's active pages into SiteState's editions index —
what MishkaCmsCore.Runtime.PageEditions.editions/2 reads on every render instead of a query.
Streams every site from the DB in tuned batches and upserts them into SiteState's ETS cache,
then the editions of their pages (load_page_editions/0).
Boot-time pass for Runtime.Stylesheet rows.
Repopulates WebRunTimeMetaDataState's routing rows from the DB — the crash/boot self-heal path
reused by WebRunTimeMetaDataState.handle_continue/2.
Gives this instance a new boot_id/0. Called once, by MishkaCmsCore.Application.start/2.
The Oban config to start this node with: config as it is on a node that serves the site
(serving?/0), and with no queue on one that does not. A mix run script, a release's eval or
iex -S mix then runs none of the jobs the serving node is there to run: a page compile built into
its own memory, a stylesheet into its own AssetsState, a share card drawn on it. A job it inserts
still lands in the table they share, and a serving node takes it.
Queues every open site's CSS/JS bundle job at the end of a serving node's boot
(create_assets_compilation_jobs/0), and none on a standalone one (serving?/0).
Whether this node serves the site: the host's endpoint (config :mishka_cms_core, endpoint:, in
the host_app: it names) is set to serve, as Phoenix.Endpoint.server?/2 reads it — mix phx.server, or server: true, which a release's PHX_SERVER sets. A node that does not is
standalone: a mix run script, a release's eval, iex -S mix, a test run, and a node whose app
names no endpoint.
Maps a site priority value to an Oban job priority (0–9, lower = higher priority).
Starts the BootHandler GenServer as part of the application supervision tree.
What a boot does to the compile jobs already queued, before it compiles anything.
Builds one routing-metadata row for WebRunTimeMetaDataState from a Page/Layout/ErrorPage
record (its :site must be loaded). section is the row kind — "Page", "Layout", or
"ErrorPage". Shared by the boot passes and load_web_metadata/0.
Functions
@spec boot_id() :: String.t() | nil
This running instance's own id, made when :mishka_cms_core starts (new_boot_id/0) and nil
before then.
Every bundle job a serving instance (serving?/0) queues carries it (asset_job/4), and
MishkaCmsCore.Runtime.Workers.RuntimeCompilerWorker builds the job on the instance it names: the
CSS, JS and sitemap a bundle job builds go into that instance's own
MishkaCmsCore.Runtime.States.AssetsState. A deploy that starts the next instance beside the old
one gives both the same node() and the same Oban queue; this id is what tells them apart. A
standalone node's bundle job names no instance, and a serving one builds it.
@spec booted_at() :: DateTime.t()
When this node started — the moment every compile job created before it stopped describing
anything: a serving node's boot cancels those still pending (take_over_compile_jobs/1) and
compiles from the database itself, without a job.
Aggregates every class-bearing field of a Component into one blob for
Tailwind to scan: template, body, helper code, the templates
- bodies of multi-clause variants stored in
extra.clauses, and the content an author places with it — itsexamplesand theextra.furnishingit arrives with, which the content editor offers as ready blocks.
It ends with the attribute lines the CSS closure reads to follow a component named through an
attribute — MishkaCmsCore.Runtime.Compilers.CssClosure.attribute_marks/1.
Builds comprehensive template content for error pages by aggregating all related templates.
This function constructs the complete template content for error pages by combining the error page templates with their associated layouts. This ensures that Tailwind CSS compilation captures all classes used across the entire error page rendering chain.
Parameters
err- Error page struct containing error page data and associated layouts
Return Value
binary()- Combined template content as a single string
Template Aggregation Process
- Root Layout Template: Includes the base layout template
- Error Page Templates: Adds all error page template variants
- Associated Layouts: Includes templates from nested layout structures
- Content Filtering: Removes nil/empty templates and combines valid content
Template Structure Processing
The function handles nested template structures and extracts content from:
- Direct template strings
- Nested layout template references
- Complex error page hierarchies
- Multiple error page variants per error configuration
Aggregate template + helper code for Page/Layout resources. Tailwind scans the result, so unparsed Elixir snippets are fine.
It is the COMPILED template, not the stored one. A page carrying a form compiles with the
confirmation region in front of it
(MishkaCmsCore.Runtime.Compilers.LiveViewCompiler.confirmable/2), and a class that only ever
exists after this file is written gets no rule — the visitor read the success message as bare
unstyled body text with no box, no padding and no centring.
@spec build_sitemaps() :: :ok | {:error, {:sitemaps_failed, [term()]}}
Builds every open site's sitemap into THIS node's AssetsState — the boot counterpart of the
site's "sitemap" job (see MishkaCmsCore.Runtime.Compilers.SitemapCompiler).
Two sites at a time, whatever the node's size, so a node booting with many sites reads their rows
a couple at a time instead of all at once. Each is built in its own task under
MishkaCmsCore.Runtime.TaskSupervisor, unlinked, so a site whose build raises or runs past a
minute is left without one and the rest are still built. The first request for it enqueues its
job.
Return Values
:ok- Every open site's sitemap is stored{:error, {:sitemaps_failed, failed}}- One entry per site not built:{:ok, {name, {:error, reason}}}, or{:exit, {name, reason}}for a build that raised or timed out
@spec cancel_leftover_jobs(Oban.name()) :: {:ok, non_neg_integer()}
Cancels every runtime compile job a previous run left pending or executing, in whichever queue it
was put — the boot rebuilds what they would have. Other workers' jobs (mail, notifications) are
left to Oban.Lifeline. Takes the Oban instance, Oban by default.
Returns a specification to start this module under a supervisor.
See Supervisor.
Maps one Task.async_stream element from a boot compile pass to a compile outcome.
Every pass runs with on_timeout: :kill_task, so an item that exceeds the pass timeout arrives
here as {:exit, :timeout} (never a raised crash) — this keeps one pathological template from
aborting the whole boot. A successful item is either nil (compiled cleanly, nothing to report)
or an {:error, _} tuple for a broken-but-skipped item.
Examples
classify_compile_stream_result({:ok, nil})
#=> nil
classify_compile_stream_result({:ok, {:error, :not_found}})
#=> {:error, :not_found}
classify_compile_stream_result({:exit, :timeout})
#=> {:compilation_exit, :timeout}
Recompiles every active dependent part of a single site — the per-site counterpart to the boot passes. Writes templates + routing for active records, compiles the precompiled modules, and rebuilds the site's CSS/JS bundle and its sitemap. Used by the Site runtime worker on activate / unarchive / rename.
Creates background jobs for asset compilation across all active sites.
This function schedules Oban jobs to compile CSS and JavaScript assets for all active sites in the system. It provides a scalable approach to asset generation by distributing the work across background workers, preventing boot process delays.
Parameters
type- String indicating the asset type to compile (defaults to "asset")"asset"- Standard asset compilation (CSS, JS)"css"- CSS-only compilation"js"- JavaScript-only compilation"sitemap"- the site's sitemap (MishkaCmsCore.Runtime.Compilers.SitemapCompiler)
Return Values
:ok- Compilation jobs successfully created{:error, reason}- Job creation failed with error details
Job Creation Process
- Site Discovery: Queries all active sites from SiteState
- Batch Processing: Processes sites in optimal chunks for job creation
- Job Scheduling: Creates Oban jobs for each site's asset compilation
- Queue Management: Distributes jobs across available worker queues
Examples
# Create standard asset compilation jobs
:ok = BootHandler.create_assets_compilation_jobs()
# Create CSS-only compilation jobs
:ok = BootHandler.create_assets_compilation_jobs("css")
# Handle job creation failures
case BootHandler.create_assets_compilation_jobs("js") do
:ok ->
Logger.info("JavaScript compilation jobs scheduled")
{:error, reason} ->
Logger.error("Failed to schedule JS jobs: #{inspect(reason)}")
endJob Configuration
# Each job is configured with:
%{
resource: "MishkaCmsCore.Runtime.Handlers.BootHandler",
action: "boot_handler",
# `nil` on a node that does not serve the site
boot_id: BootHandler.boot_id(),
data: %{site_name: "example_site", site_id: "<uuid>", type: "asset"}
}Worker Integration
# Jobs are processed by RuntimeCompilerWorker
# Worker handles actual asset compilation using:
# - TailwindCompiler for CSS generation
# - JavaScript bundling and minification
# - Asset optimization and compression
Processes JavaScript hooks and creates template files for CSS class extraction.
This function handles Phoenix LiveView JavaScript hooks by creating template files that allow the Tailwind compiler to discover CSS classes used within the JavaScript code. This ensures that styles referenced in JS hooks are included in the compiled CSS.
Return Values
:ok- All JS hooks processed successfully{:error, reason}- Processing failed with error details
Creates a template file in the appropriate directory structure for CSS compilation.
This function organizes template files by site and type, creating the necessary directory structure and writing template content to disk. The files are used by the Tailwind compiler to scan for CSS classes during asset generation.
Parameters
site_name- String name of the site (nil becomes "global")type- String indicating template type ("page", "layout", "component", etc.)name- String name of the template (will be slugified for filename)content- String content of the template
Directory Structure
Templates are organized in the following structure:
priv/static/temp/runtime/
├── site_name/
│ ├── page/
│ ├── layout/
│ ├── component/
│ ├── error/
│ └── js_hook/
└── __global__/
└── ...
Deletes a template file from the organized directory structure.
This function removes template files that are no longer needed, such as when pages, components, or layouts are deleted from the CMS. It helps maintain a clean template directory and prevents stale CSS classes from being included in compiled assets.
Parameters
site_name- String name of the site (nil becomes__global__)type- String indicating template type (page,layout,component, etc.)name- String name of the template to delete (will be slugified)
Return Value
:ok- File successfully deleted or didn't exist{:error, reason}- File deletion failed
File Safety
This function is safe to call multiple times or on non-existent files. It will not raise exceptions for missing files, making it suitable for cleanup operations where the file state is uncertain.
Determines optimal batch and chunk sizes based on dataset size for performance tuning.
This function provides adaptive batch configuration that scales with the number of items being processed. It optimizes memory usage and processing time by selecting appropriate batch sizes for different dataset volumes.
Parameters
site_count- Integer representing the total number of items to process
Return Value
Returns a tuple {batch_size, chunk_size} where:
batch_size- Number of items to load from database in each querychunk_size- Number of items to process together in parallel tasks
Scaling Strategy
- ≤ 1,000 items:
{1_000, 250}- Small datasets, conservative batching - ≤ 5,000 items:
{2_000, 500}- Medium datasets, moderate batching - ≤ 20,000 items:
{5_000, 1_000}- Large datasets, aggressive batching - ≤ 100,000 items:
{10_000, 2_000}- Very large datasets, high-volume batching - > 100,000 items:
{20_000, 5_000}- Massive datasets, maximum batching
Examples
# Small site configuration
{batch_size, chunk_size} = BootHandler.get_optimal_batch_config(500)
# Returns: {1_000, 250}
# Large enterprise configuration
{batch_size, chunk_size} = BootHandler.get_optimal_batch_config(50_000)
# Returns: {10_000, 2_000}Performance Considerations
- Memory Usage: Larger batch sizes use more memory but reduce database roundtrips
- Concurrency: Smaller chunk sizes enable better parallelization
- Database Load: Batch sizes balance query efficiency with resource usage
- System Resources: Configuration adapts to available CPU cores and memory
Retrieves the current boot status of the application.
This function provides real-time information about the boot process state, which is essential for health checks, monitoring systems, and ensuring the application is ready to serve requests.
Return Values
:ready- Boot process completed successfully, application is ready:initializing- Boot process is currently running{:failed, reason}- Boot process failed with specific error information
Monitoring Integration
# Continuous monitoring in a GenServer
def handle_info(:check_status, state) do
status = BootHandler.get_status()
# Update external monitoring systems
:telemetry.execute([:boot, :status], %{}, %{status: status})
{:noreply, %{state | boot_status: status}}
end
@spec lifeline?() :: boolean()
Whether the running Oban rescues jobs a stopped node left executing (lifeline: in the app's
Oban config). Without it, a mail or notification job interrupted by a restart stays executing.
@spec load_page_editions() :: :ok | {:error, term()}
Streams the language editions of every site's active pages into SiteState's editions index —
what MishkaCmsCore.Runtime.PageEditions.editions/2 reads on every render instead of a query.
Part of load_sites/0, so it runs at boot and whenever SiteState restarts, and from there on
every write of a page keeps the index itself — on the node that wrote it through
MishkaCmsCore.Runtime.Resources.Notifiers.PageEditions, and in SiteState on the others.
MishkaCmsCore.Runtime.PageLanguageBackfill runs before it at boot, so a page given a group
there is read with it. Only the fields an edition carries are read, never a template. A load
error is logged and returned as {:error, reason}, as in load_sites/0.
@spec load_sites() :: :ok | {:error, term()}
Streams every site from the DB in tuned batches and upserts them into SiteState's ETS cache,
then the editions of their pages (load_page_editions/0).
This is the single site-cache loader: perform_boot/0 runs it at startup, and
SiteState.handle_continue/2 reuses it to self-heal after an owner crash. Streaming (rather than
materializing all rows at once) keeps memory bounded and startup fast even with tens of thousands
of sites. A load error is logged and returned as {:error, reason} — a failure here must not
abort boot or crash-loop the cache owner.
Boot-time pass for Runtime.Stylesheet rows.
CSS content lives in the DB and is inlined at compile time by
TailwindCompiler.read_global_stylesheets/0 and read_site_stylesheets/1.
This pass:
- Streams every active stylesheet row (global + per-site).
- Writes the CSS content to a
.templatefile underpriv/static/temp/runtime/<site>/stylesheet/<name>.template. Tailwind scans those files via@sourcedirectives, so any utility-class references inside the CSS (e.g.@apply bg-primary-light, or class names mentioned in comments) get picked up. Without this step, classes only present in stylesheet content would be missing from the compiled CSS. - Logs the count.
This runs in parallel with compile_components, so total boot time is
unchanged.
@spec load_web_metadata() :: :ok | {:error, term()}
Repopulates WebRunTimeMetaDataState's routing rows from the DB — the crash/boot self-heal path
reused by WebRunTimeMetaDataState.handle_continue/2.
Streams active Page/Layout/ErrorPage rows in tuned batches and upserts ONLY their routing
metadata (id/site/path/title/section/priority) via save_batch/1 — it never writes templates or
recompiles modules (that stays in the boot passes). A DB error is logged and returned rather than
raised: this runs once per (re)start, not on the hot path, so a transient outage must not
crash-loop the owner.
Examples
# after a WebRunTimeMetaDataState crash wiped the routing tables, or at boot
BootHandler.load_web_metadata()
#=> :ok
@spec new_boot_id() :: :ok
Gives this instance a new boot_id/0. Called once, by MishkaCmsCore.Application.start/2.
The Oban config to start this node with: config as it is on a node that serves the site
(serving?/0), and with no queue on one that does not. A mix run script, a release's eval or
iex -S mix then runs none of the jobs the serving node is there to run: a page compile built into
its own memory, a stylesheet into its own AssetsState, a share card drawn on it. A job it inserts
still lands in the table they share, and a serving node takes it.
The host wraps what it starts Oban with, after anything that reads its queues, such as
AshOban.config/2:
{Oban, BootHandler.oban_config(AshOban.config(domains, Application.fetch_env!(:my_app, Oban)))}A queue signal cannot do this once Oban runs: even a local_only one is addressed to the node's
Oban name, and the serving node carries the same one — every BEAM on one machine does, and every
instance of a deploy whose release names its node the same.
@spec queue_boot_jobs() :: :ok | {:error, term()}
Queues every open site's CSS/JS bundle job at the end of a serving node's boot
(create_assets_compilation_jobs/0), and none on a standalone one (serving?/0).
@spec serving?() :: boolean()
Whether this node serves the site: the host's endpoint (config :mishka_cms_core, endpoint:, in
the host_app: it names) is set to serve, as Phoenix.Endpoint.server?/2 reads it — mix phx.server, or server: true, which a release's PHX_SERVER sets. A node that does not is
standalone: a mix run script, a release's eval, iex -S mix, a test run, and a node whose app
names no endpoint.
Maps a site priority value to an Oban job priority (0–9, lower = higher priority).
Site priorities are positive integers where higher values mean more important. Oban priorities are 0–9 where 0 is the most important.
Mapping
- Site priority >= 100 → Oban 0 (highest)
- Site priority >= 50 → Oban 1
- Site priority >= 20 → Oban 2
- Site priority >= 10 → Oban 3
- Site priority >= 5 → Oban 4
- Site priority >= 1 → Oban 5
- Everything else → Oban 6
Starts the BootHandler GenServer as part of the application supervision tree.
This function is typically called automatically by the application supervisor. The BootHandler runs as a named process and immediately begins the boot sequence upon initialization.
Parameters
opts- Optional GenServer options (defaults to empty list)
Return Values
{:ok, pid}- Successfully started the GenServer{:error, reason}- Failed to start the GenServer
Examples
# Manual start (usually not needed as it's supervised)
{:ok, pid} = BootHandler.start_link()
# Start with custom GenServer options
{:ok, pid} = BootHandler.start_link(debug: [:trace])Supervision Tree Integration
# In your application.ex
children = [
# ... other processes
MishkaCmsCore.Runtime.Handlers.BootHandler
]
@spec take_over_compile_jobs(keyword()) :: {:ok, non_neg_integer()}
What a boot does to the compile jobs already queued, before it compiles anything.
A serving node (serving?/0) cancels every one a previous run left (cancel_leftover_jobs/1) and
builds what they would have from the database, so a new instance supersedes the one it replaces. A
standalone node cancels none, because the jobs it finds are the serving node's, and sends no queue
signal: Oban addresses even a local_only one by the node's Oban name, which every BEAM on one
machine shares, and so does every instance of a deploy. It says so in the log when it runs a queue
anyway, which oban_config/1 is the way to stop. Returns how many were cancelled.
Options: :oban, the Oban instance, Oban by default.
Builds one routing-metadata row for WebRunTimeMetaDataState from a Page/Layout/ErrorPage
record (its :site must be loaded). section is the row kind — "Page", "Layout", or
"ErrorPage". Shared by the boot passes and load_web_metadata/0.
Examples
web_metadata_row(page, "Page")
#=> %{id: page.id, site: "example", path: "/about", title: "About", ...}