6

Asynchronous http requests using Netty and Scala actors

Hey hope someone can give me a hand with this.

I am trying to use the Scala Actors and Netty.io libraries to get make asynchronous http requests. (Yes I know Scala actors are being deprecated but this is a learning exercise for me)

I have written an actor HttpRequestActor that accepts a message in the form of a case class RequestPage(uri:URI).

When it receives the message it creates the necessary Netty objects need to make a http request, I have based most of the code from the [HttpSnoopClient] (http://static.netty.io/3.5/xref/org/jboss/netty/example/http/snoop/HttpSnoopClient.html) example.

I create a client and pass the current actor instance to my implementation of ChannelPipelineFactory which also passes the actor to my implementation of SimpleChannelUpstreamHandler, where I have overridden the messageReceived function.

The actor instance is passed as a listener, I create a request using the DefaultHttpRequest class and write to the channel to make the request.

There is a blocking call to an actor object using the ChannelFuture object returned from writing to the channel. When the messageRecieved function of my handler class is called I parse the response of the netty http request as a string, send a message back to actor with the content of the response and close the channel.

After the future is completed my code attempts to send a reply to the calling actor with the http content response received.

The code works, and I am able to get a reply, send it to my actor instance, print out the content and send a message to the actor instance release resources being used.

Problem is when I test it, the original call to the actor does not get a reply and the thread just stays open.

Code Sample - HttpRequestActor

my code for my HttpRequestActor class

    import scala.actors.Actor
import java.net.{InetSocketAddress,URI}
import org.jboss.netty.handler.codec.http._
import org.jboss.netty.bootstrap.ClientBootstrap
import org.jboss.netty.channel.Channel
import org.jboss.netty.channel._
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory
import org.jboss.netty.channel.group.DefaultChannelGroup
import java.util.concurrent.{Executors,CancellationException}
import org.jboss.netty.util.CharsetUtil
import scala.concurrent.{ Promise, Future }
import scala.concurrent.ExecutionContext.Implicits.global

/**
 * @author mebinum
 *
 */
class HttpRequestActor extends Actor {
    //initialize response with default uninitialized value
    private var resp:Response = _
    private val executor = Executors.newCachedThreadPool
    private val executor2 = Executors.newCachedThreadPool
    private val factory = new NioClientSocketChannelFactory(
                          executor,
                          executor2);

    private val allChannels = new DefaultChannelGroup("httpRequester")

    def act = loop {
        react {
            case RequestPage(uri) => requestUri(uri)
            case Reply(msg) => setResponse(Reply(msg))
            case NoReply => println("didnt get a reply");setResponse(NoReply)
            case NotReadable => println("got a reply but its not readable");setResponse(NotReadable)
            case ShutDown => shutDown()
        }
    }

    private def requestUri(uri:URI) = {

      makeChannel(uri) map {
          channel => {
              allChannels.add(channel)
              val request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri.toString)
              request.setHeader(HttpHeaders.Names.HOST, uri.getHost())
              request.setHeader(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.CLOSE)
              request.setHeader(HttpHeaders.Names.ACCEPT_ENCODING, HttpHeaders.Values.GZIP)

              val writeFuture = channel.write(request).awaitUninterruptibly()

              FutureReactor !? writeFuture match {
                  case future : ChannelFuture => {
                      future.addListener(new ChannelFutureListener() {
                          def operationComplete(future:ChannelFuture) {
                              // Perform post-closure operation
                              println("current response is " + resp)
                              sendResponse("look ma I finished")
                          }
                      })
                      future.getChannel().close()
                  }
              }

              this ! ShutDown
          }
      }
      //thread ends only if you send a reply from here
      //println("this is final sender " + sender)
      //reply("I am the true end")
    }

    private def makeChannel(uri:URI) = {
      val scheme = Some(uri.getScheme()).getOrElse("http")
      val host = Some(uri.getHost()).getOrElse("localhost")

      val port = Utils.getPort(uri.getPort, uri.getScheme)

      // Set up the event pipeline factory.
      val client = new ClientBootstrap(factory)
      client.setPipelineFactory(new PipelineFactory(this))

      //get the promised channel
      val channel = NettyFutureBridge(client.connect(new InetSocketAddress(host, port)))
      channel  
    }

    private def setResponse(aResponse:Response) = resp = aResponse

    private def sendResponse(msg:String) = {
      println("Sending the response " + msg)
      reply(resp)
    }

    private def shutDown() = {
        println("got a shutdown message")
        val groupFuture = allChannels.close().awaitUninterruptibly()
        factory.releaseExternalResources()
    }

    override def exceptionHandler = {
      case e : CancellationException => println("The request was cancelled"); throw e
      case tr: Throwable => println("An unknown exception happened " + tr.getCause()); throw tr
    }
}



