New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: cargo-watch and listen #43

Closed
mitsuhiko opened this Issue Jul 17, 2016 · 9 comments

Comments

Projects
None yet
3 participants
@mitsuhiko
Copy link

mitsuhiko commented Jul 17, 2016

I am playing around with web development in Rust again and it turns out that cargo-watch is actually a good first place to support auto reloading web servers during development in some circumstances. The idea is that once #25 lands one could add a flag to cargo-watch that would spawn a listening TCP socket and then pass it in an environment variable to the process.

Basically something like this:

$ cargo watch --pass-listen-socket=127.0.0.1:5000 run

When pass-listen-socket is specified a new listen socket is created like this:

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind('127.0.0.1', 5000)
s.listen(1)

And then passed to the subprocess in an environment variable:

CARGO_WATCH_LISTEN_FD=s.fileno()

Then web frameworks could pick up on it automatically like this:

use std::net::ToSocketAddrs;
use std::os::unix::io::{FromRawFd, RawFd};
use hyper::server::Server;
use hyper::net::HttpListener;

let server = env::var("CARGO_WATCH_LISTEN_FD").ok()
    .and_then(|x| x.parse().ok())
    .and_then(|fd| {
        let listener = unsafe {
            HttpListener::from_raw_fd(fd)
        };
        Some(Server::new(listener))
    })
    .or_else(|| {
        Some(Server::http(addr).unwrap())
    }).unwrap();

For Windows I'm not sure yet how to pass a handle to a subprocess but I'm sure there are ways as well.

@passcod

This comment has been minimized.

Copy link
Owner

passcod commented Jul 17, 2016

What's the advantage of this vs passing PORT and having the server bind to port as normal?

@mitsuhiko

This comment has been minimized.

Copy link

mitsuhiko commented Jul 18, 2016

@passcod if the server binds to the port the user gets connection reset between runs. Worse: you cannot bind to the same port after a process just stopped using the port which will give you a "address in use" error. There are some situations in which you can prevent that error through REUSEADDR/REUSEPORT but they need to be used with care.

This avoids the problem entirely. The cargo process sets up the socket and requests that come in will be buffered by the kernel. When the process starts up it starts picking up the clients which are hanging in the socket queue.

This is generally also used by things like systemd and in the Python world by things like uwsgi to allow an efficient reload even if things are slow.

@passcod

This comment has been minimized.

Copy link
Owner

passcod commented Jul 18, 2016

What env var name do systemd and uwsgi use, if any? I'd rather use something standard (or at leadt defacto) if it exists, rather like PORT, than make cargo watch expose something only cargo-watch-compatible servers could use.

Also, I can see the interest for production, but much less for development, and cargo watch is very much a development tool, at least currently. Do you have examples of devtools that make use of this, for reference?

@mitsuhiko

This comment has been minimized.

Copy link

mitsuhiko commented Jul 19, 2016

They all use different methods. uwsgi does not document it but uses it internally. systemd sets LISTEN_FDS which is the number of FDs that it sets up and it sets LISTEN_PID so you know if the FDs are for you.

In systemd you do something like this:

import sys
import os
import socket

if str(os.getpid()) == os.environ.get('LISTEN_PID'):
    sockets = []
    for offset in xrange(os.environ['LISTEN_FDS']):
        fd = offset + 3
        sockets.append(socket.fromfd(fd))
else:
    # manually set up sockets

This obviously does not make any sense for windows. In Windows there is no way I know about where you can pass the socket through environment variables. You would need to set up an IPC channel and then use WSADuplicateSocket from the parent to the child. This in any case would require a shim crate.

Also, I can see the interest for production, but much less for development, and cargo watch is very much a development tool, at least currently.

I am exclusively talking about development here, not about production which is why I did not even talk about systemd.

Do you have examples of devtools that make use of this, for reference?

Werkzeug's development server does this (which is used by the Flask microframework). You can see the code in question here: https://github.com/pallets/werkzeug/blob/a2a5f5a4c04c5b1fb33709bc2cdc297cd8fb46a3/werkzeug/serving.py#L649-L660

The reason why in case of Rust it needs to be in a different tool than in the framework itself is because something needs to trigger the recompilation. Where Python just evaluates bytecode whenever it starts up we here need to invoke cargo run.

Here is how it works in Flask in Python:

  • Flask starts up
  • Starts reloader
  • Starts socket
  • Forks
  • Flask starts up again in the child, this time with the app
  • Picks up the parent socket
  • If the parent detects a reload it kills the child and restarts it

Nowhere a request is lost because the FD is owned by the parent.

In case of Rust we need to look at things differently. The reloader in our case is cargo-watch. My proposal is to let cargo watch open the socket as such. It's the perfect place to start and stop cargo run and the HTTP server started up by the app after running could just pick up on this socket and everything works as fluently as it does in Python (ignoring the fact that compilation takes longer).

@mitsuhiko

This comment has been minimized.

Copy link

mitsuhiko commented Jul 19, 2016

Just to reiterate what I said before: without passing a socket to a child you cannot implement reloading reliably because the socket cannot immediately be reused (that's not entirely true but it depends on a few factors and is by no means portable).

@passcod

This comment has been minimized.

Copy link
Owner

passcod commented Dec 29, 2016

👍 Going to do this. Don't have an ETA, but it's on the todolist.

@ivegotasthma

This comment has been minimized.

Copy link

ivegotasthma commented Apr 29, 2017

@passcod Giving this a poke because #25 was closed and I'm curious about the status of this.

@passcod

This comment has been minimized.

Copy link
Owner

passcod commented Apr 29, 2017

I'm not sure that I want to do it in cargo-watch itself. I'm experimenting in creating a standalone tool that would achieve this and be usable in combination with cargo-watch (or anything else). Tentatively called "catflap", it would look like:

$ catflap -p 5000 -- cargo watch -x run

"Catflap" would open a TCP socket at port 5000 on localhost (by default, but other addresses would be targetable using e.g. 127.1.2.3:5000 etc) and set an env variable to the socked FD index, then spawn or exec the command given.

Then cargo-watch wouldn't do anything special, the env would pass through (as it does now), and the server below would then detect e.g. std::env::var("LISTEN_FD") and bind to that.

It would mean that "catflap" could be used to add this functionality to any other reloader out there (or even to non-reloader things).

Also, cargo-watch wouldn't need to half-support it: I want cargo-watch to have at least good support on all three platforms major platforms, and this feature is not great on Windows. Catflap, on the other hand, could expressly specify it only works on Linux/Unix (and perhaps on the Windows 10 Linux subsystem? haven't tested).

So, that's the direction I'm going at the moment. If/when I implement "catflap", and if it works, I'll of course chime in on here and also add a note on the readme.

@passcod

This comment has been minimized.

Copy link
Owner

passcod commented May 8, 2017

Done, published as catflap: https://github.com/passcod/catflap

@passcod passcod closed this May 8, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment