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.
1 Answer 1
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(&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.
std::sync::mpsc::sync_channel
instead for something like this.sync_channel
, unlikechannel
, guarantees that the order out will be the same as the order in... but it's also slightly more complicated. \$\endgroup\$