Explicitly support cancelling tasks.
The 'cancelled' status is now tracked by a Future that's passed to different asychronous tasks. That way it is possible to cancel all running tasks before browsing another Pillar node.
This commit is contained in:
parent
0174c28075
commit
5e237bea22
@ -149,6 +149,7 @@ def register():
|
|||||||
|
|
||||||
logging.basicConfig(level=logging.INFO,
|
logging.basicConfig(level=logging.INFO,
|
||||||
format='%(asctime)-15s %(levelname)8s %(name)s %(message)s')
|
format='%(asctime)-15s %(levelname)8s %(name)s %(message)s')
|
||||||
|
async_loop.setup_asyncio_executor()
|
||||||
gui.register()
|
gui.register()
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import traceback
|
import traceback
|
||||||
|
import concurrent.futures
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
@ -9,6 +10,20 @@ import bpy
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_asyncio_executor():
|
||||||
|
"""Sets up AsyncIO to run on a single thread.
|
||||||
|
|
||||||
|
This ensures that only one Pillar HTTP call is performed at the same time. Other
|
||||||
|
calls that could be performed in parallel are queued, and thus we can
|
||||||
|
reliably cancel them.
|
||||||
|
"""
|
||||||
|
|
||||||
|
executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.set_default_executor(executor)
|
||||||
|
# loop.set_debug(True)
|
||||||
|
|
||||||
|
|
||||||
def kick_async_loop(*args):
|
def kick_async_loop(*args):
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
@ -65,6 +65,7 @@ class MenuItem:
|
|||||||
self.label_text = label_text
|
self.label_text = label_text
|
||||||
self._thumb_path = ''
|
self._thumb_path = ''
|
||||||
self.icon = None
|
self.icon = None
|
||||||
|
self._is_folder = file_desc is None and thumb_path == 'FOLDER'
|
||||||
|
|
||||||
self.thumb_path = thumb_path
|
self.thumb_path = thumb_path
|
||||||
|
|
||||||
@ -93,7 +94,7 @@ class MenuItem:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_folder(self) -> bool:
|
def is_folder(self) -> bool:
|
||||||
return self.file_desc is None
|
return self._is_folder
|
||||||
|
|
||||||
def update_placement(self, x, y, width, height):
|
def update_placement(self, x, y, width, height):
|
||||||
"""Use OpenGL to draw this one menu item."""
|
"""Use OpenGL to draw this one menu item."""
|
||||||
@ -161,6 +162,7 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
|||||||
project_uuid = '5672beecc0261b2005ed1a33' # Blender Cloud project UUID
|
project_uuid = '5672beecc0261b2005ed1a33' # Blender Cloud project UUID
|
||||||
node_uuid = '' # Blender Cloud node UUID
|
node_uuid = '' # Blender Cloud node UUID
|
||||||
async_task = None # asyncio task for fetching thumbnails
|
async_task = None # asyncio task for fetching thumbnails
|
||||||
|
signalling_future = None # asyncio future for signalling that we want to cancel everything.
|
||||||
timer = None
|
timer = None
|
||||||
log = logging.getLogger('%s.BlenderCloudBrowser' % __name__)
|
log = logging.getLogger('%s.BlenderCloudBrowser' % __name__)
|
||||||
|
|
||||||
@ -193,7 +195,7 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
|||||||
|
|
||||||
self.current_display_content = []
|
self.current_display_content = []
|
||||||
self.loaded_images = set()
|
self.loaded_images = set()
|
||||||
self.browse_assets(context)
|
self.browse_assets()
|
||||||
|
|
||||||
context.window_manager.modal_handler_add(self)
|
context.window_manager.modal_handler_add(self)
|
||||||
self.timer = context.window_manager.event_timer_add(1/30, context.window)
|
self.timer = context.window_manager.event_timer_add(1/30, context.window)
|
||||||
@ -219,7 +221,7 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
|||||||
|
|
||||||
if selected.is_folder:
|
if selected.is_folder:
|
||||||
self.node_uuid = selected.node_uuid
|
self.node_uuid = selected.node_uuid
|
||||||
self.browse_assets(context)
|
self.browse_assets()
|
||||||
else:
|
else:
|
||||||
self.handle_item_selection(selected)
|
self.handle_item_selection(selected)
|
||||||
self._finish(context)
|
self._finish(context)
|
||||||
@ -232,14 +234,33 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
|||||||
return {'RUNNING_MODAL'}
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
def _stop_async_task(self):
|
def _stop_async_task(self):
|
||||||
|
self.log.debug('Stopping async task')
|
||||||
if self.async_task is None:
|
if self.async_task is None:
|
||||||
|
self.log.debug('No async task, trivially stopped')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Signal that we want to stop.
|
||||||
|
if not self.signalling_future.done():
|
||||||
|
self.log.info("Signalling that we want to cancel anything that's running.")
|
||||||
|
self.signalling_future.cancel()
|
||||||
|
|
||||||
|
# Wait until the asynchronous task is done.
|
||||||
if not self.async_task.done():
|
if not self.async_task.done():
|
||||||
print('Cancelling running async download task {}'.format(self.async_task))
|
self.log.info("blocking until async task is done.")
|
||||||
self.async_task.cancel()
|
loop = asyncio.get_event_loop()
|
||||||
else:
|
try:
|
||||||
|
loop.run_until_complete(self.async_task)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
self.log.info('Asynchronous task was cancelled')
|
||||||
|
return
|
||||||
|
|
||||||
|
# noinspection PyBroadException
|
||||||
|
try:
|
||||||
self.async_task.result() # This re-raises any exception of the task.
|
self.async_task.result() # This re-raises any exception of the task.
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
self.log.info('Asynchronous task was cancelled')
|
||||||
|
except Exception:
|
||||||
|
self.log.exception("Exception from asynchronous task")
|
||||||
|
|
||||||
def _finish(self, context):
|
def _finish(self, context):
|
||||||
self.log.debug('Finishing the modal operator')
|
self.log.debug('Finishing the modal operator')
|
||||||
@ -283,74 +304,69 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
|||||||
else:
|
else:
|
||||||
raise ValueError('Unable to find MenuItem(node_uuid=%r)' % node_uuid)
|
raise ValueError('Unable to find MenuItem(node_uuid=%r)' % node_uuid)
|
||||||
|
|
||||||
async def async_download_previews(self, context, thumbnails_directory):
|
async def async_download_previews(self, thumbnails_directory):
|
||||||
# If we have a node UUID, we fetch the textures
|
self.log.info('Asynchronously downloading previews to %r', thumbnails_directory)
|
||||||
# FIXME: support mixture of sub-nodes and textures under one node.
|
|
||||||
self.clear_images()
|
self.clear_images()
|
||||||
|
|
||||||
def redraw():
|
|
||||||
# region = context.region
|
|
||||||
# if region is None:
|
|
||||||
# print('Unable to redraw, region is %s' % region)
|
|
||||||
# print(' (context is %s)' % context)
|
|
||||||
# return
|
|
||||||
# region.tag_redraw()
|
|
||||||
pass
|
|
||||||
|
|
||||||
def thumbnail_loading(node_uuid, texture_node):
|
def thumbnail_loading(node_uuid, texture_node):
|
||||||
self.add_menu_item(node_uuid, None, 'SPINNER', texture_node['name'])
|
self.add_menu_item(node_uuid, None, 'SPINNER', texture_node['name'])
|
||||||
redraw()
|
|
||||||
|
|
||||||
def thumbnail_loaded(node_uuid, file_desc, thumb_path):
|
def thumbnail_loaded(node_uuid, file_desc, thumb_path):
|
||||||
# update MenuItem added above
|
|
||||||
self.update_menu_item(node_uuid, file_desc, thumb_path, file_desc['filename'])
|
self.update_menu_item(node_uuid, file_desc, thumb_path, file_desc['filename'])
|
||||||
redraw()
|
|
||||||
|
|
||||||
if self.node_uuid:
|
if self.node_uuid:
|
||||||
|
self.log.debug('Getting subnodes for parent node %r', self.node_uuid)
|
||||||
|
children = await pillar.get_nodes(parent_node_uuid=self.node_uuid,
|
||||||
|
node_type='group_textures')
|
||||||
|
|
||||||
|
self.log.debug('Finding parent of node %r', self.node_uuid)
|
||||||
# Make sure we can go up again.
|
# Make sure we can go up again.
|
||||||
parent_uuid = await pillar.parent_node_uuid(self.node_uuid)
|
parent_uuid = await pillar.parent_node_uuid(self.node_uuid)
|
||||||
self.add_menu_item(parent_uuid, None, 'FOLDER', '.. up ..')
|
self.add_menu_item(parent_uuid, None, 'FOLDER', '.. up ..')
|
||||||
|
|
||||||
|
self.log.debug('Iterating over child nodes of %r', self.node_uuid)
|
||||||
|
for child in children:
|
||||||
|
# print(' - %(_id)s = %(name)s' % child)
|
||||||
|
self.add_menu_item(child['_id'], None, 'FOLDER', child['name'])
|
||||||
|
|
||||||
directory = os.path.join(thumbnails_directory, self.project_uuid, self.node_uuid)
|
directory = os.path.join(thumbnails_directory, self.project_uuid, self.node_uuid)
|
||||||
os.makedirs(directory, exist_ok=True)
|
os.makedirs(directory, exist_ok=True)
|
||||||
|
|
||||||
|
self.log.debug('Fetching texture thumbnails for node %r', self.node_uuid)
|
||||||
await pillar.fetch_texture_thumbs(self.node_uuid, 's', directory,
|
await pillar.fetch_texture_thumbs(self.node_uuid, 's', directory,
|
||||||
thumbnail_loading=thumbnail_loading,
|
thumbnail_loading=thumbnail_loading,
|
||||||
thumbnail_loaded=thumbnail_loaded)
|
thumbnail_loaded=thumbnail_loaded,
|
||||||
|
future=self.signalling_future)
|
||||||
elif self.project_uuid:
|
elif self.project_uuid:
|
||||||
|
self.log.debug('Getting subnodes for project node %r', self.project_uuid)
|
||||||
children = await pillar.get_nodes(self.project_uuid, '')
|
children = await pillar.get_nodes(self.project_uuid, '')
|
||||||
|
|
||||||
|
self.log.debug('Iterating over child nodes of project %r', self.project_uuid)
|
||||||
for child in children:
|
for child in children:
|
||||||
print(' - %(_id)s = %(name)s' % child)
|
# print(' - %(_id)s = %(name)s' % child)
|
||||||
self.add_menu_item(child['_id'], None, 'FOLDER', child['name'])
|
self.add_menu_item(child['_id'], None, 'FOLDER', child['name'])
|
||||||
redraw()
|
|
||||||
else:
|
else:
|
||||||
# TODO: add "nothing here" icon and trigger re-draw
|
# TODO: add "nothing here" icon and trigger re-draw
|
||||||
redraw()
|
self.log.warning("Not node UUID and no project UUID, I can't do anything!")
|
||||||
|
pass
|
||||||
|
|
||||||
# Call the 'done' callback.
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.call_soon_threadsafe(self.downloading_done)
|
loop.call_soon_threadsafe(self.downloading_done)
|
||||||
|
|
||||||
def browse_assets(self, context):
|
def browse_assets(self):
|
||||||
|
self.log.debug('Browsing assets at project %r node %r', self.project_uuid, self.node_uuid)
|
||||||
self._stop_async_task()
|
self._stop_async_task()
|
||||||
self.clear_images()
|
|
||||||
|
|
||||||
# Download the previews asynchronously.
|
# Download the previews asynchronously.
|
||||||
|
self.signalling_future = asyncio.Future()
|
||||||
self.async_task = asyncio.ensure_future(
|
self.async_task = asyncio.ensure_future(
|
||||||
self.async_download_previews(context, self.thumbnails_cache))
|
self.async_download_previews(self.thumbnails_cache))
|
||||||
|
|
||||||
# Start the async manager so everything happens.
|
# Start the async manager so everything happens.
|
||||||
async_loop.ensure_async_loop()
|
async_loop.ensure_async_loop()
|
||||||
|
|
||||||
def downloading_done(self):
|
def downloading_done(self):
|
||||||
# if not self.async_task.done():
|
self.log.info('Done downloading thumbnails.')
|
||||||
# print('%s: aborting download task' % self)
|
|
||||||
# self._stop_async_task()
|
|
||||||
# else:
|
|
||||||
# print('%s: downloading done' % self)
|
|
||||||
# self.async_task.result()
|
|
||||||
pass
|
|
||||||
|
|
||||||
def draw_menu(self, context):
|
def draw_menu(self, context):
|
||||||
margin_x = 20
|
margin_x = 20
|
||||||
|
@ -3,6 +3,9 @@ import sys
|
|||||||
import os
|
import os
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
|
from contextlib import closing
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
# Add our shipped Pillar SDK wheel to the Python path
|
# Add our shipped Pillar SDK wheel to the Python path
|
||||||
if not any('pillar_sdk' in path for path in sys.path):
|
if not any('pillar_sdk' in path for path in sys.path):
|
||||||
@ -84,7 +87,8 @@ async def get_project_uuid(project_url: str) -> str:
|
|||||||
return project['_id']
|
return project['_id']
|
||||||
|
|
||||||
|
|
||||||
async def get_nodes(project_uuid: str = None, parent_node_uuid: str = None) -> list:
|
async def get_nodes(project_uuid: str = None, parent_node_uuid: str = None,
|
||||||
|
node_type: str = None) -> list:
|
||||||
"""Gets nodes for either a project or given a parent node.
|
"""Gets nodes for either a project or given a parent node.
|
||||||
|
|
||||||
@param project_uuid: the UUID of the project, or None if only querying by parent_node_uuid.
|
@param project_uuid: the UUID of the project, or None if only querying by parent_node_uuid.
|
||||||
@ -108,6 +112,9 @@ async def get_nodes(project_uuid: str = None, parent_node_uuid: str = None) -> l
|
|||||||
if project_uuid:
|
if project_uuid:
|
||||||
where['project'] = project_uuid
|
where['project'] = project_uuid
|
||||||
|
|
||||||
|
if node_type:
|
||||||
|
where['node_type'] = node_type
|
||||||
|
|
||||||
node_all = functools.partial(pillarsdk.Node.all, {
|
node_all = functools.partial(pillarsdk.Node.all, {
|
||||||
'projection': {'name': 1, 'parent': 1, 'node_type': 1,
|
'projection': {'name': 1, 'parent': 1, 'node_type': 1,
|
||||||
'properties.order': 1, 'properties.status': 1,
|
'properties.order': 1, 'properties.status': 1,
|
||||||
@ -121,24 +128,59 @@ async def get_nodes(project_uuid: str = None, parent_node_uuid: str = None) -> l
|
|||||||
return children['_items']
|
return children['_items']
|
||||||
|
|
||||||
|
|
||||||
async def download_to_file(url, filename, chunk_size=10 * 1024):
|
async def download_to_file(url, filename, chunk_size=10 * 1024, *, future: asyncio.Future = None):
|
||||||
"""Downloads a file via HTTP(S) directly to the filesystem."""
|
"""Downloads a file via HTTP(S) directly to the filesystem."""
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
await loop.run_in_executor(None, pillarsdk.utils.download_to_file, url, filename, chunk_size)
|
|
||||||
|
def perform_get_request():
|
||||||
|
return requests.get(url, stream=True, verify=True)
|
||||||
|
|
||||||
|
# Download the file in a different thread.
|
||||||
|
def download_loop():
|
||||||
|
with closing(req), open(filename, 'wb') as outfile:
|
||||||
|
for block in req.iter_content(chunk_size=chunk_size):
|
||||||
|
if is_cancelled(future):
|
||||||
|
raise asyncio.CancelledError('Downloading was cancelled')
|
||||||
|
outfile.write(block)
|
||||||
|
|
||||||
|
# Check for cancellation even before we start our GET request
|
||||||
|
if is_cancelled(future):
|
||||||
|
log.debug('Downloading was cancelled before doing the GET')
|
||||||
|
raise asyncio.CancelledError('Downloading was cancelled')
|
||||||
|
|
||||||
|
log.debug('Performing GET %s', url)
|
||||||
|
req = await loop.run_in_executor(None, perform_get_request)
|
||||||
|
log.debug('Done with GET %s', url)
|
||||||
|
|
||||||
|
# After we performed the GET request, we should check whether we should start
|
||||||
|
# the download at all.
|
||||||
|
if is_cancelled(future):
|
||||||
|
log.debug('Downloading was cancelled before downloading the GET response')
|
||||||
|
raise asyncio.CancelledError('Downloading was cancelled')
|
||||||
|
|
||||||
|
log.debug('Downloading response of GET %s', url)
|
||||||
|
await loop.run_in_executor(None, download_loop)
|
||||||
|
log.debug('Done downloading response of GET %s', url)
|
||||||
|
|
||||||
|
|
||||||
async def stream_thumb_to_file(file: pillarsdk.File, directory: str, desired_size: str):
|
async def stream_thumb_to_file(file: pillarsdk.File, directory: str, desired_size: str, *,
|
||||||
|
future: asyncio.Future = None):
|
||||||
"""Streams a thumbnail to a file.
|
"""Streams a thumbnail to a file.
|
||||||
|
|
||||||
@param file: the pillar File object that represents the image whose thumbnail to download.
|
@param file: the pillar File object that represents the image whose thumbnail to download.
|
||||||
@param directory: the directory to save the file to.
|
@param directory: the directory to save the file to.
|
||||||
@param desired_size: thumbnail size
|
@param desired_size: thumbnail size
|
||||||
@return: the absolute path of the downloaded file.
|
@return: the absolute path of the downloaded file, or None if the task was cancelled before
|
||||||
|
downloading finished.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
api = pillar_api()
|
api = pillar_api()
|
||||||
|
|
||||||
|
if is_cancelled(future):
|
||||||
|
log.debug('stream_thumb_to_file(): cancelled before fetching thumbnail URL from Pillar')
|
||||||
|
return None
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
thumb_link = await loop.run_in_executor(None, functools.partial(
|
thumb_link = await loop.run_in_executor(None, functools.partial(
|
||||||
file.thumbnail_file, desired_size, api=api))
|
file.thumbnail_file, desired_size, api=api))
|
||||||
@ -147,11 +189,15 @@ async def stream_thumb_to_file(file: pillarsdk.File, directory: str, desired_siz
|
|||||||
raise ValueError("File {} has no thumbnail of size {}"
|
raise ValueError("File {} has no thumbnail of size {}"
|
||||||
.format(file['_id'], desired_size))
|
.format(file['_id'], desired_size))
|
||||||
|
|
||||||
|
if is_cancelled(future):
|
||||||
|
log.debug('stream_thumb_to_file(): cancelled before downloading file')
|
||||||
|
return None
|
||||||
|
|
||||||
root, ext = os.path.splitext(file['file_path'])
|
root, ext = os.path.splitext(file['file_path'])
|
||||||
thumb_fname = "{0}-{1}.jpg".format(root, desired_size)
|
thumb_fname = "{0}-{1}.jpg".format(root, desired_size)
|
||||||
thumb_path = os.path.abspath(os.path.join(directory, thumb_fname))
|
thumb_path = os.path.abspath(os.path.join(directory, thumb_fname))
|
||||||
|
|
||||||
await download_to_file(thumb_link, thumb_path)
|
await download_to_file(thumb_link, thumb_path, future=future)
|
||||||
|
|
||||||
return thumb_path
|
return thumb_path
|
||||||
|
|
||||||
@ -160,7 +206,8 @@ async def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str,
|
|||||||
thumbnail_directory: str,
|
thumbnail_directory: str,
|
||||||
*,
|
*,
|
||||||
thumbnail_loading: callable,
|
thumbnail_loading: callable,
|
||||||
thumbnail_loaded: callable):
|
thumbnail_loaded: callable,
|
||||||
|
future: asyncio.Future = None):
|
||||||
"""Generator, fetches all texture thumbnails in a certain parent node.
|
"""Generator, fetches all texture thumbnails in a certain parent node.
|
||||||
|
|
||||||
@param parent_node_uuid: the UUID of the parent node. All sub-nodes will be downloaded.
|
@param parent_node_uuid: the UUID of the parent node. All sub-nodes will be downloaded.
|
||||||
@ -171,6 +218,8 @@ async def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str,
|
|||||||
show a "downloading" indicator.
|
show a "downloading" indicator.
|
||||||
@param thumbnail_loaded: callback function that takes (node_id, pillarsdk.File object,
|
@param thumbnail_loaded: callback function that takes (node_id, pillarsdk.File object,
|
||||||
thumbnail path) parameters, which is called for every thumbnail after it's been downloaded.
|
thumbnail path) parameters, which is called for every thumbnail after it's been downloaded.
|
||||||
|
@param future: Future that's inspected; if it is not None and cancelled, texture downloading
|
||||||
|
is aborted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
api = pillar_api()
|
api = pillar_api()
|
||||||
@ -185,6 +234,11 @@ async def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str,
|
|||||||
if texture_node['node_type'] != 'texture':
|
if texture_node['node_type'] != 'texture':
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if is_cancelled(future):
|
||||||
|
log.debug('fetch_texture_thumbs cancelled before finding File for texture %r',
|
||||||
|
texture_node['_id'])
|
||||||
|
return
|
||||||
|
|
||||||
# Find the File that belongs to this texture node
|
# Find the File that belongs to this texture node
|
||||||
pic_uuid = texture_node['picture']
|
pic_uuid = texture_node['picture']
|
||||||
loop.call_soon_threadsafe(functools.partial(thumbnail_loading,
|
loop.call_soon_threadsafe(functools.partial(thumbnail_loading,
|
||||||
@ -193,11 +247,23 @@ async def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str,
|
|||||||
file_desc = await loop.run_in_executor(None, file_find, pic_uuid)
|
file_desc = await loop.run_in_executor(None, file_find, pic_uuid)
|
||||||
|
|
||||||
if file_desc is None:
|
if file_desc is None:
|
||||||
print('Unable to find file for texture node {}'.format(pic_uuid))
|
log.warning('Unable to find file for texture node %s', pic_uuid)
|
||||||
thumb_path = None
|
thumb_path = None
|
||||||
else:
|
else:
|
||||||
|
if is_cancelled(future):
|
||||||
|
log.debug('fetch_texture_thumbs cancelled before downloading file %r',
|
||||||
|
file_desc['_id'])
|
||||||
|
return
|
||||||
|
|
||||||
# Save the thumbnail
|
# Save the thumbnail
|
||||||
thumb_path = await stream_thumb_to_file(file_desc, thumbnail_directory, desired_size)
|
thumb_path = await stream_thumb_to_file(file_desc, thumbnail_directory, desired_size,
|
||||||
|
future=future)
|
||||||
|
if thumb_path is None:
|
||||||
|
# The task got cancelled, we should abort too.
|
||||||
|
log.debug('fetch_texture_thumbs cancelled while downloading file %r',
|
||||||
|
file_desc['_id'])
|
||||||
|
return
|
||||||
|
|
||||||
# print('Texture node {} has file {}'.format(texture_node['_id'], thumb_path))
|
# print('Texture node {} has file {}'.format(texture_node['_id'], thumb_path))
|
||||||
|
|
||||||
loop.call_soon_threadsafe(functools.partial(thumbnail_loaded,
|
loop.call_soon_threadsafe(functools.partial(thumbnail_loaded,
|
||||||
@ -205,13 +271,35 @@ async def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str,
|
|||||||
file_desc, thumb_path))
|
file_desc, thumb_path))
|
||||||
|
|
||||||
# Download all texture nodes in parallel.
|
# Download all texture nodes in parallel.
|
||||||
texture_nodes = await get_nodes(parent_node_uuid=parent_node_uuid)
|
log.debug('Getting child nodes of node %r', parent_node_uuid)
|
||||||
|
texture_nodes = await get_nodes(parent_node_uuid=parent_node_uuid,
|
||||||
|
node_type='texture')
|
||||||
|
|
||||||
|
if is_cancelled(future):
|
||||||
|
log.warning('fetch_texture_thumbs: Texture downloading cancelled')
|
||||||
|
return
|
||||||
|
|
||||||
|
# We don't want to gather too much in parallel, as it will make cancelling take more time.
|
||||||
|
# This is caused by HTTP requests going out in parallel, and once the socket is open and
|
||||||
|
# the GET request is sent, we can't cancel until the server starts streaming the response.
|
||||||
|
chunk_size = 2
|
||||||
|
for i in range(0, len(texture_nodes), chunk_size):
|
||||||
|
chunk = texture_nodes[i:i + chunk_size]
|
||||||
|
|
||||||
|
log.debug('fetch_texture_thumbs: Gathering texture[%i:%i] for parent node %r',
|
||||||
|
i, i + chunk_size, parent_node_uuid)
|
||||||
|
coros = (handle_texture_node(texture_node)
|
||||||
|
for texture_node in chunk)
|
||||||
|
|
||||||
# raises any exception from failed handle_texture_node() calls.
|
# raises any exception from failed handle_texture_node() calls.
|
||||||
await asyncio.gather(*(handle_texture_node(texture_node)
|
await asyncio.gather(*coros)
|
||||||
for texture_node in texture_nodes))
|
|
||||||
|
|
||||||
print('Done downloading texture thumbnails')
|
log.info('fetch_texture_thumbs: Done downloading texture thumbnails')
|
||||||
|
|
||||||
|
|
||||||
|
def is_cancelled(future: asyncio.Future) -> bool:
|
||||||
|
log.debug('%s.cancelled() = %s', future, future.cancelled())
|
||||||
|
return future is not None and future.cancelled()
|
||||||
|
|
||||||
|
|
||||||
async def parent_node_uuid(node_uuid: str) -> str:
|
async def parent_node_uuid(node_uuid: str) -> str:
|
||||||
@ -228,8 +316,6 @@ async def parent_node_uuid(node_uuid: str) -> str:
|
|||||||
log.debug('Unable to find node %r, returning empty parent', node_uuid)
|
log.debug('Unable to find node %r, returning empty parent', node_uuid)
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
print('Found node {}'.format(node))
|
parent_uuid = node.parent or ''
|
||||||
try:
|
log.debug('Parent node of %r is %r', node_uuid, parent_uuid)
|
||||||
return node['parent']
|
return parent_uuid
|
||||||
except KeyError:
|
|
||||||
return ''
|
|
||||||
|
Reference in New Issue
Block a user