rails / solid_cable

A database backed ActionCable adapter
MIT License
204 stars 14 forks source link

Add Visibility on Number of Subscribers for Broadcasted Streams #42

Closed Dandush03 closed 3 weeks ago

Dandush03 commented 3 weeks ago

While using SolidCable to broadcast messages (e.g., via broadcast_append_to), the current return value is an ActiveRecord::Result. Although this seems to contain some metadata, it doesn’t provide direct feedback about how many subscribers/users actually received the broadcast.

This lack of visibility makes it difficult to track the success of the broadcast and ensure that messages are reaching their intended audience.

Expected Behavior:

In ActionCable (Rails' default adapter for real-time features), methods such as broadcast_append_to, broadcast_replace_to, etc., return the number of subscribers who successfully received the message. This allows developers to track how many users were targeted by the broadcast and confirm that it was successful.

For example, in ActionCable:

# ActionCable example:
count = ActionCable.server.broadcast("some_channel", message: "Hello")
puts count  # => 5 (number of users who received the broadcast)

This number directly reflects the total number of clients (subscribers) that received the message. Having this information is very useful when tracking message delivery, especially in applications relying on reliable real-time communication.

Current Behavior in SolidCable:

In SolidCable, the broadcast_append_to method returns an ActiveRecord::Result instead of an integer reflecting the number of recipients. While this may contain metadata about the operation, it doesn’t provide explicit feedback on how many users/subscribers actually received the broadcast.

This creates a gap in visibility, as we have no easy way of confirming whether the stream was successfully delivered to all intended users or how many users it reached.

Suggested Solution:

  1. Enhance the broadcast methods (like broadcast_append_to, broadcast_replace_to, etc.) to return the number of users/subscribers who received the broadcast, similar to the behavior of ActionCable.

    result = SolidCable.broadcast_append_to(some_channel, content: "Hello world")
    puts result.count # Expected: 5 (if 5 users received the broadcast)
  2. Optional: Provide callback hooks or logging mechanisms to confirm the success of broadcasts and report the number of recipients. This could be useful for further debugging or monitoring purposes.

Why This is Important:

Having visibility into how many users receive a broadcast is critical for applications that rely on real-time communication. Without it, it is difficult to confirm that broadcasts are reaching the right audience or to debug situations where users are not receiving updates as expected.

This would bring SolidCable in line with the behavior provided by ActionCable, making it easier for developers to manage and monitor real-time message delivery.

Example of Desired Behavior:

# Expected behavior:
result = SolidCable.broadcast_append_to(some_channel, content: "Hello world")
# The result should include the count of how many users received the broadcast:
puts result.count  # Expected: 5 (if 5 users received the broadcast)

Thank you for considering this request. I believe this feature will significantly improve the usability and visibility of SolidCable's real-time messaging capabilities.

dhh commented 3 weeks ago

Never used this feature, but agree we should have parity on the return value any way.

npezza93 commented 3 weeks ago

Never used this feature, but agree we should have parity on the return value any way.

Hmm not sure this is the case across the board, @dhh. This is indeed how the redis adapter works but the postgres adapter returns this:

image

And the async adapter will return nil or an an array of subscribers.

I think we will need to setup another table to be able to persist subscribers...but let me think on it for a bit to see if i can come up with anything better.

dhh commented 3 weeks ago

Oh. Def not worth another table.

npezza93 commented 3 weeks ago

Yea while working on the implementation, i realized we will only be able to return the number of subscribers listening at the time of broadcast. Whether or not those subscribers are still subscribed when the message is picked up in the polling can't be determined at the time of broadcast. So we could end up returning incorrect numbers.

And since the broadcast method has no strict contract across all adapters on the return value, I think im going to close this out as not planned.

@dhh let me know if you disagree and we can reopen!

Dandush03 commented 2 weeks ago

@npezza93 What if we add a broadcasted column to the relevant records? This could be a boolean field indicating whether the message has been broadcast.

There’s also another issue that could potentially be addressed with this approach. It seems related to how the ActionCable::Listener memoizes the last_id variable across requests. Because the same last_id value is being reused, the listener can't accurately determine which records have already been streamed, leading to potential duplicate broadcasts in a loop.

By introducing a broadcasted flag, we could update the status of each record during or after the broadcast to prevent duplication. This would ensure that only unprocessed records are sent in subsequent loops, improving broadcast reliability. I believe this approach offers a lightweight solution without the need for additional tables while also addressing issues with memoization during broadcasting.

Let me know if this aligns with your thoughts or if further clarification is needed!. I will attach a video of the repeatedly cast broadcast message when refreshing the page:

https://github.com/user-attachments/assets/7bc05fc7-cb7c-47d8-b732-4bce9b3ff5a4

Notice how the adapter does not know how to handle already broadcasted messages, when the rails env, is set with multithreading, depending on which thread it enter it has a diferent last_id, but does not know how to skip or handle already broadcasted messages

npezza93 commented 2 weeks ago

What if we add a broadcasted column to the relevant records? This could be a boolean field indicating whether the message has been broadcast

This wont work, because with polling, one listener may have broadcast the message but another may not have.

Let me know if this aligns with your thoughts or if further clarification is needed!. I will attach a video of the repeatedly cast broadcast message when refreshing the page:

I would need to see some code around how this example works(specifically around how messages are broadcast). When you reload the page, you get a new cable connection and a new listener with grabs the latest message id so only new stuff would be broadcast out. If you could also show the network tab for the cable connection that shows the messages that are being broadcast to the browser, that would also be helpful.

Dandush03 commented 2 weeks ago

So, I refreshed 4 times, and got 4 diferent connections, which is expected. image

The first one, did broadcast old messages (unexpeted behavior) image

The second and third, did not (expected behavior)

The fourth, broadcasted all messages again (unexpected behavior) image

This wont work, because with polling, one listener may have broadcast the message but another may not have

In my opinion, the focus shouldn’t be on how many subscribers received the broadcast at any given moment. Instead, the goal should be to mark a message as broadcasted once it has been processed, rather than checking on each request whether it was sent.

By marking the record as broadcasted, we avoid unnecessary re-broadcasts in subsequent polling cycles. This approach simplifies the logic and ensures that messages are only broadcast once, regardless of how many listeners are connected at that exact time. The emphasis shifts from tracking every individual subscriber to maintaining message integrity by preventing duplicate broadcasts.

This way, even if some listeners miss the message due to polling timing, we maintain consistent behavior without overcomplicating the process.

Dandush03 commented 2 weeks ago

regarding, code, there we are calling it as usual,

::Broadcastable::Toast.broadcast_toast(
      resource: current_user,
      type: :error,
      title: I18n.t('.feedbacks.toast.error.title'),
      content: I18n.t('.feedbacks.toast.error.content'),
      date: Time.zone.now
    )

And this is done in a SolidQueue::Job 😅 but it's been queue only once.

npezza93 commented 2 weeks ago

Would you be able to create a minimal rails app that reproduces this?

Dandush03 commented 2 weeks ago

Would you be able to create a minimal rails app that reproduces this?

sure, I'll do it later this week

npezza93 commented 2 weeks ago

Thanks!