Advanced Usage
Concurrency
A Padrino app handles several requests at once when its web server runs them on threads, processes or fibers. You choose the model by choosing the server. Pick it when you generate the project:
$ padrino g project my_app --server=puma
$ padrino g project my_app --server=falcon
This adds the server gem to the Gemfile. In an existing project, add the gem
yourself.
Puma: threads and workers
Puma runs each request on a thread from a pool. It can also
fork worker processes, each with its own thread pool. Configure both in
config/puma.rb (the generator does not create this file):
# config/puma.rb
threads 5, 5 # min, max threads per process
workers 2 # forked processes; leave out to run a single process
Puma reads this file whether you start it with padrino start -a puma or
bundle exec puma. With padrino start, the address comes from -h and -p
(default 127.0.0.1:3000), so any bind or port in config/puma.rb is
ignored. You can also set the thread pool on the command line:
$ padrino start -a puma -O Threads=1:8
Threads in one process take turns running Ruby code, but they overlap while a request waits on the database or another service. Workers are separate processes, so they can use more than one CPU core.
With two or more workers, Puma loads the app once and then forks the
workers, so they inherit any database connection opened while loading. Sequel
connects when config/database.rb runs, so close those connections before
forking, or the workers share one socket:
# config/puma.rb
before_fork { Sequel::DATABASES.each(&:disconnect) }
Active Record reconnects in forked processes by itself. For Mongoid, follow its documentation on forking servers.
Falcon: fibers
Falcon runs each request in its own fiber on Ruby's fiber scheduler. While one request waits on IO, the others keep running in the same thread.
When the project uses Falcon, the ORM generators add a fiber setting to
config/database.rb, both for padrino g project --server=falcon and when you
add an ORM later with padrino g component:
| ORM | Setting |
|---|---|
| Sequel | Sequel.extension :fiber_concurrency |
| Active Record, MiniRecord | ActiveSupport::IsolatedExecutionState.isolation_level = :fiber |
| Mongoid | Mongoid::Config.isolation_level = :fiber (Mongoid 9.1+, Ruby 3.2+) |
This keeps database connections and ORM state per request instead of per thread.
padrino start -a falcon runs a single process. To run several, use Falcon's
own command:
$ bundle exec falcon serve --bind http://0.0.0.0:3000 --count 4
Fibers don't preempt each other. CPU-heavy work, or a library call that does not yield to the fiber scheduler, holds up every other request in that process.
Thread safety
Each request gets its own instance of your app, so instance variables set in a
route, filter or helper belong to that request. Anything shared between
requests is shared by every thread: constants, class variables, globals,
settings changed at runtime and class-level caches. Don't change them while
serving requests, or guard them with a Mutex.
Give the database pool at least as many connections as you have threads: the
pool option in Active Record (default 5) or max_connections in Sequel
(default 4).
If some code can't be made thread safe, enable :lock makes the app handle one
request at a time in each process.