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 <hta@webrtc.org>
Reviewed-by: Victor Boivie <boivie@webrtc.org>
Reviewed-by: Florent Castelli <orphis@webrtc.org>
Commit-Queue: Victor Boivie <boivie@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#37396}
diff --git a/net/dcsctp/public/dcsctp_socket.h b/net/dcsctp/public/dcsctp_socket.h
index 770c674..8506397 100644
--- a/net/dcsctp/public/dcsctp_socket.h
+++ b/net/dcsctp/public/dcsctp_socket.h
@@ -54,6 +54,11 @@
   // If set, limits the number of retransmissions. This is only available
   // if the peer supports Partial Reliability Extension (RFC3758).
   absl::optional<size_t> 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 @@
   // 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 @@
   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 358e243..d072562 100644
--- a/net/dcsctp/public/types.h
+++ b/net/dcsctp/public/types.h
@@ -122,6 +122,22 @@
     return MaxRetransmits(std::numeric_limits<uint16_t>::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<class LifecycleIdTag, uint64_t> {
+ public:
+  constexpr explicit LifecycleId(const UnderlyingType& v)
+      : webrtc::StrongAlias<class LifecycleIdTag, uint64_t>(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 f673f31..123526e 100644
--- a/net/dcsctp/socket/callback_deferrer.cc
+++ b/net/dcsctp/socket/callback_deferrer.cc
@@ -160,4 +160,22 @@
   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 a7490d3..1c35dda 100644
--- a/net/dcsctp/socket/callback_deferrer.h
+++ b/net/dcsctp/socket/callback_deferrer.h
@@ -81,6 +81,12 @@
   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 803f688..8b2a772 100644
--- a/net/dcsctp/socket/mock_dcsctp_socket_callbacks.h
+++ b/net/dcsctp/socket/mock_dcsctp_socket_callbacks.h
@@ -126,6 +126,19 @@
               (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(); }