Permalink
Browse files

MemoryTape no longer maintains a read position so it is thread safe. …

…Fixes #57
  • Loading branch information...
1 parent 55bd747 commit 467c884ad76b31682b684fd3fc546f680ebbaad3 Rob Fletcher committed Aug 6, 2012
@@ -50,31 +50,23 @@ interface Tape {
int size()
/**
- * Attempts to find a recorded interaction on the tape that matches the supplied request's method and URI. If the
- * method succeeds then subsequent calls to `play` will play back the response that was found.
+ * Attempts to find a recorded interaction on the tape that matches the supplied request.
* @param request the HTTP request to match.
* @return `true` if a matching recorded interaction was found, `false` otherwise.
*/
boolean seek(Request request)
/**
- * Resets the tape so that no recorded interaction is ready to play. Subsequent calls to `play` will throw
- * `IllegalStateException` until a successful call to `seek` is made.
- */
- void reset()
-
- /**
* Plays back a previously recorded interaction to the supplied response. Status, headers and entities are copied
* from the recorded interaction to `response`.
* @param response the HTTP response to populate.
- * @throws IllegalStateException if no recorded interaction has been found by a previous call to `seek`.
+ * @throws IllegalStateException if no matching recorded interaction exists.
*/
- void play(Response response)
+ void play(Request request, Response response)
/**
- * Records a new interaction to the tape. If the tape is currently positioned to read a recorded interaction due to
- * a previous successful `seek` call then this method will overwrite the existing recorded interaction. Otherwise
- * the newly recorded interaction is appended to the tape.
+ * Records a new interaction to the tape. If `request` matches an existing interaction this method will overwrite
+ * it. Otherwise the newly recorded interaction is appended to the tape.
* @param request the request to record.
* @param response the response to record.
* @throws UnsupportedOperationException if this `Tape` implementation is not writable.
@@ -43,7 +43,7 @@ class RecordAndPlaybackProxyInterceptor implements VetoingProxyInterceptor {
} else if (tape.seek(request) && tape.isReadable()) {
log.info "playing back from tape '$tape.name'..."
response.addHeader(X_BETAMAX, "PLAY")
- tape.play(response)
+ tape.play(request, response)
true
} else if (!tape.isWritable()) {
response.setError(HTTP_FORBIDDEN, "Tape is read-only")
@@ -35,7 +35,6 @@ class MemoryTape implements Tape {
List<RecordedInteraction> interactions = []
private TapeMode mode = READ_WRITE
private Comparator<Request>[] matchRules = [method, uri]
- private int position = -1
void setMode(TapeMode mode) {
this.mode = mode
@@ -57,23 +56,20 @@ class MemoryTape implements Tape {
interactions.size()
}
+
boolean seek(Request request) {
def requestMatcher = new RequestMatcher(request, matchRules)
- position = interactions.findIndexOf {
+ interactions.any {
requestMatcher.matches(it.request)
}
- position >= 0
}
- void reset() {
- position = -1
- }
+ void play(Request request, Response response) {
+ if (!mode.readable) throw new IllegalStateException("the tape is not readable")
- void play(Response response) {
- if (!mode.readable) {
- throw new IllegalStateException("the tape is not readable")
- } else if (position < 0) {
- throw new IllegalStateException("the tape is not ready to play")
+ int position = findMatch(request)
+ if (position < 0) {
+ throw new IllegalStateException("no matching recording found")
} else {
def interaction = interactions[position]
response.status = interaction.response.status
@@ -94,15 +90,14 @@ class MemoryTape implements Tape {
}
void record(Request request, Response response) {
- if (mode.writable) {
- def interaction = new RecordedInteraction(request: recordRequest(request), response: recordResponse(response), recorded: new Date())
- if (position >= 0) {
- interactions[position] = interaction
- } else {
- interactions << interaction
- }
+ if (!mode.writable) throw new IllegalStateException("the tape is not writable")
+
+ def interaction = new RecordedInteraction(request: recordRequest(request), response: recordResponse(response), recorded: new Date())
+ int position = findMatch(request)
+ if (position >= 0) {
+ interactions[position] = interaction
} else {
- throw new IllegalStateException("the tape is not writable")
+ interactions << interaction
}
}
@@ -111,6 +106,13 @@ class MemoryTape implements Tape {
"Tape[$name]"
}
+ private int findMatch(Request request) {
+ def requestMatcher = new RequestMatcher(request, matchRules)
+ interactions.findIndexOf {
+ requestMatcher.matches(it.request)
+ }
+ }
+
private static RecordedRequest recordRequest(Request request) {
def clone = new RecordedRequest()
clone.method = request.method
@@ -54,9 +54,6 @@ class ProxyRecordAndPlaybackSpec extends Specification {
then:
recorder.tape.size() == 1
-
- cleanup:
- recorder.tape.reset()
}
@Timeout(10)
@@ -50,7 +50,7 @@ class RecordAndPlaybackProxyInterceptorSpec extends Specification {
veto
and:
- 1 * tape.play(response)
+ 1 * tape.play(request, response)
}
def "vetos a request and sets failing response code if no tape is inserted"() {
@@ -67,11 +67,11 @@ interactions:
def tape = YamlTape.readFrom(new StringReader(yaml))
and:
+ def request = new BasicRequest("GET", "http://freeside.co/betamax")
def response = new BasicResponse(HTTP_OK, "OK")
when:
- tape.seek(new BasicRequest("GET", "http://freeside.co/betamax"))
- tape.play(response)
+ tape.play(request, response)
then:
def expected = encoder ? encoder.encode("\u00a3", charset) : "\u00a3".getBytes(charset)
@@ -63,11 +63,11 @@ interactions:
def tape = YamlTape.readFrom(new StringReader(yaml))
and:
+ def request = new BasicRequest("GET", "http://freeside.co/betamax")
def response = new BasicResponse(200, "OK")
when:
- tape.seek(new BasicRequest("GET", "http://freeside.co/betamax"))
- tape.play(response)
+ tape.play(request, response)
then:
response.getHeader(CONTENT_ENCODING) == encoding
@@ -0,0 +1,59 @@
+package co.freeside.betamax.tape
+
+import co.freeside.betamax.Tape
+import co.freeside.betamax.proxy.Request
+import co.freeside.betamax.util.message.BasicRequest
+import co.freeside.betamax.util.message.BasicResponse
+import spock.lang.Issue
+import spock.lang.Shared
+import spock.lang.Specification
+
+import java.util.concurrent.CountDownLatch
+
+import static java.util.concurrent.TimeUnit.SECONDS
+
+@Issue('https://github.com/robfletcher/betamax/issues/57')
+class MultiThreadedTapeAccessSpec extends Specification {
+
+ @Shared Tape tape = new MemoryTape(name: 'multi_threaded_tape_access_spec')
+
+ void 'the correct response is replayed to each thread'() {
+ given: 'a number of requests'
+ List<Request> requests = (0..<threads).collect { i ->
+ def request = new BasicRequest('GET', "http://example.com/$i")
+ request.addHeader('X-Thread', i.toString())
+ request
+ }
+ println requests
+
+ and: 'some existing responses on tape'
+ requests.eachWithIndex { request, i ->
+ def response = new BasicResponse(status: 200, reason: 'OK', body: i.toString())
+ tape.record(request, response)
+ }
+
+ when: 'requests are replayed concurrently'
+ def finished = new CountDownLatch(threads)
+ def responses = [:]
+ requests.eachWithIndex { request, i ->
+ Thread.start {
+ def response = new BasicResponse()
+ tape.play(request, response)
+ responses[requests[i].getHeader('X-Thread')] = response.bodyAsText.text
+ finished.countDown()
+ }
+ }
+
+ then: 'all threads complete'
+ finished.await(1, SECONDS)
+
+ and: 'the correct response is returned to each request'
+ responses.every { key, value ->
+ key == value
+ }
+
+ where:
+ threads = 10
+ }
+
+}
@@ -30,13 +30,12 @@ class TapeSpec extends Specification {
}
def cleanup() {
- tape.reset()
tape.mode = READ_WRITE
}
def "reading from an empty tape throws an exception"() {
when: "an empty tape is played"
- tape.play(new BasicResponse())
+ tape.play(getRequest, new BasicResponse())
then: "an exception is thrown"
thrown IllegalStateException
@@ -63,9 +62,6 @@ class TapeSpec extends Specification {
}
def "can overwrite a recorded interaction"() {
- given: "the tape is ready to play"
- tape.seek(getRequest)
-
when: "a recording is made"
tape.record(getRequest, plainTextResponse)
@@ -93,11 +89,8 @@ class TapeSpec extends Specification {
given: "an http response to play back to"
def response = new BasicResponse()
- and: "the tape is ready to play"
- tape.seek(getRequest)
-
when: "the tape is played"
- tape.play(response)
+ tape.play(getRequest, response)
then: "the recorded response data is copied onto the response"
response.status == plainTextResponse.status
@@ -119,30 +112,12 @@ class TapeSpec extends Specification {
interaction.request.body == request.bodyAsText.text
}
- def "can reset the tape position"() {
- given: "the tape is ready to read"
- tape.seek(getRequest)
-
- when: "the tape position is reset"
- tape.reset()
-
- and: "the tape is played"
- tape.play(new BasicResponse())
-
- then: "an exception is thrown"
- def e = thrown(IllegalStateException)
- e.message == "the tape is not ready to play"
- }
-
def "a write-only tape cannot be read from"() {
given: "the tape is put into write-only mode"
tape.mode = WRITE_ONLY
- and: "the tape is ready to read"
- tape.seek(getRequest)
-
when: "the tape is played"
- tape.play(new BasicResponse())
+ tape.play(getRequest, new BasicResponse())
then: "an exception is thrown"
def e = thrown(IllegalStateException)

0 comments on commit 467c884

Please sign in to comment.