Skip to content
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

Make expectation recording thread-safe. #521

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

fumoboy007
Copy link

To handle a mocked invocation, we do the following:

  1. Get the first stub that matches the invocation.
  2. If the stub is an expectation, remove the stub from the arrays of stubs and expectations.
  3. Pass the invocation to the stub.

The first two steps are currently not atomic, so there is a race condition. For example:

  1. Add expectation 1 for method A.
  2. Add expectation 2 for same method A.
  3. [Thread 1] Call method A.
  4. [Thread 2] Call method A.
  5. [Thread 1] Get expectation 1 since it matches the invocation.
  6. [Thread 2] Get expectation 1 since it matches the invocation.
  7. [Thread 1] Remove expectation 1 from the arrays.
  8. [Thread 2] Remove expectation 1 from the arrays.

In the above example, the invocations of the same method in Thread 1 and Thread 2 both match expectation 1; expectation 2 does not get matched.

The solution is to make the matching and the removal atomic. We can do so by wrapping the relevant code in @synchronized(stubs). (Even though stubForInvocation: also performs @synchronized(stubs), we can feel free to do the same in the caller without fear of deadlocks since @synchronized uses a recursive lock.)

Added a new unit test that was failing before the fix and is now passing after the fix.

To handle a mocked invocation, we do the following:
1. Get the first stub that matches the invocation.
2. If the stub is an expectation, remove the stub from the arrays of stubs and expectations.
3. Pass the invocation to the stub.

The first two steps are currently not atomic, so there is a race condition. For example:
1. Add expectation 1 for method A.
2. Add expectation 2 for same method A.
3. [Thread 1] Call method A.
4. [Thread 2] Call method A.
5. [Thread 1] Get expectation 1 since it matches the invocation.
6. [Thread 2] Get expectation 1 since it matches the invocation.
7. [Thread 1] Remove expectation 1 from the arrays.
8. [Thread 2] Remove expectation 1 from the arrays.

In the above example, the invocations of the same method in Thread 1 and Thread 2 both match expectation 1; expectation 2 does not get matched.

The solution is to make the matching and the removal atomic. We can do so by wrapping the relevant code in `@synchronized(stubs)`. (Even though `stubForInvocation:` also performs `@synchronized(stubs)`, we can feel free to do the same in the caller without fear of deadlocks since `@synchronized` uses a recursive lock.)

Added a new unit test that was failing before the fix and is now passing after the fix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant