Add startApp() for non-blocking app execution#4349
Conversation
shikokuchuo
left a comment
There was a problem hiding this comment.
From a conversations with @hadley, we could tweak the UI further so that:
- Starting a new app automatically stops the old one
- Have it automatically run non-blocking if used by an agent (via env vars)
|
I like the model of automatically stopping the previously running app as it matches the existing model pretty closely; it just skips the step of having to Ctrl + C to quit the current app. I would hope that eventually nonblocking mode becomes the default, but defaulting to only during LLM usage would be a good place to start. |
|
Thanks @hadley, starting a new app now automatically stops the old one, and non-blocking is the default for LLMs. |
runApp()startApp() for non-blocking app execution
Ah, of course - all done now! |
cpsievert
left a comment
There was a problem hiding this comment.
A few issues spotted during review — details in inline comments.
Aligns `startApp()` with `runApp()` by setting `options(warn, pool.scheduler)` before `as.shiny.appobj()` and passing `ops` through. Folds the `findVal` precedence block into `.setupShinyApp()`; missingness is checked in the caller's frame via a `caller = parent.frame()` default arg, since `runApp()`/`startApp()` formals carry defaults.
`local_otel_promise_domain()` binds the domain to the caller's frame, which in `startApp()` exits before any request is served. A persistent global install would leak into unrelated user promises between ticks. Wrap the synchronous setup phase and each service iteration in `with_otel_promise_domain()`. Callbacks are wrapped at registration time, so promises created during `onStart`, handlers, and observers stay instrumented when they fire. The domain is dormant between ticks, so it stays out of user promises at the console.
cpsievert
left a comment
There was a problem hiding this comment.
A few targeted follow-ups from local verification. The main correctness issue is the startup-time stop path in startApp(). I also think the fixed 1 ms polling loop is worth revisiting before this lands. Details are inline.
# Conflicts: # NEWS.md
cpsievert
left a comment
There was a problem hiding this comment.
Great work, thanks @shikokuchuo!
Motivation
Shiny, being an httpuv server, inherently does not need to block — it uses the
laterevent loop, which is designed for cooperative concurrency. Adding a non-blocking mode:Changes
Adds
startApp(), a non-blocking counterpart torunApp():Implementation
ShinyAppHandle(R6 class): Returned bystartApp(). Provides methods for lifecycle management (stop(),status(),url()) and accessing the app's return value (result())..setupShinyApp(): Shared initialization extracted fromrunApp(), used by bothrunApp()andstartApp().serviceNonBlocking(): Runs the httpuv event loop vialatercallbacks instead of a blocking while loop..createCleanup(): Consolidated cleanup logic shared between blocking and non-blocking modes.Safeguards