Repository: deepset-ai/haystack

Issue:
`AsyncPipeline.run` doesn't work correctly when used in async environments like a async API route handler
We already know that `AsyncPipeline.run` doesn't work correctly in a Jupyter Notebook environment (see https://github.com/deepset-ai/haystack/issues/8951), but it's also causing error when used in other async envs like a API route handler.

The main issue is that `AsyncPipeline.run` uses `asyncio.run` under the hood, which creates an event loop, runs the pipeline in it and then _closes_ it. Usually, the close of the event loop created by asyncio.run creates issues on any created resource which expect it to be still open. 

So, on the first run, usually everything works, but on the second one any reference to the previous event loop can cause a `RunTime` errors or other errors.

It can be replicated e.g. with this simple FastAPI app, calling `GET /` two times:
```python
import uvicorn
from haystack import AsyncPipeline, PredefinedPipeline
from fastapi import FastAPI

app = FastAPI()
async_pipeline = AsyncPipeline.from_template(PredefinedPipeline.CHAT_WITH_WEBSITE)


@app.get("/")
def root():
    return async_pipeline.run(
        {
            "fetcher": {"urls": ["https://haystack.deepset.ai:"]},
            "prompt": {"query": "what is Haystack?"},
        }
    )


if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)
```

Since we already provide `run_async` method which works perfectly in those cases, a possible solution can be simply to raise a custom `RunTimeError` pointing the user to call the correct method. So could be something like the following:
```python
# async_pipeline.py

def run(self, ...) -> Dict[str, Any]:

    try:
        # Check if we're already in an event loop
        asyncio.get_running_loop()

        # If we get here, we're in an event loop
        raise RuntimeError(
            "Cannot call run() from within an async context. "
            "Use 'await pipeline.run_async(data)' instead."
        )
    except RuntimeError:
        # No running loop, safe to use asyncio.run()
        return asyncio.run(
            self.run_async(...)
        )
```


Discussion:

Patch:
diff --git a/haystack/core/pipeline/async_pipeline.py b/haystack/core/pipeline/async_pipeline.py
index 5132cc2212..dc5553b6d2 100644
--- a/haystack/core/pipeline/async_pipeline.py
+++ b/haystack/core/pipeline/async_pipeline.py
@@ -676,7 +676,20 @@ def run(
             Or if a Component fails or returns output in an unsupported type.
         :raises PipelineMaxComponentRuns:
             If a Component reaches the maximum number of times it can be run in this Pipeline.
+        :raises RuntimeError:
+            If called from within an async context. Use `run_async` instead.
         """
-        return asyncio.run(
-            self.run_async(data=data, include_outputs_from=include_outputs_from, concurrency_limit=concurrency_limit)
-        )
+        try:
+            asyncio.get_running_loop()
+        except RuntimeError:
+            # No running loop: safe to use asyncio.run()
+            return asyncio.run(
+                self.run_async(
+                    data=data, include_outputs_from=include_outputs_from, concurrency_limit=concurrency_limit
+                )
+            )
+        else:
+            # Running loop present: do not create the coroutine and do not call asyncio.run()
+            raise RuntimeError(
+                "Cannot call run() from within an async context. Use 'await pipeline.run_async(...)' instead."
+            )
diff --git a/releasenotes/notes/fix-asyncpipeline-run-error-e82ede5c4f37f485.yaml b/releasenotes/notes/fix-asyncpipeline-run-error-e82ede5c4f37f485.yaml
new file mode 100644
index 0000000000..86f24b418f
--- /dev/null
+++ b/releasenotes/notes/fix-asyncpipeline-run-error-e82ede5c4f37f485.yaml
@@ -0,0 +1,4 @@
+---
+fixes:
+  - |
+    Raise a `RuntimeError` when `AsyncPipeline.run` is called from within an async context, indicating that `run_async` should be used instead.


Test patch:
diff --git a/test/core/pipeline/test_async_pipeline.py b/test/core/pipeline/test_async_pipeline.py
index 39afa3d6c9..a7b1612566 100644
--- a/test/core/pipeline/test_async_pipeline.py
+++ b/test/core/pipeline/test_async_pipeline.py
@@ -4,6 +4,8 @@
 
 import asyncio
 
+import pytest
+
 from haystack import AsyncPipeline
 
 
@@ -22,3 +24,22 @@ async def run_all():
     component_spans = [sp for sp in spying_tracer.spans if sp.operation_name == "haystack.component.run_async"]
     for span in component_spans:
         assert span.tags["haystack.component.visits"] == 1
+
+
+def test_run_in_sync_context(waiting_component):
+    pp = AsyncPipeline()
+    pp.add_component("wait", waiting_component())
+
+    result = pp.run({"wait_for": 0.001})
+
+    assert result == {"wait": {"waited_for": 0.001}}
+
+
+def test_run_in_async_context_raises_runtime_error():
+    pp = AsyncPipeline()
+
+    async def call_run():
+        pp.run({})
+
+    with pytest.raises(RuntimeError, match="Cannot call run\\(\\) from within an async context"):
+        asyncio.run(call_run())