[AC-152] Fixes moz ads telemetry UniFFI callback leak - #7520
Conversation
| pub struct MARSTransport<T: Telemetry> { | ||
| http_cache: Option<HttpCache>, | ||
| telemetry: T, | ||
| telemetry: Option<T>, |
There was a problem hiding this comment.
I'm on board with dropping the caller-provided telemetry callback, but instead of Option<T>, could we default to a NoopTelemetry implementation?
My concern with Option<T> is that it pushes the "is telemetry configured?" check out to every call site and if we end up needing telemetry in more places, that cost multiplies. It's a bit like the log!() macro: having to write if tracing::is_configured() { log!() } everywhere would be pretty annoying, and I'd argue that's the telemetry layer's job to handle, not the caller's.
There's also a longer-term angle: we may want internal telemetry eventually, either by using Glean directly inside a-s components or if we migrate to a different system. A no-op default gives us a seam for that without touching call sites.
If we do want to keep the Option internally, one option is a thin telemetry wrapper that holds the Option<Box<dyn Telemetry>> (or a reference to the trait object) and exposes the same infallible API, so the branching lives in one place.
There was a problem hiding this comment.
If we push the concept of the telemetry wrapper (just as a brain teaser not something I think we need) that could be something like:
struct Telemetry {
layers: Mutex<HashMap<String, Arc<dyn TelemetryLayer>>>,
}
impl Telemetry {
fn new() -> Self {
Self { layers: Mutex::new(HashMap::new()) }
}
// builder-style so it can be chained in `new`
fn add_layer(self, key: &str, layer: Arc<dyn TelemetryLayer>) -> Self {
self.layers.lock().unwrap().insert(key.to_string(), layer);
self
}
fn remove_layer(&self, key: &str) {
self.layers.lock().unwrap().remove(key);
}
fn record(&self, event: &Event) {
for layer in self.layers.lock().unwrap().values() {
layer.record(event);
}
}
}
struct SomeClient {
telemetry: Telemetry,
}
impl SomeClient {
fn new(glean_telemetry: Arc<dyn TelemetryLayer>) -> Self {
let telemetry = Telemetry::new()
.add_layer("some", Arc::new(SomeInternalTelemetry))
.add_layer("log", Arc::new(LoggerTelemetry))
.add_layer("glean", glean_telemetry);
Self { telemetry }
}
fn shutdown(&self) {
self.telemetry.remove_layer("glean");
}
}| pub fn shutdown_db(&mut self) -> Result<(), rusqlite::Error> { | ||
| if let Some(cache) = self.http_cache.take() { | ||
| cache.shutdown()?; | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
There was a problem hiding this comment.
nit: this feels similar to https://doc.rust-lang.org/std/ops/trait.Drop.html
Maybe we could have some Shutdown trait, that could make things clearer?
There was a problem hiding this comment.
Just curious but what would happen if we drop the client in HNT? Like if we do something like delete AdsClient.instance in JS code. Would that call Drop trait?
A risk that I anticipate is we can "shutdown" the client and then we get panic when we try to call it back whereas if we could just drop it there would be less risk of making a mistake.
In theory this should not happen because surface code would be careful and shutdown only on exit but better safe than sorry I guess?
This provides the
shutdownfunction for addressing the crash on early ads-client -> HNT builds. When quit, we get:This requires a surface to deload several hanging parts of ads-client, specifically:
We provide such a function here, for use in upcoming future m-c PR to use on shutdown.
Notes
MozAdsTelemetryWrapper, or alternatively creating a new trait likeTryTelemetrybelow that optionally record, that gives a new method toOption<dyn Telemetry>, and swapping all calls to useTryTelemetryinstead. I prefer the explicit usage as shown in the PR, withif let Some(xxx)but not tied to it if we prefer one of these.Pull Request checklist
[ci full]to the PR title.