centrifugal / centrifugo

Scalable real-time messaging server in a language-agnostic way. Self-hosted alternative to Pubnub, Pusher, Ably. Set up once and forever.
https://centrifugal.dev
Apache License 2.0
8.44k stars 598 forks source link

[feature] Silently Reject Publish Attempts on Proxied Channel Without Sending Client Errors #774

Closed mcagridurgut closed 9 months ago

mcagridurgut commented 9 months ago

Firstly, thanks for this project, been helping me a lot.

I am trying to implement a feature to my application where it receives messages from channels but only publish on tick-rate. The server receives messages from channels, immediately saves it and rejects users message to be published to channel. ( I publish the merged messages to the channel with server API on a separate goroutine )

I've configured a Centrifugo channel to use a publish proxy, intending to intercept and process messages server-side. The channel configuration in my centrifugo.json looks like this:

{
  "name": "public",
  "join_leave": true,
  "force_push_join_leave": true,
  "publish": true,
  "allow_publish_for_subscriber": false,
  "publish_proxy_name": "publish_public"
}

I have implemented a proxy middleware in Go, proxyPublicHandler, to handle the proxied publish requests. The middleware processes the messages and saves it. Here's a simplified version of the handler:

func proxyPublicHandler(w http.ResponseWriter, r *http.Request) {
    var incomingMessage ProxyMessage
    err := json.NewDecoder(r.Body).Decode(&incomingMessage)
    if err != nil {
        log.Println("Error decoding request body at public:", err)
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    messageChannel <- incomingMessage.Data // I process these messages on a separate goroutine

    okMessage := map[string]interface{}{"status": map[string]interface{} {}}
    jsonResult, err := json.Marshal(okMessage)
    if err != nil {
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }
    w.Header().Set("Content-Type", "application/json")
    w.Write(jsonResult)
}

The problem is that this type of proxy allows subscriber to publish messages to channel as if there is no tick-rate on my back-end. Therefore I tried to reject every coming request to the channel like this:

okMessage := map[string]interface{}{"error": map[string]interface{}{"code": 1000, "message": "you are not allowed to publish"}}

Above configuration from proxy practically works but at user side I receive error messages for every publish I try to make. I do not want that.

A possible solution on my side can be changing the data field of the published message to an empty object or only allow user to publish from their user-limited channels so that other users won't receive any message between tick-rates.

However I thought this could be a common scenario for any user who wants to implement some sort of tick-rate based back-end for their application so I therefore look for a way to silently reject publish attempts on proxied channel without sending client an error code.

FZambia commented 9 months ago

Hello @mcagridurgut

Publish operation on client side expects some result from server within timeout, so we need to return something. Returning error is not ideal – I understand, returning success is possible – but this seems not ideal from semantic side of view - i.e. if you issue publish and receive OK – you expect message to be published to channel and received by all subscribers. Also, if you suppress some publications – it may eventually affect metrics, analytics.

What you are doing in the app seems like not a publishing, but client-side RPC. Client sends inputs to server, server just accepts them, this eventually results into real publishing by a server on tick. So did you consider using rpc client-side method and RPC proxy? In this case you can simply return OK to client after accepting input, then publish to channels from the backend in the required rate. Semantically correct and correct in terms of observability.

mcagridurgut commented 9 months ago

Thanks @FZambia

Thank you for your swift response and the insightful suggestion regarding the use of client-side RPC in place of direct publishing for handling the messages from clients. You are right that the action I try to make is no "publish" and the idea of using RPC for semantically correct operations and improved observability makes a lot of sense in the context of my application's requirements.

However, I have some concerns regarding potential performance implications of this approach. Intuitively, I thought that direct WebSocket communication would offer the best performance for real-time interactions, given its low latency and overhead. The plan was to design all outgoing traffic from the client using WebSockets to ensure fast and efficient communication.

I know that this might be a little out of scope of this issue but could you please provide some insights or share your experience on how using RPC over WebSockets compares in terms of performance to direct WebSocket communication? Specifically, I'm interested in understanding:

Thank you once again for your assistance.

FZambia commented 9 months ago

Intuitively, I thought that direct WebSocket communication would offer the best performance for real-time interactions, given its low latency and overhead. The plan was to design all outgoing traffic from the client using WebSockets to ensure fast and efficient communication.

Looks like I need to improve the doc – RPC calls when using Centrifugo real-time SDKs are also made through the WebSocket connection. In this case just instead of Publish frame client sends RPC frame – then WebSocket frame reaches Centrifugo, then in both cases Centrifugo calls proxy endpoint. So it will be the same as you had with publish in terms of latency and transport used from client-side.

The concern here though – that theoretically you could benefit from not having Centrifugo between client and your app at all - i.e. if you have latency-critical app and the latency introduced by Centrifugo-to-backend communication is becoming an issue - then you can think on having your own WebSocket service and keep business logic inside it. In Centrifugal ecosystem we provide https://github.com/centrifugal/centrifuge library for Go (the core of Centrifugo server) which allows doing this. But using that library means you will loose many benefits of Centrifugo built on top of that library. But your client side will stay the same since https://github.com/centrifugal/centrifuge and Centrifugo share the same client protocol.

mcagridurgut commented 9 months ago

Thanks for thoughtful response, closing the issue