Compare commits

..

9 Commits

Author SHA1 Message Date
b4f71745b0 Released 1.7.1 2017-06-13 14:50:56 +02:00
1d41fce1ae Updated changelog 2017-06-13 14:47:33 +02:00
e636fde4ce Bumped version to 1.7.1 2017-06-13 14:42:20 +02:00
82a9dc5226 Two-stage timeouts for Pillar calls 2017-06-13 14:39:29 +02:00
1f40915ac8 Added some debug log 2017-06-13 14:39:29 +02:00
32693c0f64 Fixed erroneous return type declaration 2017-06-13 14:39:29 +02:00
c38748eb05 Shorten URLs in debug logging 2017-06-13 14:39:29 +02:00
ac85bea111 Some asyncio tweaks. 2017-06-13 14:39:29 +02:00
7b5613ce77 Fixed issue with multiple asyncio loops on Windows.
The biggest issue was the construction of an asyncio.Semaphore() while the
default loop is alive, and then creating a new loop on win32.

I've also taken the opportunity to explicitly pass our loop to some calls,
rather than expecting them to use the correct one automagically, and added
some more explicit timeout handling to the semaphore usage.
2017-06-13 13:35:05 +02:00
6 changed files with 53 additions and 22 deletions

View File

@@ -1,6 +1,11 @@
# Blender Cloud changelog # Blender Cloud changelog
## Version 1.7.1 (2017-06-13)
- Fixed asyncio issues on Windows
## Version 1.7.0 (2017-06-09) ## Version 1.7.0 (2017-06-09)
- Fixed reloading after upgrading from 1.4.4 (our last public release). - Fixed reloading after upgrading from 1.4.4 (our last public release).
@@ -10,7 +15,7 @@
## Version 1.6.4 (2017-04-21) ## Version 1.6.4 (2017-04-21)
- Added file exclusion filter for Flamenco. A filter like "*.abc;*.mkv;*.mov" can be - Added file exclusion filter for Flamenco. A filter like `*.abc;*.mkv;*.mov` can be
used to prevent certain files from being copied to the job storage directory. used to prevent certain files from being copied to the job storage directory.
Requires a Blender that is bundled with BAM 1.1.7 or newer. Requires a Blender that is bundled with BAM 1.1.7 or newer.

View File

