spray/spray — Issue #90: Apparent race condition in spray client pipelining
Author: quelgar
Created: 2012-03-05 00:35:50+00:00
State: open

A can reproduce, pretty reliably, a NullPointerException when using the Spray client (version 0.9.0-RC4):

```
[DEBUG] [5/03/12 11:16 AM] [akka:event-driven:dispatcher:spray-can-client-1] [LoadTest.MyConduit] Dispatching GET request to http://XXXXX:8080/XXXXXXXXXXX
[DEBUG] [5/03/12 11:16 AM] [akka:event-driven:dispatcher:global-2] [LoadTest.MyConduit] Received 'cc.spray.can.HttpClientException: Cannot send request due to closed connection' in response to GET request to http://XXXXX:8080/XXXXXXXXXXX with 5 retries left, retrying...
11:16:39.404 [akka:event-driven:dispatcher:global-19] ERROR cc.spray.can.HttpClient - HttpClient crashed, about to restart...
message: Send(cc.spray.can.HttpClient$ClientConnection@76e992d,HttpRequest(GET,/XXXXXXXXXXX,List(),[B@6e0a5d2a,HTTP/1.1),List(java.nio.HeapByteBuffer[pos=0 lim=90 cap=90]),None,Some())
reason: java.lang.NullPointerException
[DEBUG] [5/03/12 11:16 AM] [akka:event-driven:dispatcher:spray-can-client-1] [LoadTest.MyConduit] Dispatching GET request to http://XXXXX:8080/XXXXXXXXXXX
```

and in the same log:

```
[DEBUG] [5/03/12 11:16 AM] [akka:event-driven:dispatcher:spray-can-client-1] [LoadTest.MyConduit] Dispatching GET request to http://XXXXX:8080/XXXXXXXXXXX
[ERROR] [5/03/12 11:16 AM] [akka:event-driven:dispatcher:spray-can-client-1] [LocalActorRef] null
java.lang.NullPointerException
at cc.spray.can.LinkedList.$plus$eq(LinkedList.scala:49)
at cc.spray.can.HttpClient.cc$spray$can$HttpClient$$prepareWriting(HttpClient.scala:177)
at cc.spray.can.HttpClient$$anonfun$receive$1.apply(HttpClient.scala:104)
at cc.spray.can.HttpClient$$anonfun$receive$1.apply(HttpClient.scala:97)
at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:46)
at akka.actor.Actor$class.apply(Actor.scala:545)
at cc.spray.can.HttpPeer.apply(HttpPeer.scala:73)
at akka.actor.LocalActorRef.invoke(ActorRef.scala:910)
at akka.dispatch.MessageInvocation.invoke(MessageHandling.scala:25)
at akka.dispatch.ExecutableMailbox$class.processMailbox(ExecutorBasedEventDrivenDispatcher.scala:216)
at akka.dispatch.ExecutorBasedEventDrivenDispatcher$$anon$4.processMailbox(ExecutorBasedEventDrivenDispatcher.scala:123)
at akka.dispatch.ExecutableMailbox$class.run(ExecutorBasedEventDrivenDispatcher.scala:195)
at akka.dispatch.ExecutorBasedEventDrivenDispatcher$$anon$4.run(ExecutorBasedEventDrivenDispatcher.scala:123)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
at java.lang.Thread.run(Thread.java:722)
at akka.dispatch.MonitorableThread.run(ThreadPoolBuilder.scala:192)
```

Here is the test program I'm using:

```
import java.net.URI
import cc.spray.client.{Get, ConduitConfig, HttpConduit, DispatchStrategies}
import akka.dispatch.Future
import cc.spray.can.HttpClient
import akka.config.Supervision._
import akka.actor.{Supervisor, PoisonPill, Actor}
import cc.spray.typeconversion.DefaultUnmarshallers

object LoadTest {

import DefaultUnmarshallers.StringUnmarshaller

private val config = ConduitConfig(
maxConnections = 10,
dispatchStrategy = DispatchStrategies.Pipelined,
maxRetries = 5)

private final class MyConduit(host: String, port: Int) extends HttpConduit(host, port, config) {

val pipeline = simpleRequest ~> sendReceive ~> unmarshal[String]

}

def loadTest(uri: URI) {

val c = new MyConduit(uri.getHost, uri.getPort)

val uriString = new URI(null, null, uri.getPath, uri.getQuery, null).toASCIIString

println("*** Getting " + uriString)

for (_ <- 1 to 3) {
val futures = for (_ <- 1 to 100) yield {
// Thread.sleep(20L)
c.pipeline(Get(uri = uriString))
}
Future.sequence(futures).get.foreach(x => println("*** " + x))
}
}

def main(args: Array[String]) {
Supervisor(
SupervisorConfig(
OneForOneStrategy(List(classOf[Exception]), 3, 100),
List(Supervise(Actor.actorOf(new HttpClient()), Permanent))
)
)

loadTest(new URI(args.head))

Actor.registry.foreach(_ ! PoisonPill)
Actor.registry.shutdownAll()
}

}
```

I believe it's a race condition somewhere, as uncommenting the `Thread.sleep(20)` seems to avoid the problem. Switching to non-pipelined dispatch also avoids the problem.