trait Response
case class RequestPage(url:URI)

case class Reply(content:String) extends Response
case object NoReply extends Response
case object NotReadable extends Response
case object ShutDown

object FutureReactor extends Actor{
  def act = //loop {
      react {
        case future: ChannelFuture => {
            if (future.isCancelled) {
                throw new CancellationException()
            }
            if (!future.isSuccess()) {
                future.getCause().printStackTrace()
                throw future.getCause()
            }
            if(future.isSuccess() && future.isDone()){
                future.getChannel().getCloseFuture().awaitUninterruptibly()
                reply(future)
            }
        }
      }
    //}
  this.start
}


class ClientHandler(listener:Actor) extends SimpleChannelUpstreamHandler {

  override def exceptionCaught( ctx:ChannelHandlerContext, e:ExceptionEvent){
    e.getCause().printStackTrace()
    e.getChannel().close();
    throw e.getCause()
  }

  override def messageReceived(ctx:ChannelHandlerContext,  e:MessageEvent) = {
        var contentString = ""
        var httpResponse:Response =  null.asInstanceOf[Response]

        e.getMessage match {
          case (response: HttpResponse) if !response.isChunked => {
              println("STATUS: " + response.getStatus);
              println("VERSION: " + response.getProtocolVersion);
              println

              val content = response.getContent();
              if (content.readable()) {
                  contentString = content.toString(CharsetUtil.UTF_8)
                  httpResponse = Reply(contentString)
                  //notify actor

              }else{
                 httpResponse = NotReadable
              }
          }
          case chunk: HttpChunk if !chunk.isLast => {
            //get chunked content
            contentString = chunk.getContent().toString(CharsetUtil.UTF_8)
            httpResponse = Reply(contentString)
          }
          case _ => httpResponse = NoReply
        }
         println("sending actor my response")
         listener ! httpResponse
         println("closing the channel")
         e.getChannel().close()
         //send the close event

    }


}


class PipelineFactory(listener:Actor) extends ChannelPipelineFactory {

    def  getPipeline(): ChannelPipeline = {
            // Create a default pipeline implementation.
            val pipeline = org.jboss.netty.channel.Channels.pipeline()

            pipeline.addLast("codec", new HttpClientCodec())

            // Remove the following line if you don't want automatic content decompression.
            pipeline.addLast("inflater", new HttpContentDecompressor())

            // Uncomment the following line if you don't want to handle HttpChunks.
            //pipeline.addLast("aggregator", new HttpChunkAggregator(1048576))

            pipeline.addLast("decoder", new HttpRequestDecoder())
            //assign the handler
            pipeline.addLast("handler", new ClientHandler(listener))

            pipeline;
    }
}


object NettyFutureBridge { 
  import scala.concurrent.{ Promise, Future }
  import scala.util.Try
  import java.util.concurrent.CancellationException 
  import org.jboss.netty.channel.{ Channel, ChannelFuture, ChannelFutureListener }

  def apply(nettyFuture: ChannelFuture): Future[Channel] = { 
    val p = Promise[Channel]() 
    nettyFuture.addListener(new ChannelFutureListener { 
      def operationComplete(future: ChannelFuture): Unit = p complete Try( 
        if (future.isSuccess) {
          println("Success")
          future.getChannel
        }
        else if (future.isCancelled) {
          println("Was cancelled")
          throw new CancellationException 
        }

        else {
          future.getCause.printStackTrace()
          throw future.getCause
        })
    }) 
    p.future 
  }
} 

Code to test it

val url = "http://hiverides.com"

test("Http Request Actor can recieve and react to message"){
    val actor = new HttpRequestActor()
    actor.start

    val response = actor !? new RequestPage(new URI(url)) 
    match {
      case Reply(msg) => {
          println("this is the reply response in test")
          assert(msg != "")
          println(msg)
        }
      case NoReply => println("Got No Reply")
      case NotReadable => println("Got a not Reachable")
      case None => println("Got a timeout")
      case s:Response => println("response string \n" + s)
      case x => {println("Got a value not sure what it is"); println(x);}

    }
  }

Libraries used: - Scala 2.9.2 - Netty.io 3.6.1.Final - Junit 4.7 - scalatest 1.8 - I am also using @viktorklang NettyFutureBridge object gist to create a scala future for the Channel object returned