@@ -21,7 +21,7 @@
bl_info = { bl_info = {
'name': 'Blender Cloud', 'name': 'Blender Cloud',
"author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis", "author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis",
'version': (1, 7, 0), 'version': (1, 7, 1),
'blender': (2, 77, 0), 'blender': (2, 77, 0),
'location': 'Addon Preferences panel, and Ctrl+Shift+Alt+A anywhere for texture browser', 'location': 'Addon Preferences panel, and Ctrl+Shift+Alt+A anywhere for texture browser',
'description': 'Texture library browser and Blender Sync. Requires the Blender ID addon ' 'description': 'Texture library browser and Blender Sync. Requires the Blender ID addon '

View File

@@ -33,17 +33,12 @@ _loop_kicking_operator_running = False
def setup_asyncio_executor(): def setup_asyncio_executor():
"""Sets up AsyncIO to run on a single thread. """Sets up AsyncIO to run properly on each platform."""
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.
"""
import sys import sys
executor = concurrent.futures.ThreadPoolExecutor()
if sys.platform == 'win32': if sys.platform == 'win32':
asyncio.get_event_loop().close()
# On Windows, the default event loop is SelectorEventLoop, which does # On Windows, the default event loop is SelectorEventLoop, which does
# not support subprocesses. ProactorEventLoop should be used instead. # not support subprocesses. ProactorEventLoop should be used instead.
# Source: https://docs.python.org/3/library/asyncio-subprocess.html # Source: https://docs.python.org/3/library/asyncio-subprocess.html
@@ -51,9 +46,15 @@ def setup_asyncio_executor():
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
else: else:
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
executor = concurrent.futures.ThreadPoolExecutor(max_workers=10)
loop.set_default_executor(executor) loop.set_default_executor(executor)
# loop.set_debug(True) # loop.set_debug(True)
from . import pillar
# No more than this many Pillar calls should be made simultaneously
pillar.pillar_semaphore = asyncio.Semaphore(3, loop=loop)
def kick_async_loop(*args) -> bool: def kick_async_loop(*args) -> bool:
"""Performs a single iteration of the asyncio event loop. """Performs a single iteration of the asyncio event loop.

46
blender_cloud/pillar.py Normal file → Executable file
View File

@@ -115,6 +115,12 @@ def with_existing_dir(filename: str, open_mode: str, encoding=None):
yield file_object yield file_object
def _shorten(somestr: str, maxlen=40) -> str:
"""Shortens strings for logging"""
return (somestr[:maxlen - 3] + '...') if len(somestr) > maxlen else somestr
def save_as_json(pillar_resource, json_filename): def save_as_json(pillar_resource, json_filename):
with with_existing_dir(json_filename, 'w') as outfile: with with_existing_dir(json_filename, 'w') as outfile:
log.debug('Saving metadata to %r' % json_filename) log.debug('Saving metadata to %r' % json_filename)
@@ -200,8 +206,11 @@ def pillar_api(pillar_endpoint: str = None, caching=True) -> pillarsdk.Api:
return _pillar_api[caching] return _pillar_api[caching]
# No more than this many Pillar calls should be made simultaneously # This is an asyncio.Semaphore object, which is late-instantiated to be sure
pillar_semaphore = asyncio.Semaphore(3) # the asyncio loop has been created properly. On Windows we create a new one,
# which can cause this semaphore to still be linked against the old default
# loop.
pillar_semaphore = None
async def pillar_call(pillar_func, *args, caching=True, **kwargs): async def pillar_call(pillar_func, *args, caching=True, **kwargs):
@@ -214,8 +223,21 @@ async def pillar_call(pillar_func, *args, caching=True, **kwargs):
partial = functools.partial(pillar_func, *args, api=pillar_api(caching=caching), **kwargs) partial = functools.partial(pillar_func, *args, api=pillar_api(caching=caching), **kwargs)
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
async with pillar_semaphore: # Use explicit calls to acquire() and release() so that we have more control over
# how long we wait and how we handle timeouts.
try:
await asyncio.wait_for(pillar_semaphore.acquire(), timeout=10, loop=loop)
except asyncio.TimeoutError:
log.info('Waiting for semaphore to call %s', pillar_func.__name__)
try:
await asyncio.wait_for(pillar_semaphore.acquire(), timeout=50, loop=loop)
except asyncio.TimeoutError:
raise RuntimeError('Timeout waiting for Pillar Semaphore!')
try:
return await loop.run_in_executor(None, partial) return await loop.run_in_executor(None, partial)
finally:
pillar_semaphore.release()
def sync_call(pillar_func, *args, caching=True, **kwargs): def sync_call(pillar_func, *args, caching=True, **kwargs):
@@ -441,9 +463,9 @@ async def download_to_file(url, filename, *,
log.debug('Downloading was cancelled before doing the GET') log.debug('Downloading was cancelled before doing the GET')
raise asyncio.CancelledError('Downloading was cancelled') raise asyncio.CancelledError('Downloading was cancelled')
log.debug('Performing GET %s', url) log.debug('Performing GET %s', _shorten(url))
response = await loop.run_in_executor(None, perform_get_request) response = await loop.run_in_executor(None, perform_get_request)
log.debug('Status %i from GET %s', response.status_code, url) log.debug('Status %i from GET %s', response.status_code, _shorten(url))
response.raise_for_status() response.raise_for_status()
if response.status_code == 304: if response.status_code == 304:
@@ -457,9 +479,9 @@ async def download_to_file(url, filename, *,
log.debug('Downloading was cancelled before downloading the GET response') log.debug('Downloading was cancelled before downloading the GET response')
raise asyncio.CancelledError('Downloading was cancelled') raise asyncio.CancelledError('Downloading was cancelled')
log.debug('Downloading response of GET %s', url) log.debug('Downloading response of GET %s', _shorten(url))
await loop.run_in_executor(None, download_loop) await loop.run_in_executor(None, download_loop)
log.debug('Done downloading response of GET %s', url) log.debug('Done downloading response of GET %s', _shorten(url))
# We're done downloading, now we have something cached we can use. # We're done downloading, now we have something cached we can use.
log.debug('Saving header cache to %s', header_store) log.debug('Saving header cache to %s', header_store)
@@ -534,7 +556,8 @@ async def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str,
for texture_node in texture_nodes) for texture_node in texture_nodes)
# raises any exception from failed handle_texture_node() calls. # raises any exception from failed handle_texture_node() calls.
await asyncio.gather(*coros) loop = asyncio.get_event_loop()
await asyncio.gather(*coros, loop=loop)
log.info('fetch_texture_thumbs: Done downloading texture thumbnails') log.info('fetch_texture_thumbs: Done downloading texture thumbnails')
@@ -747,7 +770,8 @@ async def download_texture(texture_node,
future=future) future=future)
downloaders.append(dlr) downloaders.append(dlr)
return await asyncio.gather(*downloaders, return_exceptions=True) loop = asyncio.get_event_loop()
return await asyncio.gather(*downloaders, return_exceptions=True, loop=loop)
async def upload_file(project_id: str, file_path: pathlib.Path, *, async def upload_file(project_id: str, file_path: pathlib.Path, *,
@@ -773,9 +797,9 @@ async def upload_file(project_id: str, file_path: pathlib.Path, *,
log.debug('Uploading was cancelled before doing the POST') log.debug('Uploading was cancelled before doing the POST')
raise asyncio.CancelledError('Uploading was cancelled') raise asyncio.CancelledError('Uploading was cancelled')
log.debug('Performing POST %s', url) log.debug('Performing POST %s', _shorten(url))
response = await loop.run_in_executor(None, upload) response = await loop.run_in_executor(None, upload)
log.debug('Status %i from POST %s', response.status_code, url) log.debug('Status %i from POST %s', response.status_code, _shorten(url))
response.raise_for_status() response.raise_for_status()
resp = response.json() resp = response.json()

View File

@@ -457,7 +457,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
return menu_item return menu_item
def update_menu_item(self, node, *args) -> MenuItem: def update_menu_item(self, node, *args):
node_uuid = node['_id'] node_uuid = node['_id']
# Just make this thread-safe to be on the safe side. # Just make this thread-safe to be on the safe side.
@@ -538,6 +538,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
self.add_menu_item(node, None, 'SPINNER', texture_node['name']) self.add_menu_item(node, None, 'SPINNER', texture_node['name'])
def thumbnail_loaded(node, file_desc, thumb_path): def thumbnail_loaded(node, file_desc, thumb_path):
self.log.debug('Node %s thumbnail loaded', node['_id'])
self.update_menu_item(node, file_desc, thumb_path) self.update_menu_item(node, file_desc, thumb_path)
await pillar.fetch_texture_thumbs(node_uuid, 's', directory, await pillar.fetch_texture_thumbs(node_uuid, 's', directory,

View File

@@ -227,7 +227,7 @@ setup(
'wheels': BuildWheels}, 'wheels': BuildWheels},
name='blender_cloud', name='blender_cloud',
description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.', description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.',
version='1.7.0', version='1.7.1',
author='Sybren A. Stüvel', author='Sybren A. Stüvel',
author_email='sybren@stuvel.eu', author_email='sybren@stuvel.eu',
packages=find_packages('.'), packages=find_packages('.'),