-
-
Notifications
You must be signed in to change notification settings - Fork 123
feat(client): observe what the client sends, not only what it receives #1260
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
ec361a3
3624623
323ba7d
c303545
30d37a2
ce7e574
2e0f19b
8219c05
50ca61e
f6b9bf7
f5e9abf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -103,6 +103,105 @@ impl Drop for RawNodeLease { | |
| } | ||
| } | ||
|
|
||
| /// Lease that keeps sent-frame events enabled for one consumer. | ||
| /// | ||
| /// Dropping the final lease disables forwarding. The lease holds only a weak | ||
| /// client reference, so it cannot keep the client alive. | ||
| #[must_use = "dropping the lease immediately releases sent-frame forwarding"] | ||
| pub struct SentFrameLease { | ||
| client: std::sync::Weak<Client>, | ||
| } | ||
|
|
||
| impl Drop for SentFrameLease { | ||
| fn drop(&mut self) { | ||
| let Some(client) = self.client.upgrade() else { | ||
| return; | ||
| }; | ||
| client.sent_frame_tap.release(); | ||
| } | ||
| } | ||
|
|
||
| /// Publishes the plaintext frames that reached the transport as | ||
| /// [`Event::SentFrame`](wacore::types::events::Event::SentFrame). | ||
| /// | ||
| /// The client owns it and hands the noise sender a clone of the `Arc`, the same | ||
| /// way it hands over [`SessionStats`](wacore::stats::SessionStats): the gate has | ||
| /// to be readable from the one point every send crosses, and that task cannot | ||
| /// hold the client without keeping it alive. | ||
| pub(crate) struct SentFrameTap { | ||
| /// Number of consumers currently requesting the event. | ||
| forwarding: AtomicUsize, | ||
| bus: wacore::types::events::CoreEventBus, | ||
| /// Proves the no-lease path builds nothing, rather than only that it | ||
| /// dispatches nothing. | ||
| #[cfg(test)] | ||
| published: AtomicUsize, | ||
| } | ||
|
|
||
| impl SentFrameTap { | ||
| pub(crate) fn new(bus: wacore::types::events::CoreEventBus) -> Self { | ||
| Self { | ||
| forwarding: AtomicUsize::new(0), | ||
| bus, | ||
| #[cfg(test)] | ||
| published: AtomicUsize::new(0), | ||
| } | ||
| } | ||
|
|
||
| /// Enable forwarding for one consumer. The public door is | ||
| /// [`Client::acquire_sent_frame_forwarding`], which pairs this with a lease | ||
| /// that releases it on drop; a caller here owns that pairing itself. | ||
| pub(crate) fn acquire(&self) { | ||
| let incremented = self | ||
| .forwarding | ||
| .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |count| { | ||
| count.checked_add(1) | ||
| }) | ||
| .is_ok(); | ||
| assert!(incremented, "sent-frame forwarding lease counter overflow"); | ||
| } | ||
|
|
||
| pub(crate) fn release(&self) { | ||
| let previous = self.forwarding.fetch_sub(1, Ordering::Relaxed); | ||
| debug_assert!(previous > 0, "sent-frame forwarding lease underflow"); | ||
| } | ||
|
|
||
| #[inline] | ||
| pub(crate) fn enabled(&self) -> bool { | ||
| self.forwarding.load(Ordering::Relaxed) != 0 | ||
| } | ||
|
|
||
| /// Hand one frame to the observers. | ||
| /// | ||
| /// The dispatch is caught: a consumer that only watches must not be able to | ||
| /// take the send pipeline down with it, and this runs on the noise sender | ||
| /// task, whose death would end every send on the connection. Containment is | ||
| /// per dispatch, not per handler, so a panicking observer costs this frame | ||
| /// for the observers behind it — the bus offers no per-handler isolation for | ||
| /// any kind, and plugins already wrap their own handlers. A handler that | ||
| /// *blocks* still stalls sends, the contract every handler has on the read | ||
| /// loop. | ||
| pub(crate) fn publish(&self, plaintext: bytes::Bytes) { | ||
| #[cfg(test)] | ||
| self.published.fetch_add(1, Ordering::Relaxed); | ||
| let dispatch = std::panic::AssertUnwindSafe(|| { | ||
| self.bus.dispatch(Event::SentFrame( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: A panicking Prompt for AI agents |
||
| wacore::types::events::SentFrame::builder() | ||
| .plaintext(plaintext) | ||
| .build(), | ||
| )); | ||
| }); | ||
| if std::panic::catch_unwind(dispatch).is_err() { | ||
| warn!("A sent-frame observer panicked; the send pipeline is unaffected."); | ||
| } | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| #[cfg(test)] | ||
| pub(crate) fn published(&self) -> usize { | ||
| self.published.load(Ordering::Relaxed) | ||
| } | ||
| } | ||
|
|
||
| /// Filter for matching incoming stanzas (nodes) by tag and attributes. | ||
| /// | ||
| /// Used with [`Client::wait_for_node`] to wait for specific stanzas. | ||
|
|
@@ -1453,6 +1552,10 @@ pub struct Client { | |
| /// forwarding. | ||
| decrypted_payload_forwarding: AtomicUsize, | ||
|
|
||
| /// Gate and publisher for `Event::SentFrame`. Behind an `Arc` because the | ||
| /// noise sender task reads it; see [`SentFrameTap`]. | ||
| pub(crate) sent_frame_tap: Arc<SentFrameTap>, | ||
|
|
||
| /// Stanza interceptors, behind the same copy-on-write snapshot the event | ||
| /// bus uses: reading one costs a refcount bump, so the read loop allocates | ||
| /// nothing per stanza. Registering is the rare side, and pays the copy. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.