test: refactor concurrency test using orchestrate#709
Conversation
Towards #691
| log = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def _syncpoint_692(): |
There was a problem hiding this comment.
That name is super puzzling -- can you provide an explanation, or a link to an issue, in the comment? Even, better, give it a name that indicates where it is used, e.g., _syncpoint_update_key, or something similar.
There was a problem hiding this comment.
I see the link in the testcase below to #692: seems like it would be good to repeat here. Also, I wonder about wrapping it in something like if __debug__, to avoid the overhead of even an empty python function call. Of course, that would require geting __debug__ set during the test runs (IIRC it is set by default, and only switched off with -O on the command line, which would also disable assert statments).
There was a problem hiding this comment.
Poking around, it looks like you're correct about when __debug__ is True. I'm not sure how common it is in practice to pass -O in production. I know I've never made a habit of it.
Another solution I thought about was getting rid of the no-op function and using something like # pragma: SYNCPOINT syncname to declare syncpoints, then do some coverage style postprocessing to insert the calls during testing. If you think the current implementation is mind-bending, though...
It's almost certainly not even something worth considering, unless orchestrate gets some kind of life outside of just NDB.
There was a problem hiding this comment.
Why not build this on sys.settrace? Then you wouldn't have to modify the code under tests or cause extra calls and associated overhead.
You could pass in breakpoints for each functions as file-line locations to yield.
There was a problem hiding this comment.
Well, I didn't know about it, for starters. I can take a peek. On first blush, it seems like using line numbers is pretty fragile. I'd want a more robust way to define the syncpoint/breakpoint.
There was a problem hiding this comment.
Looks like I could probably use this to implement the # pragma: SYNCPOINT idea.
There was a problem hiding this comment.
I actually have something like this working now. Will try and make it presentable tomorrow.
There was a problem hiding this comment.
An interesting side-effect of using sys.settrace is it breaks coverage, since it also uses sys.settrace. ;-)
| tests can help with this. | ||
|
|
||
| As soon as any error or failure is detected, no more scenarios are run | ||
| and that error is propagated to the main thread. |
There was a problem hiding this comment.
I really appreciate the extensive writeup here of the rationale. Even with its help, following the implementation below is more than a bit brain-bending.
There was a problem hiding this comment.
Sorry. I was trying to be sensitive to the fact that it would be difficult to follow without a lot of explanation.
There was a problem hiding this comment.
I wonder if using the same test function twice contributes to the bendy-ness: maybe using a simple test (no syncpoints) along with the one you have already would clarify any? Maybe as an additional example?
|
Is the |
28dee77 to
e9e33fe
Compare
I missed the Kokoro error, so I can't comment specifically on whatever you saw earlier. There is a little flakiness to lots of the system tests due to eventual consistency. |
| def advance(self): | ||
| """Allow a call to the wrapper to proceed. | ||
| if os.environ.get("REDIS_CACHE_URL"): | ||
| yield redis_cache |
There was a problem hiding this comment.
Added "pragma: NO COVER". Don't want coverage to depend on which caches are available.
| """ | ||
| self._futures.popleft().set_result(None) | ||
| def memcache_cache(): | ||
| return global_cache_module.MemcacheCache.from_environment() |
| if syncpoints: | ||
| syncpoints.pop() | ||
| else: | ||
| do_nothing() # pragma: SYNCPOINT |
There was a problem hiding this comment.
This shows a limitation of the current settrace approach, in that the comment must be on a reachable line, so the call to do_nothing is just here to have something to hang the pragma off of. It would be more satisfying to be able to write something like:
if syncpoints:
syncpoints.pop()
else:
# pragma: SYNCPOINT
test_calls.append(name)
I spent some time trying to figure out how to make this possible and concluded it was complicated enough to leave out for the first pass. Unfortunately, neither tokenize nor ast, on their own, provide enough information to figure out which side of the else the pragma is on and assign to a line of code. It should be possible to use both to figure it out, but it's not trivial.
There was a problem hiding this comment.
I think it's fine for the pragma to mark the syncpoint.
| """ | ||
|
|
||
|
|
||
| def _get_syncpoints(filename): |
There was a problem hiding this comment.
This is a very simplistic implementation that requires every pragma be located on a reachable line of code. This is probably sufficient. A more sophisticated algorithm that allowed pragmas to appear on their own lines is possible, but not trivial. See this comment.
tseaver
left a comment
There was a problem hiding this comment.
ISTM that orchestrate might find its home in python-testutils -- other code with tricksy concurrency (e.g., pubsub flow control, firestore watch, etc.) might find it useful.
That's indeed a possibility. For example, see one of the unit tests for publisher flow controller that tests a specific sequence of There are quite a few |
Towards googleapis/google-cloud-python#15862