Skip to content

[SYNPY-1824] fix syn.sendMessage in async context#1370

Open
linglp wants to merge 2 commits intodevelopfrom
SYNPY-1824
Open

[SYNPY-1824] fix syn.sendMessage in async context#1370
linglp wants to merge 2 commits intodevelopfrom
SYNPY-1824

Conversation

@linglp
Copy link
Copy Markdown
Contributor

@linglp linglp commented Apr 24, 2026

Problem:

See error:

[gw0] darwin -- Python 3.14.3 /Library/Frameworks/Python.framework/Versions/3.14/bin/python

args = ([SyncUploadItem(entity=File(id='syn22033015', name='21087bcf-5907-4d74-be00-2de4bf149fc6_local_test_file.txt', path='...id=None, is_preview=False, external_url=None)), used=[], executed=[], activity_name=None, activity_description=None)],)
kwargs = {}, attempt = 0, destination = '3705170'
output = [File(id='syn22033015', name='21087bcf-5907-4d74-be00-2de4bf149fc6_local_test_file.txt', path='/var/folders/xf/zt5rjbc...8de1/5d993c20-7b6f-4600-b5da-66442fb9935c_local_test_file.txt', preview_id=None, is_preview=False, external_url=None))]

    @functools.wraps(func)
    async def with_retry_and_messaging(*args, **kwargs):
        attempt = 0
        destination = syn.getUserProfile()["ownerId"]
        while attempt <= retries:
            try:
                output = await func(*args, **kwargs)
>               syn.sendMessage(
                    [destination],
                    messageSubject,
                    messageBody="Call to %s completed successfully!"
                    % func.__name__,
                )

/Users/runner/work/synapsePythonClient/synapsePythonClient/synapseutils/monitor.py:138: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/Users/runner/work/synapsePythonClient/synapsePythonClient/synapseclient/client.py:8974: in sendMessage
    return wrap_async_to_sync(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

coroutine = <coroutine object Synapse.sendMessage_async at 0x11488c480>

    def wrap_async_to_sync(coroutine: Coroutine[Any, Any, Any]) -> Any:
        """Wrap an async function to be called in a sync context."""
        loop = None
    
        try:
            loop = asyncio.get_running_loop()
        except RuntimeError:
            pass
    
        if loop and sys.version_info >= (3, 14, 0):
>           raise RuntimeError(
                f"Python 3.14+ detected an active event loop, which prevents automatic async-to-sync conversion.\n"
                f"This is a limitation of asyncio in Python 3.14+.\n\n"
                f"To resolve this, use the async function directly:\n"
                f"  • Instead of: result = your_function()\n"
                f"  • Use: result = await {coroutine.__name__}()\n\n"
                f"For Jupyter/IPython notebooks: You can use 'await' directly in cells.\n"
                f"For other async contexts: Ensure you're in an async function and use 'await'."
E               RuntimeError: Python 3.14+ detected an active event loop, which prevents automatic async-to-sync conversion.
E               This is a limitation of asyncio in Python 3.14+.
E               
E               To resolve this, use the async function directly:
E                 • Instead of: result = your_function()
E                 • Use: result = await sendMessage_async()
E               
E               For Jupyter/IPython notebooks: You can use 'await' directly in cells.
E               For other async contexts: Ensure you're in an async function and use 'await'.

/Users/runner/work/synapsePythonClient/synapsePythonClient/synapseclient/core/async_utils.py:88: RuntimeError

During handling of the above exception, another exception occurred:

self = <test_storable_container_async.TestSyncToSynapse object at 0x114073250>
project_model = Project(id='syn22030587', name='integration_test_projecte5a749cb-d340-49cd-a718-416f062157a9', description=None, etag=...ws=[], datasets=[], datasetcollections=[], materializedviews=[], virtualtables=[], annotations={}, parent_id='syn4489')
tmp_path = PosixPath('/private/var/folders/xf/zt5rjbcn7_33flm02zx_md_00000gn/T/pytest-of-runner/pytest-1/popen-gw0/test_upload_new_files_from_man3')

    async def test_upload_new_files_from_manifest(
        self, project_model: Project, tmp_path: Path
    ) -> None:
        """Files listed in the manifest that don't yet exist in Synapse are created."""
        # GIVEN two local files and a manifest that points them at the project
        file_a = _create_local_test_file("content of file A")
        name_a = file_a.name
        file_b = _create_local_test_file("content of file B")
        name_b = file_b.name
    
        manifest_path = _write_manifest(
            [
                {
                    "path": str(file_a),
                    "parentId": project_model.id,
                    "name": name_a,
                },
                {
                    "path": str(file_b),
                    "parentId": project_model.id,
                    "name": name_b,
                },
            ],
            tmp_path,
        )
    
        # WHEN I sync to Synapse
>       await project_model.sync_to_synapse_async(
            manifest_path=str(manifest_path), synapse_client=self.syn
        )

/Users/runner/work/synapsePythonClient/synapsePythonClient/tests/integration/synapseclient/models/async/test_storable_container_async.py:103: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/Users/runner/work/synapsePythonClient/synapsePythonClient/synapseclient/core/async_utils.py:52: in otel_trace_method_wrapper
    return await func(self, *arg, **kwargs)
/Users/runner/work/synapsePythonClient/synapsePythonClient/synapseclient/models/mixins/storable_container.py:620: in sync_to_synapse_async
    uploaded_files = await upload_fn(items)
/Users/runner/work/synapsePythonClient/synapsePythonClient/synapseutils/monitor.py:149: in with_retry_and_messaging
    syn.sendMessage(
/Users/runner/work/synapsePythonClient/synapsePythonClient/synapseclient/client.py:8974: in sendMessage
    return wrap_async_to_sync(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

coroutine = <coroutine object Synapse.sendMessage_async at 0x11488f010>

    def wrap_async_to_sync(coroutine: Coroutine[Any, Any, Any]) -> Any:
        """Wrap an async function to be called in a sync context."""
        loop = None
    
        try:
            loop = asyncio.get_running_loop()
        except RuntimeError:
            pass
    
        if loop and sys.version_info >= (3, 14, 0):
>           raise RuntimeError(
                f"Python 3.14+ detected an active event loop, which prevents automatic async-to-sync conversion.\n"
                f"This is a limitation of asyncio in Python 3.14+.\n\n"
                f"To resolve this, use the async function directly:\n"
                f"  • Instead of: result = your_function()\n"
                f"  • Use: result = await {coroutine.__name__}()\n\n"
                f"For Jupyter/IPython notebooks: You can use 'await' directly in cells.\n"
                f"For other async contexts: Ensure you're in an async function and use 'await'."
E               RuntimeError: Python 3.14+ detected an active event loop, which prevents automatic async-to-sync conversion.
E               This is a limitation of asyncio in Python 3.14+.
E               
E               To resolve this, use the async function directly:
E                 • Instead of: result = your_function()
E                 • Use: result = await sendMessage_async()
E               
E               For Jupyter/IPython notebooks: You can use 'await' directly in cells.
E               For other async contexts: Ensure you're in an async function and use 'await'.

/Users/runner/work/synapsePythonClient/synapsePythonClient/synapseclient/core/async_utils.py:88: RuntimeError

This is affecting:
Screenshot 2026-04-24 at 5 00 18 PM

@linglp linglp marked this pull request as ready for review April 24, 2026 21:01
@linglp linglp requested a review from a team as a code owner April 24, 2026 21:01
Copilot AI review requested due to automatic review settings April 24, 2026 21:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes notify_me_async in synapseutils.monitor so it can run under Python 3.14+ without triggering asyncio’s “active event loop prevents async-to-sync conversion” error when sending completion/failure notifications.

Changes:

  • Replace syn.sendMessage(...) with await syn.sendMessage_async(...) inside the async retry/notification wrapper (success + exception paths).
Comments suppressed due to low confidence (1)

synapseutils/monitor.py:158

  • Add unit coverage for notify_me_async to prevent regressions (especially for Python 3.14+ event-loop behavior). There are existing tests for the sync notifyMe decorator, but none validating that the async decorator awaits syn.sendMessage_async on success and on retry/failure paths.
    def notify_decorator(func):
        @functools.wraps(func)
        async def with_retry_and_messaging(*args, **kwargs):
            attempt = 0
            destination = syn.getUserProfile()["ownerId"]
            while attempt <= retries:
                try:
                    output = await func(*args, **kwargs)
                    await syn.sendMessage_async(
                        [destination],
                        messageSubject,
                        messageBody="Call to %s completed successfully!"
                        % func.__name__,
                    )
                    return output
                except Exception as e:
                    syn.logger.exception(
                        f"Encountered a temporary Failure during execution.  Will retry {retries - attempt} more times."
                    )
                    await syn.sendMessage_async(
                        [destination],
                        messageSubject,
                        messageBody=(
                            "Encountered a temporary Failure during upload.  "
                            "Will retry %i more times. \n\n Error message was:\n%s\n\n%s"
                            % (retries - attempt, e, traceback.format_exc())
                        ),
                    )
                    attempt += 1

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread synapseutils/monitor.py
Comment thread synapseutils/monitor.py
@andrewelamb
Copy link
Copy Markdown
Contributor

@linglp LGTM! I just had a couple of questions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants