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.
Files changed (65) hide show
  1. prefect/_internal/compatibility/migration.py +124 -0
  2. prefect/_internal/concurrency/__init__.py +2 -2
  3. prefect/_internal/concurrency/primitives.py +1 -0
  4. prefect/_internal/pydantic/annotations/pendulum.py +2 -2
  5. prefect/_internal/pytz.py +1 -1
  6. prefect/blocks/core.py +1 -1
  7. prefect/client/orchestration.py +96 -22
  8. prefect/client/schemas/actions.py +1 -1
  9. prefect/client/schemas/filters.py +6 -0
  10. prefect/client/schemas/objects.py +10 -3
  11. prefect/client/subscriptions.py +3 -2
  12. prefect/context.py +1 -27
  13. prefect/deployments/__init__.py +3 -0
  14. prefect/deployments/base.py +4 -2
  15. prefect/deployments/deployments.py +3 -0
  16. prefect/deployments/steps/pull.py +1 -0
  17. prefect/deployments/steps/utility.py +2 -1
  18. prefect/engine.py +3 -0
  19. prefect/events/cli/automations.py +1 -1
  20. prefect/events/clients.py +7 -1
  21. prefect/exceptions.py +9 -0
  22. prefect/filesystems.py +22 -11
  23. prefect/flow_engine.py +116 -154
  24. prefect/flows.py +83 -34
  25. prefect/infrastructure/provisioners/container_instance.py +1 -0
  26. prefect/infrastructure/provisioners/ecs.py +2 -2
  27. prefect/input/__init__.py +4 -0
  28. prefect/logging/formatters.py +2 -2
  29. prefect/logging/handlers.py +2 -2
  30. prefect/logging/loggers.py +1 -1
  31. prefect/plugins.py +1 -0
  32. prefect/records/cache_policies.py +3 -3
  33. prefect/records/result_store.py +10 -3
  34. prefect/results.py +27 -55
  35. prefect/runner/runner.py +1 -1
  36. prefect/runner/server.py +1 -1
  37. prefect/runtime/__init__.py +1 -0
  38. prefect/runtime/deployment.py +1 -0
  39. prefect/runtime/flow_run.py +1 -0
  40. prefect/runtime/task_run.py +1 -0
  41. prefect/settings.py +15 -2
  42. prefect/states.py +15 -4
  43. prefect/task_engine.py +190 -33
  44. prefect/task_runners.py +9 -3
  45. prefect/task_runs.py +3 -3
  46. prefect/task_worker.py +29 -9
  47. prefect/tasks.py +133 -57
  48. prefect/transactions.py +87 -15
  49. prefect/types/__init__.py +1 -1
  50. prefect/utilities/asyncutils.py +3 -3
  51. prefect/utilities/callables.py +16 -4
  52. prefect/utilities/dockerutils.py +5 -3
  53. prefect/utilities/engine.py +11 -0
  54. prefect/utilities/filesystem.py +4 -5
  55. prefect/utilities/importtools.py +29 -0
  56. prefect/utilities/services.py +2 -2
  57. prefect/utilities/urls.py +195 -0
  58. prefect/utilities/visualization.py +1 -0
  59. prefect/variables.py +4 -0
  60. prefect/workers/base.py +35 -0
  61. {prefect_client-3.0.0rc2.dist-info → prefect_client-3.0.0rc3.dist-info}/METADATA +2 -2
  62. {prefect_client-3.0.0rc2.dist-info → prefect_client-3.0.0rc3.dist-info}/RECORD +65 -62
  63. {prefect_client-3.0.0rc2.dist-info → prefect_client-3.0.0rc3.dist-info}/LICENSE +0 -0
  64. {prefect_client-3.0.0rc2.dist-info → prefect_client-3.0.0rc3.dist-info}/WHEEL +0 -0
  65. {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, result_factory: ResultFactory, key: str = None
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(state.data, key=key)
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(retval, key=key),
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(data=await result_factory.create_result(data, key=key))
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, result_factory=result_factory, key=transaction.key
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 enter_run_context(self, client: Optional[SyncPrefectClient] = None):
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
- yield self
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.logger.log(
478
- level=(
479
- logging.INFO if self.state.is_completed() else logging.ERROR
480
- ),
481
- msg=f"Finished in state {display_state}",
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
- # flush all logs if this is not a "top" level run
485
- if not (FlowRunContext.get() or TaskRunContext.get()):
486
- from_sync.call_soon_in_loop_thread(
487
- create_call(APILogHandler.aflush)
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
- self.logger.info(
503
- f"Waiting for scheduled time {scheduled_time} for task {self.task.name!r}"
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
- with self.enter_run_context():
525
- self.logger.debug(
526
- f"Executing task {self.task.name!r} for task run {self.task_run.name!r}..."
527
- )
528
- self.begin_run()
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.enter_run_context():
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=100, ttl=60
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.info("Starting TaskRunWaiter")
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.info(
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
- logger.info(
143
- f"Subscribing to tasks: {' | '.join(t.task_key.split('.')[-1] for t in self.tasks)}"
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
- logger.info(f"Received task run: {task_run.id} - {task_run.name}")
154
- self._runs_task_group.start_soon(self._submit_scheduled_task_run, task_run)
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._submit_scheduled_task_run(task_run)
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...")