jonhoo / stream-cancel

A Rust library for interrupting asynchronous streams.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Example on how to cancel a stream from within a FnMut closure

veeg opened this issue · comments

Hi, i'd like to preface this with that I am a novice rust user.

From all the examples and test code for this crate, I only see examples that purposely drop the trigger in outer scope, before the tokio runtime is ran. However, all my use cases revolves around shutting down one or more streams based on an external event, all whom are detected within some stream itself.

Example:

/// MWE

extern crate signal_hook;
extern crate stream_cancel;
extern crate tokio;

use signal_hook::iterator::Signals;
use stream_cancel::{StreamExt, Tripwire};
use tokio::prelude::*;
use tokio::runtime::current_thread;

fn main() -> Result<(), Box<std::error::Error>> {
    println!("MWE");

    // Create a (currently single threaded) runtime to execute event loop.
    let mut runtime = current_thread::Runtime::new()?;

    let (trigger, tripwire) = Tripwire::new();

    // Setup signal handling
    let signal_future = Signals::new(&[signal_hook::SIGINT])?
        .into_async()?
        .take_until(tripwire)
        .for_each(|sig| {
            match sig {
                signal_hook::SIGINT => {
                    println!("got SIGINT, exiting");
                    trigger.cancel()                   // <-- This is an error
                }
                _ => unreachable!(),
            }
            Ok(())
        }).map_err(|_e| println!("error print"));

    runtime.spawn(signal_future);

    // Kick off the singel threaded runtime
    runtime.run()?;

    Ok(())
}

Which results in the following error

error[E0507]: cannot move out of captured outer variable in an `FnMut` closure
  --> src/main.rs:28:21
   |
18 |     let (trigger, tripwire) = Tripwire::new();
   |          ------- captured outer variable
...
28 |                     trigger.cancel()                   // <-- This is an error
   |                     ^^^^^^^ cannot move out of captured outer variable in an `FnMut` closure

Which is totally reasonable, however, I'm at a loss on how to implement this functionality when all the examples rely on explicitly dropping the the object, which as far as I understand, cannot occur within a closure.
Can anyone assist and mayhaps add a documentation example for this use-case?

I don't think you actually need stream-cancel in this instance :) Just make it:

.take_while(|sig| if let signal_hook::SIGINT = sig { false } else { true })

Since a Trigger takes a self and isn't Clone (so that you don't try to cancel the same stream more than once), you can't use it in for_each, since for_each will be called many times. If there's a compelling use-case for being able to do multiple concurrent cancellations, that's something that could be made to work, although it would come at a small performance cost.

As an aside, even if Trigger didn't consume self, you would need to use the move prefix for the closure to move the Trigger into the closure :)

This is all well and true. However, this was merely an example which stems from my first use-case, which is also stripped for more cleanup logic. I already solved this without using stream-cancel in another fashion. I opened this issue because I was unable to figure out how this crate could be employed for my wider use-case.

Take the following scenario then:

  • Accept TCP connection
  • Authenticate peer
  • Handle remote procedure call, one of which results in a shutdown/restart of the current server.

Now, this RPC logic does necessarily execute in the context of the for_each closure, albeit nested within some function abstractions. Gracefully shutting down this and other streams that may run on the server thus require access to the trigger in some way or form without dropping it.
Using the take_while logic here would not suffice, because you require more activity on the stream before the predicate could return false.

So what I am after here is either an example of some paradigm in Rust that I do not know of to handle this scenario, or perhaps a discussion on how stream-cancel can be adapted to provide this interface.

It's a little hard to give a good answer to this without concrete code to discuss. A common trick that people use for things like controlled shutdown that consumes self (like all dropping does) is to wrap it in an Option (so Option<Trigger>). You can then use Option::take to get the trigger, and then call cancel on it.

Again, it would be possible to have a Trigger that only takes &mut self, and perhaps one that is even Clone, though this would come at a performance cost (albeit a minor one).

I ran into a similar problem, I have many tasks running and I want to shut-down all of them if any of them hit an error.

I came up with a SharedTrigger struct that will wrap Trigger; Arc<Mutex<Option<Trigger>>> so it can be cloned.

/// Wraps a `stream_cancel::Trigger` so it can be shared
/// across tasks.
#[derive(Clone)]
pub struct SharedTrigger(Arc<Mutex<Option<Trigger>>>);

impl SharedTrigger {
    pub fn new(t: Trigger) -> Self {
        SharedTrigger(Arc::new(Mutex::new(Some(t))))
    }
    /// Triggers the exit. This will halt all
    /// associated tasks that have been wrapped.
    pub fn trigger(&mut self) {
        self.0.lock().take();
    }
    /// Wraps a `Stream` so it will trigger on error.
    pub fn wrap<T, E: std::fmt::Debug>(
        &self,
        s: impl Stream<Item = T, Error = E>,
    ) -> impl Stream<Item = T, Error = ()> {
        s.map_err(self.trigger_fn())
    }
    /// Wraps a `Future` so it will trigger on error.
    pub fn wrap_fut<T, E>(
        &self,
        s: impl Future<Item = T, Error = E>,
    ) -> impl Future<Item = T, Error = ()> {
        s.map_err(self.trigger_fn())
    }
    pub fn trigger_fn<E>(&self) -> impl FnMut(E) -> () {
        let mut cloned = self.clone();

        move |e| {
            cloned.trigger();
        }
    }
}

It can then be used with something like:

let (exit, valve) = Valve::new();
let exit = SharedTrigger::new(exit);

let s = valve.wrap(stream::iter(vec![1, 2, 3]));

let s2 = valve(stream::iter_result(vec![Error(some_error)]));


// wrap stream so any error shuts it down.
exit.wrap(s);

// Can also pass `trigger_fn` into a `map_err` combinator
s2.map_err(exit.trigger_fn());


// Or trigger the exit on demand:
exit.trigger();

This will cause the trigger to fire whenever an error is encountered.

If this is generally useful, I can submit a PR with this functionality.

I think what you're proposing is slightly different to what @veeg asked for above. A shared Trigger seems kind of useful though, so I'd be happy to take a look at a PR! I think my primary observation from the code you've posted above (beyond missing documentation) is that I think adding a Valve shouldn't remove the error value from the Stream or Future :)