What would it take to serialize a Future?

I want an async runtime to support serializing its active tasks - those which support it, at least - and then deserialize that list to resume work (think hot-reloading; e.g. to add new endpoints to server, which shouldn't break old connections). I guess I'd need to change insides of the runtime, but would it need more?

If you want your runtime to work with async fns you'll also have to modify the compiler, since those are not serializable in any way. Even then I think it will be hard to implement this in a way that can support most async fns.

Now I'm imagining: instead of diligently reading every async call for notes on "cancellation safety", you'd have to dig up the details for "resume safety", whatever that would look like.

If you serialize a future that's in the middle of reading from a TCP stream, what happens when you resume that? Similar for file I/O, would you tolerate the second half of a file read giving you the new contents of a different (renamed) file?

Serialization... it's on app/lib author, of course. They could add logic up to "send the socket descriptor to a helper process, and after the reload receive it back".

Mutable references are going to be a problem, but they are not truly needed for the first iteration; I could go with serializing handwritten Futures.

Only it has a problem too, I don't see a method which would look as fn(dyn Future) -> Result<dyn Serialize, dyn Future> anywhere...

I don't think making the socket handler tasks serializable would help with that, actually. The issue with hot reloading is less that the tasks themselves need to be restarted (which they do) but rather that the whole process needs to be restarted, at least from the exec call (on unix-alikes), which usually entails closing open sockets. Being able to carry the tasks over between executions doesn't help if they depend on sockets that have been closed in order to be able to make useful progress.

In principle that restart can preserve handles if it's done carefully, but in practice that usually relies on an external protocol, such as inetd's "stdin is your socket" system or the socket activation APIs provided by systemd.

You probably don't ever want to serialize an impl Future directly.

Better to look at saving like crash recovery metadata that can be loaded on start to pick up where you left off like browsers to and editors do and ...

Q) What would it take to serialize a Future?

A) A time machine.

:slight_smile:

Serialization is inherently stateless. Future's are the opposite: most are opaque state machine trait objects only the compiler knows anything about, with a whole lot of an implicitly captured state from within the runtime of the process. Any proper serialization/deserialization even for a trivial case:

Summary
use std::thread::*;
use std::time::*;

type Thread = JoinHandle<()>;

struct Timer {
    id: usize,
    set_at: SystemTime,
    duration: Duration,
    active: Option<Thread>,
}

impl Timer {
    fn new(id: usize, duration: Duration) -> Self {
        Self {
            id,
            duration,
            active: None,
            set_at: SystemTime::now(),
        }
    }
}

use std::pin::*;
use std::task::*;
impl Future for Timer {
    type Output = ();

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let Timer {
            duration,
            set_at,
            active,
            ..
        } = &mut *self;
        let elapsed = set_at.elapsed().expect("elapsed");
        let rem = *duration - elapsed;
        if rem < Duration::ZERO {
            *active = None;
            return Poll::Ready(());
        }
        if active.is_none() {
            let waker = cx.waker().clone();
            let handle = std::thread::spawn(move || {
                std::thread::sleep(rem);
                waker.wake_by_ref();
            });
            *active = Some(handle);
        }
        Poll::Pending
    }
}

might require a trait Restore: Future, an async runtime that is built to support such a trait, a reliable way to both fn declare<F: Restore>(&self) and fn restore(&self) on the futures it happens to spawn, as well as a way to restore/"attach" any runtime/execution-specific state any given F: Restore is meant to touch upon completion. Which is completely unfeasible for any composable async blocks chaining your F: Restore to another T: Future.

Summary
// is this meant to be restorable?
// as it would be an absolute nightmare
// to even *attempt* to de/serialize properly.
async fn timer_print() {
    let delay = Duration::from_secs(1);
    Timer::new(delay).await;
    println!("timer done");
}

