-
Notifications
You must be signed in to change notification settings - Fork 749
Fixing data races in subscriptions #880
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
a23f8d1
57a253c
a4fdef3
b8818a0
533f668
f0e2350
2ac94cb
e3a69c1
b87edcb
e4875e3
33c3649
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,12 +8,14 @@ class StarWarsSubscriptionTests: XCTestCase { | |
| let SERVER: String = "ws://localhost:8080/websocket" | ||
|
|
||
| var client: ApolloClient! | ||
| var webSocketTransport: WebSocketTransport! | ||
|
|
||
| override func setUp() { | ||
| super.setUp() | ||
|
|
||
| let networkTransport = WebSocketTransport(request: URLRequest(url: URL(string: SERVER)!)) | ||
| client = ApolloClient(networkTransport: networkTransport) | ||
| WebSocketTransport.provider = ApolloWebSocket.self | ||
| webSocketTransport = WebSocketTransport(request: URLRequest(url: URL(string: SERVER)!)) | ||
| client = ApolloClient(networkTransport: webSocketTransport) | ||
| } | ||
|
|
||
| // MARK: Subscriptions | ||
|
|
@@ -252,4 +254,107 @@ class StarWarsSubscriptionTests: XCTestCase { | |
| subJedi.cancel() | ||
| subNewHope.cancel() | ||
| } | ||
|
|
||
| // MARK: Data races tests | ||
|
|
||
| func testConcurrentSubscribing() { | ||
| let firstSubscription = ReviewAddedSubscription(episode: .empire) | ||
| let secondSubscription = ReviewAddedSubscription(episode: .empire) | ||
|
|
||
| let expectation = self.expectation(description: "Subscribers connected and received events") | ||
| expectation.expectedFulfillmentCount = 2 | ||
|
|
||
| var sub1: Cancellable? | ||
| var sub2: Cancellable? | ||
|
|
||
| let queue = DispatchQueue(label: "com.apollographql.testing", attributes: .concurrent) | ||
|
|
||
| queue.async { | ||
| sub1 = self.client.subscribe(subscription: firstSubscription) { _ in | ||
| expectation.fulfill() | ||
| } | ||
| } | ||
|
|
||
| queue.async { | ||
| sub2 = self.client.subscribe(subscription: secondSubscription) { _ in | ||
| expectation.fulfill() | ||
| } | ||
| } | ||
|
|
||
| // dispatched with a barrier flag to make sure | ||
| // this is performed after subscription calls | ||
| queue.sync(flags: .barrier) { | ||
| // dispatched on the processing queue to make sure | ||
| // this is performed after subscribers are processed | ||
| self.webSocketTransport.websocket.callbackQueue.async { | ||
| _ = self.client.perform(mutation: CreateReviewForEpisodeMutation(episode: .empire, review: ReviewInput(stars: 5, commentary: "The greatest movie ever!"))) | ||
| } | ||
| } | ||
|
|
||
| waitForExpectations(timeout: 10, handler: nil) | ||
| sub1?.cancel() | ||
| sub2?.cancel() | ||
| } | ||
|
|
||
| func testConcurrentSubscriptionCancellations() { | ||
| let firstSubscription = ReviewAddedSubscription(episode: .empire) | ||
| let secondSubscription = ReviewAddedSubscription(episode: .empire) | ||
|
|
||
| let expectation = self.expectation(description: "Subscriptions cancelled") | ||
| expectation.expectedFulfillmentCount = 2 | ||
|
|
||
| let sub1 = client.subscribe(subscription: firstSubscription) { _ in } | ||
| let sub2 = client.subscribe(subscription: secondSubscription) { _ in } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth adding a post of a review, then failing either of these if something comes through since they're both being cancelled immediately?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can do that if you think it looks better, but I think it wouldn't really matter. I feel like these tests only make sense if TSAN is turned on anyway.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not particularly concerned about the way it looks, but it'd be nice to have a way to test these without TSAN turn on since right now we're not able to turn it on for the library as a whole (yet)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ^still thinking on this - is this something where we should at least be able to say "These should never get called, even without TSAN on?'
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I see what you mean. Kinda misunderstood you before. I will perform a mutation after calling
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is what I'm trying to do: But what happens is that final mutation sometimes triggers a subscription response in another test -
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think waiting for expectations is totally reasonable - you also can do an inverted expectation where the test fails if it actually gets called - so instead of having the explicit |
||
|
|
||
| DispatchQueue.global().async { | ||
| sub1.cancel() | ||
| expectation.fulfill() | ||
|
designatednerd marked this conversation as resolved.
|
||
| } | ||
| DispatchQueue.global().async { | ||
| sub2.cancel() | ||
| expectation.fulfill() | ||
| } | ||
|
|
||
| waitForExpectations(timeout: 10, handler: nil) | ||
| } | ||
|
|
||
| func testConcurrentSubscriptionAndConnectionClose() { | ||
| let empireReviewSubscription = ReviewAddedSubscription(episode: .empire) | ||
| let expectation = self.expectation(description: "Connection closed") | ||
|
|
||
| DispatchQueue.global().async { | ||
| let sub = self.client.subscribe(subscription: empireReviewSubscription) { _ in } | ||
|
aivcec marked this conversation as resolved.
Outdated
|
||
| sub.cancel() | ||
| } | ||
|
|
||
| _ = self.client.perform(mutation: CreateReviewForEpisodeMutation(episode: .empire, review: ReviewInput(stars: 5, commentary: "The greatest movie ever!"))) | ||
|
|
||
| DispatchQueue.global().async { | ||
| self.webSocketTransport.closeConnection() | ||
| expectation.fulfill() | ||
|
designatednerd marked this conversation as resolved.
|
||
| } | ||
|
|
||
| waitForExpectations(timeout: 10, handler: nil) | ||
| } | ||
|
|
||
| func testConcurrentConnectAndCloseConnection() { | ||
| WebSocketTransport.provider = MockWebSocket.self | ||
| let webSocketTransport = WebSocketTransport(request: URLRequest(url: URL(string: SERVER)!)) | ||
| let expectation = self.expectation(description: "Connection closed") | ||
| expectation.expectedFulfillmentCount = 2 | ||
|
|
||
| DispatchQueue.global().async { | ||
| if let websocket = webSocketTransport.websocket as? MockWebSocket { | ||
| websocket.reportDidConnect() | ||
| expectation.fulfill() | ||
| } | ||
| } | ||
|
|
||
| DispatchQueue.global().async { | ||
| webSocketTransport.closeConnection() | ||
| expectation.fulfill() | ||
| } | ||
|
|
||
| waitForExpectations(timeout: 10, handler: nil) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import Foundation | ||
|
designatednerd marked this conversation as resolved.
|
||
|
|
||
| class Atomic<T> { | ||
| private let lock = NSLock() | ||
| private var _value: T | ||
|
|
||
| init(_ value: T) { | ||
| _value = value | ||
| } | ||
|
|
||
| var value: T { | ||
| get { | ||
| lock.lock() | ||
| defer { lock.unlock() } | ||
|
|
||
| return _value | ||
| } | ||
| set { | ||
| lock.lock() | ||
| defer { lock.unlock() } | ||
|
|
||
| _value = newValue | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.