event_monitor: refactor the implementation to support concurrent access

This patch modifies `event_monitor` to ensure that concurrent access to
`event_log` from multiple threads is safe. Previously, the `event_log`
function would acquire a reference to the event log file and write
to it without doing any synchronization, which made it prone to
data races. This issue likely went under the radar because the
relevant `SAFETY` comment on the unsafe block was incomplete.

The new implementation spawns a dedicated thread named `event-monitor`
solely for writing to the file. It uses the MPMC channel exposed by
`flume` to pass messages to the `event-monitor` thread. Since
`flume::Sender<T>` implements `Sync`, it is safe for multiple threads
to share it and send messages to the `event-monitor` thread.
This is not possible with `std::sync::mpsc::Sender<T>` since it's
`!Sync`, meaning it is not safe for it to be shared between different
threads.

The `event_monitor::set_monitor` function now only initializes
the required global state and returns an instance of the
`Monitor` struct. This decouples the actual logging logic from the
`event_monitor` crate. The `event-monitor` thread is then spawned by
the `vmm` crate.

Signed-off-by: Omer Faruk Bayram <omer.faruk@sartura.hr>
This commit is contained in:
Omer Faruk Bayram
2023-07-30 00:53:36 +03:00
committed by Rob Bradford
parent 3d82867fe2
commit 02e1c54426
5 changed files with 244 additions and 60 deletions

View File

@@ -7,33 +7,11 @@ use serde::Serialize;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::io;
use std::os::unix::io::AsRawFd;
use std::time::{Duration, Instant};
static mut MONITOR: Option<(File, Instant)> = None;
/// This function must only be called once from the main process before any threads
/// are created to avoid race conditions
pub fn set_monitor(file: File) -> Result<(), std::io::Error> {
// SAFETY: there is only one caller of this function, so MONITOR is written to only once
assert!(unsafe { MONITOR.is_none() });
let fd = file.as_raw_fd();
// SAFETY: FFI call to configure the fd
let ret = unsafe {
let mut flags = libc::fcntl(fd, libc::F_GETFL);
flags |= libc::O_NONBLOCK;
libc::fcntl(fd, libc::F_SETFL, flags)
};
if ret < 0 {
return Err(std::io::Error::last_os_error());
}
// SAFETY: MONITOR is None. Nobody else can hold a reference to it.
unsafe {
MONITOR = Some((file, Instant::now()));
};
Ok(())
}
static mut MONITOR: Option<MonitorHandle> = None;
#[derive(Serialize)]
struct Event<'a> {
@@ -43,19 +21,71 @@ struct Event<'a> {
properties: Option<&'a HashMap<Cow<'a, str>, Cow<'a, str>>>,
}
pub struct Monitor {
pub rx: flume::Receiver<String>,
pub file: File,
}
struct MonitorHandle {
tx: flume::Sender<String>,
start: Instant,
}
fn set_file_nonblocking(file: &File) -> io::Result<()> {
let fd = file.as_raw_fd();
// SAFETY: FFI call to configure the fd
let ret = unsafe {
let mut flags = libc::fcntl(fd, libc::F_GETFL);
flags |= libc::O_NONBLOCK;
libc::fcntl(fd, libc::F_SETFL, flags)
};
if ret < 0 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
/// This function must only be called once from the main thread before any threads
/// are created to avoid race conditions.
pub fn set_monitor(file: File) -> io::Result<Monitor> {
// SAFETY: there is only one caller of this function, so MONITOR is written to only once
assert!(unsafe { MONITOR.is_none() });
set_file_nonblocking(&file)?;
let (tx, rx) = flume::unbounded();
let monitor = Monitor { rx, file };
// SAFETY: MONITOR is None. Nobody else can hold a reference to it.
unsafe {
MONITOR = Some(MonitorHandle {
tx,
start: Instant::now(),
});
};
Ok(monitor)
}
pub fn event_log(source: &str, event: &str, properties: Option<&HashMap<Cow<str>, Cow<str>>>) {
// SAFETY: MONITOR is always in a valid state (None or Some).
if let Some((file, start)) = unsafe { MONITOR.as_ref() } {
let e = Event {
timestamp: start.elapsed(),
// SAFETY: `MONITOR` is always in a valid state (None or Some), because it
// is set only once before any threads are spawned, and it's not mutated
// afterwards. This function only creates immutable references to `MONITOR`.
// Because `MONITOR.tx` is `Sync`, it's safe to share `MONITOR` across
// threads, making this function thread-safe.
if let Some(monitor_handle) = unsafe { MONITOR.as_ref() } {
let event = Event {
timestamp: monitor_handle.start.elapsed(),
source,
event,
properties,
};
serde_json::to_writer_pretty(file, &e).ok();
let mut file = file;
file.write_all(b"\n\n").ok();
if let Ok(event) = serde_json::to_string_pretty(&event) {
monitor_handle.tx.send(event).ok();
}
}
}
@@ -79,5 +109,4 @@ macro_rules! event {
$crate::event_log($source, $event, Some(&properties))
}
};
}