Repository: pulp/pulpcore

Issue:
When profiling pipeline stages, an error is raised
Based on https://docs.pulpproject.org/pulpcore/en/latest/plugins/api-reference/profiling.html?highlight=profiling, I have enabled profiling by setting `PROFILE_STAGES_API = True`. When I executed the sync pipeline, the following error was raised:

```
                "description": "You cannot call this from an async context - use a thread or sync_to_async.",
                "traceback": "  File \"/home/vagrant/devel/pulpcore/pulpcore/tasking/pulpcore_worker.py\", line 442, in _perform_task\n    result = func(*args, **kwargs)\n  File \"/home/vagrant/devel/pulp_ostree/pulp_ostree/app/tasks/synchronizing.py\", line 71, in synchronize\n    return dv.create()\n  File \"/home/vagrant/devel/pulpcore/pulpcore/plugin/stages/declarative_version.py\", line 161, in create\n    loop.run_until_complete(pipeline)\n  File \"/usr/lib64/python3.10/asyncio/base_events.py\", line 641, in run_until_complete\n    return future.result()\n  File \"/home/vagrant/devel/pulpcore/pulpcore/plugin/stages/api.py\", line 215, in create_pipeline\n    out_q = ProfilingQueue.make_and_record_queue(stages[i + 1], i + 1, maxsize)\n  File \"/home/vagrant/devel/pulpcore/pulpcore/plugin/stages/profiler.py\", line 108, in make_and_record_queue\n    create_profile_db_and_connection()\n  File \"/home/vagrant/devel/pulpcore/pulpcore/plugin/stages/profiler.py\", line 142, in create_profile_db_and_connection\n    current_task = Task.current()\n  File \"/home/vagrant/devel/pulpcore/pulpcore/app/models/task.py\", line 230, in current\n    task = Task.objects.get(pk=task_id)\n  File \"/usr/local/lib/pulp/lib64/python3.10/site-packages/django/db/models/manager.py\", line 85, in manager_method\n    return getattr(self.get_queryset(), name)(*args, **kwargs)\n  File \"/usr/local/lib/pulp/lib64/python3.10/site-packages/django/db/models/query.py\", line 431, in get\n    num = len(clone)\n  File \"/usr/local/lib/pulp/lib64/python3.10/site-packages/django/db/models/query.py\", line 262, in __len__\n    self._fetch_all()\n  File \"/usr/local/lib/pulp/lib64/python3.10/site-packages/django/db/models/query.py\", line 1324, in _fetch_all\n    self._result_cache = list(self._iterable_class(self))\n  File \"/usr/local/lib/pulp/lib64/python3.10/site-packages/django/db/models/query.py\", line 51, in __iter__\n    results = compiler.execute_sql(chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size)\n  File \"/usr/local/lib/pulp/lib64/python3.10/site-packages/django/db/models/sql/compiler.py\", line 1173, in execute_sql\n    cursor = self.connection.cursor()\n  File \"/usr/local/lib/pulp/lib64/python3.10/site-packages/django/utils/asyncio.py\", line 31, in inner\n    raise SynchronousOnlyOperation(message)\n"
```


Discussion:
The following changes helped me to resolve the error:
```diff
diff --git a/pulpcore/plugin/stages/api.py b/pulpcore/plugin/stages/api.py
index 0fd2a3ab9..444e087ef 100644
--- a/pulpcore/plugin/stages/api.py
+++ b/pulpcore/plugin/stages/api.py
@@ -212,7 +212,7 @@ async def create_pipeline(stages, maxsize=1):
         history.add(stage)
         if i < len(stages) - 1:
             if settings.PROFILE_STAGES_API:
-                out_q = ProfilingQueue.make_and_record_queue(stages[i + 1], i + 1, maxsize)
+                out_q = await ProfilingQueue.make_and_record_queue(stages[i + 1], i + 1, maxsize)
             else:
                 out_q = asyncio.Queue(maxsize=maxsize)
         else:
diff --git a/pulpcore/plugin/stages/profiler.py b/pulpcore/plugin/stages/profiler.py
index de42ba63f..2f854013d 100644
--- a/pulpcore/plugin/stages/profiler.py
+++ b/pulpcore/plugin/stages/profiler.py
@@ -1,3 +1,5 @@
+from asgiref.sync import sync_to_async
+
 from asyncio import Queue
 import pathlib
 import time
@@ -92,7 +94,7 @@ class ProfilingQueue(Queue):
         return super().put_nowait(item)
 
     @staticmethod
-    def make_and_record_queue(stage, num, maxsize):
+    async def make_and_record_queue(stage, num, maxsize):
         """
         Create a ProfileQueue that is associated with the stage it feeds and record it in sqlite3.
 
@@ -105,7 +107,7 @@ class ProfilingQueue(Queue):
             ProfilingQueue: The configured ProfilingQueue that was also recorded in the db.
         """
         if CONN is None:
-            create_profile_db_and_connection()
+            await create_profile_db_and_connection()
         stage_id = uuid.uuid4()
         stage_name = ".".join([stage.__class__.__module__, stage.__class__.__name__])
         sql = "INSERT INTO stages (uuid, name, num) VALUES ('{uuid}','{stage}','{num}')"
@@ -116,7 +118,7 @@ class ProfilingQueue(Queue):
         return in_q
 
 
-def create_profile_db_and_connection():
+async def create_profile_db_and_connection():
     """
     Create a profile db from this tasks UUID and a sqlite3 connection to that databases.
 
@@ -139,7 +141,7 @@ def create_profile_db_and_connection():
     """
     debug_data_dir = "/var/lib/pulp/debug/"
     pathlib.Path(debug_data_dir).mkdir(parents=True, exist_ok=True)
-    current_task = Task.current()
+    current_task = await sync_to_async(Task.current)()
     if current_task:
         db_path = debug_data_dir + str(current_task.pk)
     else:
```

