Python-slack-sdk: API calls spawn threads without releasing them

Created on 16 Jul 2019  ·  10Comments  ·  Source: slackapi/python-slack-sdk

Description

API calls seem to spawn threads without ever releasing them. I'm not sure if this is an issue with aiohttp or with how slackclient uses it. Or perhaps I am misusing/misunderstanding the client?

What type of issue is this?

  • [x] bug
  • [ ] enhancement (feature request)
  • [x] question
  • [ ] documentation related
  • [ ] testing related
  • [ ] discussion

Requirements

  • [x] I've read and understood the Contributing guidelines and have done my best effort to follow them.
  • [x] I've read and agree to the Code of Conduct.
  • [x] I've searched for any related issues and avoided creating a duplicate issue.

Bug Report

Reproducible in:

slackclient version: 2.1.0

python version: 3.6.7

OS version(s): Ubuntu 18.04.1 LTS (LXC on Proxmox)

Steps to reproduce:

  1. Post a few messages using slackclient's chat_postMessage or anything that involves an api call.
  2. Run htop or top -H
  3. Observe many sleeping python threads (they are released once the program ends, though, so you'll have to watch while the program is running)

Expected result:

Each API call should make its request and then release/end the thread.

Actual result:

Upon API call completion, sleeping threads are leftover. I think this could be a slow memory leak if many calls are made over time.

Attachments:

image
Code to generate the screenshot above:

import os
import slack
import time
from dotenv import load_dotenv

load_dotenv()

client = slack.WebClient(token=os.getenv("SLACK_TOKEN"))

for x in range(10):
    print("Posting message")
    client.chat_postMessage(
        channel=os.getenv("TEST_CHANNEL_ID"),
        text="Hello from your app! :tada:"
    )

    time.sleep(1)

time.sleep(10)

I also tested running async and got the same result. Please let me know if you need any additional information from me. Thank you for your work on this great client!

2x concurrency bug web-client

All 10 comments

I also noted this while trying to check where all the threads came from new threads created while doing api calls:
client.chat_postMessage(channel=channel, text=text)

Got more and more threads lurking about:

slackclient version: 2.0.1 and 2.1.0 (same issue after upgrade)
python version: 3.7.3
OS version(s): Ubuntu 18.04.2 LTS

Before chat_postMessage call:

Threads: 5
threading.CurrentThread() = <Thread(ThreadPoolExecutor-4_0, started daemon 139764785219328)>
threading.enumerate():
<_MainThread(MainThread, started 139764916946752)>
<Thread(ThreadPoolExecutor-0_0, started daemon 139764810880768)>
<Thread(ThreadPoolExecutor-0_1, started daemon 139764793612032)>
<Thread(ThreadPoolExecutor-2_0, started daemon 139764776826624)>
<Thread(ThreadPoolExecutor-4_0, started daemon 139764785219328)>

After chat_postMessage call:

Threads: 6
threading.CurrentThread() = <Thread(ThreadPoolExecutor-4_0, started daemon 139764785219328)>
threading.enumerate():
<_MainThread(MainThread, started 139764916946752)>
<Thread(ThreadPoolExecutor-0_0, started daemon 139764810880768)>
<Thread(ThreadPoolExecutor-0_1, started daemon 139764793612032)>
<Thread(ThreadPoolExecutor-2_0, started daemon 139764776826624)>
<Thread(ThreadPoolExecutor-4_0, started daemon 139764785219328)>
<Thread(ThreadPoolExecutor-2_1, started daemon 139764766238464)>

Is anyone other than me and @iamlawn seeing this behavior? Are we just doing something incorrectly?

Hi folks, this is indeed a bug! I'll work on having this fixed soon.

Thank you! If it helps, through my own digging it seems like the threads are spawned around the aiohttp sessions created in this method. On a whim, I tried saving newly created sessions to self.session which stopped spawning additional threads for requests sent within a few seconds, but new treads are still spawned for requests made after 15 seconds or so.

If I understand this issue correctly, the pull request #544 has addressed this issue and the fix is included since version. 2.3.1.

@mkwarman
If you're still interested in this issue, could you verify if the issue has been resolved with 2.3.1+?

Hi @seratch, thank you for giving an update on this. I am still seeing the behavior as of slackclient 2.5.0. In the attached image I have one gunicorn worker process that has just made a few API calls after restarting a few minutes ago. The threads are still shown in htop. As far as I know, they aren't causing any problems since they're basically idle, but I would expect them to go away once they are no longer being used. In my app, I use a Flask endpoint to receive slack events and then make posts using a single slack client instantiated on startup.
image

@mkwarman Thanks for your quick response! This is really helpful. I will check this out.

@mkwarman
I just ran a small unit test on my local machine (it's not Linux but macOS though). I observed the difference between Python 3.6 and 3.8. If you don't mind it, could you verify the behavior with Python 3.8 in your environment? I'll also verify it on Linux later but if I could get your help, I'm grateful to you.

import logging
import multiprocessing
import os
import threading
import unittest

from integration_tests.helpers import async_test
from slack import WebClient

class TestWebClient(unittest.TestCase):
    """Runs integration tests with real Slack API

    https://github.com/slackapi/python-slackclient/issues/480
    """

    def setUp(self):
        self.logger = logging.getLogger(__name__)
        self.user_token = os.environ["SLACK_BOT_TOKEN"]
        self.sync_client: WebClient = WebClient(token=self.user_token, run_async=False)
        self.async_client: WebClient = WebClient(token=self.user_token, run_async=True)

    def tearDown(self):
        pass

    def test_issue_480_processes(self):
        client = self.sync_client
        before = len(multiprocessing.active_children())
        for idx in range(10):
            response = client.api_test()
            self.assertIsNotNone(response)
        after = len(multiprocessing.active_children())
        self.assertEqual(0, after - before)

    @async_test
    async def test_issue_480_processes_async(self):
        client = self.async_client
        before = len(multiprocessing.active_children())
        for idx in range(10):
            response = await client.api_test()
            self.assertIsNotNone(response)
        after = len(multiprocessing.active_children())
        self.assertEqual(0, after - before)

    # fails with Python 3.6
    def test_issue_480_threads(self):
        client = self.sync_client
        before = threading.active_count()
        for idx in range(10):
            response = client.api_test()
            self.assertIsNotNone(response)
        after = threading.active_count()
        self.assertEqual(0, after - before)

    # fails with Python 3.6
    @async_test
    async def test_issue_480_threads_async(self):
        client = self.async_client
        before = threading.active_count()
        for idx in range(10):
            response = await client.api_test()
            self.assertIsNotNone(response)
        after = threading.active_count()
        self.assertEqual(0, after - before)

Python 3.6.10

Running pytest…
/Users/ksera/github/python-slackclient/env3.6/bin/python -m pytest --cov-report=xml --cov=/Users/ksera/github/python-slackclient/slack integration_tests/web/test_issue_480.py
========================================================================================================= test session starts ==========================================================================================================
platform darwin -- Python 3.6.10, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /Users/ksera/github/python-slackclient, inifile: pytest.ini
plugins: cov-2.8.1
collected 4 items                                                                                                                                                                                                                      

integration_tests/web/test_issue_480.py ..FF                                                                                                                                                                                     [100%]

=============================================================================================================== FAILURES ===============================================================================================================
_________________________________________________________________________________________________ TestWebClient.test_issue_480_threads _________________________________________________________________________________________________

self = <test_issue_480.TestWebClient testMethod=test_issue_480_threads>

    def test_issue_480_threads(self):
        client = self.sync_client
        before = threading.active_count()
        for idx in range(10):
            response = client.api_test()
            self.assertIsNotNone(response)
        after = threading.active_count()
>       self.assertEqual(0, after - before)
E       AssertionError: 0 != 10

integration_tests/web/test_issue_480.py:53: AssertionError

args = (<test_issue_480.TestWebClient testMethod=test_issue_480_threads_async>,), kwargs = {}, current_loop = <_UnixSelectorEventLoop running=False closed=False debug=False>

    def wrapper(*args, **kwargs):
        current_loop: AbstractEventLoop = asyncio.get_event_loop()
>       return current_loop.run_until_complete(coro(*args, **kwargs))

integration_tests/helpers.py:13: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../.pyenv/versions/3.6.10/lib/python3.6/asyncio/base_events.py:488: in run_until_complete
    return future.result()
integration_tests/web/test_issue_480.py:63: in test_issue_480_threads_async
    self.assertEqual(0, after - before)
E   AssertionError: 0 != 10

---------- coverage: platform darwin, python 3.6.10-final-0 ----------
Coverage XML written to file coverage.xml

======================================================================================================= short test summary info ========================================================================================================
FAILED integration_tests/web/test_issue_480.py::TestWebClient::test_issue_480_threads - AssertionError: 0 != 10
FAILED integration_tests/web/test_issue_480.py::TestWebClient::test_issue_480_threads_async - AssertionError: 0 != 10
===================================================================================================== 2 failed, 2 passed in 18.50s =====================================================================================================
(env3.6) $ 

Python 3.8.2

Running pytest…
/Users/ksera/github/python-slackclient/env/bin/python -m pytest --cov-report=xml --cov=/Users/ksera/github/python-slackclient/slack integration_tests/web/test_issue_480.py
========================================================================================================== test session starts ==========================================================================================================
platform darwin -- Python 3.8.2, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /Users/ksera/github/python-slackclient, inifile: pytest.ini
plugins: cov-2.8.1
collected 4 items                                                                                                                                                                                                                       

integration_tests/web/test_issue_480.py ....                                                                                                                                                                                      [100%]

---------- coverage: platform darwin, python 3.8.2-final-0 -----------
Coverage XML written to file coverage.xml

==================================================================================================== 4 passed, 81 warnings in 19.48s ====================================================================================================
(env) $ 

Hey @seratch thank you very much for researching this. I built python 3.8.2 locally and created a new virtualenv pointed to it. After restarting my project and triggering a few API calls I can confirm that the issue is no longer present, I do not see any extra threads hanging around anymore 👍
image

@mkwarman Happy to hear that! Allow me to close this issue now.

Was this page helpful?
0 / 5 - 0 ratings