// this is doable, yet
// any/all of the chain/then handlers
// will have to be configured/known/set beforehand.
fn main() {
  let rt = CustomAsyncRT::new();
  rt.auto_restore<Timer>(|timer| {
    if timer.id = 0xDEAD {
      // ...
    }
  });
  // attempt to load/fetch/deserialize
  // from wherever/however, as soon
  // as you're finished loading?
  rt.attempt_restore();

  // with enough `TypeId` shenanigans or similar,
  // you can detect if any given `Future` passed in
  // is restorable or not, and if so, serialize/save it 
  let timer = Timer::new(..);
  rt.spawn(timer);

  // depending on the way your runtime 
  // will be configured to `poll` from its task queue,
  // you can periodically re-serialize/store the restorable's
  // you make any progress on as well, I suppose
}

Such runtime wouldn't be compatible with async fn, and couldn't be made compatible at Rust's level. Such futures are opaque objects with internal state that may contain pointer references to arbitrary data on stack and elsewhere, and making that serializable is a messier problem than writing a precise garbage collector.

Theoretically you could require Future + Serialize, but in practice nobody in the ecosystem implements the serialize part, so it would only work with futures you create yourself.

A practical solution would be to solve it with something higher-level than async runtime (in Rust's meaning of the term), and make some resumable list of in-progress requests/tasks defined by you.

I now understood why I can't cast dyn Future to dyn Serialize!

The problem is, if it was possible, the compiler would have to test each Future implementor to understand if it is also Serialize, and generate vtables for all which are. It could be done, but in some language which is not today's Rust.

#[derive(Debug, Serialize, Deserialize)] async { foo().await; 42 } is also a dream from lands far away...

And another issue is what to do to futures which aren't serializable; if we want to run them to completion, we shouldn't suspend their sub-futures. So, it would probably require changes to the async runtime.


Thank you for the replies!

Depending on your architecture, if you use an actor network, you can propagate state changes via messages. I use such a technique to update a Zigbee network when new devices join, and also plan to use it to reload saved state on startup (which is currently only done through the respective actor's constructor).

I'm not sure if it would even worth the effort.

There are less convoluted and more robust solutions to this problem.

Two particular that come to mind:

  • Microservices
  • Dynamic config reload (stuff lives on the heap)

Hot reloading usually involves a structure that can switch parts without terminating the host process.
But the way I interpret your idea/request is to resume a modified version of a halted process.
Frankly it sounds like a complete nightmare.

If your intent instead is to simply resume work after a reload you need to serialize your tasks including the state. A robust messaging system could help with that since you can use a call back architecture that carries the process information with it.
That's way easier.

I'm not quite sure what exactly you are trying to solve.
It currently seems to me you want a solution in search of a problem.

Even if it was possible, how could you deserialize them back? You will need to somehow ensure the server reconnects to clients and the future continues from the same TCP state as before, with exactly the same amount of bytes sent. Maybe if you'll write your OS too, and run both client and server there, it could be possible. :face_with_hand_over_mouth:

The practical approach for this would be to pick a reasonable high level state to manually represent as an enum that you can implement serialization on.

It's not easy or ergonomic, but it's not really a practical thing to make easy and ergonomic, as this thread is explaining.

Well there are also some webservers out there that spinn up a new instance and gradually replace the old one until all old connections are closed.
Since e.g. http is usually very short lived it feels almost instantaneously.

Migrating ports is possible to some degree too.
But as said its complicated with lots and lots of finger crossing...

If you have clear tasks with defined states its better to serialize those, nuke everything and start fresh.
If you hand of work to something else entirely don't rely on ephemeral connections use robust one way signals originating from the thing that actually did the work.
Makes recovering trivial.

Modern tech has enough power that it shouldn't become a bottleneck.

Embedded different story, but even those are pretty much OP nowadays and mostly completely underutilized.

It's probably feasible to have an attribute macro rewrite an async fn into a Future state machine like that (and maybe also attach a hash of its shape so you get at least some semblance of safety :sweat_smile:), but it would have to borrow-check "manually" in the macro implementation to carry self references across .await.

I really don't think it's worth the complexity even if it works perfectly, though. For a service architecture point of view, if I wanted to shift work over like that, I'd set up a reverse proxy in front of it that lets me shift new connections to a new instance of the process, to let the old one gracefully exit once it's done with its current ones. For longer-running tasks, it's good to use something that allows stateful reconnections at a protocol level (like SSE), which then hopefully allows task handoff automatically.