Initial creation of page

Havoc Pennington 2013-10-07 13:48:21 -07:00
parent a43fac13d6
commit 92edb5eb56
1 changed files with 397 additions and 0 deletions

397
Client-server-split.md Normal file

@ -0,0 +1,397 @@
This is a discussion document about a client-server setup where a
per-project sbt daemon would be shared among editors, IDEs, and
any other clients a developer may be using.
Some issues will have to be figured out through prototyping, but
we are doing our best to frame the overall problem and anticipate
solutions.
## The concept
Each project-to-be-built should have a “build model,” implemented
as a per-project server process, where "view-controllers" (aka
clients) may be apps such as:
* IDEs (Eclipse, ENSIME, Idea, etc.)
* Command line tools
* Standalone tools such as Activator, profilers, etc.
* The limited on-reload in-browser UI offered by Play
* Notification tools (using growl, gnome-shell, stuff like that)
## Why (what are we solving)?
In brief:
1. *lots of programs need to work with the build* yet
2. *the build needs to be centrally coordinated to avoid breakage*.
Longer version:
* A projects build is single-process / single-instance, doing otherwise would require greatly complicating how tasks are implemented (e.g. to have cross-process locks).
* But we also have multiple programs that care about the build: IDE, editor, play run, Activator, command line. Each of these clients wants to trigger tasks and display their output.
* Even if it were not broken, its wasteful in both CPU and RAM to have tons of sbt instances or multiple compiles.
* None of the clients are “primary” or “special,” it makes sense to run any one by itself, and also makes sense to run many different combinations of multiple.
## Demo
To think concretely about the design, imagine the following demo:
1. open an sbt command line console, Eclipse, a “play run,” and
Activator, all on a Play app
2. load localhost:9000/whatever in your browser, triggering a compile of the Play app
3. the compile logs appear simultaneously in the command line sbt
console, Eclipse, and Activator. Errors show in all of them and
in the browser window.
4. There's notification of completed compiles via growl or other
platform hooks.
The compile could also be triggered in Eclipse, in the sbt command
console, etc. not just on browser load. The point is that all
these clients are “views” on the same build instance.
## Prior Art
We have a crude prototype in sbt-remote-control (see
https://github.com/sbt/sbt-remote-control) where we have learned
something about running sbt in a separate process under the
control of a graphical user interface.
sbt-remote-control can teach us a lot about requirements. However,
the implementation of sbt-remote-control is pretty much one big
hack, and it does not allow multiple clients to share the same sbt
process.
## Some details to think about
### Problem: startup, discovery, and lifecycle of the build server
As robustly as possible, we want to be sure we start ONE build
server per project, even if multiple clients are launched in quick
succession. In broad outline this might be done by having a file
such as `myproject/.sbt-server/url` or otherwise storing the
location of the build server inside each project directory.
Appropriate locking should be used. The build server should
probably self-destruct when all clients are gone.
### Problem: long-running tasks block the build server
Some tasks are "infinite" (such as `run`), hanging around until
main() exits or until the developer kills the task. Other tasks
are simply long-running, such as `compile`. During a long-running
task, clients may want to cancel/restart the task, or may want to
be able to fetch other information (such as the project name, or
the classpath).
One possible solution would be to cache (or pre-cache) information
when possible. The trouble of course is that we don't know a
priori which tasks are safe to cache or quick to execute. Right
now, somewhat accidentally, we have memoized settings such as
`name` which are distinct from always-reexecuted tasks, but we may
not even want to keep that situation forever.
Another possibility would be to allow tasks to background
themselves in some way. For example, the `run` task might start up
a web server and keep track of its pid, then return control to
sbt. Another attempt to `run` would not relaunch the web server,
if it was already active. There could be a general way to stop a
"backgrounded" task, and a general way to get a list of active
backgrounded tasks.
### Problem: capturing stdout/stderr
We would want to capture output from any child processes and
probably from sbt itself (in case someone is using println to
debug their sbt plugin). This output then needs to go to all the
clients for display.
### Problem: supporting multiple sbt versions in clients
Within reason, we'd like our tools to work with any project.
As a practical matter, we may need an API that can backend to sbt
0.13 so tools can start porting to it sooner.
One concept might be two API "layers":
1. “Coarse” or “portable” layer maps fairly closely to e.g. the
user-visible buttons in Activator and high-level things Eclipse
wants to do, and abstracts across sbt versions and even
potentially someday other non-sbt build tools if people want. So here we
can “compile” for example and get some logs, which is a pretty
least-common-denominator kind of thing.
2. “Fine” layer is an exact mapping to all settings available in
a specific sbt version, along with detailed task results,
etc. provided by that version of sbt.
The coarse layer would be implemented over the fine layer presumably.
Open question: does the coarse-to-fine mapping live in the client jar or in the build server?
### Problem: who watches files and takes action accordingly?
Say some source files change; who will detect it and do something
about it? We have `sbt ~compile` now, plus IDEs and Activator have
similar functionality.
Watching files for changes kind of sucks to do in every client,
because it can be expensive and/or bad for battery life.
So the sbt server side could do the file watch, but then who takes
action when a "files changed" event occurs?
Maybe each client registers tasks to run on "sources changed" and
we de-dup on the server, or just rely on compile doing nothing if
there's nothing to do?
### Problem: Mapping Scala types to wire protocol
Whenever we run a task, we need to be able to deserialize the
task's input parameters and serialize the task's result. This will
mean some kind of registration of (de)serializers.
### Problem: Task cancellation
In sbt-remote-control, currently we are stopping the “run” or
“compile” tasks by simply killing the sbt process.
We could go that route (killing either the entire build server, or
some child of it), or we could build on sbt's current Ctrl+C
handling.
Some tasks such as `run` could get stuck due to bugs in the
program being executed, or bugs in sbt plugins.
### Problem: State across build server restarts
In general, nothing catastrophic should happen if the build server
crashes or gets killed (even `kill -9`). Otherwise, people will
get cranky.
Some possible strategies for this are:
* lose the work queue, logs, etc. on crash and clients just drop
all old tasks and logs from their UI on crash
* lose the work queue, logs, etc. on crash and clients try to
keep logs themselves and restore their tasks on crash
* persist work queue, logs, etc. and state appears unchanged to
clients on reconnect
Making the build server stateful across crashes is probably
fragile. It's as hard as a journaling file system or
database. Also, relevant state may well change between build
server runs (e.g. a git checkout, or whatever).
But if clients have to manually handle errors on every single API
call, none of them will do so. So the client API needs to do
something better than just throw when it gets a socket
error. Potentially the client API should keep the client's work
queue locally and try to restore it to the build server if the
client has to reconnect.
### Problem: Plugins which use the terminal directly
Interaction with user needs to change from direct JLine/console
usage to some kind of API, which would be proxied through a wire
protocol.
Some tasks in sbt itself also do this (for example mainClass
asking which main class to run if none has been configured).
Special cases to handle could include:
* No interactive client is attached.
* Multiple clients are attached; if the user answers a question
in one client, the others should remove their prompt or dialog.
### Problem: Completion
A command line client with autocomplete will need to ask the
server for completions.
### Problem: Notification of build reload
Running commands, or reloading the build, may mean the classpath
has changed, or even that the Scala version or sbt version has
changed. Clients will have to be notified.
Eclipse should run some paranoia checks every time the build
changes, for example ensuring that the Scala version is supported
by Eclipse.
### Problem: Comprehensive error list
In some cases incremental compilation does not output the complete
list of compile errors, because they were shown on a previous
compile run. This is confusing for IDEs and editors which would
like to show all errors. Should the build server automatically
take care of this and guarantee that `compile` always yields the
exhaustive list of errors and warnings for all files?
### Problem: How do clients filter what they get over the socket?
For efficiency, maybe clients need to ask for things like copious
debug output before we start shoveling it over the
socket. However, we should not prematurely optimize this (round
trips are bad too, not only bandwidth - in some cases it can be
better to send everything async rather than encouraging a ton of
remote “gets”). Its not clear exactly what we do here; debug
logging may be the only thing thats really a local-socket
bandwidth issue. So maybe its just ability to set log level
per-client.
### Problem: How long is the history?
It seems useful to keep history of tasks along with their event
logs, but we dont want this to be infinite, so how do we know
when to truncate it?
### Problem: Tasks and plugins sending events
Tasks may want to broadcast typed events to clients. The types
involved may not be known to the sbt core. Examples of events:
* Structured compile warnings/errors (avoiding log parsing)
* Notification that "Play is now running at
http://localhost/9000"
* Progress indication (percent complete)
### Problem: Tasks and plugins receiving requests
If a task is long-running or backgrounded, it may want to support
some kind of custom typed requests from clients (and replies to
same). The main examples we have right now are pretty generic,
such as cancellation; do we need custom requests or only some
fixed generic ones?
## Client API
We would have a jar used to interact with the build server. This jar should:
* Automatically start and restart the build server.
* Handle server restarts by reconnecting / reloading.
* Provide a nice, typesafe interface for interacting with sbt
concealing protocol cruft.
## Ideas: what could the model provided by the build server look like?
Each client makes requests to the server. Since we already have
meanings for "task" and "command," let's just call these requests
a ServerRequest for now. A ServerRequest might typically (or
always?) indicate a task or command to be executed, but would also
include additional data.
Wire protocol requests would sometimes create a ServerRequest, but
other times might manipulate them (for example, canceling a
ServerRequest) or manage client-server concerns such as which
events a client would like to receive.
The server-side model might have these entities:
* An ordered list of past ServerRequests (shared among all clients, but clients can tell which are “theirs”)
* An ordered list of future ServerRequests (also shared among all clients, but tagged by client)
* An optional set of currently-active ServerRequests (plural because we may want to keep some open while running others)..
* “Source files changed” events properly ordered with respect to the list of ServerRequest (i.e. if source files change after the start of compile, the “source files changed” event is after that compile).
* “Build config files changed” events properly ordered with respect to the list of ServerRequest.
* Configuration for what to do on source / build files changed events (automatically add compile request to the queue, for example, on source files changed).
Each ServerRequest might have:
* An id for use in referring to it. (this is per-ServerRequest, so if I send 10 compile requests its different for all).
* The client which submitted it.
* A log of all events that occurred during the request. The most common event will be “log message” probably, but there are also a few other things like compile errors or “play is now on port 9000” or what-have-you. Of course we also send change notification out to clients whenever this log has new stuff.
* Whether it has been cancelled, succeeded, or failed; with the result if any.
* What the ServerRequest actually is (generally “run this task” or “run this command” where the task is specified in the usual way by a ScopedKey).
* Potentially some view of the “contained” tasks (perhaps for multiple projects, and dependency graph of each task). This detail may require an extra wire protocol request to get it, only if we end up needing it in one of the clients. We could punt this down the road a bit.
Each client might have:
* A unique ID it can use to identify requests it submitted, etc. (maybe this is just an object reference or the identify of the socket)
* A human-readable name for display if we have a ServerRequest
queue shown in the UI or whatever
A very literal rendering of the model might be a UI like this:
* Completed Requests
* root/update (from Eclipse) _logs_
* core/compile (from command line) _logs_
* Active Requests
* core/run (from Activator) _logs_ _stop_
* Pending requests
* root/name (from Activator) _cancel_
* web/whatever (from browser reload) _cancel_
In fact, the build server itself could export a little HTML page
with a UI like that, as a handy way to check on its state.
At any time, a client needs to be able to 1) load the current model then 2) track changes since the loaded snapshot.
The protocol could work by having change events that move from one
snapshot to another. So if I have model version 50 then theres
some change event that allows me to transform it from 50 to
51. This way, any view can sync to the model, by first loading its
current state, then receiving change events from that state
forward. An example of a change event would be “append this log
message to this task,” just any delta that can be applied to the
model.
## Ideas: Backgroundable Tasks
The idea here is that we have a notion of a task that can remain
“active” and that sbt would track which tasks are currently
active. The canonical example would be the Play “run” task.
When executed, if the task is inactive then it gets a “Start”
operation which returns the tasks result type; if the task is
already active then it gets a “Continue” operation which also
returns the tasks result type. Tasks have a third operation,
“Stop,” which also returns the tasks result type. And like any
task, an active task can be cancelled (which means the task goes
inactive and returns an error). Start, Continue, and Stop would
all receive the tasks dependencies as parameters.
This means that the task has more than one state that the task execution engine retains. Specifically, active tasks have three primary functions:
1. Start the task, loading additional information into the state
2. Handle a new request for the task, while the task is active
* Pull state that was stored from the “start” run
* Pull current values of task dependencies
* Check to see what work needs to be done
3. Go inactive (Clean up the saved state, stop the forked
process, etc.). This needs to be done when reloading a build or
possibly upon client request. E.g. possibly active task requests
can optionally block a client (like sbt> run) so that only logs
show up. Issuing a ctrl-c would make the task go inactive.
Note: Its possible to encode active tasks on top of “storeAs”,
etc (I think), which may be ok for clients. The main reason we
can do this is if access to the task execution engine (and
therefore state transitions) are serialized by the build server.
## Ideas: command-line flexibility
The sbt server concept opens up more kinds of command line client,
if people are interested. Right now, due to startup time, the
command line client has to be its own shell. sbt server enables
something more like git's suite of separate commands used directly
from the unix shell. Individual commands could in principle be
little more than a `wget` to localhost, so they should have
trivial startup time.
## Ideas: Wire protocol
Using HTTP/websocket and JSON for the wire protocol would make it
much easier to write quick clients or clients in non-JVM
frameworks. For example, on Mac or Linux, it might be neat to
write a quick little client for notifications (using growl or
gnome-shell or whatever). curl and wget may also be useful for
testing.
There's also JavaScript to consider; the build server could have a
cheesy, small JavaScript app built in for viewing and manipulating
the work queue, and tools such as Activator may be partially
implemented in JavaScript.
The protocol should have request/response pairing using request
serials or request IDs, so that responses may be reordered. That
is, it should not rely on responses coming back in the same order
that requests were sent.
## Other ideas
Many more will no doubt emerge over time, please share yours.