user3621841
user3621841

Reputation: 855

Server Sent Events with Grails - How to prevent the controller from closing the connection

this question is related to: Grails Server Sent Event

I am trying to implement SSE on grails v2.4 but I cannot prevent grails from closing the connection. What I have is:

import org.codehaus.groovy.grails.web.servlet.GrailsApplicationAttributes as GA
class SseController  {
  def heartbeat = {
    response.contentType = 'text/event-stream'
    response.characterEncoding = 'UTF-8'
    response.setHeader('Cache-Control', 'no-cache')
    response.setHeader('Connection', 'keep-alive')
    response << 'data: 12345\n\n'
    response.flushBuffer()

    def grails_request = request.getAttribute(GA.WEB_REQUEST)
    grails_request.setRenderView(false)
  }
}

But if I do so, the client browser reports the following after subscribing to the channel:

Open [object Event]
Data:12345
Error [object Event]

The Error I get is due to the fact that the connection is closed after the 'heartbeat' action is finished in the controller. If I add a while loop to keep the action running like this...

import org.codehaus.groovy.grails.web.servlet.GrailsApplicationAttributes as GA
class SseController  {
  def heartbeat = {
    response.contentType = 'text/event-stream'
    response.characterEncoding = 'UTF-8'
    response.setHeader('Cache-Control', 'no-cache')
    response.setHeader('Connection', 'keep-alive')
    response << 'data: 12345\n\n'
    response.flushBuffer()

    def grails_request = request.getAttribute(GA.WEB_REQUEST)
    grails_request.setRenderView(false)

    while(true) {
      sleep(10000)
    }
  }
}

... then the data is NEVER received by the client browser. The response.flushBuffer() command has no effect whatsoever. The connection is NOT closed, that's alright, but the data is NOT sent to the client.

So the right solution would involve getting rid of the while loop and at the same time telling Grails NOT TO CLOSE the connection after the action is executed.

Anyone knows how to do this? By the way, I tried using both Tomcat and Jetty servers and the result is the same on both; the messages are all sent at once after the controller-action has been finished.

Upvotes: 0

Views: 1108

Answers (1)

Graeme Rocher
Graeme Rocher

Reputation: 7985

Native support for Server Sent Events has been implemented for Grails 3.2.

The plugin sources can be found here

You could port this plugin to Grails 2 if you wish to continue using Grails 2. However, if you are intent in rolling your own solution then the key to implementing is that you need to start a non-blocking asynchronous response. You can see how this is done in the RxResultTransformer class of the plugin.

The key part is this:

webRequest.setRenderView(false)

// Create the Async web request and register it with the WebAsyncManager so Spring is aware
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request)

AsyncWebRequest asyncWebRequest = new AsyncGrailsWebRequest(
        request,
        response,
        webRequest.servletContext)

asyncManager.setAsyncWebRequest(asyncWebRequest)
// Start async processing and create the GrailsAsync object
asyncWebRequest.startAsync()
request.setAttribute(GrailsApplicationAttributes.ASYNC_STARTED, true)
GrailsAsyncContext asyncContext = new GrailsAsyncContext(asyncWebRequest.asyncContext, webRequest)
response.setContentType(CONTENT_TYPE_EVENT_STREAM);
response.flushBuffer()

You would then need to start another container thread that periodically sent data back to the client. The plugin does this using RxJava:

Observable newObservable = Observable.create( { Subscriber newSub ->
    asyncContext.start {
        // your code here
    }
} as Observable.OnSubscribe)
newObservable.subscribe(subscriber)

If you don't want to use RxJava then you can simply use a while loop or whatever works for you.

asyncContext.start {
      while(true) {
          // write event
      }   
}

Upvotes: 1

Related Questions