-
Notifications
You must be signed in to change notification settings - Fork 112
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
Cannot load "My Subscriptions" #529
Comments
Yeah, in hindsight this was not the best solution to the previous problem. I'm in the process of changing all requests to use a single connection pool, with a single session per thread reusing the same connection pool, with the threads being managed in a FIFO fixed length queue. Should fix this problem. |
I looked at I didn't go with it because at the time I wasn't sure it would have solved the problem. In my environment the root cause could have been any number of things. I also found thread failure is sporadic with a decreasing frequency with each script run. In fact, the original issue can also be overcome by hitting In any case, I think |
Hmm the Can you add the following before the
|
Yeah, I have approx 215 subs in my feed - not sure if that would be considered "large" though... Just to be clear, I did check system proc limits with I dropped to the I still think it is a race condition, but couldn't say where or why... |
Thanks for that. Seems like there are two inter-related problems. A large number of threads are spawned all at once, possibly hitting the per process thread limit based on how much system memory you have and max thread stack size. Unfortunately I am not sure on the specifics of how this is exactly determined, but Python now defaults to limiting the max threads it opens in a thread pool to be your CPU core count + 4 up to a max of 32 i.e. a range of 5 to 32, so will probably do the same. The second problem is that some threads are closing in a timely manner, but others are not. The flood of connections may be causing your network to choke or for Youtube to be throttling or rate limiting responses. Will likely not be an issue when using a more reasonable amount of threads. |
It might be worth mentioning that the list does usually load within 30 seconds, despite the use of |
The The original problem was that the large number of simultaneous requests was causing a large number of connections to be created, which in turn caused the SSL handshake of the connections to fail, without being caught. The original PR fixed the problem with multiple simultaneous connections by using a single session and connection pool, and also limiting the number of concurrent connections and reusing them when the requests were complete. However it didn't check if the requests were timing out or otherwise failing, and the original code would block indefinitely waiting for the threads to complete: https://github.com/anxdpanic/plugin.video.youtube/pull/478/files#diff-5ecde54fa534f31c07c435544e8497cb462674901a9d688be492bfbbde50b579R824-R852 My idea at the time was to add timeouts to the requests, and the 30s I just didn't think this through enough to realise the issue with creating the threads in the first place, as I don't really use this particular functionality. |
Thanks for the explanation @MoojMidge. I see there's been some further improvements in that section of code since the pull you referenced (#478), such as the timeout tupple on the Looking a little deeper, I see the HTTPAdapter (currently at line 828 in master), i.e. Normally It's probably worth mentioning the HTTPAdapter default All of the following don't produce any errors:
I'd like to confirm what is happening in urllib3 but I'm having some trouble enabling debug logging for same. Any hints? |
Not really something I know too much about, but I'll try to respond based on my current understanding of the issue. Note that I may interchangeably refer to parts of the
Yes and no. Yes - it can prevent the remaining threads, with the pending requests, from ending, while waiting for the requests currently in the pool to complete. No - I don't think it is the root cause of the issue. See below.
I don't think this is correct. There is only one By default, each individual If If Because the additional connections are being prevented from being created, this causes the threads to stay running for longer, however all four example adaptor instances that didn't produce error for you, are all essentially doing the same thing - creating new connections for all 215 subscriptions you have, all at once, which is what was causing the original problem for other people. It is just a trade-off between managing network resources and CPU/memory resources.
That's right, but it shouldn't create new pools unless required. However, from what I can tell This is what I am intending to do, because while the issue with fetching subscriptions results in connections to only a single host, there are multiple other requests being made to different Google/Youtube hosts at various times in this plugin, that will all benefit from using a unified request mechanism. In this way connections can be reused throughout the plugin, which should both be faster, while also reducing memory and socket usage, and also preventing Kodi from hanging when there are network issues. For this, the default This is currently a WIP, the common module is done and works pretty well across the plugin. The next step is to finish the thread management.
Adding the following to
|
My understanding is that Unfortunately, I found your suggestion above for You are correct that a new When Just to be clear, in my testing with FYI, release 1.26.16 of I also think that consolidating the request mechanics into a unified function is a great idea. Given the defaults, I note the I think |
FYI, "final" diff: --- ./plugin.video.youtube-7.0.2.2.matrix.1_release/resources/lib/youtube_plugin/youtube/client/youtube.py
+++ ./plugin.video.youtube-7.0.2.2.matrix.1_wip/resources/lib/youtube_plugin/youtube/client/youtube.py
@@ -17,6 +17,8 @@
import requests
+from time import sleep
+from urllib3.util import Retry
from .login_client import LoginClient
from ..youtube_exceptions import YouTubeException
from ..helper.video_info import VideoInfo
@@ -26,6 +28,7 @@
_context = Context(plugin_id='plugin.video.youtube')
+#requests.urllib3.add_stderr_logger()
class YouTube(LoginClient):
def __init__(self, config=None, language='en-US', region='US', items_per_page=50, access_token='', access_token_tv=''):
@@ -825,7 +828,15 @@
session = requests.Session()
session.headers = headers
session.verify = self._verify
- adapter = requests.adapters.HTTPAdapter(pool_maxsize=5, pool_block=True)
+ retries = Retry(
+ total=3,
+ backoff_factor=0.1,
+ status_forcelist=[500, 502, 503, 504]
+ )
+ adapter = requests.adapters.HTTPAdapter(
+ pool_block=True,
+ max_retries=retries
+ )
session.mount("https://", adapter)
responses = []
@@ -847,7 +858,21 @@
responses)
)
threads.append(thread)
- thread.start()
+ for _ in range(5):
+ try:
+ thread.start()
+ except Exception as e:
+ err = e
+ _context.log_error('Failed to start thread ' + str(len(threads)) + ' for channel_id ' + channel_id)
+ #_context.log_debug(f'{threading.active_count() = }')
+ #_context.log_debug(f'{threading.enumerate() = }')
+ sleep(0.15)
+ continue
+ else:
+ _context.log_debug('Success starting thread ' + str(len(threads)) + ' for channel_id ' + channel_id)
+ break
+ else:
+ raise err
for thread in threads:
thread.join(30) EDIT: Removed |
@MoojMidge , I've been playing with I'm just doing some more testing and will trim my edits before posting here... |
Ok, so using This is all predicated on the Seeing the --- CodeSafe/__dev/kodi/youtube/plugin.video.youtube-7.0.2.2.matrix.1_release/resources/lib/youtube_plugin/youtube/client/youtube.py
+++ CodeSafe/__dev/kodi/youtube/plugin.video.youtube-7.0.2.2.matrix.1_wip/resources/lib/youtube_plugin/youtube/client/youtube.py
@@ -12,11 +12,13 @@
import json
import re
import threading
+import concurrent.futures
import traceback
import xml.etree.ElementTree as ET
import requests
+from urllib3.util import Retry
from .login_client import LoginClient
from ..youtube_exceptions import YouTubeException
from ..helper.video_info import VideoInfo
@@ -825,11 +827,14 @@
session = requests.Session()
session.headers = headers
session.verify = self._verify
- adapter = requests.adapters.HTTPAdapter(pool_maxsize=5, pool_block=True)
+ retries = Retry(total=3, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504])
+ adapter = requests.adapters.HTTPAdapter(pool_block=True, max_retries=retries)
session.mount("https://", adapter)
responses = []
- def fetch_xml(_url, _responses):
+ def fetch_xml(_e_args):
+ _url = f'https://www.youtube.com/feeds/videos.xml?channel_id={_e_args[0]}'
+ _responses = _e_args[1]
try:
_response = session.get(_url, timeout=(3.05, 27))
_response.raise_for_status()
@@ -839,18 +844,35 @@
return
_responses.append(_response)
- threads = []
- for channel_id in sub_channel_ids:
- thread = threading.Thread(
- target=fetch_xml,
- args=('https://www.youtube.com/feeds/videos.xml?channel_id=' + channel_id,
- responses)
- )
- threads.append(thread)
- thread.start()
-
- for thread in threads:
- thread.join(30)
+ def tpool_handler():
+ with concurrent.futures.ThreadPoolExecutor() as fetch_tpool:
+ e_args = []
+ fetch_args = []
+ for channel_id in sub_channel_ids:
+ e_args = [channel_id, responses]
+ fetch_args.append(e_args)
+ try:
+ futures = {fetch_tpool.submit(fetch_xml, task): task for task in fetch_args}
+ except Exception as e:
+ fetch_tpool.shutdown(cancel_futures=True)
+ return e
+ else:
+ while len(futures) > 0:
+ pend_futures = {}
+ done, not_done = concurrent.futures.wait(futures, return_when=concurrent.futures.FIRST_COMPLETED)
+ for tsk in done:
+ if tsk.exception(): raise tsk.exception()
+ for tsk in not_done:
+ task = futures[tsk]
+ pend_futures[tsk] = task
+ futures = pend_futures
+ return True
+
+ tp_status = tpool_handler()
+ if not tp_status == True:
+ _context.log_error('FATAL : Failed to start Channel XML Fetch ThreadPool executor or component.')
+ raise tp_status
+
session.close()
for response in responses: Here it is again with some error and minor debug logging: --- CodeSafe/__dev/kodi/youtube/plugin.video.youtube-7.0.2.2.matrix.1_release/resources/lib/youtube_plugin/youtube/client/youtube.py
+++ CodeSafe/__dev/kodi/youtube/plugin.video.youtube-7.0.2.2.matrix.1_wip/resources/lib/youtube_plugin/youtube/client/youtube.py
@@ -12,11 +12,13 @@
import json
import re
import threading
+import concurrent.futures
import traceback
import xml.etree.ElementTree as ET
import requests
+from urllib3.util import Retry
from .login_client import LoginClient
from ..youtube_exceptions import YouTubeException
from ..helper.video_info import VideoInfo
@@ -825,11 +827,14 @@
session = requests.Session()
session.headers = headers
session.verify = self._verify
- adapter = requests.adapters.HTTPAdapter(pool_maxsize=5, pool_block=True)
+ retries = Retry(total=3, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504])
+ adapter = requests.adapters.HTTPAdapter(pool_block=True, max_retries=retries)
session.mount("https://", adapter)
responses = []
- def fetch_xml(_url, _responses):
+ def fetch_xml(_e_args):
+ _url = f'https://www.youtube.com/feeds/videos.xml?channel_id={_e_args[0]}'
+ _responses = _e_args[1]
try:
_response = session.get(_url, timeout=(3.05, 27))
_response.raise_for_status()
@@ -839,18 +844,45 @@
return
_responses.append(_response)
- threads = []
- for channel_id in sub_channel_ids:
- thread = threading.Thread(
- target=fetch_xml,
- args=('https://www.youtube.com/feeds/videos.xml?channel_id=' + channel_id,
- responses)
- )
- threads.append(thread)
- thread.start()
-
- for thread in threads:
- thread.join(30)
+ def tpool_handler():
+ with concurrent.futures.ThreadPoolExecutor() as fetch_tpool:
+ e_args = []
+ fetch_args = []
+
+ for channel_id in sub_channel_ids:
+ e_args = [channel_id, responses]
+ fetch_args.append(e_args)
+
+ try:
+ futures = {fetch_tpool.submit(fetch_xml, task): task for task in fetch_args}
+ except Exception as e:
+ fetch_tpool.shutdown(cancel_futures=True)
+ return e
+ else:
+ while len(futures) > 0:
+ pend_futures = {}
+ done, not_done = concurrent.futures.wait(futures, return_when=concurrent.futures.FIRST_COMPLETED)
+ _context.log_debug(f'Channel XML Fetch Active Threads : {len(futures)} | Done : {len(done)} | Not Done : {len(not_done)}')
+ for tsk in done:
+ if tsk.exception():
+ _context.log_error(f'FATAL : Failed to fetch xml data for channel_id {task[2]}')
+ raise tsk.exception()
+ else:
+ task = futures[tsk]
+ _context.log_debug(f'SUCCESS : Fetched xml data for channel_id {task[2]}')
+ for tsk in not_done:
+ task = futures[tsk]
+ pend_futures[tsk] = task
+ futures = pend_futures
+ return True
+
+ tp_status = tpool_handler()
+ if not tp_status == True:
+ _context.log_error('FATAL : Failed to start Channel XML Fetch ThreadPool executor or component.')
+ raise tp_status
+ else:
+ _context.log_debug(f'SUCCESS : Exited Channel XML Fetch ThreadPool cleanly | Status : {tp_status}')
+
session.close()
for response in responses: This one has full error and debug logging (but without timekeeping and statistical logging), and includes urllib3 debug logging and a retry capability for threadpool start and xml fetch operations (the latter being largely redundant when using the included --- CodeSafe/__dev/kodi/youtube/plugin.video.youtube-7.0.2.2.matrix.1_release/resources/lib/youtube_plugin/youtube/client/youtube.py
+++ CodeSafe/__dev/kodi/youtube/plugin.video.youtube-7.0.2.2.matrix.1_wip/resources/lib/youtube_plugin/youtube/client/youtube.py
@@ -12,11 +12,13 @@
import json
import re
import threading
+import concurrent.futures
import traceback
import xml.etree.ElementTree as ET
import requests
+from urllib3.util import Retry
from .login_client import LoginClient
from ..youtube_exceptions import YouTubeException
from ..helper.video_info import VideoInfo
@@ -24,8 +26,16 @@
from ...kodion.utils import datetime_parser
from ...kodion.utils import to_unicode
+# Maximum times to attempt to fetch channel data.
+# Session HTTPAdapter already also tries 3 times.
+# This value acts as a multiplier of session retries.
+MAX_FETCH_XML_RETRIES = 1
+MAX_THREADSTART_RETRIES = 5
+
+
_context = Context(plugin_id='plugin.video.youtube')
+requests.urllib3.add_stderr_logger()
class YouTube(LoginClient):
def __init__(self, config=None, language='en-US', region='US', items_per_page=50, access_token='', access_token_tv=''):
@@ -825,11 +835,13 @@
session = requests.Session()
session.headers = headers
session.verify = self._verify
- adapter = requests.adapters.HTTPAdapter(pool_maxsize=5, pool_block=True)
+ retries = Retry(total=3, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504])
+ adapter = requests.adapters.HTTPAdapter(pool_block=True, max_retries=retries)
session.mount("https://", adapter)
- responses = []
- def fetch_xml(_url, _responses):
+ def fetch_xml(_e_args):
+ _url = f'https://www.youtube.com/feeds/videos.xml?channel_id={_e_args[2]}'
+ _responses = _e_args[3]
try:
_response = session.get(_url, timeout=(3.05, 27))
_response.raise_for_status()
@@ -837,20 +849,76 @@
_context.log_debug('Response: {0}'.format(error.response and error.response.text))
_context.log_error('Failed |%s|' % traceback.print_exc())
return
+ except Exception as e:
+ _context.log_error(f'Task # {_e_args[0]} failed to perform a clean Channel XML Fetch for channel_id {_e_args[2]}.')
+ raise e
_responses.append(_response)
- threads = []
- for channel_id in sub_channel_ids:
- thread = threading.Thread(
- target=fetch_xml,
- args=('https://www.youtube.com/feeds/videos.xml?channel_id=' + channel_id,
- responses)
- )
- threads.append(thread)
- thread.start()
-
- for thread in threads:
- thread.join(30)
+ def tpool_handler(_attempt):
+ with concurrent.futures.ThreadPoolExecutor() as fetch_tpool:
+ task_idx = 0
+ retry_ctr = 0
+ e_args = []
+ fetch_args = []
+ _context.log_debug(f'Channel XML Fetch ThreadPool has {fetch_tpool._max_workers} workers...')
+
+ for channel_id in sub_channel_ids:
+ e_args = [task_idx, retry_ctr, channel_id, responses]
+ fetch_args.append(e_args)
+ task_idx +=1
+
+ try:
+ futures = {fetch_tpool.submit(fetch_xml, task): task for task in fetch_args}
+ except Exception as e:
+ _context.log_error(f'Failed to start Channel XML Fetch ThreadPool executor or component on attempt # {(_attempt + 1)} of {MAX_THREADSTART_RETRIES}. ')
+ try:
+ futures
+ except NameError:
+ _context.log_error(f'No Threads to cancel. Retrying...')
+ except:
+ _context.log_error(f'Threads to be cancelled before retrying : {len(futures)}')
+ finally:
+ fetch_tpool.shutdown(cancel_futures=True)
+ return e
+ else:
+ _context.log_debug('SUCCESS : All jobs have been submitted to the Channel XML Fetch ThreadPool.')
+ while len(futures) > 0:
+ pend_futures = {}
+ done, not_done = concurrent.futures.wait(futures, return_when=concurrent.futures.FIRST_COMPLETED)
+ _context.log_debug(f'Channel XML Fetch Active Threads : {len(futures)} | Done : {len(done)} | Not Done : {len(not_done)}')
+ for tsk in done:
+ if tsk.exception():
+ task = futures[tsk]
+ if task[1] < MAX_FETCH_XML_RETRIES:
+ task[1] += 1
+ _context.log_error(f'Failed to fetch xml for channel_id {task[2]}. Attempting retry # {task[1]} of {MAX_FETCH_XML_RETRIES}...')
+ pend_futures[fetch_tpool.submit(fetch_xml, task)] = task
+ else:
+ _context.log_error(f'FATAL : Failed to fetch xml data for channel_id {task[2]}')
+ raise tsk.exception()
+ else:
+ task = futures[tsk]
+ _context.log_debug(f'SUCCESS : Fetched xml data for channel_id {task[2]}')
+ for tsk in not_done:
+ task = futures[tsk]
+ pend_futures[tsk] = task
+ futures = pend_futures
+ return True
+
+ for attempt in range(MAX_THREADSTART_RETRIES):
+ responses = []
+ tp_status = tpool_handler(attempt)
+ if not tp_status == True:
+ _context.log_debug(f'Channel XML Fetch ThreadPool failed. Retrying now... | Status : {tp_status}')
+ continue
+ else:
+ _context.log_debug(f'SUCCESS : Exited Channel XML Fetch ThreadPool cleanly | Status : {tp_status}')
+ break
+ else:
+ if not tp_status == True:
+ _context.log_error('FATAL : Failed to start Channel XML Fetch ThreadPool executor or component.')
+ raise tp_status
+
session.close()
for response in responses: I hope some of it is of use... 8^d |
It definitely is. Thanks for the investigation, will take me a while to look through this properly, just a bit busy with other things at the moment. |
I should probably point out the limitations. Importantly, I used the This didn't really work - I suspect because these particular threads are so short lived. Often the callback would execute and the future object would be torn down which would raise an undefined exception. This could actually be an upstream bug, as really the future shouldn't be torn down until after the callback returns. Anyway, in the end, I used the The When an ordered response is required then a sort operation can be performed using an index (e.g. Creating the thread pool using Hope that helps explain some of the choices made... 8^d |
Update for v7.0.3.2: --- plugin.video.youtube-7.0.3.2_release/resources/lib/youtube_plugin/youtube/client/youtube.py
+++ ThreadPool_XML_Fetch_7.0.3.2_release/resources/lib/youtube_plugin/youtube/client/youtube.py
@@ -11,6 +11,7 @@
from __future__ import absolute_import, division, unicode_literals
import threading
+import concurrent.futures
import xml.etree.ElementTree as ET
from copy import deepcopy
from itertools import chain, islice
@@ -1507,23 +1508,41 @@
responses = []
- def fetch_xml(_url, _responses):
+ def fetch_xml(_e_args):
+ _url = f'https://www.youtube.com/feeds/videos.xml?channel_id={_e_args[0]}'
+ _responses = _e_args[1]
_response = self.request(_url, headers=headers)
if _response:
_responses.append(_response)
- threads = []
- for channel_id in sub_channel_ids:
- thread = threading.Thread(
- target=fetch_xml,
- args=('https://www.youtube.com/feeds/videos.xml?channel_id=' + channel_id,
- responses)
- )
- threads.append(thread)
- thread.start()
-
- for thread in threads:
- thread.join(30)
+ def tpool_handler():
+ with concurrent.futures.ThreadPoolExecutor() as fetch_tpool:
+ e_args = []
+ fetch_args = []
+ for channel_id in sub_channel_ids:
+ e_args = [channel_id, responses]
+ fetch_args.append(e_args)
+ try:
+ futures = {fetch_tpool.submit(fetch_xml, task): task for task in fetch_args}
+ except Exception as e:
+ fetch_tpool.shutdown(cancel_futures=True)
+ return e
+ else:
+ while len(futures) > 0:
+ pend_futures = {}
+ done, not_done = concurrent.futures.wait(futures, return_when=concurrent.futures.FIRST_COMPLETED)
+ for tsk in done:
+ if tsk.exception(): raise tsk.exception()
+ for tsk in not_done:
+ task = futures[tsk]
+ pend_futures[tsk] = task
+ futures = pend_futures
+ return True
+
+ tp_status = tpool_handler()
+ if not tp_status == True:
+ _context.log_error('FATAL : Failed to start Channel XML Fetch ThreadPool executor or component.')
+ raise tp_status
for response in responses:
if response: |
Update for 7.0.4: --- plugin.video.youtube-7.0.4.nexus.1/resources/lib/youtube_plugin/youtube/client/youtube.py 2024-03-22 09:53:34.000000000 +1100
+++ plugin.video.youtube-7.0.4.nexus.1_threadpool_xml_fetch/resources/lib/youtube_plugin/youtube/client/youtube.py 2024-03-25 18:20:28.983669273 +1100
@@ -11,6 +11,7 @@
from __future__ import absolute_import, division, unicode_literals
import threading
+import concurrent.futures
import xml.etree.ElementTree as ET
from copy import deepcopy
from itertools import chain, islice
@@ -1515,23 +1516,41 @@
responses = []
- def fetch_xml(_url, _responses):
+ def fetch_xml(_e_args):
+ _url = f'https://www.youtube.com/feeds/videos.xml?channel_id={_e_args[0]}'
+ _responses = _e_args[1]
_response = self.request(_url, headers=headers)
if _response:
_responses.append(_response)
- threads = []
- for channel_id in sub_channel_ids:
- thread = threading.Thread(
- target=fetch_xml,
- args=('https://www.youtube.com/feeds/videos.xml?channel_id=' + channel_id,
- responses)
- )
- threads.append(thread)
- thread.start()
-
- for thread in threads:
- thread.join(30)
+ def tpool_handler():
+ with concurrent.futures.ThreadPoolExecutor() as fetch_tpool:
+ e_args = []
+ fetch_args = []
+ for channel_id in sub_channel_ids:
+ e_args = [channel_id, responses]
+ fetch_args.append(e_args)
+ try:
+ futures = {fetch_tpool.submit(fetch_xml, task): task for task in fetch_args}
+ except Exception as e:
+ fetch_tpool.shutdown(cancel_futures=True)
+ return e
+ else:
+ while len(futures) > 0:
+ pend_futures = {}
+ done, not_done = concurrent.futures.wait(futures, return_when=concurrent.futures.FIRST_COMPLETED)
+ for tsk in done:
+ if tsk.exception(): raise tsk.exception()
+ for tsk in not_done:
+ task = futures[tsk]
+ pend_futures[tsk] = task
+ futures = pend_futures
+ return True
+
+ tp_status = tpool_handler()
+ if not tp_status == True:
+ self._context.log_error('FATAL : Failed to start Channel XML Fetch ThreadPool executor or component.')
+ raise tp_status
for response in responses:
if response: |
Update for v7.0.5+beta.1: --- plugin.video.youtube-7.0.5+beta.1.matrix.1/resources/lib/youtube_plugin/youtube/client/youtube.py 2024-03-28 16:41:56.000000000 +1100
+++ plugin.video.youtube-7.0.5+beta.1.matrix.1_threadpool_xml_fetch/resources/lib/youtube_plugin/youtube/client/youtube.py 2024-03-29 19:01:06.845441805 +1100
@@ -11,6 +11,7 @@
from __future__ import absolute_import, division, unicode_literals
import threading
+import concurrent.futures
import xml.etree.ElementTree as ET
from copy import deepcopy
from itertools import chain, islice
@@ -1524,23 +1525,41 @@
responses = []
- def fetch_xml(_url, _responses):
+ def fetch_xml(_e_args):
+ _url = f'https://www.youtube.com/feeds/videos.xml?channel_id={_e_args[0]}'
+ _responses = _e_args[1]
_response = self.request(_url, headers=headers)
if _response:
_responses.append(_response)
- threads = []
- for channel_id in sub_channel_ids:
- thread = threading.Thread(
- target=fetch_xml,
- args=('https://www.youtube.com/feeds/videos.xml?channel_id=' + channel_id,
- responses)
- )
- threads.append(thread)
- thread.start()
-
- for thread in threads:
- thread.join(30)
+ def tpool_handler():
+ with concurrent.futures.ThreadPoolExecutor() as fetch_tpool:
+ e_args = []
+ fetch_args = []
+ for channel_id in sub_channel_ids:
+ e_args = [channel_id, responses]
+ fetch_args.append(e_args)
+ try:
+ futures = {fetch_tpool.submit(fetch_xml, task): task for task in fetch_args}
+ except Exception as e:
+ fetch_tpool.shutdown(cancel_futures=True)
+ return e
+ else:
+ while len(futures) > 0:
+ pend_futures = {}
+ done, not_done = concurrent.futures.wait(futures, return_when=concurrent.futures.FIRST_COMPLETED)
+ for tsk in done:
+ if tsk.exception(): raise tsk.exception()
+ for tsk in not_done:
+ task = futures[tsk]
+ pend_futures[tsk] = task
+ futures = pend_futures
+ return True
+
+ tp_status = tpool_handler()
+ if not tp_status == True:
+ self._context.log_error('FATAL : Failed to start Channel XML Fetch ThreadPool executor or component.')
+ raise tp_status
for response in responses:
if response: |
Update for 7.0.5+beta.4 (includes edit for #679): --- plugin.video.youtube-7.0.5+beta.4/resources/lib/youtube_plugin/youtube/client/youtube.py
+++ plugin.video.youtube-7.0.5+beta.4_threadpool_xml_fetch/resources/lib/youtube_plugin/youtube/client/youtube.py
@@ -11,6 +11,7 @@
from __future__ import absolute_import, division, unicode_literals
import threading
+import concurrent.futures
import xml.etree.ElementTree as ET
from copy import deepcopy
from itertools import chain, islice
@@ -1524,23 +1525,41 @@
responses = []
- def fetch_xml(_url, _responses):
+ def fetch_xml(_e_args):
+ _url = f'https://www.youtube.com/feeds/videos.xml?channel_id={_e_args[0]}'
+ _responses = _e_args[1]
_response = self.request(_url, headers=headers)
if _response:
_responses.append(_response)
- threads = []
- for channel_id in sub_channel_ids:
- thread = threading.Thread(
- target=fetch_xml,
- args=('https://www.youtube.com/feeds/videos.xml?channel_id=' + channel_id,
- responses)
- )
- threads.append(thread)
- thread.start()
-
- for thread in threads:
- thread.join(30)
+ def tpool_handler():
+ with concurrent.futures.ThreadPoolExecutor() as fetch_tpool:
+ e_args = []
+ fetch_args = []
+ for channel_id in sub_channel_ids:
+ e_args = [channel_id, responses]
+ fetch_args.append(e_args)
+ try:
+ futures = {fetch_tpool.submit(fetch_xml, task): task for task in fetch_args}
+ except Exception as e:
+ fetch_tpool.shutdown(cancel_futures=True)
+ return e
+ else:
+ while len(futures) > 0:
+ pend_futures = {}
+ done, not_done = concurrent.futures.wait(futures, return_when=concurrent.futures.FIRST_COMPLETED)
+ for tsk in done:
+ if tsk.exception(): raise tsk.exception()
+ for tsk in not_done:
+ task = futures[tsk]
+ pend_futures[tsk] = task
+ futures = pend_futures
+ return True
+
+ tp_status = tpool_handler()
+ if not tp_status == True:
+ self._context.log_error('FATAL : Failed to start Channel XML Fetch ThreadPool executor or component.')
+ raise tp_status
do_encode = not current_system_version.compatible(19, 0)
--- plugin.video.youtube-7.0.5+beta.4/resources/lib/youtube_plugin/youtube/helper/video_info.py
+++ plugin.video.youtube-7.0.5+beta.4_threadpool_xml_fetch/resources/lib/youtube_plugin/youtube/helper/video_info.py
@@ -596,7 +596,7 @@
48: '48000/1000', # 48.00 fps
50: '50000/1000', # 50.00 fps
60: '60000/1000', # 60.00 fps
- },
+ }
FRACTIONAL_FPS_SCALE = {
0: '{0}000/1000', # --.00 fps
24: '24000/1001', # 23.976 fps |
Update for v7.0.5: --- plugin.video.youtube-7.0.5_release/resources/lib/youtube_plugin/youtube/client/youtube.py
+++ plugin.video.youtube-7.0.5_threadpool_xml_fetch/resources/lib/youtube_plugin/youtube/client/youtube.py
@@ -11,6 +11,7 @@
from __future__ import absolute_import, division, unicode_literals
import threading
+import concurrent.futures
import xml.etree.ElementTree as ET
from copy import deepcopy
from itertools import chain, islice
@@ -1524,23 +1525,41 @@
responses = []
- def fetch_xml(_url, _responses):
+ def fetch_xml(_e_args):
+ _url = f'https://www.youtube.com/feeds/videos.xml?channel_id={_e_args[0]}'
+ _responses = _e_args[1]
_response = self.request(_url, headers=headers)
if _response:
_responses.append(_response)
- threads = []
- for channel_id in sub_channel_ids:
- thread = threading.Thread(
- target=fetch_xml,
- args=('https://www.youtube.com/feeds/videos.xml?channel_id=' + channel_id,
- responses)
- )
- threads.append(thread)
- thread.start()
-
- for thread in threads:
- thread.join(30)
+ def tpool_handler():
+ with concurrent.futures.ThreadPoolExecutor() as fetch_tpool:
+ e_args = []
+ fetch_args = []
+ for channel_id in sub_channel_ids:
+ e_args = [channel_id, responses]
+ fetch_args.append(e_args)
+ try:
+ futures = {fetch_tpool.submit(fetch_xml, task): task for task in fetch_args}
+ except Exception as e:
+ fetch_tpool.shutdown(cancel_futures=True)
+ return e
+ else:
+ while len(futures) > 0:
+ pend_futures = {}
+ done, not_done = concurrent.futures.wait(futures, return_when=concurrent.futures.FIRST_COMPLETED)
+ for tsk in done:
+ if tsk.exception(): raise tsk.exception()
+ for tsk in not_done:
+ task = futures[tsk]
+ pend_futures[tsk] = task
+ futures = pend_futures
+ return True
+
+ tp_status = tpool_handler()
+ if not tp_status == True:
+ self._context.log_error('FATAL : Failed to start Channel XML Fetch ThreadPool executor or component.')
+ raise tp_status
do_encode = not current_system_version.compatible(19, 0) |
Update for 7.0.6+beta.1: --- plugin.video.youtube-7.0.6+beta.1_release/resources/lib/youtube_plugin/youtube/client/youtube.py
+++ plugin.video.youtube-7.0.6+beta.1_threadpool_xml_fetch/resources/lib/youtube_plugin/youtube/client/youtube.py
@@ -11,6 +11,7 @@
from __future__ import absolute_import, division, unicode_literals
import threading
+import concurrent.futures
import xml.etree.ElementTree as ET
from copy import deepcopy
from itertools import chain, islice
@@ -1525,23 +1526,41 @@
responses = []
- def fetch_xml(_url, _responses):
+ def fetch_xml(_e_args):
+ _url = f'https://www.youtube.com/feeds/videos.xml?channel_id={_e_args[0]}'
+ _responses = _e_args[1]
_response = self.request(_url, headers=headers)
if _response:
_responses.append(_response)
- threads = []
- for channel_id in sub_channel_ids:
- thread = threading.Thread(
- target=fetch_xml,
- args=('https://www.youtube.com/feeds/videos.xml?channel_id=' + channel_id,
- responses)
- )
- threads.append(thread)
- thread.start()
-
- for thread in threads:
- thread.join(30)
+ def tpool_handler():
+ with concurrent.futures.ThreadPoolExecutor() as fetch_tpool:
+ e_args = []
+ fetch_args = []
+ for channel_id in sub_channel_ids:
+ e_args = [channel_id, responses]
+ fetch_args.append(e_args)
+ try:
+ futures = {fetch_tpool.submit(fetch_xml, task): task for task in fetch_args}
+ except Exception as e:
+ fetch_tpool.shutdown(cancel_futures=True)
+ return e
+ else:
+ while len(futures) > 0:
+ pend_futures = {}
+ done, not_done = concurrent.futures.wait(futures, return_when=concurrent.futures.FIRST_COMPLETED)
+ for tsk in done:
+ if tsk.exception(): raise tsk.exception()
+ for tsk in not_done:
+ task = futures[tsk]
+ pend_futures[tsk] = task
+ futures = pend_futures
+ return True
+
+ tp_status = tpool_handler()
+ if not tp_status == True:
+ self._context.log_error('FATAL : Failed to start Channel XML Fetch ThreadPool executor or component.')
+ raise tp_status
do_encode = not current_system_version.compatible(19, 0) |
Update for v7.0.6.3: --- plugin.video.youtube-7.0.6.3_release/resources/lib/youtube_plugin/youtube/client/youtube.py
+++ plugin.video.youtube-7.0.6.3_threadpool_xml_fetch/resources/lib/youtube_plugin/youtube/client/youtube.py
@@ -11,6 +11,7 @@
from __future__ import absolute_import, division, unicode_literals
import threading
+import concurrent.futures
import xml.etree.ElementTree as ET
from copy import deepcopy
from itertools import chain, islice
@@ -1525,23 +1526,41 @@
responses = []
- def fetch_xml(_url, _responses):
+ def fetch_xml(_e_args):
+ _url = f'https://www.youtube.com/feeds/videos.xml?channel_id={_e_args[0]}'
+ _responses = _e_args[1]
_response = self.request(_url, headers=headers)
if _response:
_responses.append(_response)
- threads = []
- for channel_id in sub_channel_ids:
- thread = threading.Thread(
- target=fetch_xml,
- args=('https://www.youtube.com/feeds/videos.xml?channel_id=' + channel_id,
- responses)
- )
- threads.append(thread)
- thread.start()
-
- for thread in threads:
- thread.join(30)
+ def tpool_handler():
+ with concurrent.futures.ThreadPoolExecutor() as fetch_tpool:
+ e_args = []
+ fetch_args = []
+ for channel_id in sub_channel_ids:
+ e_args = [channel_id, responses]
+ fetch_args.append(e_args)
+ try:
+ futures = {fetch_tpool.submit(fetch_xml, task): task for task in fetch_args}
+ except Exception as e:
+ fetch_tpool.shutdown(cancel_futures=True)
+ return e
+ else:
+ while len(futures) > 0:
+ pend_futures = {}
+ done, not_done = concurrent.futures.wait(futures, return_when=concurrent.futures.FIRST_COMPLETED)
+ for tsk in done:
+ if tsk.exception(): raise tsk.exception()
+ for tsk in not_done:
+ task = futures[tsk]
+ pend_futures[tsk] = task
+ futures = pend_futures
+ return True
+
+ tp_status = tpool_handler()
+ if not tp_status == True:
+ self._context.log_error('FATAL : Failed to start Channel XML Fetch ThreadPool executor or component.')
+ raise tp_status
do_encode = not current_system_version.compatible(19, 0) |
@benyamin-codez - I have had to make some more changes to My Subscriptions, that I hadn't planned on doing until more substantial work was done on moving other unrelated functionality to use the v1 API, so that parallel requests can be transparently handled by the plugin requests module. Long story short - I have also implemented a more naive implementation of the thread pool fix you are using, as a stepping stone to the end goal. The main reason I didn't use your patch is because I am ostensibly trying to maintain basic functionality for Kodi 18 (using Python 2.7), in which The git history is here: Main threading related changes are: Before I merge this, can you test? https://github.com/MoojMidge/plugin.video.youtube/releases/tag/v7.0.7%2Bbeta.2 If you want, you can also PR your patch and I can edit it so the forthcoming changes will merge cleanly. |
@MoojMidge, many thanks for that. I've checked on two RPi3B+ and your changes do resolve the problem in the OP. Given your backwards compatibility restraints, I thought your solution was quite elegant. I should mention that it is demonstrably slower on initial cache population, but only marginally slower otherwise - at least it seems slightly slower. I'm not sure if this is due to your threading implementation or perhaps another change...? Using |
How are you measuring this? I think the issue is that I changed what was being done in each thread, so rather than just making the request, each thread was also processing the feed xml i.e. doing a bunch of cpu bound rather than I/O bound operations. Also there was an unnecessary lock for writing to the output, and also an excessive amount of attempts to acquire the input read/modify lock, and apparently those are fairly expensive operations. Can you see if MoojMidge@513a9f4 restores the speed?
On |
Yes, that was a significant improvement. This round I obtained some rough run times using debug log time stamps. I forced I think the main difference is that your solution is executing the whole_job * max_threads, whereas mine is only executing each worker * max_threads. So your solution is quite efficient given you still need to do the locking for your On run-times, I should point out that the fetch has run a bit slower since I reverted to using your Given the above, do you still want me to submit the PR...? 2 seconds isn't much, but I guess a little noticeable. I don't mind submitting the PR if you think you'll make use of it. |
Can you see how the latest beta works? https://github.com/anxdpanic/plugin.video.youtube/releases/tag/v7.0.7%2Bbeta.2 |
@MoojMidge, I wasn't able to get v7.0.7+beta.2 working, per #769. 8^d It looks like your solution has matured quite a bit since I last had a peek. I look forward to giving it a go. |
The intent is to make this a more generic helper method that can be used in other parts of the plugin, so still needs some more work, but let's see how it works for this initial application |
Well it is certainly very quick..! At 10 to 11 seconds a run using |
It was a bit faster for me on a fast multi-core windows laptop, but good to hear that the same or better speed gains can be observed on more limited devices where it matters more. |
Context
Please provide any relevant information about your setup
Expected Behavior
When selecting "My Subscriptions" the subscription feed loads and is displayed.
Current Behavior
When selecting "My Subscriptions" the subscription feed continually loads (never stops) and an error message is displayed stating that a new thread cannot be started.
Steps to Reproduce
Please provide detailed steps for reproducing the issue.
Log
If you really want it let me know and I will consider dropping one, but know that I don't really have the time to perform necessary sanitization of GPS and other identifying data from it...
Additional Information
Platform is a Raspberry Pi Model 3B+.
I suspected a possible race condition on resource constrained hardware.
I modified per the following to resolve the problem.
Following this, the log shows many threads restarting on second try, and none needing more than three tries.
Hope that helps...!
Ben
The text was updated successfully, but these errors were encountered: