Skip to content

[AC-152] Fixes moz ads telemetry UniFFI callback leak - #7520

Open
thesuzerain wants to merge 4 commits into
mainfrom
AC-152-Fixes-MozAdsTelemetry-UniFFI-callback-leak
Open

[AC-152] Fixes moz ads telemetry UniFFI callback leak#7520
thesuzerain wants to merge 4 commits into
mainfrom
AC-152-Fixes-MozAdsTelemetry-UniFFI-callback-leak

Conversation

@thesuzerain

@thesuzerain thesuzerain commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

This provides the shutdown function for addressing the crash on early ads-client -> HNT builds. When quit, we get:

console.error: "UniFFI Callback interface error during xpcom-shutdown: Error: UniFFI interface MozAdsTelemetry has 1 registered callbacks at xpcom-shutdown.

This requires a surface to deload several hanging parts of ads-client, specifically:

  • any open sqlite connections (matching some other patterns elsewhere in application-services)
  • any references hanging uniffi callbacks
    We provide such a function here, for use in upcoming future m-c PR to use on shutdown.

Notes

  • I'm not sure if there's a good way to do a test here that would also catch any future callbacks added.
  • We can reduce LoC here by putting a mutex inside the MozAdsTelemetryWrapper, or alternatively creating a new trait like TryTelemetry below that optionally record, that gives a new method to Option<dyn Telemetry>, and swapping all calls to use TryTelemetry instead. I prefer the explicit usage as shown in the PR, with if let Some(xxx) but not tied to it if we prefer one of these.
pub trait TryTelemetry {
    fn try_record(&self, event: &dyn Any);
}

impl<T: Telemetry> TryTelemetry for Option<T> {
   ...
}

Pull Request checklist

  • Breaking changes: This PR follows our breaking change policy
    • This PR follows the breaking change policy:
      • This PR has no breaking API changes, or
      • There are corresponding PRs for our consumer applications that resolve the breaking changes and have been approved
  • Quality: This PR builds and tests run cleanly
    • Note:
      • For changes that need extra cross-platform testing, consider adding [ci full] to the PR title.
      • If this pull request includes a breaking change, consider cutting a new release after merging.
  • Tests: This PR includes thorough tests or an explanation of why it does not
  • Changelog: This PR includes a changelog entry in CHANGELOG.md or an explanation of why it does not need one
    • Any breaking changes to Swift or Kotlin binding APIs are noted explicitly
  • Dependencies: This PR follows our dependency management guidelines
    • Any new dependencies are accompanied by a summary of the due diligence applied in selecting them.

@thesuzerain thesuzerain changed the title Ac 152 fixes moz ads telemetry uni ffi callback leak [AC-152] Fixes moz ads telemetry UniFFI callback leak Jul 31, 2026
@thesuzerain
thesuzerain marked this pull request as ready for review July 31, 2026 15:34
@thesuzerain
thesuzerain requested a review from a team as a code owner July 31, 2026 15:34
@thesuzerain
thesuzerain requested review from Almaju and luc-lisi and removed request for a team July 31, 2026 15:34
pub struct MARSTransport<T: Telemetry> {
http_cache: Option<HttpCache>,
telemetry: T,
telemetry: Option<T>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Almaju Almaju Jul 31, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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");
    }
}

Comment on lines +32 to +38
pub fn shutdown_db(&mut self) -> Result<(), rusqlite::Error> {
if let Some(cache) = self.http_cache.take() {
cache.shutdown()?;
}
Ok(())
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@Almaju Almaju left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants