Siding
An alternative Rals application preloader that focuses to never serves state code.
Siding's design point is that guarantee. It derives the set of files it watches from what the boot actually loaded, rather than from a maintained list, and it revalidates before every invocation. So "you never run against stale code" is structural rather than best-effort, and when it cannot accelerate, your command still runs — unaccelerated, never refused.
Design
The watch set is the whole argument. A hand-maintained list is incomplete by construction: it is maintained by a person while the boot process is free to load anything. All files are loaded at boot, none is watched, and all of them persist across invocations after you change them. You then debug behavior that does not match the source in front of you. Deriving the set from what boot actually loaded makes it complete instead, because the program is observing itself rather than trusting a list.
Checking on every invocation is what makes it a guarantee. Filesystem events are missable sometimes.
So a watcher can only ever be best-effort, so Siding uses one solely to get a head start on the rebuild,
never to decide whether the application is current. That decision is made by revalidating before the fork,
on the path that cannot be skipped. Siding uses filesystem events by default, but you can switch it
by SIDING_WATCH. If you want to use polling, please specify poll.
Supported versions and platforms
| Supported | Below the line | |
|---|---|---|
| Platform | Linux, macOS (WSL counts as Linux) | Commands run unaccelerated |
| Commands | rails, rake, rspec, test |
Commands run unaccelerated |
test is Rails' own bin/test (minitest); invoke it as siding test .... siding init does not
generate a bin/test binstub, since bin/rails test already covers the same entry point — prefix
test explicitly when you use it directly.
rails server (and its alias rails s) are accelerated. But, two rails subcommands
stay below the line: rails server -d/--daemon (daemonizing detaches from the process siding
manages, which would leave nothing for it to signal or clean up) and rails dev:cache (a
one-shot toggle of the running application's caching mode, not something to accelerate). Below the
line means passed through unaccelerated, never refused.
siding status and siding doctor describe the warm application process siding manages — not the
Rails web server you get from rails server. Once rails server is itself accelerated, it's easy
to conflate the two: "server" in siding's own output always means the former.
Installation
Add it to the development and test groups of your application's Gemfile:
group :development, :test do
gem "siding"
end
Then:
bundle install
That's enough to use it — prefix any command with siding, as shown below. Optionally, run
bundle exec siding init to generate bin/rails, bin/rake, and bin/rspec binstub shims, so
that the plain, unprefixed commands you already run are accelerated automatically. It reports
exactly which files it wrote.
init exists purely to remove the friction of remembering the siding prefix. Each shim it writes
is a one-line exec("siding", ...) wrapper — nothing else depends on it, deleting the file reverts
to the plain, unaccelerated command, and an existing bin/rails you've edited by hand is never
overwritten. siding <command> behaves identically whether or not you've ever run init.
Usage
Prefix any command you would normally run:
siding rspec test/models/user_test.rb
siding rails runner 'puts User.count'
siding rails db:migrate
The accelerated run is observationally indistinguishable from the unaccelerated one: same stdout and stderr, byte for byte and in the same order; same exit status, including death by signal; the same interactive behavior, including debuggers and a real tty.
Siding's own logger never appear on stdout or stderr. They go to your controlling terminal, so a redirected or piped stream contains exactly what your command wrote and nothing else.
Other commands
| Command | What it does |
|---|---|
siding start |
Boots a warm application without running a command. Idempotent, and never a prerequisite — the first accelerated command boots the same thing |
siding status |
Whether a warm application exists, when it booted, what it has served, and recent staleness events |
siding stop |
Stops everything belonging to this project. Idempotent |
siding restart |
stop followed by a fresh boot |
siding doctor |
Why a given invocation was or was not accelerated: platform support, runtime directory state, recent boot failures |
siding init |
Generates binstub shims |
Environment variables
| Variable | Effect |
|---|---|
SIDING_DISABLE |
Truthy value runs the invocation unaccelerated. Works per-command and per-shell |
SIDING_TIMEOUT |
Maximum wait for a boot before surfacing the situation rather than hanging |
SIDING_IDLE_TIMEOUT |
Idle period after which the warm application exits (default 15 minutes) |
SIDING_LOG |
Raises diagnostic verbosity. Changes how much is said, never where |
SIDING_WATCH |
events (default) or poll. Chooses how watchcat wakes a speculative reboot while idle, never what it is validated against |
Siding is active in development and test, and stays inactive in production-like environments.
The gate is an allowlist: an unrecognized environment name defaults to inactive.
There is no option that turns staleness validation off. That is the guarantee the tool is for, and a tool that can be put into an unsafe state on purpose will be found in one by accident.
Ruby API
A correctly-behaving application needs none of this. It exists for the cases automatic derivation cannot reach on its own, and for tooling that wants to know what state a run happened against.
Fork life cycle hooks. Forking leaves a child with dead background threads and sockets shared with its parent. A gem that holds either releases it before the fork and re-establishes it after:
Siding.before_fork { MyConnectionPool.disconnect! }
Siding.after_fork { MyConnectionPool.reconnect! }
Boot components declaration. Siding derives what it watches from what your boot actually loaded, so ordinary code needs no declaration. Use this only for what that cannot see — a data file read at boot, a generated artifact:
Siding.boot_component "config/feature_flags.yml"
If you find yourself needing this for ordinary application code, the derivation has a gap. That is a bug worth reporting, not a line worth adding.
Introspection. Truthful in an unaccelerated run too, so no guard is needed:
Siding.accelerated? # => true / false
Siding.resolution # => "fresh", "reloaded_in_worker", "rebuild", or nil
Siding.revision # => a label for the source state this run served
Siding.boot_seconds # => how long the warm application took to boot
Siding.invocation # => all of the above, as a Hash
Development
After checking out the repo, run bin/setup to install dependencies. Then:
bundle exec rake test # unit + integration; expected green on every commit
bundle exec rake test:pty # interactive fidelity, needs a real pty
The integration suite drives the real executable against the fixture application in
test/fixtures/rails_app/. Its bundle and databases are a prerequisite of those tasks and are
prepared automatically on a fresh clone, so there is nothing to install by hand.
Before changing behavior, read CLAUDE.md — its principles and invariants are binding, not
advisory.
To install the gem onto your local machine, run bundle exec rake install. To release a new
version, update the version number in version.rb, then run bundle exec rake release.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/y-yagi/siding. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the Siding project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.