From 5e21262a44e10f9062c81e890ce7120b404b1505 Mon Sep 17 00:00:00 2001 From: Victor Boivie Date: Fri, 27 May 2022 09:21:20 +0200 Subject: [PATCH] dcsctp: Add API for lifecycle events This CL adds the API to enable message lifecycle events to be generated. Those can in turn be used to generate metrics, e.g. latency metrics tracking the time to send a message, the time until it's acknowledged, and metrics tracking how often messages are expired. This will be used to validate that message interleaving really improves latency for high priority data channels. The actual implementation of the API will be provided in follow-up CLs. Bug: webrtc:5696 Change-Id: Ic06f8244d1c79a336975e35479130521dff17519 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/264141 Reviewed-by: Harald Alvestrand Reviewed-by: Victor Boivie Reviewed-by: Florent Castelli Commit-Queue: Victor Boivie Cr-Commit-Position: refs/heads/main@{#37396} --- net/dcsctp/public/dcsctp_socket.h | 92 ++++++++++++++++++- net/dcsctp/public/types.h | 16 ++++ net/dcsctp/socket/callback_deferrer.cc | 18 ++++ net/dcsctp/socket/callback_deferrer.h | 6 ++ .../socket/mock_dcsctp_socket_callbacks.h | 13 +++ 5 files changed, 144 insertions(+), 1 deletion(-) diff --git a/net/dcsctp/public/dcsctp_socket.h b/net/dcsctp/public/dcsctp_socket.h index 770c6746f9..8506397581 100644 --- a/net/dcsctp/public/dcsctp_socket.h +++ b/net/dcsctp/public/dcsctp_socket.h @@ -54,6 +54,11 @@ struct SendOptions { // If set, limits the number of retransmissions. This is only available // if the peer supports Partial Reliability Extension (RFC3758). absl::optional max_retransmissions = absl::nullopt; + + // If set, will generate lifecycle events for this message. See e.g. + // `DcSctpSocketCallbacks::OnLifecycleMessageFullySent`. This value is decided + // by the client and the library will provide it to all lifecycle callbacks. + LifecycleId lifecycle_id = LifecycleId::NotSet(); }; enum class ErrorKind { @@ -389,6 +394,91 @@ class DcSctpSocketCallbacks { // buffer, for all streams) falls to or below the threshold specified in // `DcSctpOptions::total_buffered_amount_low_threshold`. virtual void OnTotalBufferedAmountLow() {} + + // == Lifecycle Events == + // + // If a `lifecycle_id` is provided as `SendOptions`, lifecycle callbacks will + // be triggered as the message is processed by the library. + // + // The possible transitions are shown in the graph below: + // + // DcSctpSocket::Send ────────────────────────┐ + // │ │ + // │ │ + // v v + // OnLifecycleMessageFullySent ───────> OnLifecycleMessageExpired + // │ │ + // │ │ + // v v + // OnLifeCycleMessageDelivered ────────────> OnLifecycleEnd + + // OnLifecycleMessageFullySent will be called when a message has been fully + // sent, meaning that the last fragment has been produced from the send queue + // and sent on the network. Note that this will trigger at most once per + // message even if the message was retransmitted due to packet loss. + // + // This is a lifecycle event. + // + // Note that it's NOT ALLOWED to call into this library from within this + // callback. + virtual void OnLifecycleMessageFullySent(LifecycleId lifecycle_id) {} + + // OnLifecycleMessageExpired will be called when a message has expired. If it + // was expired with data remaining in the send queue that had not been sent + // ever, `maybe_delivered` will be set to false. If `maybe_delivered` is true, + // the message has at least once been sent and may have been correctly + // received by the peer, but it has expired before the receiver managed to + // acknowledge it. This means that if `maybe_delivered` is true, it's unknown + // if the message was lost or was delivered, and if `maybe_delivered` is + // false, it's guaranteed to not be delivered. + // + // It's guaranteed that `OnLifecycleMessageDelivered` is not called if this + // callback has triggered. + // + // This is a lifecycle event. + // + // Note that it's NOT ALLOWED to call into this library from within this + // callback. + virtual void OnLifecycleMessageExpired(LifecycleId lifecycle_id, + bool maybe_delivered) {} + + // OnLifecycleMessageDelivered will be called when a non-expired message has + // been acknowledged by the peer as delivered. + // + // Note that this will trigger only when the peer moves its cumulative TSN ack + // beyond this message, and will not fire for messages acked using + // gap-ack-blocks as those are renegable. This means that this may fire a bit + // later than the message was actually first "acked" by the peer, as - + // according to the protocol - those acks may be unacked later by the client. + // + // It's guaranteed that `OnLifecycleMessageExpired` is not called if this + // callback has triggered. + // + // This is a lifecycle event. + // + // Note that it's NOT ALLOWED to call into this library from within this + // callback. + virtual void OnLifecycleMessageDelivered(LifecycleId lifecycle_id) {} + + // OnLifecycleEnd will be called when a lifecycle event has reached its end. + // It will be called when processing of a message is complete, no matter how + // it completed. It will be called after all other lifecycle events, if any. + // + // Note that it's possible that this callback triggers without any other + // lifecycle callbacks having been called before in case of errors, such as + // attempting to send an empty message or failing to enqueue a message if the + // send queue is full. + // + // NOTE: When the socket is deallocated, there will be no `OnLifecycleEnd` + // callbacks sent for messages that were enqueued. But as long as the socket + // is alive, `OnLifecycleEnd` callbacks are guaranteed to be sent as messages + // are either expired or successfully acknowledged. + // + // This is a lifecycle event. + // + // Note that it's NOT ALLOWED to call into this library from within this + // callback. + virtual void OnLifecycleEnd(LifecycleId lifecycle_id) {} }; // The DcSctpSocket implementation implements the following interface. @@ -444,7 +534,7 @@ class DcSctpSocketInterface { virtual StreamPriority GetStreamPriority(StreamID stream_id) const = 0; // Sends the message `message` using the provided send options. - // Sending a message is an asynchrous operation, and the `OnError` callback + // Sending a message is an asynchronous operation, and the `OnError` callback // may be invoked to indicate any errors in sending the message. // // The association does not have to be established before calling this method. diff --git a/net/dcsctp/public/types.h b/net/dcsctp/public/types.h index 358e243fc5..d0725620d8 100644 --- a/net/dcsctp/public/types.h +++ b/net/dcsctp/public/types.h @@ -122,6 +122,22 @@ class MaxRetransmits return MaxRetransmits(std::numeric_limits::max()); } }; + +// An identifier that can be set on sent messages, and picked by the sending +// client. If different from `::NotSet()`, lifecycle events will be generated, +// and eventually `DcSctpSocketCallbacks::OnLifecycleEnd` will be called to +// indicate that the lifecycle isn't tracked any longer. The value zero (0) is +// not a valid lifecycle identifier, and will be interpreted as not having it +// set. +class LifecycleId : public webrtc::StrongAlias { + public: + constexpr explicit LifecycleId(const UnderlyingType& v) + : webrtc::StrongAlias(v) {} + + constexpr bool IsSet() const { return value_ != 0; } + + static constexpr LifecycleId NotSet() { return LifecycleId(0); } +}; } // namespace dcsctp #endif // NET_DCSCTP_PUBLIC_TYPES_H_ diff --git a/net/dcsctp/socket/callback_deferrer.cc b/net/dcsctp/socket/callback_deferrer.cc index f673f31d74..123526e782 100644 --- a/net/dcsctp/socket/callback_deferrer.cc +++ b/net/dcsctp/socket/callback_deferrer.cc @@ -160,4 +160,22 @@ void CallbackDeferrer::OnTotalBufferedAmountLow() { deferred_.emplace_back( [](DcSctpSocketCallbacks& cb) { cb.OnTotalBufferedAmountLow(); }); } + +void CallbackDeferrer::OnLifecycleMessageExpired(LifecycleId lifecycle_id, + bool maybe_delivered) { + // Will not be deferred - call directly. + underlying_.OnLifecycleMessageExpired(lifecycle_id, maybe_delivered); +} +void CallbackDeferrer::OnLifecycleMessageFullySent(LifecycleId lifecycle_id) { + // Will not be deferred - call directly. + underlying_.OnLifecycleMessageFullySent(lifecycle_id); +} +void CallbackDeferrer::OnLifecycleMessageDelivered(LifecycleId lifecycle_id) { + // Will not be deferred - call directly. + underlying_.OnLifecycleMessageDelivered(lifecycle_id); +} +void CallbackDeferrer::OnLifecycleEnd(LifecycleId lifecycle_id) { + // Will not be deferred - call directly. + underlying_.OnLifecycleEnd(lifecycle_id); +} } // namespace dcsctp diff --git a/net/dcsctp/socket/callback_deferrer.h b/net/dcsctp/socket/callback_deferrer.h index a7490d3cab..1c35dda6cf 100644 --- a/net/dcsctp/socket/callback_deferrer.h +++ b/net/dcsctp/socket/callback_deferrer.h @@ -81,6 +81,12 @@ class CallbackDeferrer : public DcSctpSocketCallbacks { void OnBufferedAmountLow(StreamID stream_id) override; void OnTotalBufferedAmountLow() override; + void OnLifecycleMessageExpired(LifecycleId lifecycle_id, + bool maybe_delivered) override; + void OnLifecycleMessageFullySent(LifecycleId lifecycle_id) override; + void OnLifecycleMessageDelivered(LifecycleId lifecycle_id) override; + void OnLifecycleEnd(LifecycleId lifecycle_id) override; + private: void Prepare(); void TriggerDeferred(); diff --git a/net/dcsctp/socket/mock_dcsctp_socket_callbacks.h b/net/dcsctp/socket/mock_dcsctp_socket_callbacks.h index 803f688a84..8b2a772fa3 100644 --- a/net/dcsctp/socket/mock_dcsctp_socket_callbacks.h +++ b/net/dcsctp/socket/mock_dcsctp_socket_callbacks.h @@ -126,6 +126,19 @@ class MockDcSctpSocketCallbacks : public DcSctpSocketCallbacks { (override)); MOCK_METHOD(void, OnBufferedAmountLow, (StreamID stream_id), (override)); MOCK_METHOD(void, OnTotalBufferedAmountLow, (), (override)); + MOCK_METHOD(void, + OnLifecycleMessageExpired, + (LifecycleId lifecycle_id, bool maybe_delivered), + (override)); + MOCK_METHOD(void, + OnLifecycleMessageFullySent, + (LifecycleId lifecycle_id), + (override)); + MOCK_METHOD(void, + OnLifecycleMessageDelivered, + (LifecycleId lifecycle_id), + (override)); + MOCK_METHOD(void, OnLifecycleEnd, (LifecycleId lifecycle_id), (override)); bool HasPacket() const { return !sent_packets_.empty(); }