How can I send a reply back to the actor object with the content of response from Netty and end the thread?

Any help will be much appreciated

Mike E.
  • 2,163
  • 1
  • 15
  • 13
  • In case you didn't already know about it, check out [Dispatch](http://dispatch.databinder.net/Dispatch.html) – Dylan Jan 06 '13 at 15:32
  • thanks for the link Dylan, the library looks comprehensive, I still would like an simple solution and to really to understand what I am doing wrong – Mike E. Jan 06 '13 at 22:16
  • I'm on the verge of checking in some code that uses Netty and Scala 2.10 futures. It's tested and it works, but it doesn't use actors. However, maybe it can help with the problem in this case. I'll let you know when it's done. – Sam Stainsby Jan 10 '13 at 22:39
  • In the meantime, I recommend using Wireshark or similar to see what's happening on the wire. – Sam Stainsby Jan 10 '13 at 22:40
  • hey Sam that will be awesome. I was toying with the idea of switching to Akka which is wat 2.10 uses I assume, please do tell me when you have it up. – Mike E. Jan 11 '13 at 00:03
  • Too easy - I put it up just now. The project is https://github.com/stainsby/uniscala-couch, and the basic HTTP client is in here: https://github.com/stainsby/uniscala-couch/tree/master/src/main/scala/net/uniscala/couch/http – Sam Stainsby Jan 11 '13 at 07:53
  • Why do you say the code works? It means you are trying that in other way than your test? – Edmondo Jan 11 '13 at 13:24
  • I am not with you @Edmondo1984 what do you mean? – Mike E. Jan 12 '13 at 01:45
  • @MikeE. Are you using Scala 2.10.RC or Scala 2.9.2 ? I could not find scala.concurrent.* in Scala 2.9.2. What I was able to understood from you code sample is you are setting the http response (which is from the netty messageReceived() callback) in var resp: Response and try to reply it back to the original sender on channel.write() future. The problem is channel.write() future completes before the messageReceived() callback. I can see some unnecessary channel.close(), Since you have Shutdown() message, why do need to call it in many places. – Jestan Nirojan Jan 12 '13 at 07:27
  • Hey @JestanNirojan I am using 2.9.2 but I might add that I have dependencies on Scalatra, so the if its not in the standard library it might be from there. var resp:Response is actually not a HttpResponse but a case class I created. I set the value of resp when the calling actor which I pass to ClientHandler receives a Reply(msg).This works as I am able to get a response back and display the value of Reply(msg). Yep you might be right about the excess close calls, that is just me trying to get it work. – Mike E. Jan 13 '13 at 01:44
  • @MikeE. This is my attempt of creating Netty actors using Akka :) , http://git.io/KSXjjA – Jestan Nirojan Jan 13 '13 at 12:55
  • "The code works, and I am able to get a reply, send it to my actor instance, print out the content and send a message to the actor instance release resources being used. Problem is when I test it, the original call to the actor does not get a reply and the thread just stays open." Why do you say the code works, how are you trying to use it to say it works if the test fails? – Edmondo Jan 14 '13 at 08:34
  • @Edmondo1984 run the code and you will see what I am talking about. – Mike E. Jan 16 '13 at 03:34
  • Mike E. How do you run it? – Edmondo Jan 16 '13 at 07:44
  • using Junit and scalatest. in eclipse run the the test code as a JUnit test – Mike E. Jan 19 '13 at 05:50

1 Answers1

0

I don't know Scala, but I had a similar issue. Try specifying the content-length header of the response.

In plain java:

HttpRequest r = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, uri);
            ChannelBuffer buffer = ChannelBuffers.copiedBuffer(input);
            r.setHeader(HttpHeaders.Names.HOST, "host");
            r.setHeader(HttpHeaders.Names.CONTENT_TYPE, "application/octet-stream");
            r.setHeader(HttpHeaders.Names.CONTENT_LENGTH, buffer.readableBytes());
            r.setHeader(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.CLOSE);
            r.setContent(buffer);

Otherwise the server has no idea when the content is completed from the client, unless the client closes the connection.

You can also use chunked encoding, but you'll have to implement the chunk encoding yourself (At least I don't know of a library in Netty that does it).

aaron
  • 168
  • 1
  • 5
  • Thanks for the response @aaron, tried it out but didnt make a difference. Think the issue might actually have to be with my Scala actor code and not with my netty implementation – Mike E. Jan 10 '13 at 04:59