jonhoo / faktory-rs

Rust bindings for Faktory clients and workers

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Making consumer jobs closures is super inconvenient.

icefoxen opened this issue · comments

Sad but true. I want to have this:

    /// Same as `run()` but gets jobs from a faktory server.
impl Foo {
    fn run_faktory() -> Result<()> {
        let faktory_server = "tcp://localhost:7419";
        let mut c: faktory::ConsumerBuilder<&(Fn(faktory::Job) -> Result<()> + Send + Sync + 'static)> = faktory::ConsumerBuilder::default();

        c.register("pin", |job: faktory::Job| -> Result<()> {
            self.do_stuff()?;
            Ok(())
        });

        c.register("unpin", |job: faktory::Job| -> Result<()> {
            self.do_other_stuff()?;
            Ok(())
        });
        
        let mut consumer = c.connect(Some(faktory_server)).unwrap();
        consumer.run_to_completion::<_, ()>(&["default"]);
        if let Err(e) = consumer.run(&["default"]) {
            println!("Worker failed: {}", e);
        }
        Ok(())
    }
}

First problem: c gets closures of two different types. This is an error 'cause closure types are never equivalent. Okay, so we take references to them. This works, but only because of HECKIN BLACK MAGIC; if you do this:

        c.register("pin", &|job: faktory::Job| -> Result<()> {
            self.do_stuff()?;
            Ok(())
        });

it works, but if you do:

        let closure = |job: faktory::Job| -> Result<()> {
            self.do_stuff()?;
            Ok(())
        });
        c.register("pin", &closure);

it can't figure out that the lifetime for the closure is valid.

Okay, this is inconvenient but not impossible. But self.do_stuff() borrows self, which means the closure is not 'static and Can't Work. Okay, so we factor self out into another structure, which is initialized before c.register() and moved into the closure:

        let mock_self = String::from("foo");
        c.register("pin", &move |job: faktory::Job| -> Result<()> {
            println!("Foo is: {}", mock_self);
            Ok(())
        });

But for some reason the move annihilates rustc's magic ability to figure out the closure's lifetime is valid. And on stable, boxed closures do not implement Fn.

It gets even worse from there because the real mock_self I want to use is not Send but I might be able to engineer around that. But the lifetimes still Don't Work.

Summary: Rust closures suck ass. Sad but true. The only good way around it is to make the method take a trait object instead:

trait Runnable {
    fn run(job: faktory::Job) -> Result<()>;
}
impl Runnable for Box<T> where T: Runnable ...

impl ConnectionBuilder {
    pub fn register<K>(&mut self, kind: K, handler: F) -> &mut Self where
        K: Into<String>, F: Runnable { ... }
}

or something like that. Then your "mock-self" object can just implement Runnable, the object has a specific type and lifetime and all that jazz and you don't have to

Maybe I'm wrong and this works fine and I can't figure out how to make all the bits fit together. But every time I use non-trivial closures in Rust they're utterly awful. If you have any suggestions on how to make them better, I'm all ears.

Actually it seems that making your ConsumerBuilder store Box<Fn(...)> explicitly rather than Fn() makes life much easier: https://play.rust-lang.org/?gist=92c0c764866a1025b48aa329b720b8aa&version=stable

That is hopefully a relatively minor change. I can make a PR to try it out if you're interested.

Yeah, this makes total sense. I made it generic over F so that implementors could choose to make their ConsumerBuilder generic over Box<Fn(Job) -> Result<(), E>>, but that may have been a case of over-engineering. The cost of dynamic dispatch is unlikely to matter in the context of job scheduling, so it's more important to get the flexibility that Box<Fn> gives you. I'd be happy to merge something like this!

Hey @icefoxen, still think you might want to take a stab at implementing this? The Box<Fn(...)> approach seems totally reasonable, and would be a great extension + simplification to the library!

Possibly, life has been really busy though. I'll see if I can take a go at it, I just have a big pile of stuff to do these days.

Looks like this was actually done in #16 and maybe we both just forgot about it?

Oh, wow, yeah! Good catch!