11import typing as t
22from concurrent .futures import Executor , Future , ThreadPoolExecutor
33from threading import Lock
4+ from contextvars import Context
45
56from sqlmesh .core .snapshot import SnapshotId , SnapshotInfoLike
67from sqlmesh .utils .dag import DAG
@@ -30,6 +31,9 @@ class ConcurrentDAGExecutor(t.Generic[H]):
3031 raise_on_error: If set to True raises an exception on a first encountered error,
3132 otherwises returns a tuple which contains a list of failed nodes and a list of
3233 skipped nodes.
34+ context_factory: Optional factory called at each run admission. Each node runs
35+ in a separate copy of that context, including serial execution. By default
36+ no context is captured or copied.
3337 """
3438
3539 def __init__ (
@@ -38,11 +42,14 @@ def __init__(
3842 fn : t .Callable [[H ], None ],
3943 tasks_num : int ,
4044 raise_on_error : bool ,
45+ context_factory : t .Optional [t .Callable [[], Context ]] = None ,
4146 ):
4247 self .dag = dag
4348 self .fn = fn
4449 self .tasks_num = tasks_num
4550 self .raise_on_error = raise_on_error
51+ self .context_factory = context_factory
52+ self ._context : t .Optional [Context ] = None
4653
4754 self ._init_state ()
4855
@@ -55,14 +62,18 @@ def run(self) -> t.Tuple[t.List[NodeExecutionFailedError[H]], t.List[H]]:
5562 Returns:
5663 A pair which contains a list of node errors and a list of skipped nodes.
5764 """
65+ self ._context = self .context_factory () if self .context_factory else None
5866 if self ._finished_future .done ():
5967 self ._init_state ()
6068
61- with ThreadPoolExecutor (max_workers = self .tasks_num ) as pool :
62- with self ._unprocessed_nodes_lock :
63- self ._submit_next_nodes (pool )
64- self ._finished_future .result ()
65- return self ._node_errors , self ._skipped_nodes
69+ try :
70+ with ThreadPoolExecutor (max_workers = self .tasks_num ) as pool :
71+ with self ._unprocessed_nodes_lock :
72+ self ._submit_next_nodes (pool )
73+ self ._finished_future .result ()
74+ return self ._node_errors , self ._skipped_nodes
75+ finally :
76+ self ._context = None
6677
6778 def _process_node (self , node : H , executor : Executor ) -> None :
6879 with self ._unprocessed_nodes_lock :
@@ -112,7 +123,12 @@ def _submit_next_nodes(self, executor: Executor, processed_node: t.Optional[H] =
112123
113124 for submitted_node in submitted_nodes :
114125 self ._unprocessed_nodes .pop (submitted_node )
115- executor .submit (self ._process_node , submitted_node , executor )
126+ if self ._context is None :
127+ executor .submit (self ._process_node , submitted_node , executor )
128+ else :
129+ executor .submit (
130+ self ._context .copy ().run , self ._process_node , submitted_node , executor
131+ )
116132
117133 def _skip_next_nodes (self , parent : H ) -> None :
118134 if self ._finished_future .done ():
@@ -156,6 +172,7 @@ def concurrent_apply_to_snapshots(
156172 tasks_num : int ,
157173 reverse_order : bool = False ,
158174 raise_on_error : bool = True ,
175+ context_factory : t .Optional [t .Callable [[], Context ]] = None ,
159176) -> t .Tuple [t .List [NodeExecutionFailedError [SnapshotId ]], t .List [SnapshotId ]]:
160177 """Applies a function to the given collection of snapshots concurrently while
161178 preserving the topological order between snapshots.
@@ -168,6 +185,9 @@ def concurrent_apply_to_snapshots(
168185 raise_on_error: If set to True raises an exception on a first encountered error,
169186 otherwises returns a tuple which contains a list of failed nodes and a list of
170187 skipped nodes.
188+ context_factory: Optional factory called at each run admission. Each node runs
189+ in a separate copy of that context, including serial execution. By default
190+ no context is captured or copied.
171191
172192 Raises:
173193 NodeExecutionFailedError if `raise_on_error` is set to True and execution fails for any snapshot.
@@ -189,6 +209,7 @@ def concurrent_apply_to_snapshots(
189209 lambda s_id : fn (snapshots_by_id [s_id ]),
190210 tasks_num ,
191211 raise_on_error = raise_on_error ,
212+ context_factory = context_factory ,
192213 )
193214
194215
@@ -197,6 +218,7 @@ def concurrent_apply_to_dag(
197218 fn : t .Callable [[H ], None ],
198219 tasks_num : int ,
199220 raise_on_error : bool = True ,
221+ context_factory : t .Optional [t .Callable [[], Context ]] = None ,
200222) -> t .Tuple [t .List [NodeExecutionFailedError [H ]], t .List [H ]]:
201223 """Applies a function to the given DAG concurrently while preserving the topological
202224 order between snapshots.
@@ -208,6 +230,9 @@ def concurrent_apply_to_dag(
208230 raise_on_error: If set to True raises an exception on a first encountered error,
209231 otherwises returns a tuple which contains a list of failed nodes and a list of
210232 skipped nodes.
233+ context_factory: Optional factory called at each run admission. Each node runs
234+ in a separate copy of that context, including serial execution. By default
235+ no context is captured or copied.
211236
212237 Raises:
213238 NodeExecutionFailedError if `raise_on_error` is set to True and execution fails for any snapshot.
@@ -219,21 +244,24 @@ def concurrent_apply_to_dag(
219244 raise ConfigError (f"Invalid number of concurrent tasks { tasks_num } " )
220245
221246 if tasks_num == 1 :
222- return sequential_apply_to_dag (dag , fn , raise_on_error )
247+ return sequential_apply_to_dag (dag , fn , raise_on_error , context_factory )
223248
224249 return ConcurrentDAGExecutor (
225250 dag ,
226251 fn ,
227252 tasks_num ,
228253 raise_on_error ,
254+ context_factory ,
229255 ).run ()
230256
231257
232258def sequential_apply_to_dag (
233259 dag : DAG [H ],
234260 fn : t .Callable [[H ], None ],
235261 raise_on_error : bool = True ,
262+ context_factory : t .Optional [t .Callable [[], Context ]] = None ,
236263) -> t .Tuple [t .List [NodeExecutionFailedError [H ]], t .List [H ]]:
264+ context = context_factory () if context_factory else None
237265 dependencies = dag .graph
238266
239267 node_errors : t .List [NodeExecutionFailedError [H ]] = []
@@ -248,7 +276,10 @@ def sequential_apply_to_dag(
248276 continue
249277
250278 try :
251- fn (node )
279+ if context is None :
280+ fn (node )
281+ else :
282+ context .copy ().run (fn , node )
252283 except Exception as ex :
253284 error = NodeExecutionFailedError (node )
254285 error .__cause__ = ex
@@ -266,19 +297,27 @@ def concurrent_apply_to_values(
266297 values : t .Sequence [A ],
267298 fn : t .Callable [[A ], R ],
268299 tasks_num : int ,
300+ context_factory : t .Optional [t .Callable [[], Context ]] = None ,
269301) -> t .List [R ]:
270302 """Applies a function to the given collection of values concurrently.
271303
272304 Args:
273305 values: Target values.
274306 fn: The function that will be applied concurrently to each value.
275307 tasks_num: The number of concurrent tasks.
308+ context_factory: Optional factory called once when values are admitted. Each value runs
309+ in a separate copy of that context, including serial execution. By default no
310+ context is captured or copied.
276311
277312 Returns:
278313 A list of results.
279314 """
315+ context = context_factory () if context_factory else None
316+
280317 if tasks_num == 1 :
281- return [fn (value ) for value in values ]
318+ if context is None :
319+ return [fn (value ) for value in values ]
320+ return [context .copy ().run (fn , value ) for value in values ]
282321
283322 futures : t .List [Future ] = [Future () for _ in values ]
284323
@@ -292,6 +331,9 @@ def _process_value(value: A, index: int) -> None:
292331
293332 with ThreadPoolExecutor (max_workers = tasks_num ) as pool :
294333 for index , value in enumerate (values ):
295- pool .submit (_process_value , value , index )
334+ if context is None :
335+ pool .submit (_process_value , value , index )
336+ else :
337+ pool .submit (context .copy ().run , _process_value , value , index )
296338
297339 return [f .result () for f in futures ]
0 commit comments