1
\$\begingroup\$

I've written a simple asynchronous logger (playground):

mod logger {

    use std::sync::mpsc::{channel, Sender};
    use time::now;

    pub type LogError = std::sync::mpsc::SendError<String>;

    /// Runs a function that can logs asynchronously.
    pub fn log<F, T>(f: F) -> T
    where
        F: FnOnce(Logger) -> T,
    {
        let (sender, receiver) = channel();
        let handle = std::thread::spawn(move || {
            while let Ok(msg) = receiver.recv() {
                println!("{}", msg);
            }
            println!("LOG FINISHED");
        });
        let result = f(Logger { sender });

        handle.join().unwrap(); // Error handling?

        result
    }

    /// The actual logger.
    #[derive(Clone)]
    pub struct Logger {
        sender: Sender<String>,
    }

    impl Logger {
        /// Logs an information.
        pub fn info(&self, s: impl std::fmt::Display) -> Result<(), LogError> {
            let s = format!("[{}] [INFO ] {}", now().rfc3339(), s);

            self.sender.send(s)
        }

        /// Logs an error.
        pub fn error(&self, s: impl std::fmt::Display) -> Result<(), LogError> {
            let s = format!("[{}] [ERROR] {}", now().rfc3339(), s);

            self.sender.send(s)
        }
    }

}

// Demontration:
fn main() -> Result<(), logger::LogError> {
    logger::log(|logger| {
        logger.info("Hello world")?;

        // Cloning without overhead:
        logger.clone().error("Oops, error")
    })
}

The concept is simple: I spawn a thread with a receiver, and the logger sends the text to it.

To make this work, I was forced to use a callback to correctly join the thread when the user's code is done. Is there a better/prettier way to do that?

And also, is that pattern efficient? I know that I can buffer the lines inside the spawned thread, but I am talking about the whole pattern.

\$\endgroup\$
0
\$\begingroup\$

I have found a way to use a runtime struct instead of a closure. Logger needs to borrow the runtime, and it must implement Drop so that Runtime::drop is not called before Logger::drop.

mod logger {

    use std::marker::PhantomData;
    use std::sync::mpsc::{channel, Sender};
    use std::thread::JoinHandle;
    use time::now;

    pub type LogError = std::sync::mpsc::SendError<String>;

    pub struct Runtime {
        sender: Option<Sender<String>>,
        thread_handle: Option<JoinHandle<()>>,
    }

    impl Runtime {
        pub fn new() -> Self {
            let (sender, receiver) = channel();
            let thread_handle = std::thread::spawn(move || {
                while let Ok(msg) = receiver.recv() {
                    println!("{}", msg);
                }
                println!("LOG FINISHED");
            });

            Runtime {
                sender: Some(sender),
                thread_handle: Some(thread_handle),
            }
        }

        pub fn logger<'a>(&'a self) -> Logger {
            Logger {
                sender: self.sender.clone().unwrap(),
                _marker: PhantomData,
            }
        }
    }

    impl Drop for Runtime {
        fn drop(&mut self) {
            // Removes the last sender alive, so that the thread quits.
            let _ = self.sender.take();

            if let Some(handle) = self.thread_handle.take() {
                if let Err(e) = handle.join() {
                    eprintln!("Error while exiting the logger manager: {:?}", e);
                }
            }
        }
    }

    /// The actual logger.
    #[derive(Clone)]
    pub struct Logger<'a> {
        sender: Sender<String>,
        _marker: PhantomData<&'a ()>,
    }

    impl Logger<'_> {
        /// Logs an information.
        pub fn info(&self, s: impl std::fmt::Display) -> Result<(), LogError> {
            let s = format!("[{}] [INFO ] {}", now().rfc3339(), s);

            self.sender.send(s)
        }

        /// Logs an error.
        pub fn error(&self, s: impl std::fmt::Display) -> Result<(), LogError> {
            let s = format!("[{}] [ERROR] {}", now().rfc3339(), s);

            self.sender.send(s)
        }
    }

    impl Drop for Logger<'_> {
        fn drop(&mut self) {
            // The non-trivial drop prevents the logger to outlives the manager.
        }
    }
}

// Demontration:
fn main() -> Result<(), logger::LogError> {
    let log_manager = logger::Runtime::new();
    let logger = log_manager.logger();

    logger.info("Hello world")?;
    // Cloning without overhead:
    logger.clone().error("Oops, error")?;

    Ok(())
}

If one try to make a logger outlive the runtime, it will not compile:

fn main() -> Result<(), logger::LogError> {
    let log_manager = logger::Runtime::new();
    let logger = log_manager.logger();

    logger.clone().info("Hello world")
}

Gives:

error[E0597]: `log_manager` does not live long enough
  --> src/main.rs:85:18
   |
85 |     let logger = log_manager.logger();
   |                  ^^^^^^^^^^^ borrowed value does not live long enough
86 | 
87 |     logger.clone().info("Hello world")
   |     -------------- a temporary with access to the borrow is created here ...
88 | }
   | -
   | |
   | `log_manager` dropped here while still borrowed
   | ... and the borrow might be used here, when that temporary is dropped and runs the `Drop` code for type `logger::Logger`
   |
   = note: The temporary is part of an expression at the end of a block. Consider forcing this temporary to be dropped sooner, before the block's local variables are dropped. For example, you could save the expression's value in a new local variable `x` and then make `x` be the expression at the end of the block.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.