@@ -34,7 +34,6 @@ def __init__(
34
34
node_concurrency : int ,
35
35
) -> None :
36
36
self ._tools_manager = tools_manager
37
- # TODO: Add concurrency control in execution
38
37
self ._node_concurrency = node_concurrency
39
38
self ._task_start_time = {}
40
39
self ._task_last_log_time = {}
@@ -55,7 +54,9 @@ async def execute(
55
54
"Current thread is not main thread, skip signal handler registration in AsyncNodesScheduler."
56
55
)
57
56
57
+ # Semaphore should be created in the loop, otherwise it will not work.
58
58
loop = asyncio .get_running_loop ()
59
+ self ._semaphore = asyncio .Semaphore (self ._node_concurrency , loop = loop )
59
60
monitor = threading .Thread (
60
61
target = monitor_long_running_coroutine ,
61
62
args = (loop , self ._task_start_time , self ._task_last_log_time , self ._dag_manager_completed_event ),
@@ -129,6 +130,10 @@ def _execute_nodes(
129
130
self ._create_node_task (node , dag_manager , context , executor ): node for node in dag_manager .pop_ready_nodes ()
130
131
}
131
132
133
+ async def run_task_with_semaphore (self , coroutine ):
134
+ async with self ._semaphore :
135
+ return await coroutine
136
+
132
137
def _create_node_task (
133
138
self ,
134
139
node : Node ,
@@ -139,12 +144,17 @@ def _create_node_task(
139
144
f = self ._tools_manager .get_tool (node .name )
140
145
kwargs = dag_manager .get_node_valid_inputs (node , f )
141
146
if inspect .iscoroutinefunction (f ):
147
+ # For async task, it will not be executed before calling create_task.
142
148
task = context .invoke_tool_async (node , f , kwargs )
143
149
else :
150
+ # For sync task, convert it to async task and run it in executor thread.
151
+ # Even though the task is put to the thread pool, thread.start will only be triggered after create_task.
144
152
task = self ._sync_function_to_async_task (executor , context , node , f , kwargs )
145
153
# Set the name of the task to the node name for debugging purpose
146
154
# It does not need to be unique by design.
147
- return asyncio .create_task (task , name = node .name )
155
+ # Wrap the coroutine in a task with asyncio.create_task to schedule it for event loop execution
156
+ # The task is created and added to the event loop, but the exact execution depends on loop's scheduling
157
+ return asyncio .create_task (self .run_task_with_semaphore (task ), name = node .name )
148
158
149
159
@staticmethod
150
160
async def _sync_function_to_async_task (
@@ -154,6 +164,7 @@ async def _sync_function_to_async_task(
154
164
f ,
155
165
kwargs ,
156
166
):
167
+ # The task will not be executed before calling create_task.
157
168
return await asyncio .get_running_loop ().run_in_executor (executor , context .invoke_tool , node , f , kwargs )
158
169
159
170
0 commit comments