Resolving patch:
diff --git a/pulpcore/plugin/stages/api.py b/pulpcore/plugin/stages/api.py
--- a/pulpcore/plugin/stages/api.py
+++ b/pulpcore/plugin/stages/api.py
@@ -212,7 +212,7 @@ async def run(self):
         history.add(stage)
         if i < len(stages) - 1:
             if settings.PROFILE_STAGES_API:
-                out_q = ProfilingQueue.make_and_record_queue(stages[i + 1], i + 1, maxsize)
+                out_q = await ProfilingQueue.make_and_record_queue(stages[i + 1], i + 1, maxsize)
             else:
                 out_q = asyncio.Queue(maxsize=maxsize)
         else:
diff --git a/pulpcore/plugin/stages/profiler.py b/pulpcore/plugin/stages/profiler.py
--- a/pulpcore/plugin/stages/profiler.py
+++ b/pulpcore/plugin/stages/profiler.py
@@ -1,3 +1,4 @@
+from asgiref.sync import sync_to_async
 from asyncio import Queue
 import pathlib
 import time
@@ -66,15 +67,22 @@ def put_nowait(self, item):
             except KeyError:
                 pass
             else:
-                service_time = now - item.extra_data["last_get_time"]
-                sql = (
-                    "INSERT INTO traffic (uuid, waiting_time, service_time) VALUES ("
-                    "'{uuid}','{waiting_time}','{service_time}')"
-                )
-                formatted_sql = sql.format(
-                    uuid=self.stage_uuid, waiting_time=last_waiting_time, service_time=service_time
-                )
-                CONN.cursor().execute(formatted_sql)
+                last_get_time = item.extra_data["last_get_time"]
+                # the extra_data dictionary might be initialized from within the plugin as
+                # 'defaultdict' returning empty lists by default; with this if statement,
+                # we prevent errors like "(unsupported operand type(s) for -: 'float' and 'list')"
+                if last_get_time or last_get_time == 0:
+                    service_time = now - last_get_time
+                    sql = (
+                        "INSERT INTO traffic (uuid, waiting_time, service_time) VALUES ("
+                        "'{uuid}','{waiting_time}','{service_time}')"
+                    )
+                    formatted_sql = sql.format(
+                        uuid=self.stage_uuid,
+                        waiting_time=last_waiting_time,
+                        service_time=service_time,
+                    )
+                    CONN.cursor().execute(formatted_sql)
 
             interarrival_time = now - self.last_arrival_time
             sql = (
@@ -92,7 +100,7 @@ def put_nowait(self, item):
         return super().put_nowait(item)
 
     @staticmethod
-    def make_and_record_queue(stage, num, maxsize):
+    async def make_and_record_queue(stage, num, maxsize):
         """
         Create a ProfileQueue that is associated with the stage it feeds and record it in sqlite3.
 
@@ -105,7 +113,7 @@ def make_and_record_queue(stage, num, maxsize):
             ProfilingQueue: The configured ProfilingQueue that was also recorded in the db.
         """
         if CONN is None:
-            create_profile_db_and_connection()
+            await create_profile_db_and_connection()
         stage_id = uuid.uuid4()
         stage_name = ".".join([stage.__class__.__module__, stage.__class__.__name__])
         sql = "INSERT INTO stages (uuid, name, num) VALUES ('{uuid}','{stage}','{num}')"
@@ -116,7 +124,7 @@ def make_and_record_queue(stage, num, maxsize):
         return in_q
 
 
-def create_profile_db_and_connection():
+async def create_profile_db_and_connection():
     """
     Create a profile db from this tasks UUID and a sqlite3 connection to that databases.
 
@@ -139,7 +147,7 @@ def create_profile_db_and_connection():
     """
     debug_data_dir = "/var/lib/pulp/debug/"
     pathlib.Path(debug_data_dir).mkdir(parents=True, exist_ok=True)
-    current_task = Task.current()
+    current_task = await sync_to_async(Task.current)()
     if current_task:
         db_path = debug_data_dir + str(current_task.pk)
     else:


Test patch: