-
Notifications
You must be signed in to change notification settings - Fork 852
Revised sync mode WebClient/RTMClient to address concurrency issues #662
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
Conversation
Codecov Report
@@ Coverage Diff @@
## master #662 +/- ##
==========================================
- Coverage 86.19% 85.28% -0.92%
==========================================
Files 17 17
Lines 2413 2568 +155
Branches 198 237 +39
==========================================
+ Hits 2080 2190 +110
- Misses 262 284 +22
- Partials 71 94 +23
Continue to review full report at Codecov.
|
| callback, rtm_client=self, web_client=web_client, data=data | ||
| ) | ||
|
|
||
| while future.running(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing this part addresses #569
| @RTMClient.run_on(event="message") | ||
| # even though run_async=False, handlers for RTM events can be a coroutine | ||
| async def send_reply(**payload): | ||
| def send_reply(**payload): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
coroutines no longer work when run_async=False. I think it's much more valid.
| self.web_client = WebClient( | ||
| token=self.bot_token, | ||
| run_async=False, | ||
| loop=asyncio.new_event_loop(), # TODO: this doesn't work without this |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unnecessary as run_async=False no longer uses an event loop internally
|
|
||
|
|
||
| # This doesn't work | ||
| # Fixed in 2.6.0: This doesn't work |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WebClient w/ run_async=False is now thread-safe.
slack/rtm/client.py
Outdated
| ): | ||
| self.token = token.strip() | ||
| self.run_async = run_async | ||
| self.thread_pool_executor = ThreadPoolExecutor( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is not so critical but it's just a minor improvement
slack/web/__init__.py
Outdated
| import slack.version as ver | ||
|
|
||
|
|
||
| def get_user_agent(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extracted to reuse in UrllibWebClient - the method needs to be outside the BaseClient to avoid circular import issues.
| # Using this is no longer recommended - just keep this for backward-compatibility | ||
| return self._event_loop.run_until_complete(future) | ||
| else: | ||
| return self._sync_send(api_url=api_url, req_args=req_args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is the new way
slack/web/slack_response.py
Outdated
| "Use WebClient with run_async=False and use_sync_aiohttp=False." | ||
| ) | ||
| raise e.SlackRequestError(msg) | ||
| response = self._client._sync_request( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As this method is not a coroutine, using sync client also for run_async=True clients. Regarding run_async=True, it works anyways but we can revisit this to make it completely non-blocking in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes here fixes #626
slack/web/urllib_client.py
Outdated
| self.default_headers = default_headers | ||
| self.web_client = web_client | ||
|
|
||
| def api_call( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's also possible to use this method for any API calls. As described in slack_response.py, pagination iterator doesn't work when directly using this class. To use the feature, developers should use WebClient with run_async=False. The reason I gave up supporting the interaction with this class is circular import issues with BaseClient.
| "status_code": 200, | ||
| } | ||
| coro.return_value = SlackResponse(**data) | ||
| corofunc = Mock(name="mock_rtm_response", side_effect=asyncio.coroutine(coro)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed some existing mock utilities depending on asyncio. The dependency caused the difficulties for detecting potential concurrency issues when run_async=False.
|
I've merged a fix for #650 in this pull request. |
| self.fail("Raising an error here was expected") | ||
| except Exception as e: | ||
| self.assertEqual(str(e), "The server responded with: {'ok': False, 'error': 'invalid_auth'}") | ||
| self.assertEqual( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
slack/rtm/client.py
Outdated
| ) | ||
| else: | ||
| self._execute_in_thread(callback, data) | ||
| await self._execute_in_thread( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
Looks good at high level 👍 |
|
|
||
| # If you see the following errors with #stop() method calls, call `RTMClient#async_stop()` instead | ||
| # | ||
| # /python3.8/asyncio/base_events.py:641: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is unrelated to the code review suggestions. The tests have been passed but I overlooked this warning for two test cases.
| if ( | ||
| self.auto_reconnect | ||
| and not self._stopped | ||
| and error_code != "invalid_auth" # "invalid_auth" is unrecoverable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
@aoberoi @stevengill Thanks for your insightful review. I've updated this pull request and now it's ready for view again. |
* slackapi#530 Fixed by changing _execute_in_thread to be a coroutine * slackapi#569 Resolved by removing a blocking loop (while future.running()) * slackapi#645 WebClient(run_async=False) no longer depends on asyncio by default * slackapi#633 WebClient(run_async=False) doesn't internally depend on aiohttp * slackapi#631 When run_async=True, RTM listner can be a normal function and WebClient is free from the event loop * slackapi#630 WebClient no longer depends on aiohttp when run_async=False * slackapi#497 Fixed when run_async=False / can be closed as we don't support run_async=True for this use case (in Flask)
* Get rid of thread pool executor as we no longer need threads internally * Add async_stop() method for safer termination of RTMClient for the cases having unexpected exceptions in callbacks * Revert the behavior of run_async=True to allow using non-async methods * Simplify the Retry-After header value extraction code
* Merge UrllibWebClient's functionalities into BaseClient not to increase unnecesesary complexity such as circular import issues * Call show_2020_01_deprecation() only once * Test if values are dict and they're empty * Rename _sync_request to _request_for_pagination to be clearer
|
I've rebased this branch on the latest master branch. It's ready to merge once I get reviewers' approvals. |
aoberoi
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just one comment that needs attention here: #662 (comment).
I don't think its critical, but probably worth looking at once more. Approved!
Summary
WebClientandRTMClientwithrun_async=Falsehave been having many issues such as #497 #530 #569 #630 #631 #633 #645 . This pull request fixes the following issues by revising the internals of WebClient and RTMClient when run_async=False.The revised
WebClientnever relies on aiohttp whenrun_async=False(the default). In the case, the API client simply sends HTTP requests utilizing the Python standard APIs (urllib). If a user would like to fall back to the previous behavior using aiohttp in a blocking way, it's still possible to use it by settinguse_sync_aiohttp=Truein addition torun_async=False. But I strongly recommend switching to the new one.RTMClientstill tightly depends on asyncio for WebSocket management. Some error handling issues #558 #611 #522 are still unfixed. I'll address those separately.SlackResponseto always useUrllibWebClient.As I mentioned above, #558 #611 #522 are outside of the scope of this pull request. They may be fixed in the forthcoming pull requests.
Requirements (place an
xin each[ ])