Skip to content

Commit

Permalink
🦺(backend) raise explicit error when using LRSHTTP in async events loop
Browse files Browse the repository at this point in the history
Running the (synchronous) LRSHTTP methods in an events loop currently raises
an error (because asyncio does not allow nested events loops). This solution
raises an explicit error inciting the user to use AsyncLRSHTTP when this
is the case.
  • Loading branch information
Leobouloc committed Aug 10, 2023
1 parent adf943c commit 4679fb5
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ have an authority field matching that of the user
- API: Forwarding PUT now uses PUT (instead of POST)
- Models: The xAPI `context.contextActivities.category` field is now mandatory
in the video and virtual classroom profiles. [BC]
- Backends: `LRSHTTP` methods must not be used in `asyncio` events loop (BC)

## [3.9.0] - 2023-07-21

Expand Down
25 changes: 25 additions & 0 deletions src/ralph/backends/http/lrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,49 @@
from ralph.backends.http.async_lrs import AsyncLRSHTTP


def _ensure_running_loop_uniqueness(func):
"""Raise an error when methods are used in a running asyncio events loop."""

def wrap(*args, **kwargs):
"""Wrapper for decorator function."""
try:
loop = asyncio.get_running_loop()
except RuntimeError:
return func(*args, **kwargs)
if loop.is_running():
raise RuntimeError(
f"This event loop is already running. You must use "
f"`AsyncLRSHTTP.{func.__name__}` (instead of `LRSHTTP."
f"{func.__name__}`), or run this code outside the current"
" event loop."
)
return func(*args, **kwargs)

return wrap


class LRSHTTP(AsyncLRSHTTP):
"""LRS HTTP backend."""

# pylint: disable=invalid-overridden-method

name = "lrs"

@_ensure_running_loop_uniqueness
def status(self, *args, **kwargs):
"""HTTP backend check for server status."""
return asyncio.get_event_loop().run_until_complete(
super().status(*args, **kwargs)
)

@_ensure_running_loop_uniqueness
def list(self, *args, **kwargs):
"""Raise error for unsuported `list` method."""
return asyncio.get_event_loop().run_until_complete(
super().list(*args, **kwargs)
)

@_ensure_running_loop_uniqueness
def read(self, *args, **kwargs):
"""Get statements from LRS `target` endpoint.
Expand All @@ -36,6 +60,7 @@ def read(self, *args, **kwargs):
except StopAsyncIteration:
pass

@_ensure_running_loop_uniqueness
def write(self, *args, **kwargs):
"""Write `data` records to the `target` endpoint and return their count.
Expand Down
45 changes: 44 additions & 1 deletion tests/backends/http/test_lrs.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Tests for Ralph Async LRS HTTP backend."""

import asyncio
import re
from unittest.mock import AsyncMock

import pytest
Expand All @@ -13,6 +14,48 @@
lrs_settings = settings.BACKENDS.HTTP.LRS


@pytest.mark.anyio
@pytest.mark.parametrize("method", ["status", "list", "write", "read"])
async def test_backend_http_lrs_in_async_setting(monkeypatch, method):
"""Test that backend returns the proper error when run in async function."""

# Define mock responses
if method == "read":
read_mock_response = [{"hello": "world"}, {"save": "pandas"}]

async def response_mock(*args, **kwargs):
"""Mock a read function."""
# pylint: disable=invalid-name
# pylint: disable=unused-argument
for statement in read_mock_response:
yield statement

else:
response_mock = AsyncMock(return_value=HTTPBackendStatus.OK)
monkeypatch.setattr(AsyncLRSHTTP, method, response_mock)

async def async_function():
"""Encapsulate the synchronous method in an asynchronous function."""
lrs = LRSHTTP()
if method == "read":
list(getattr(lrs, method)())
else:
getattr(lrs, method)()

# Check that the proper error is raised
with pytest.raises(
RuntimeError,
match=re.escape(
(
f"This event loop is already running. You must use "
f"`AsyncLRSHTTP.{method}` (instead of `LRSHTTP.{method}`)"
", or run this code outside the current event loop."
)
),
):
await async_function()


def test_backend_http_lrs_default_properties():
"""Test default LRS properties."""
lrs = LRSHTTP()
Expand Down Expand Up @@ -54,7 +97,7 @@ def test_backends_http_lrs_inheritence(monkeypatch):
read_chunk_size = 11

async def read_mock(*args, **kwargs):
"""Mock a read read function."""
"""Mock a read function."""
# pylint: disable=invalid-name
# pylint: disable=unused-argument

Expand Down

0 comments on commit 4679fb5

Please sign in to comment.