How it works
One binary runs as three processes. A cycle in the middle one keeps every app's copies where they should be.
Three processes
- The
shinyctlcommand reads a manifest and asksshinyctl-apito act, over the socket~/.shinyctl/api.sock. The firstapplystartsshinyctl-apiif it isn't running. shinyctl-apiis a background process, one per user. It owns every app's state, starts the copies, restarts them, and tells the load balancer where they are.- The load balancer, a process of its own, listens on every app's port and sends each request to one of that app's copies. It keeps the list of copies in memory, and
shinyctl-apireplaces that list without a reload, so no connection is dropped when it changes.
The apps and the load balancer keep running if shinyctl-api stops. A new shinyctl-api adopts them, with the same process IDs.
The cycle
shinyctl-api runs the same five steps for each app every 5 seconds, and at once whenever something happens: a command from you, or a copy that exits. An exit is noticed the moment it happens; the 5-second check catches anything else.
- ObserveWhich copies are alive and ready, and how many sessions each holds.
- DecideEach copy's role (active, warm or draining) and what to start or stop.
- RecordWrite the decision to the app's
state.json. - PushSend the copies table to the load balancer, which swaps it without a reload.
- ActStart and stop R processes, and watch them for exits.
What is stored is what happened: processes, start times, crash counts, and when a copy began to drain. What should happen (roles, what to start, what to stop) is worked out again on every cycle, so nothing about a role can go stale on disk.
Copies and roles
A copy is one R process running your app, numbered from 1. Of the copies that are running and ready:
- active: the oldest
replicascopies. They take new visitors. - warm: the rest. They are running and ready, and get no new visitors while every active copy is up. When an active copy fails, a warm copy takes its visitors at once.
- draining: a copy being replaced. It keeps the visitors it has, gets no new ones, and stops when they are gone or
drain-timeoutpasses.
A copy that is starting has no role yet; status shows - until it is ready.
Sticky sessions
Copies don't share memory, so a Shiny session has to stay with its R process. The load balancer sets a cookie named lb_<port> (for example lb_8080) that keeps each visitor on one copy for as long as it runs. This needs the browser to keep the cookie: it doesn't hold for an app embedded in another site's frame, or in Safari with third-party cookies blocked.
A visitor without a copy yet gets one of the active copies, picked by lb-policy:
| lb-policy | Picks |
|---|---|
least-conn | The copy with the fewest open connections. The default. |
round-robin | Each active copy in turn. |
random | Any active copy. |
first | Always the oldest active copy. |
Ports
The load balancer listens on the port in your manifest. Each copy runs on a port of its own between 20000 and 29999, on 127.0.0.1 only, which is why an app's port can't be in that range. Before sending visitors to a copy, shinyctl-api checks that the socket on its port belongs to that copy; if another program took the port, the copy moves to another one.
Your app sees requests coming from 127.0.0.1. The visitor's address is the first entry of the X-Forwarded-For header.
Files
Everything shinyctl keeps is under ~/.shinyctl/, except each app's state.json, which lives where its manifest says.
| Path | Contents |
|---|---|
api.log | The log of shinyctl-api and of the load balancer. |
logs/<app-group>-<app-name>.log | Copy 1's output; copy N's is in …@N.log. Logs rotate at 1 GiB. |
registry.json | Every app shinyctl-api manages. |
api.sock | The socket the command talks to. |
proxy/ | The load balancer's settings, copies table, cookie key, lock and socket. |
Words
| Word | Meaning |
|---|---|
| copy | One R process of an app, numbered from 1. |
| role | active, warm or draining: how the load balancer treats a copy. |
| outdated | A copy running an older version than the manifest: another revision or other settings. |
| revision | A number in state.json that restart raises, so copies started before it are outdated. |
| switch | The moment a redeploy hands new visitors to the new copies; the old ones then drain. |
| settle | The 10 seconds a draining copy is kept even with no sessions, for a page that opens its WebSocket just after. |
| stand-in | A warm copy that serves in place of an active one that is down, before the table changes. |