prefect-client 3.0.0rc2__py3-none-any.whl → 3.0.0rc3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- prefect/_internal/compatibility/migration.py +124 -0
- prefect/_internal/concurrency/__init__.py +2 -2
- prefect/_internal/concurrency/primitives.py +1 -0
- prefect/_internal/pydantic/annotations/pendulum.py +2 -2
- prefect/_internal/pytz.py +1 -1
- prefect/blocks/core.py +1 -1
- prefect/client/orchestration.py +96 -22
- prefect/client/schemas/actions.py +1 -1
- prefect/client/schemas/filters.py +6 -0
- prefect/client/schemas/objects.py +10 -3
- prefect/client/subscriptions.py +3 -2
- prefect/context.py +1 -27
- prefect/deployments/__init__.py +3 -0
- prefect/deployments/base.py +4 -2
- prefect/deployments/deployments.py +3 -0
- prefect/deployments/steps/pull.py +1 -0
- prefect/deployments/steps/utility.py +2 -1
- prefect/engine.py +3 -0
- prefect/events/cli/automations.py +1 -1
- prefect/events/clients.py +7 -1
- prefect/exceptions.py +9 -0
- prefect/filesystems.py +22 -11
- prefect/flow_engine.py +116 -154
- prefect/flows.py +83 -34
- prefect/infrastructure/provisioners/container_instance.py +1 -0
- prefect/infrastructure/provisioners/ecs.py +2 -2
- prefect/input/__init__.py +4 -0
- prefect/logging/formatters.py +2 -2
- prefect/logging/handlers.py +2 -2
- prefect/logging/loggers.py +1 -1
- prefect/plugins.py +1 -0
- prefect/records/cache_policies.py +3 -3
- prefect/records/result_store.py +10 -3
- prefect/results.py +27 -55
- prefect/runner/runner.py +1 -1
- prefect/runner/server.py +1 -1
- prefect/runtime/__init__.py +1 -0
- prefect/runtime/deployment.py +1 -0
- prefect/runtime/flow_run.py +1 -0
- prefect/runtime/task_run.py +1 -0
- prefect/settings.py +15 -2
- prefect/states.py +15 -4
- prefect/task_engine.py +190 -33
- prefect/task_runners.py +9 -3
- prefect/task_runs.py +3 -3
- prefect/task_worker.py +29 -9
- prefect/tasks.py +133 -57
- prefect/transactions.py +87 -15
- prefect/types/__init__.py +1 -1
- prefect/utilities/asyncutils.py +3 -3
- prefect/utilities/callables.py +16 -4
- prefect/utilities/dockerutils.py +5 -3
- prefect/utilities/engine.py +11 -0
- prefect/utilities/filesystem.py +4 -5
- prefect/utilities/importtools.py +29 -0
- prefect/utilities/services.py +2 -2
- prefect/utilities/urls.py +195 -0
- prefect/utilities/visualization.py +1 -0
- prefect/variables.py +4 -0
- prefect/workers/base.py +35 -0
- {prefect_client-3.0.0rc2.dist-info → prefect_client-3.0.0rc3.dist-info}/METADATA +2 -2
- {prefect_client-3.0.0rc2.dist-info → prefect_client-3.0.0rc3.dist-info}/RECORD +65 -62
- {prefect_client-3.0.0rc2.dist-info → prefect_client-3.0.0rc3.dist-info}/LICENSE +0 -0
- {prefect_client-3.0.0rc2.dist-info → prefect_client-3.0.0rc3.dist-info}/WHEEL +0 -0
- {prefect_client-3.0.0rc2.dist-info → prefect_client-3.0.0rc3.dist-info}/top_level.txt +0 -0
prefect/settings.py
CHANGED
@@ -42,6 +42,7 @@ dependent on the value of other settings or perform other dynamic effects.
|
|
42
42
|
|
43
43
|
import logging
|
44
44
|
import os
|
45
|
+
import socket
|
45
46
|
import string
|
46
47
|
import warnings
|
47
48
|
from contextlib import contextmanager
|
@@ -84,6 +85,7 @@ from prefect._internal.schemas.validators import validate_settings
|
|
84
85
|
from prefect.exceptions import MissingProfileError
|
85
86
|
from prefect.utilities.names import OBFUSCATED_PREFIX, obfuscate
|
86
87
|
from prefect.utilities.pydantic import add_cloudpickle_reduction
|
88
|
+
from prefect.utilities.slugify import slugify
|
87
89
|
|
88
90
|
T = TypeVar("T")
|
89
91
|
|
@@ -417,6 +419,18 @@ def warn_on_misconfigured_api_url(values):
|
|
417
419
|
return values
|
418
420
|
|
419
421
|
|
422
|
+
def default_result_storage_block_name(
|
423
|
+
settings: Optional["Settings"] = None, value: Optional[str] = None
|
424
|
+
):
|
425
|
+
"""
|
426
|
+
`value_callback` for `PREFECT_DEFAULT_RESULT_STORAGE_BLOCK_NAME` that sets the default
|
427
|
+
value to the hostname of the machine.
|
428
|
+
"""
|
429
|
+
if value is None:
|
430
|
+
return f"local-file-system/{slugify(socket.gethostname())}-storage"
|
431
|
+
return value
|
432
|
+
|
433
|
+
|
420
434
|
def default_database_connection_url(settings, value):
|
421
435
|
templater = template_with_settings(PREFECT_HOME, PREFECT_API_DATABASE_PASSWORD)
|
422
436
|
|
@@ -1575,8 +1589,7 @@ PREFECT_EXPERIMENTAL_ENABLE_SCHEDULE_CONCURRENCY = Setting(bool, default=False)
|
|
1575
1589
|
# Defaults -----------------------------------------------------------------------------
|
1576
1590
|
|
1577
1591
|
PREFECT_DEFAULT_RESULT_STORAGE_BLOCK = Setting(
|
1578
|
-
Optional[str],
|
1579
|
-
default=None,
|
1592
|
+
Optional[str], default=None, value_callback=default_result_storage_block_name
|
1580
1593
|
)
|
1581
1594
|
"""The `block-type/block-document` slug of a block to use as the default result storage."""
|
1582
1595
|
|
prefect/states.py
CHANGED
@@ -205,7 +205,10 @@ async def exception_to_failed_state(
|
|
205
205
|
|
206
206
|
|
207
207
|
async def return_value_to_state(
|
208
|
-
retval: R,
|
208
|
+
retval: R,
|
209
|
+
result_factory: ResultFactory,
|
210
|
+
key: Optional[str] = None,
|
211
|
+
expiration: Optional[datetime.datetime] = None,
|
209
212
|
) -> State[R]:
|
210
213
|
"""
|
211
214
|
Given a return value from a user's function, create a `State` the run should
|
@@ -238,7 +241,9 @@ async def return_value_to_state(
|
|
238
241
|
# Unless the user has already constructed a result explicitly, use the factory
|
239
242
|
# to update the data to the correct type
|
240
243
|
if not isinstance(state.data, BaseResult):
|
241
|
-
state.data = await result_factory.create_result(
|
244
|
+
state.data = await result_factory.create_result(
|
245
|
+
state.data, key=key, expiration=expiration
|
246
|
+
)
|
242
247
|
|
243
248
|
return state
|
244
249
|
|
@@ -278,7 +283,9 @@ async def return_value_to_state(
|
|
278
283
|
return State(
|
279
284
|
type=new_state_type,
|
280
285
|
message=message,
|
281
|
-
data=await result_factory.create_result(
|
286
|
+
data=await result_factory.create_result(
|
287
|
+
retval, key=key, expiration=expiration
|
288
|
+
),
|
282
289
|
)
|
283
290
|
|
284
291
|
# Generators aren't portable, implicitly convert them to a list.
|
@@ -291,7 +298,11 @@ async def return_value_to_state(
|
|
291
298
|
if isinstance(data, BaseResult):
|
292
299
|
return Completed(data=data)
|
293
300
|
else:
|
294
|
-
return Completed(
|
301
|
+
return Completed(
|
302
|
+
data=await result_factory.create_result(
|
303
|
+
data, key=key, expiration=expiration
|
304
|
+
)
|
305
|
+
)
|
295
306
|
|
296
307
|
|
297
308
|
@sync_compatible
|
prefect/task_engine.py
CHANGED
@@ -3,8 +3,10 @@ import logging
|
|
3
3
|
import time
|
4
4
|
from contextlib import ExitStack, contextmanager
|
5
5
|
from dataclasses import dataclass, field
|
6
|
+
from textwrap import dedent
|
6
7
|
from typing import (
|
7
8
|
Any,
|
9
|
+
AsyncGenerator,
|
8
10
|
Callable,
|
9
11
|
Coroutine,
|
10
12
|
Dict,
|
@@ -25,7 +27,6 @@ import pendulum
|
|
25
27
|
from typing_extensions import ParamSpec
|
26
28
|
|
27
29
|
from prefect import Task
|
28
|
-
from prefect._internal.concurrency.api import create_call, from_sync
|
29
30
|
from prefect.client.orchestration import SyncPrefectClient
|
30
31
|
from prefect.client.schemas import TaskRun
|
31
32
|
from prefect.client.schemas.objects import State, TaskRunInput
|
@@ -43,7 +44,6 @@ from prefect.exceptions import (
|
|
43
44
|
UpstreamTaskError,
|
44
45
|
)
|
45
46
|
from prefect.futures import PrefectFuture
|
46
|
-
from prefect.logging.handlers import APILogHandler
|
47
47
|
from prefect.logging.loggers import get_logger, patch_print, task_run_logger
|
48
48
|
from prefect.records.result_store import ResultFactoryStore
|
49
49
|
from prefect.results import ResultFactory, _format_user_supplied_storage_key
|
@@ -64,11 +64,12 @@ from prefect.states import (
|
|
64
64
|
)
|
65
65
|
from prefect.transactions import Transaction, transaction
|
66
66
|
from prefect.utilities.asyncutils import run_coro_as_sync
|
67
|
-
from prefect.utilities.callables import call_with_parameters
|
67
|
+
from prefect.utilities.callables import call_with_parameters, parameters_to_args_kwargs
|
68
68
|
from prefect.utilities.collections import visit_collection
|
69
69
|
from prefect.utilities.engine import (
|
70
70
|
_get_hook_name,
|
71
71
|
emit_task_run_state_change_event,
|
72
|
+
link_state_to_result,
|
72
73
|
propose_state_sync,
|
73
74
|
resolve_to_final_result,
|
74
75
|
)
|
@@ -219,7 +220,7 @@ class TaskRunEngine(Generic[P, R]):
|
|
219
220
|
return_data=False,
|
220
221
|
max_depth=-1,
|
221
222
|
remove_annotations=True,
|
222
|
-
context={},
|
223
|
+
context={"current_task_run": self.task_run, "current_task": self.task},
|
223
224
|
)
|
224
225
|
|
225
226
|
def begin_run(self):
|
@@ -298,9 +299,17 @@ class TaskRunEngine(Generic[P, R]):
|
|
298
299
|
if result_factory is None:
|
299
300
|
raise ValueError("Result factory is not set")
|
300
301
|
|
302
|
+
if self.task.cache_expiration is not None:
|
303
|
+
expiration = pendulum.now("utc") + self.task.cache_expiration
|
304
|
+
else:
|
305
|
+
expiration = None
|
306
|
+
|
301
307
|
terminal_state = run_coro_as_sync(
|
302
308
|
return_value_to_state(
|
303
|
-
result,
|
309
|
+
result,
|
310
|
+
result_factory=result_factory,
|
311
|
+
key=transaction.key,
|
312
|
+
expiration=expiration,
|
304
313
|
)
|
305
314
|
)
|
306
315
|
transaction.stage(
|
@@ -333,10 +342,24 @@ class TaskRunEngine(Generic[P, R]):
|
|
333
342
|
scheduled_time=pendulum.now("utc").add(seconds=delay)
|
334
343
|
)
|
335
344
|
else:
|
345
|
+
delay = None
|
336
346
|
new_state = Retrying()
|
347
|
+
|
348
|
+
self.logger.info(
|
349
|
+
f"Task run failed with exception {exc!r} - "
|
350
|
+
f"Retry {self.retries + 1}/{self.task.retries} will start "
|
351
|
+
f"{str(delay) + ' second(s) from now' if delay else 'immediately'}"
|
352
|
+
)
|
353
|
+
|
337
354
|
self.set_state(new_state, force=True)
|
338
355
|
self.retries = self.retries + 1
|
339
356
|
return True
|
357
|
+
elif self.retries >= self.task.retries:
|
358
|
+
self.logger.error(
|
359
|
+
f"Task run failed with exception {exc!r} - Retries are exhausted"
|
360
|
+
)
|
361
|
+
return False
|
362
|
+
|
340
363
|
return False
|
341
364
|
|
342
365
|
def handle_exception(self, exc: Exception) -> None:
|
@@ -373,7 +396,7 @@ class TaskRunEngine(Generic[P, R]):
|
|
373
396
|
self.set_state(state, force=True)
|
374
397
|
|
375
398
|
@contextmanager
|
376
|
-
def
|
399
|
+
def setup_run_context(self, client: Optional[SyncPrefectClient] = None):
|
377
400
|
from prefect.utilities.engine import (
|
378
401
|
_resolve_custom_task_run_name,
|
379
402
|
should_log_prints,
|
@@ -454,13 +477,16 @@ class TaskRunEngine(Generic[P, R]):
|
|
454
477
|
validated_state=self.task_run.state,
|
455
478
|
)
|
456
479
|
|
457
|
-
|
480
|
+
with self.setup_run_context():
|
481
|
+
yield self
|
458
482
|
|
459
483
|
except Exception:
|
460
484
|
# regular exceptions are caught and re-raised to the user
|
461
485
|
raise
|
462
|
-
except (Pause, Abort):
|
486
|
+
except (Pause, Abort) as exc:
|
463
487
|
# Do not capture internal signals as crashes
|
488
|
+
if isinstance(exc, Abort):
|
489
|
+
self.logger.error("Task run was aborted: %s", exc)
|
464
490
|
raise
|
465
491
|
except GeneratorExit:
|
466
492
|
# Do not capture generator exits as crashes
|
@@ -474,18 +500,37 @@ class TaskRunEngine(Generic[P, R]):
|
|
474
500
|
display_state = (
|
475
501
|
repr(self.state) if PREFECT_DEBUG_MODE else str(self.state)
|
476
502
|
)
|
477
|
-
self.
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
503
|
+
level = logging.INFO if self.state.is_completed() else logging.ERROR
|
504
|
+
msg = f"Finished in state {display_state}"
|
505
|
+
if self.state.is_pending():
|
506
|
+
msg += (
|
507
|
+
"\nPlease wait for all submitted tasks to complete"
|
508
|
+
" before exiting your flow by calling `.wait()` on the "
|
509
|
+
"`PrefectFuture` returned from your `.submit()` calls."
|
510
|
+
)
|
511
|
+
msg += dedent(
|
512
|
+
"""
|
513
|
+
|
514
|
+
Example:
|
483
515
|
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
516
|
+
from prefect import flow, task
|
517
|
+
|
518
|
+
@task
|
519
|
+
def say_hello(name):
|
520
|
+
print f"Hello, {name}!"
|
521
|
+
|
522
|
+
@flow
|
523
|
+
def example_flow():
|
524
|
+
say_hello.submit(name="Marvin)
|
525
|
+
say_hello.wait()
|
526
|
+
|
527
|
+
example_flow()
|
528
|
+
"""
|
488
529
|
)
|
530
|
+
self.logger.log(
|
531
|
+
level=level,
|
532
|
+
msg=msg,
|
533
|
+
)
|
489
534
|
|
490
535
|
self._is_started = False
|
491
536
|
self._client = None
|
@@ -499,10 +544,8 @@ class TaskRunEngine(Generic[P, R]):
|
|
499
544
|
async def wait_until_ready(self):
|
500
545
|
"""Waits until the scheduled time (if its the future), then enters Running."""
|
501
546
|
if scheduled_time := self.state.state_details.scheduled_time:
|
502
|
-
|
503
|
-
|
504
|
-
)
|
505
|
-
await anyio.sleep((scheduled_time - pendulum.now("utc")).total_seconds())
|
547
|
+
sleep_time = (scheduled_time - pendulum.now("utc")).total_seconds()
|
548
|
+
await anyio.sleep(sleep_time if sleep_time > 0 else 0)
|
506
549
|
self.set_state(
|
507
550
|
Retrying() if self.state.name == "AwaitingRetry" else Running(),
|
508
551
|
force=True,
|
@@ -521,15 +564,11 @@ class TaskRunEngine(Generic[P, R]):
|
|
521
564
|
dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
|
522
565
|
) -> Generator[None, None, None]:
|
523
566
|
with self.initialize_run(task_run_id=task_run_id, dependencies=dependencies):
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
self.
|
529
|
-
try:
|
530
|
-
yield
|
531
|
-
finally:
|
532
|
-
self.call_hooks()
|
567
|
+
self.begin_run()
|
568
|
+
try:
|
569
|
+
yield
|
570
|
+
finally:
|
571
|
+
self.call_hooks()
|
533
572
|
|
534
573
|
@contextmanager
|
535
574
|
def transaction_context(self) -> Generator[Transaction, None, None]:
|
@@ -552,9 +591,12 @@ class TaskRunEngine(Generic[P, R]):
|
|
552
591
|
def run_context(self):
|
553
592
|
timeout_context = timeout_async if self.task.isasync else timeout
|
554
593
|
# reenter the run context to ensure it is up to date for every run
|
555
|
-
with self.
|
594
|
+
with self.setup_run_context():
|
556
595
|
try:
|
557
596
|
with timeout_context(seconds=self.task.timeout_seconds):
|
597
|
+
self.logger.debug(
|
598
|
+
f"Executing task {self.task.name!r} for task run {self.task_run.name!r}..."
|
599
|
+
)
|
558
600
|
yield self
|
559
601
|
except TimeoutError as exc:
|
560
602
|
self.handle_timeout(exc)
|
@@ -641,6 +683,117 @@ async def run_task_async(
|
|
641
683
|
return engine.state if return_type == "state" else engine.result()
|
642
684
|
|
643
685
|
|
686
|
+
def run_generator_task_sync(
|
687
|
+
task: Task[P, R],
|
688
|
+
task_run_id: Optional[UUID] = None,
|
689
|
+
task_run: Optional[TaskRun] = None,
|
690
|
+
parameters: Optional[Dict[str, Any]] = None,
|
691
|
+
wait_for: Optional[Iterable[PrefectFuture]] = None,
|
692
|
+
return_type: Literal["state", "result"] = "result",
|
693
|
+
dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
|
694
|
+
context: Optional[Dict[str, Any]] = None,
|
695
|
+
) -> Generator[R, None, None]:
|
696
|
+
if return_type != "result":
|
697
|
+
raise ValueError("The return_type for a generator task must be 'result'")
|
698
|
+
|
699
|
+
engine = TaskRunEngine[P, R](
|
700
|
+
task=task,
|
701
|
+
parameters=parameters,
|
702
|
+
task_run=task_run,
|
703
|
+
wait_for=wait_for,
|
704
|
+
context=context,
|
705
|
+
)
|
706
|
+
|
707
|
+
with engine.start(task_run_id=task_run_id, dependencies=dependencies):
|
708
|
+
while engine.is_running():
|
709
|
+
run_coro_as_sync(engine.wait_until_ready())
|
710
|
+
with engine.run_context(), engine.transaction_context() as txn:
|
711
|
+
# TODO: generators should default to commit_mode=OFF
|
712
|
+
# because they are dynamic by definition
|
713
|
+
# for now we just prevent this branch explicitly
|
714
|
+
if False and txn.is_committed():
|
715
|
+
txn.read()
|
716
|
+
else:
|
717
|
+
call_args, call_kwargs = parameters_to_args_kwargs(
|
718
|
+
task.fn, engine.parameters or {}
|
719
|
+
)
|
720
|
+
gen = task.fn(*call_args, **call_kwargs)
|
721
|
+
try:
|
722
|
+
while True:
|
723
|
+
gen_result = next(gen)
|
724
|
+
# link the current state to the result for dependency tracking
|
725
|
+
#
|
726
|
+
# TODO: this could grow the task_run_result
|
727
|
+
# dictionary in an unbounded way, so finding a
|
728
|
+
# way to periodically clean it up (using
|
729
|
+
# weakrefs or similar) would be good
|
730
|
+
link_state_to_result(engine.state, gen_result)
|
731
|
+
yield gen_result
|
732
|
+
except StopIteration as exc:
|
733
|
+
engine.handle_success(exc.value, transaction=txn)
|
734
|
+
except GeneratorExit as exc:
|
735
|
+
engine.handle_success(None, transaction=txn)
|
736
|
+
gen.throw(exc)
|
737
|
+
|
738
|
+
return engine.result()
|
739
|
+
|
740
|
+
|
741
|
+
async def run_generator_task_async(
|
742
|
+
task: Task[P, R],
|
743
|
+
task_run_id: Optional[UUID] = None,
|
744
|
+
task_run: Optional[TaskRun] = None,
|
745
|
+
parameters: Optional[Dict[str, Any]] = None,
|
746
|
+
wait_for: Optional[Iterable[PrefectFuture]] = None,
|
747
|
+
return_type: Literal["state", "result"] = "result",
|
748
|
+
dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
|
749
|
+
context: Optional[Dict[str, Any]] = None,
|
750
|
+
) -> AsyncGenerator[R, None]:
|
751
|
+
if return_type != "result":
|
752
|
+
raise ValueError("The return_type for a generator task must be 'result'")
|
753
|
+
engine = TaskRunEngine[P, R](
|
754
|
+
task=task,
|
755
|
+
parameters=parameters,
|
756
|
+
task_run=task_run,
|
757
|
+
wait_for=wait_for,
|
758
|
+
context=context,
|
759
|
+
)
|
760
|
+
|
761
|
+
with engine.start(task_run_id=task_run_id, dependencies=dependencies):
|
762
|
+
while engine.is_running():
|
763
|
+
await engine.wait_until_ready()
|
764
|
+
with engine.run_context(), engine.transaction_context() as txn:
|
765
|
+
# TODO: generators should default to commit_mode=OFF
|
766
|
+
# because they are dynamic by definition
|
767
|
+
# for now we just prevent this branch explicitly
|
768
|
+
if False and txn.is_committed():
|
769
|
+
txn.read()
|
770
|
+
else:
|
771
|
+
call_args, call_kwargs = parameters_to_args_kwargs(
|
772
|
+
task.fn, engine.parameters or {}
|
773
|
+
)
|
774
|
+
gen = task.fn(*call_args, **call_kwargs)
|
775
|
+
try:
|
776
|
+
while True:
|
777
|
+
# can't use anext in Python < 3.10
|
778
|
+
gen_result = await gen.__anext__()
|
779
|
+
# link the current state to the result for dependency tracking
|
780
|
+
#
|
781
|
+
# TODO: this could grow the task_run_result
|
782
|
+
# dictionary in an unbounded way, so finding a
|
783
|
+
# way to periodically clean it up (using
|
784
|
+
# weakrefs or similar) would be good
|
785
|
+
link_state_to_result(engine.state, gen_result)
|
786
|
+
yield gen_result
|
787
|
+
except (StopAsyncIteration, GeneratorExit) as exc:
|
788
|
+
engine.handle_success(None, transaction=txn)
|
789
|
+
if isinstance(exc, GeneratorExit):
|
790
|
+
gen.throw(exc)
|
791
|
+
|
792
|
+
# async generators can't return, but we can raise failures here
|
793
|
+
if engine.state.is_failed():
|
794
|
+
engine.result()
|
795
|
+
|
796
|
+
|
644
797
|
def run_task(
|
645
798
|
task: Task[P, Union[R, Coroutine[Any, Any, R]]],
|
646
799
|
task_run_id: Optional[UUID] = None,
|
@@ -680,7 +833,11 @@ def run_task(
|
|
680
833
|
dependencies=dependencies,
|
681
834
|
context=context,
|
682
835
|
)
|
683
|
-
if task.isasync:
|
836
|
+
if task.isasync and task.isgenerator:
|
837
|
+
return run_generator_task_async(**kwargs)
|
838
|
+
elif task.isgenerator:
|
839
|
+
return run_generator_task_sync(**kwargs)
|
840
|
+
elif task.isasync:
|
684
841
|
return run_task_async(**kwargs)
|
685
842
|
else:
|
686
843
|
return run_task_sync(**kwargs)
|
prefect/task_runners.py
CHANGED
@@ -202,12 +202,13 @@ class TaskRunner(abc.ABC, Generic[F]):
|
|
202
202
|
|
203
203
|
|
204
204
|
class ThreadPoolTaskRunner(TaskRunner[PrefectConcurrentFuture]):
|
205
|
-
def __init__(self):
|
205
|
+
def __init__(self, max_workers: Optional[int] = None):
|
206
206
|
super().__init__()
|
207
207
|
self._executor: Optional[ThreadPoolExecutor] = None
|
208
|
+
self._max_workers = max_workers
|
208
209
|
|
209
210
|
def duplicate(self) -> "ThreadPoolTaskRunner":
|
210
|
-
return type(self)()
|
211
|
+
return type(self)(max_workers=self._max_workers)
|
211
212
|
|
212
213
|
def submit(
|
213
214
|
self,
|
@@ -278,7 +279,7 @@ class ThreadPoolTaskRunner(TaskRunner[PrefectConcurrentFuture]):
|
|
278
279
|
|
279
280
|
def __enter__(self):
|
280
281
|
super().__enter__()
|
281
|
-
self._executor = ThreadPoolExecutor()
|
282
|
+
self._executor = ThreadPoolExecutor(max_workers=self._max_workers)
|
282
283
|
return self
|
283
284
|
|
284
285
|
def __exit__(self, exc_type, exc_value, traceback):
|
@@ -287,6 +288,11 @@ class ThreadPoolTaskRunner(TaskRunner[PrefectConcurrentFuture]):
|
|
287
288
|
self._executor = None
|
288
289
|
super().__exit__(exc_type, exc_value, traceback)
|
289
290
|
|
291
|
+
def __eq__(self, value: object) -> bool:
|
292
|
+
if not isinstance(value, ThreadPoolTaskRunner):
|
293
|
+
return False
|
294
|
+
return self._max_workers == value._max_workers
|
295
|
+
|
290
296
|
|
291
297
|
# Here, we alias ConcurrentTaskRunner to ThreadPoolTaskRunner for backwards compatibility
|
292
298
|
ConcurrentTaskRunner = ThreadPoolTaskRunner
|
prefect/task_runs.py
CHANGED
@@ -71,7 +71,7 @@ class TaskRunWaiter:
|
|
71
71
|
self.logger = get_logger("TaskRunWaiter")
|
72
72
|
self._consumer_task: Optional[asyncio.Task] = None
|
73
73
|
self._observed_completed_task_runs: TTLCache[uuid.UUID, bool] = TTLCache(
|
74
|
-
maxsize=
|
74
|
+
maxsize=10000, ttl=600
|
75
75
|
)
|
76
76
|
self._completion_events: Dict[uuid.UUID, asyncio.Event] = {}
|
77
77
|
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
@@ -85,7 +85,7 @@ class TaskRunWaiter:
|
|
85
85
|
"""
|
86
86
|
if self._started:
|
87
87
|
return
|
88
|
-
self.logger.
|
88
|
+
self.logger.debug("Starting TaskRunWaiter")
|
89
89
|
loop_thread = get_global_loop()
|
90
90
|
|
91
91
|
if not asyncio.get_running_loop() == loop_thread._loop:
|
@@ -111,7 +111,7 @@ class TaskRunWaiter:
|
|
111
111
|
) as subscriber:
|
112
112
|
async for event in subscriber:
|
113
113
|
try:
|
114
|
-
self.logger.
|
114
|
+
self.logger.debug(
|
115
115
|
f"Received event: {event.resource['prefect.resource.id']}"
|
116
116
|
)
|
117
117
|
task_run_id = uuid.UUID(
|
prefect/task_worker.py
CHANGED
@@ -86,7 +86,7 @@ class TaskWorker:
|
|
86
86
|
)
|
87
87
|
|
88
88
|
self._runs_task_group: anyio.abc.TaskGroup = anyio.create_task_group()
|
89
|
-
self._executor = ThreadPoolExecutor()
|
89
|
+
self._executor = ThreadPoolExecutor(max_workers=limit if limit else None)
|
90
90
|
self._limiter = anyio.CapacityLimiter(limit) if limit else None
|
91
91
|
|
92
92
|
@property
|
@@ -116,7 +116,7 @@ class TaskWorker:
|
|
116
116
|
except InvalidStatusCode as exc:
|
117
117
|
if exc.status_code == 403:
|
118
118
|
logger.error(
|
119
|
-
"Could not establish a connection to the `/task_runs/subscriptions/scheduled`"
|
119
|
+
"403: Could not establish a connection to the `/task_runs/subscriptions/scheduled`"
|
120
120
|
f" endpoint found at:\n\n {PREFECT_API_URL.value()}"
|
121
121
|
"\n\nPlease double-check the values of your"
|
122
122
|
" `PREFECT_API_URL` and `PREFECT_API_KEY` environment variables."
|
@@ -139,19 +139,41 @@ class TaskWorker:
|
|
139
139
|
raise StopTaskWorker
|
140
140
|
|
141
141
|
async def _subscribe_to_task_scheduling(self):
|
142
|
-
|
143
|
-
|
142
|
+
base_url = PREFECT_API_URL.value()
|
143
|
+
if base_url is None:
|
144
|
+
raise ValueError(
|
145
|
+
"`PREFECT_API_URL` must be set to use the task worker. "
|
146
|
+
"Task workers are not compatible with the ephemeral API."
|
147
|
+
)
|
148
|
+
task_keys_repr = " | ".join(
|
149
|
+
t.task_key.split(".")[-1].split("-")[0] for t in self.tasks
|
144
150
|
)
|
151
|
+
logger.info(f"Subscribing to runs of task(s): {task_keys_repr}")
|
145
152
|
async for task_run in Subscription(
|
146
153
|
model=TaskRun,
|
147
154
|
path="/task_runs/subscriptions/scheduled",
|
148
155
|
keys=[task.task_key for task in self.tasks],
|
149
156
|
client_id=self._client_id,
|
157
|
+
base_url=base_url,
|
150
158
|
):
|
159
|
+
logger.info(f"Received task run: {task_run.id} - {task_run.name}")
|
151
160
|
if self._limiter:
|
152
161
|
await self._limiter.acquire_on_behalf_of(task_run.id)
|
153
|
-
|
154
|
-
|
162
|
+
self._runs_task_group.start_soon(
|
163
|
+
self._safe_submit_scheduled_task_run, task_run
|
164
|
+
)
|
165
|
+
|
166
|
+
async def _safe_submit_scheduled_task_run(self, task_run: TaskRun):
|
167
|
+
try:
|
168
|
+
await self._submit_scheduled_task_run(task_run)
|
169
|
+
except BaseException as exc:
|
170
|
+
logger.exception(
|
171
|
+
f"Failed to submit task run {task_run.id!r}",
|
172
|
+
exc_info=exc,
|
173
|
+
)
|
174
|
+
finally:
|
175
|
+
if self._limiter:
|
176
|
+
self._limiter.release_on_behalf_of(task_run.id)
|
155
177
|
|
156
178
|
async def _submit_scheduled_task_run(self, task_run: TaskRun):
|
157
179
|
logger.debug(
|
@@ -258,15 +280,13 @@ class TaskWorker:
|
|
258
280
|
context=run_context,
|
259
281
|
)
|
260
282
|
await asyncio.wrap_future(future)
|
261
|
-
if self._limiter:
|
262
|
-
self._limiter.release_on_behalf_of(task_run.id)
|
263
283
|
|
264
284
|
async def execute_task_run(self, task_run: TaskRun):
|
265
285
|
"""Execute a task run in the task worker."""
|
266
286
|
async with self if not self.started else asyncnullcontext():
|
267
287
|
if self._limiter:
|
268
288
|
await self._limiter.acquire_on_behalf_of(task_run.id)
|
269
|
-
await self.
|
289
|
+
await self._safe_submit_scheduled_task_run(task_run)
|
270
290
|
|
271
291
|
async def __aenter__(self):
|
272
292
|
logger.debug("Starting task worker...")
|