coco-framework 0.1.0__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.
- coco/__init__.py +3 -0
- coco/agents/__init__.py +9 -0
- coco/agents/_subagent_transformer.py +307 -0
- coco/agents/factory.py +2164 -0
- coco/agents/middleware/__init__.py +100 -0
- coco/agents/middleware/_execution.py +385 -0
- coco/agents/middleware/_redaction.py +454 -0
- coco/agents/middleware/_retry.py +134 -0
- coco/agents/middleware/_trace_policy.py +61 -0
- coco/agents/middleware/context_editing.py +306 -0
- coco/agents/middleware/file_search.py +433 -0
- coco/agents/middleware/human_in_the_loop.py +680 -0
- coco/agents/middleware/internal_call_transformer.py +108 -0
- coco/agents/middleware/model_call_limit.py +267 -0
- coco/agents/middleware/model_fallback.py +417 -0
- coco/agents/middleware/model_retry.py +327 -0
- coco/agents/middleware/pii.py +878 -0
- coco/agents/middleware/provider_tool_search.py +307 -0
- coco/agents/middleware/shell_tool.py +949 -0
- coco/agents/middleware/summarization.py +910 -0
- coco/agents/middleware/todo.py +357 -0
- coco/agents/middleware/tool_call_limit.py +495 -0
- coco/agents/middleware/tool_emulator.py +251 -0
- coco/agents/middleware/tool_error.py +209 -0
- coco/agents/middleware/tool_retry.py +421 -0
- coco/agents/middleware/tool_selection.py +515 -0
- coco/agents/middleware/types.py +2201 -0
- coco/agents/structured_output.py +472 -0
- coco/chat_models/__init__.py +7 -0
- coco/chat_models/base.py +1082 -0
- coco/embeddings/__init__.py +18 -0
- coco/embeddings/base.py +279 -0
- coco/mcp/__init__.py +36 -0
- coco/mcp/adapter.py +245 -0
- coco/mcp/elicitation.py +394 -0
- coco/mcp/tools.py +307 -0
- coco/messages/__init__.py +73 -0
- coco/py.typed +0 -0
- coco/rate_limiters/__init__.py +13 -0
- coco/tools/__init__.py +22 -0
- coco/tools/tool_node.py +20 -0
- coco_anthropic/__init__.py +15 -0
- coco_anthropic/_client_utils.py +81 -0
- coco_anthropic/_compat.py +228 -0
- coco_anthropic/_sdk_compat.py +124 -0
- coco_anthropic/_version.py +3 -0
- coco_anthropic/chat_models.py +3349 -0
- coco_anthropic/data/__init__.py +1 -0
- coco_anthropic/data/_profiles.py +491 -0
- coco_anthropic/data/profile_augmentations.toml +69 -0
- coco_anthropic/experimental.py +140 -0
- coco_anthropic/llms.py +428 -0
- coco_anthropic/middleware/__init__.py +25 -0
- coco_anthropic/middleware/anthropic_tools.py +1197 -0
- coco_anthropic/middleware/bash.py +84 -0
- coco_anthropic/middleware/file_search.py +360 -0
- coco_anthropic/middleware/prompt_caching.py +276 -0
- coco_anthropic/output_parsers.py +93 -0
- coco_anthropic/py.typed +0 -0
- coco_core/__init__.py +20 -0
- coco_core/_api/__init__.py +87 -0
- coco_core/_api/beta_decorator.py +266 -0
- coco_core/_api/deprecation.py +637 -0
- coco_core/_api/internal.py +23 -0
- coco_core/_api/path.py +50 -0
- coco_core/_import_utils.py +41 -0
- coco_core/_protocol.py +625 -0
- coco_core/_security/__init__.py +36 -0
- coco_core/_security/_exceptions.py +9 -0
- coco_core/_security/_policy.py +313 -0
- coco_core/_security/_ssrf_protection.py +155 -0
- coco_core/_security/_transport.py +254 -0
- coco_core/agents.py +258 -0
- coco_core/caches.py +272 -0
- coco_core/callbacks/__init__.py +132 -0
- coco_core/callbacks/base.py +1229 -0
- coco_core/callbacks/file.py +266 -0
- coco_core/callbacks/manager.py +2826 -0
- coco_core/callbacks/stdout.py +123 -0
- coco_core/callbacks/streaming_stdout.py +154 -0
- coco_core/callbacks/usage.py +124 -0
- coco_core/chat_history.py +267 -0
- coco_core/chat_loaders.py +26 -0
- coco_core/chat_sessions.py +19 -0
- coco_core/cross_encoders.py +18 -0
- coco_core/document_loaders/__init__.py +36 -0
- coco_core/document_loaders/base.py +155 -0
- coco_core/document_loaders/blob_loaders.py +38 -0
- coco_core/documents/__init__.py +55 -0
- coco_core/documents/base.py +347 -0
- coco_core/documents/compressor.py +74 -0
- coco_core/documents/transformers.py +79 -0
- coco_core/embeddings/__init__.py +31 -0
- coco_core/embeddings/embeddings.py +78 -0
- coco_core/embeddings/fake.py +129 -0
- coco_core/env.py +22 -0
- coco_core/example_selectors/__init__.py +47 -0
- coco_core/example_selectors/base.py +60 -0
- coco_core/example_selectors/length_based.py +131 -0
- coco_core/example_selectors/semantic_similarity.py +366 -0
- coco_core/exceptions.py +166 -0
- coco_core/globals.py +72 -0
- coco_core/indexing/__init__.py +53 -0
- coco_core/indexing/api.py +954 -0
- coco_core/indexing/base.py +661 -0
- coco_core/indexing/in_memory.py +104 -0
- coco_core/language_models/__init__.py +116 -0
- coco_core/language_models/_compat_bridge.py +844 -0
- coco_core/language_models/_utils.py +343 -0
- coco_core/language_models/base.py +500 -0
- coco_core/language_models/chat_model_stream.py +1525 -0
- coco_core/language_models/chat_models.py +2748 -0
- coco_core/language_models/fake.py +137 -0
- coco_core/language_models/fake_chat_models.py +396 -0
- coco_core/language_models/llms.py +1569 -0
- coco_core/language_models/model_profile.py +183 -0
- coco_core/load/__init__.py +44 -0
- coco_core/load/_validation.py +191 -0
- coco_core/load/dump.py +120 -0
- coco_core/load/load.py +828 -0
- coco_core/load/mapping.py +1097 -0
- coco_core/load/serializable.py +406 -0
- coco_core/messages/__init__.py +198 -0
- coco_core/messages/ai.py +854 -0
- coco_core/messages/base.py +518 -0
- coco_core/messages/block_translators/__init__.py +112 -0
- coco_core/messages/block_translators/anthropic.py +534 -0
- coco_core/messages/block_translators/bedrock.py +108 -0
- coco_core/messages/block_translators/bedrock_converse.py +329 -0
- coco_core/messages/block_translators/coco_v0.py +303 -0
- coco_core/messages/block_translators/google_genai.py +610 -0
- coco_core/messages/block_translators/google_vertexai.py +21 -0
- coco_core/messages/block_translators/groq.py +157 -0
- coco_core/messages/block_translators/openai.py +1089 -0
- coco_core/messages/chat.py +64 -0
- coco_core/messages/content.py +1488 -0
- coco_core/messages/function.py +62 -0
- coco_core/messages/human.py +70 -0
- coco_core/messages/modifier.py +33 -0
- coco_core/messages/system.py +70 -0
- coco_core/messages/tool.py +418 -0
- coco_core/messages/utils.py +2442 -0
- coco_core/output_parsers/__init__.py +101 -0
- coco_core/output_parsers/base.py +361 -0
- coco_core/output_parsers/format_instructions.py +16 -0
- coco_core/output_parsers/json.py +139 -0
- coco_core/output_parsers/list.py +249 -0
- coco_core/output_parsers/openai_functions.py +324 -0
- coco_core/output_parsers/openai_tools.py +383 -0
- coco_core/output_parsers/pydantic.py +141 -0
- coco_core/output_parsers/string.py +63 -0
- coco_core/output_parsers/transform.py +175 -0
- coco_core/output_parsers/xml.py +301 -0
- coco_core/outputs/__init__.py +66 -0
- coco_core/outputs/chat_generation.py +147 -0
- coco_core/outputs/chat_result.py +39 -0
- coco_core/outputs/generation.py +80 -0
- coco_core/outputs/llm_result.py +112 -0
- coco_core/outputs/run_info.py +22 -0
- coco_core/prompt_values.py +161 -0
- coco_core/prompts/__init__.py +101 -0
- coco_core/prompts/base.py +508 -0
- coco_core/prompts/chat.py +1495 -0
- coco_core/prompts/dict.py +177 -0
- coco_core/prompts/few_shot.py +483 -0
- coco_core/prompts/few_shot_with_templates.py +239 -0
- coco_core/prompts/image.py +177 -0
- coco_core/prompts/loading.py +296 -0
- coco_core/prompts/message.py +97 -0
- coco_core/prompts/prompt.py +312 -0
- coco_core/prompts/string.py +399 -0
- coco_core/prompts/structured.py +214 -0
- coco_core/py.typed +0 -0
- coco_core/rate_limiters.py +254 -0
- coco_core/retrievers.py +328 -0
- coco_core/runnables/__init__.py +136 -0
- coco_core/runnables/base.py +6725 -0
- coco_core/runnables/branch.py +457 -0
- coco_core/runnables/config.py +712 -0
- coco_core/runnables/configurable.py +710 -0
- coco_core/runnables/fallbacks.py +663 -0
- coco_core/runnables/graph.py +741 -0
- coco_core/runnables/graph_ascii.py +366 -0
- coco_core/runnables/graph_mermaid.py +503 -0
- coco_core/runnables/graph_png.py +215 -0
- coco_core/runnables/history.py +631 -0
- coco_core/runnables/passthrough.py +849 -0
- coco_core/runnables/retry.py +379 -0
- coco_core/runnables/router.py +239 -0
- coco_core/runnables/schema.py +188 -0
- coco_core/runnables/utils.py +798 -0
- coco_core/stores.py +291 -0
- coco_core/structured_query.py +203 -0
- coco_core/sys_info.py +154 -0
- coco_core/tools/__init__.py +95 -0
- coco_core/tools/base.py +1948 -0
- coco_core/tools/convert.py +497 -0
- coco_core/tools/render.py +67 -0
- coco_core/tools/retriever.py +94 -0
- coco_core/tools/simple.py +208 -0
- coco_core/tools/structured.py +322 -0
- coco_core/tracers/__init__.py +47 -0
- coco_core/tracers/_compat.py +95 -0
- coco_core/tracers/_context.py +337 -0
- coco_core/tracers/_streaming.py +53 -0
- coco_core/tracers/base.py +955 -0
- coco_core/tracers/coco.py +279 -0
- coco_core/tracers/context.py +205 -0
- coco_core/tracers/core.py +766 -0
- coco_core/tracers/event_stream.py +1101 -0
- coco_core/tracers/log_stream.py +765 -0
- coco_core/tracers/memory_stream.py +200 -0
- coco_core/tracers/root_listeners.py +130 -0
- coco_core/tracers/run_collector.py +40 -0
- coco_core/tracers/schemas.py +14 -0
- coco_core/tracers/stdout.py +205 -0
- coco_core/utils/__init__.py +111 -0
- coco_core/utils/_gateway.py +274 -0
- coco_core/utils/_merge.py +218 -0
- coco_core/utils/aiter.py +357 -0
- coco_core/utils/env.py +86 -0
- coco_core/utils/formatting.py +80 -0
- coco_core/utils/function_calling.py +869 -0
- coco_core/utils/html.py +132 -0
- coco_core/utils/image.py +15 -0
- coco_core/utils/input.py +82 -0
- coco_core/utils/interactive_env.py +12 -0
- coco_core/utils/iter.py +229 -0
- coco_core/utils/json.py +230 -0
- coco_core/utils/json_schema.py +273 -0
- coco_core/utils/mustache.py +706 -0
- coco_core/utils/pydantic.py +631 -0
- coco_core/utils/strings.py +69 -0
- coco_core/utils/usage.py +61 -0
- coco_core/utils/utils.py +528 -0
- coco_core/utils/uuid.py +57 -0
- coco_core/vectorstores/__init__.py +53 -0
- coco_core/vectorstores/base.py +1111 -0
- coco_core/vectorstores/in_memory.py +546 -0
- coco_core/vectorstores/utils.py +163 -0
- coco_core/version.py +3 -0
- coco_framework-0.1.0.dist-info/METADATA +184 -0
- coco_framework-0.1.0.dist-info/RECORD +400 -0
- coco_framework-0.1.0.dist-info/WHEEL +4 -0
- coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
- coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
- coco_graph/_internal/__init__.py +4 -0
- coco_graph/_internal/_cache.py +31 -0
- coco_graph/_internal/_config.py +474 -0
- coco_graph/_internal/_constants.py +140 -0
- coco_graph/_internal/_fields.py +213 -0
- coco_graph/_internal/_future.py +220 -0
- coco_graph/_internal/_pydantic.py +275 -0
- coco_graph/_internal/_queue.py +124 -0
- coco_graph/_internal/_replay.py +90 -0
- coco_graph/_internal/_retry.py +29 -0
- coco_graph/_internal/_runnable.py +969 -0
- coco_graph/_internal/_scratchpad.py +19 -0
- coco_graph/_internal/_serde.py +253 -0
- coco_graph/_internal/_timeout.py +25 -0
- coco_graph/_internal/_typing.py +54 -0
- coco_graph/cache/base/__init__.py +48 -0
- coco_graph/cache/base/py.typed +0 -0
- coco_graph/cache/memory/__init__.py +73 -0
- coco_graph/cache/redis/__init__.py +144 -0
- coco_graph/cache/sqlite/__init__.py +120 -0
- coco_graph/callbacks.py +394 -0
- coco_graph/channels/__init__.py +29 -0
- coco_graph/channels/any_value.py +72 -0
- coco_graph/channels/base.py +121 -0
- coco_graph/channels/binop.py +155 -0
- coco_graph/channels/delta.py +202 -0
- coco_graph/channels/ephemeral_value.py +79 -0
- coco_graph/channels/last_value.py +151 -0
- coco_graph/channels/named_barrier_value.py +167 -0
- coco_graph/channels/topic.py +94 -0
- coco_graph/channels/untracked_value.py +73 -0
- coco_graph/checkpoint/base/__init__.py +861 -0
- coco_graph/checkpoint/base/id.py +109 -0
- coco_graph/checkpoint/base/py.typed +0 -0
- coco_graph/checkpoint/memory/__init__.py +698 -0
- coco_graph/checkpoint/memory/py.typed +0 -0
- coco_graph/checkpoint/postgres/__init__.py +602 -0
- coco_graph/checkpoint/postgres/_ainternal.py +23 -0
- coco_graph/checkpoint/postgres/_internal.py +21 -0
- coco_graph/checkpoint/postgres/aio.py +691 -0
- coco_graph/checkpoint/postgres/base.py +666 -0
- coco_graph/checkpoint/postgres/py.typed +0 -0
- coco_graph/checkpoint/postgres/shallow.py +967 -0
- coco_graph/checkpoint/serde/__init__.py +0 -0
- coco_graph/checkpoint/serde/_msgpack.py +97 -0
- coco_graph/checkpoint/serde/base.py +64 -0
- coco_graph/checkpoint/serde/encrypted.py +80 -0
- coco_graph/checkpoint/serde/event_hooks.py +52 -0
- coco_graph/checkpoint/serde/jsonplus.py +883 -0
- coco_graph/checkpoint/serde/py.typed +0 -0
- coco_graph/checkpoint/serde/types.py +68 -0
- coco_graph/checkpoint/sqlite/__init__.py +645 -0
- coco_graph/checkpoint/sqlite/_delta.py +172 -0
- coco_graph/checkpoint/sqlite/aio.py +738 -0
- coco_graph/checkpoint/sqlite/py.typed +0 -0
- coco_graph/checkpoint/sqlite/utils.py +116 -0
- coco_graph/config.py +196 -0
- coco_graph/constants.py +64 -0
- coco_graph/errors.py +241 -0
- coco_graph/func/__init__.py +620 -0
- coco_graph/graph/__init__.py +12 -0
- coco_graph/graph/_branch.py +225 -0
- coco_graph/graph/_node.py +103 -0
- coco_graph/graph/message.py +437 -0
- coco_graph/graph/state.py +1978 -0
- coco_graph/graph/ui.py +227 -0
- coco_graph/managed/__init__.py +3 -0
- coco_graph/managed/base.py +31 -0
- coco_graph/managed/is_last_step.py +24 -0
- coco_graph/prebuilt/__init__.py +23 -0
- coco_graph/prebuilt/_tool_call_stream.py +117 -0
- coco_graph/prebuilt/_tool_call_transformer.py +165 -0
- coco_graph/prebuilt/chat_agent_executor.py +1015 -0
- coco_graph/prebuilt/interrupt.py +105 -0
- coco_graph/prebuilt/py.typed +0 -0
- coco_graph/prebuilt/tool_node.py +2030 -0
- coco_graph/prebuilt/tool_validator.py +221 -0
- coco_graph/pregel/__init__.py +3 -0
- coco_graph/pregel/_algo.py +1460 -0
- coco_graph/pregel/_call.py +298 -0
- coco_graph/pregel/_checkpoint.py +331 -0
- coco_graph/pregel/_config.py +0 -0
- coco_graph/pregel/_draw.py +294 -0
- coco_graph/pregel/_executor.py +223 -0
- coco_graph/pregel/_io.py +174 -0
- coco_graph/pregel/_log.py +3 -0
- coco_graph/pregel/_loop.py +1988 -0
- coco_graph/pregel/_messages.py +461 -0
- coco_graph/pregel/_read.py +312 -0
- coco_graph/pregel/_retry.py +854 -0
- coco_graph/pregel/_runner.py +941 -0
- coco_graph/pregel/_tools.py +268 -0
- coco_graph/pregel/_utils.py +221 -0
- coco_graph/pregel/_validate.py +120 -0
- coco_graph/pregel/_write.py +192 -0
- coco_graph/pregel/debug.py +302 -0
- coco_graph/pregel/main.py +4364 -0
- coco_graph/pregel/protocol.py +288 -0
- coco_graph/pregel/types.py +38 -0
- coco_graph/py.typed +0 -0
- coco_graph/runtime.py +350 -0
- coco_graph/store/base/__init__.py +1322 -0
- coco_graph/store/base/batch.py +371 -0
- coco_graph/store/base/embed.py +436 -0
- coco_graph/store/base/py.typed +0 -0
- coco_graph/store/memory/__init__.py +592 -0
- coco_graph/store/memory/py.typed +0 -0
- coco_graph/store/postgres/__init__.py +4 -0
- coco_graph/store/postgres/aio.py +592 -0
- coco_graph/store/postgres/base.py +1480 -0
- coco_graph/store/postgres/py.typed +0 -0
- coco_graph/store/sqlite/__init__.py +4 -0
- coco_graph/store/sqlite/aio.py +632 -0
- coco_graph/store/sqlite/base.py +1514 -0
- coco_graph/stream/__init__.py +45 -0
- coco_graph/stream/_convert.py +32 -0
- coco_graph/stream/_mux.py +523 -0
- coco_graph/stream/_types.py +330 -0
- coco_graph/stream/run_stream.py +735 -0
- coco_graph/stream/stream_channel.py +341 -0
- coco_graph/stream/transformers.py +1039 -0
- coco_graph/types.py +1079 -0
- coco_graph/typing.py +48 -0
- coco_graph/utils/__init__.py +1 -0
- coco_graph/utils/config.py +4 -0
- coco_graph/utils/runnable.py +3 -0
- coco_graph/version.py +13 -0
- coco_graph/warnings.py +69 -0
- coco_openai/__init__.py +20 -0
- coco_openai/_compat.py +45 -0
- coco_openai/_version.py +3 -0
- coco_openai/chat_models/__init__.py +6 -0
- coco_openai/chat_models/_client_utils.py +684 -0
- coco_openai/chat_models/_compat.py +525 -0
- coco_openai/chat_models/azure.py +1227 -0
- coco_openai/chat_models/base.py +5654 -0
- coco_openai/chat_models/codex.py +557 -0
- coco_openai/chatgpt_oauth.py +1063 -0
- coco_openai/data/__init__.py +1 -0
- coco_openai/data/_profiles.py +1655 -0
- coco_openai/data/profile_augmentations.toml +127 -0
- coco_openai/embeddings/__init__.py +6 -0
- coco_openai/embeddings/azure.py +240 -0
- coco_openai/embeddings/base.py +820 -0
- coco_openai/llms/__init__.py +6 -0
- coco_openai/llms/azure.py +249 -0
- coco_openai/llms/base.py +919 -0
- coco_openai/middleware/__init__.py +11 -0
- coco_openai/middleware/openai_moderation.py +484 -0
- coco_openai/output_parsers/__init__.py +9 -0
- coco_openai/output_parsers/tools.py +9 -0
- coco_openai/py.typed +0 -0
- coco_openai/tools/__init__.py +5 -0
- coco_openai/tools/custom_tool.py +111 -0
|
@@ -0,0 +1,941 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import concurrent.futures
|
|
5
|
+
import inspect
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
import weakref
|
|
9
|
+
from collections.abc import (
|
|
10
|
+
AsyncIterator,
|
|
11
|
+
Awaitable,
|
|
12
|
+
Callable,
|
|
13
|
+
Collection,
|
|
14
|
+
Iterable,
|
|
15
|
+
Iterator,
|
|
16
|
+
Mapping,
|
|
17
|
+
Sequence,
|
|
18
|
+
)
|
|
19
|
+
from functools import partial
|
|
20
|
+
from typing import (
|
|
21
|
+
Any,
|
|
22
|
+
Generic,
|
|
23
|
+
TypeVar,
|
|
24
|
+
cast,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from coco_core.callbacks import Callbacks
|
|
28
|
+
|
|
29
|
+
from coco_graph._internal._constants import (
|
|
30
|
+
CONF,
|
|
31
|
+
CONFIG_KEY_CALL,
|
|
32
|
+
CONFIG_KEY_SCRATCHPAD,
|
|
33
|
+
ERROR,
|
|
34
|
+
ERROR_SOURCE_NODE,
|
|
35
|
+
INTERRUPT,
|
|
36
|
+
NO_WRITES,
|
|
37
|
+
RESUME,
|
|
38
|
+
RETURN,
|
|
39
|
+
)
|
|
40
|
+
from coco_graph._internal._future import chain_future, run_coroutine_threadsafe
|
|
41
|
+
from coco_graph._internal._scratchpad import PregelScratchpad
|
|
42
|
+
from coco_graph._internal._typing import MISSING
|
|
43
|
+
from coco_graph.constants import TAG_HIDDEN
|
|
44
|
+
from coco_graph.errors import GraphBubbleUp, GraphInterrupt
|
|
45
|
+
from coco_graph.pregel._algo import Call
|
|
46
|
+
from coco_graph.pregel._executor import Submit
|
|
47
|
+
from coco_graph.pregel._retry import arun_with_retry, run_with_retry
|
|
48
|
+
from coco_graph.types import (
|
|
49
|
+
CachePolicy,
|
|
50
|
+
PregelExecutableTask,
|
|
51
|
+
RetryPolicy,
|
|
52
|
+
TimeoutPolicy,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
F = TypeVar("F", concurrent.futures.Future, asyncio.Future)
|
|
56
|
+
E = TypeVar("E", threading.Event, asyncio.Event)
|
|
57
|
+
|
|
58
|
+
# List of filenames to exclude from exception traceback
|
|
59
|
+
# Note: Frames will be removed if they are the last frame in traceback, recursively
|
|
60
|
+
EXCLUDED_FRAME_FNAMES = (
|
|
61
|
+
"coco_graph/pregel/retry.py",
|
|
62
|
+
"coco_graph/pregel/runner.py",
|
|
63
|
+
"coco_graph/pregel/executor.py",
|
|
64
|
+
"coco_graph/utils/runnable.py",
|
|
65
|
+
"coco_core/runnables/config.py",
|
|
66
|
+
"concurrent/futures/thread.py",
|
|
67
|
+
"concurrent/futures/_base.py",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
SKIP_RERAISE_SET: weakref.WeakSet[concurrent.futures.Future | asyncio.Future] = (
|
|
71
|
+
weakref.WeakSet()
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class FuturesDict(Generic[F, E], dict[F, PregelExecutableTask | None]):
|
|
76
|
+
event: E
|
|
77
|
+
callback: weakref.ref[Callable[[PregelExecutableTask, BaseException | None], None]]
|
|
78
|
+
# Stop condition is injected by PregelRunner instead of hard-coded here.
|
|
79
|
+
# This lets the runner treat graph-error-handled exceptions as non-fatal
|
|
80
|
+
# so `on_done` does not trigger an early stop for those futures.
|
|
81
|
+
should_stop: Callable[[set[F]], bool]
|
|
82
|
+
counter: int
|
|
83
|
+
done: set[F]
|
|
84
|
+
lock: threading.Lock
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
event: E,
|
|
89
|
+
callback: weakref.ref[
|
|
90
|
+
Callable[[PregelExecutableTask, BaseException | None], None]
|
|
91
|
+
],
|
|
92
|
+
should_stop: Callable[[set[F]], bool],
|
|
93
|
+
future_type: type[F],
|
|
94
|
+
# used for generic typing, newer py supports FutureDict[...](...)
|
|
95
|
+
) -> None:
|
|
96
|
+
super().__init__()
|
|
97
|
+
self.lock = threading.Lock()
|
|
98
|
+
self.event = event
|
|
99
|
+
self.callback = callback
|
|
100
|
+
self.should_stop = should_stop
|
|
101
|
+
self.counter = 0
|
|
102
|
+
self.done: set[F] = set()
|
|
103
|
+
|
|
104
|
+
def __setitem__(
|
|
105
|
+
self,
|
|
106
|
+
key: F,
|
|
107
|
+
value: PregelExecutableTask | None,
|
|
108
|
+
) -> None:
|
|
109
|
+
super().__setitem__(key, value) # type: ignore[index]
|
|
110
|
+
if value is not None:
|
|
111
|
+
with self.lock:
|
|
112
|
+
self.event.clear()
|
|
113
|
+
self.counter += 1
|
|
114
|
+
key.add_done_callback(partial(self.on_done, value))
|
|
115
|
+
|
|
116
|
+
def on_done(
|
|
117
|
+
self,
|
|
118
|
+
task: PregelExecutableTask,
|
|
119
|
+
fut: F,
|
|
120
|
+
) -> None:
|
|
121
|
+
# Called automatically by future.add_done_callback registered in __setitem__.
|
|
122
|
+
try:
|
|
123
|
+
if cb := self.callback():
|
|
124
|
+
cb(task, _exception(fut))
|
|
125
|
+
finally:
|
|
126
|
+
with self.lock:
|
|
127
|
+
self.done.add(fut)
|
|
128
|
+
self.counter -= 1
|
|
129
|
+
# Wake waiter when all tracked futures are done, or when runner-level
|
|
130
|
+
# stop condition is met (for example, a non-handled fatal exception).
|
|
131
|
+
if self.counter == 0 or self.should_stop(self.done):
|
|
132
|
+
self.event.set()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class PregelRunner:
|
|
136
|
+
"""Responsible for executing a set of Pregel tasks concurrently, committing
|
|
137
|
+
their writes, yielding control to caller when there is output to emit, and
|
|
138
|
+
interrupting other tasks if appropriate."""
|
|
139
|
+
|
|
140
|
+
def __init__(
|
|
141
|
+
self,
|
|
142
|
+
*,
|
|
143
|
+
submit: weakref.ref[Submit],
|
|
144
|
+
put_writes: weakref.ref[Callable[[str, Sequence[tuple[str, Any]]], None]],
|
|
145
|
+
use_astream: bool = False,
|
|
146
|
+
node_finished: Callable[[str], None] | None = None,
|
|
147
|
+
node_error_handler_map: Mapping[str, str] | None = None,
|
|
148
|
+
schedule_error_handler: Callable[
|
|
149
|
+
[PregelExecutableTask, BaseException], PregelExecutableTask | None
|
|
150
|
+
]
|
|
151
|
+
| None = None,
|
|
152
|
+
aschedule_error_handler: Callable[
|
|
153
|
+
[PregelExecutableTask, BaseException],
|
|
154
|
+
Awaitable[PregelExecutableTask | None],
|
|
155
|
+
]
|
|
156
|
+
| None = None,
|
|
157
|
+
) -> None:
|
|
158
|
+
self.submit = submit
|
|
159
|
+
self.put_writes = put_writes
|
|
160
|
+
self.use_astream = use_astream
|
|
161
|
+
self.node_finished = node_finished
|
|
162
|
+
self.node_error_handler_map = dict(node_error_handler_map or {})
|
|
163
|
+
self.error_handler_nodes = set(self.node_error_handler_map.values())
|
|
164
|
+
self.schedule_error_handler = schedule_error_handler
|
|
165
|
+
self.aschedule_error_handler = aschedule_error_handler
|
|
166
|
+
# Exception object ids that are already routed to graph-level error handler.
|
|
167
|
+
# These ids are consulted by stop/panic checks to avoid re-raising handled
|
|
168
|
+
# exceptions via the normal fatal path in the same run.
|
|
169
|
+
self._handled_exception_ids: set[int] = set()
|
|
170
|
+
|
|
171
|
+
def _should_route_to_error_handler(self, task: PregelExecutableTask) -> bool:
|
|
172
|
+
if task.name in self.error_handler_nodes:
|
|
173
|
+
return False
|
|
174
|
+
return task.name in self.node_error_handler_map
|
|
175
|
+
|
|
176
|
+
def tick(
|
|
177
|
+
self,
|
|
178
|
+
tasks: Iterable[PregelExecutableTask],
|
|
179
|
+
*,
|
|
180
|
+
reraise: bool = True,
|
|
181
|
+
timeout: float | None = None,
|
|
182
|
+
retry_policy: Sequence[RetryPolicy] | None = None,
|
|
183
|
+
get_waiter: Callable[[], concurrent.futures.Future[None]] | None = None,
|
|
184
|
+
schedule_task: Callable[
|
|
185
|
+
[PregelExecutableTask, int, Call | None],
|
|
186
|
+
PregelExecutableTask | None,
|
|
187
|
+
],
|
|
188
|
+
) -> Iterator[None]:
|
|
189
|
+
tasks = tuple(tasks)
|
|
190
|
+
futures = FuturesDict(
|
|
191
|
+
callback=weakref.WeakMethod(self.commit),
|
|
192
|
+
event=threading.Event(),
|
|
193
|
+
should_stop=partial(
|
|
194
|
+
_should_stop_others, handled_exception_ids=self._handled_exception_ids
|
|
195
|
+
),
|
|
196
|
+
future_type=concurrent.futures.Future,
|
|
197
|
+
)
|
|
198
|
+
# give control back to the caller
|
|
199
|
+
yield
|
|
200
|
+
# fast path if single task with no timeout and no waiter
|
|
201
|
+
if len(tasks) == 0:
|
|
202
|
+
return
|
|
203
|
+
elif len(tasks) == 1 and timeout is None and get_waiter is None:
|
|
204
|
+
t = tasks[0]
|
|
205
|
+
scheduled_error_handler = False
|
|
206
|
+
try:
|
|
207
|
+
run_with_retry(
|
|
208
|
+
t,
|
|
209
|
+
retry_policy,
|
|
210
|
+
configurable={
|
|
211
|
+
CONFIG_KEY_CALL: partial(
|
|
212
|
+
_call,
|
|
213
|
+
weakref.ref(t),
|
|
214
|
+
retry_policy=retry_policy,
|
|
215
|
+
futures=weakref.ref(futures),
|
|
216
|
+
schedule_task=schedule_task,
|
|
217
|
+
submit=self.submit,
|
|
218
|
+
),
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
self.commit(t, None)
|
|
222
|
+
except Exception as exc:
|
|
223
|
+
self.commit(t, exc)
|
|
224
|
+
if (
|
|
225
|
+
not isinstance(exc, GraphBubbleUp)
|
|
226
|
+
and self._should_route_to_error_handler(t)
|
|
227
|
+
and self.schedule_error_handler is not None
|
|
228
|
+
):
|
|
229
|
+
self._handled_exception_ids.add(id(exc))
|
|
230
|
+
if handler_task := self.schedule_error_handler(t, exc):
|
|
231
|
+
tasks = (handler_task,)
|
|
232
|
+
scheduled_error_handler = True
|
|
233
|
+
# Continue to the regular scheduling path for handler execution.
|
|
234
|
+
if reraise and futures:
|
|
235
|
+
if id(exc) not in self._handled_exception_ids:
|
|
236
|
+
# will be re-raised after futures are done
|
|
237
|
+
fut: concurrent.futures.Future = concurrent.futures.Future()
|
|
238
|
+
fut.set_exception(exc)
|
|
239
|
+
futures.done.add(fut)
|
|
240
|
+
elif reraise and id(exc) not in self._handled_exception_ids:
|
|
241
|
+
if tb := exc.__traceback__:
|
|
242
|
+
while tb.tb_next is not None and any(
|
|
243
|
+
tb.tb_frame.f_code.co_filename.endswith(name)
|
|
244
|
+
for name in EXCLUDED_FRAME_FNAMES
|
|
245
|
+
):
|
|
246
|
+
tb = tb.tb_next
|
|
247
|
+
exc.__traceback__ = tb
|
|
248
|
+
raise
|
|
249
|
+
if not futures and not scheduled_error_handler:
|
|
250
|
+
# maybe `t` scheduled another task
|
|
251
|
+
return
|
|
252
|
+
else:
|
|
253
|
+
if not scheduled_error_handler:
|
|
254
|
+
tasks = () # don't reschedule this task
|
|
255
|
+
# add waiter task if requested
|
|
256
|
+
if get_waiter is not None:
|
|
257
|
+
futures[get_waiter()] = None
|
|
258
|
+
# schedule tasks
|
|
259
|
+
for t in tasks:
|
|
260
|
+
fut = self.submit()( # type: ignore[misc]
|
|
261
|
+
run_with_retry,
|
|
262
|
+
t,
|
|
263
|
+
retry_policy,
|
|
264
|
+
configurable={
|
|
265
|
+
CONFIG_KEY_CALL: partial(
|
|
266
|
+
_call,
|
|
267
|
+
weakref.ref(t),
|
|
268
|
+
retry_policy=retry_policy,
|
|
269
|
+
futures=weakref.ref(futures),
|
|
270
|
+
schedule_task=schedule_task,
|
|
271
|
+
submit=self.submit,
|
|
272
|
+
),
|
|
273
|
+
},
|
|
274
|
+
__reraise_on_exit__=reraise,
|
|
275
|
+
)
|
|
276
|
+
futures[fut] = t
|
|
277
|
+
# execute tasks, and wait for one to fail or all to finish.
|
|
278
|
+
# each task is independent from all other concurrent tasks
|
|
279
|
+
# yield updates/debug output as each task finishes
|
|
280
|
+
end_time = timeout + time.monotonic() if timeout else None
|
|
281
|
+
handled_futures: set[concurrent.futures.Future[Any]] = set()
|
|
282
|
+
while len(futures) > (1 if get_waiter is not None else 0):
|
|
283
|
+
done, inflight = concurrent.futures.wait(
|
|
284
|
+
futures,
|
|
285
|
+
return_when=concurrent.futures.FIRST_COMPLETED,
|
|
286
|
+
timeout=(max(0, end_time - time.monotonic()) if end_time else None),
|
|
287
|
+
)
|
|
288
|
+
if not done:
|
|
289
|
+
break # timed out
|
|
290
|
+
done_for_stop: set[concurrent.futures.Future[Any]] = set()
|
|
291
|
+
for fut in done:
|
|
292
|
+
task = futures.pop(fut)
|
|
293
|
+
if task is None:
|
|
294
|
+
# waiter task finished, schedule another
|
|
295
|
+
if inflight and get_waiter is not None:
|
|
296
|
+
futures[get_waiter()] = None
|
|
297
|
+
elif (
|
|
298
|
+
(task_exc := _exception(fut))
|
|
299
|
+
and self._should_route_to_error_handler(task)
|
|
300
|
+
and not isinstance(task_exc, GraphBubbleUp)
|
|
301
|
+
):
|
|
302
|
+
self._handled_exception_ids.add(id(task_exc))
|
|
303
|
+
SKIP_RERAISE_SET.add(fut)
|
|
304
|
+
handled_futures.add(fut)
|
|
305
|
+
if self.schedule_error_handler is not None:
|
|
306
|
+
if handler_task := self.schedule_error_handler(task, task_exc):
|
|
307
|
+
handler_fut = self.submit()( # type: ignore[misc]
|
|
308
|
+
run_with_retry,
|
|
309
|
+
handler_task,
|
|
310
|
+
retry_policy,
|
|
311
|
+
configurable={
|
|
312
|
+
CONFIG_KEY_CALL: partial(
|
|
313
|
+
_call,
|
|
314
|
+
weakref.ref(handler_task),
|
|
315
|
+
retry_policy=retry_policy,
|
|
316
|
+
futures=weakref.ref(futures),
|
|
317
|
+
schedule_task=schedule_task,
|
|
318
|
+
submit=self.submit,
|
|
319
|
+
),
|
|
320
|
+
},
|
|
321
|
+
__reraise_on_exit__=reraise,
|
|
322
|
+
)
|
|
323
|
+
futures[handler_fut] = handler_task
|
|
324
|
+
else:
|
|
325
|
+
done_for_stop.add(fut)
|
|
326
|
+
else:
|
|
327
|
+
# remove references to loop vars
|
|
328
|
+
del fut, task
|
|
329
|
+
# maybe stop other tasks
|
|
330
|
+
if _should_stop_others(
|
|
331
|
+
done_for_stop, handled_exception_ids=self._handled_exception_ids
|
|
332
|
+
):
|
|
333
|
+
break
|
|
334
|
+
# give control back to the caller
|
|
335
|
+
yield
|
|
336
|
+
# wait for done callbacks
|
|
337
|
+
futures.event.wait(
|
|
338
|
+
timeout=(max(0, end_time - time.monotonic()) if end_time else None)
|
|
339
|
+
)
|
|
340
|
+
# give control back to the caller
|
|
341
|
+
yield
|
|
342
|
+
# panic on failure or timeout
|
|
343
|
+
try:
|
|
344
|
+
_panic_or_proceed(
|
|
345
|
+
futures.done.union(f for f, t in futures.items() if t is not None),
|
|
346
|
+
panic=reraise,
|
|
347
|
+
handled_exception_ids=self._handled_exception_ids,
|
|
348
|
+
handled_futures=handled_futures,
|
|
349
|
+
)
|
|
350
|
+
except Exception as exc:
|
|
351
|
+
if tb := exc.__traceback__:
|
|
352
|
+
while tb.tb_next is not None and any(
|
|
353
|
+
tb.tb_frame.f_code.co_filename.endswith(name)
|
|
354
|
+
for name in EXCLUDED_FRAME_FNAMES
|
|
355
|
+
):
|
|
356
|
+
tb = tb.tb_next
|
|
357
|
+
exc.__traceback__ = tb
|
|
358
|
+
raise
|
|
359
|
+
|
|
360
|
+
async def atick(
|
|
361
|
+
self,
|
|
362
|
+
tasks: Iterable[PregelExecutableTask],
|
|
363
|
+
*,
|
|
364
|
+
reraise: bool = True,
|
|
365
|
+
timeout: float | None = None,
|
|
366
|
+
retry_policy: Sequence[RetryPolicy] | None = None,
|
|
367
|
+
get_waiter: Callable[[], asyncio.Future[None]] | None = None,
|
|
368
|
+
schedule_task: Callable[
|
|
369
|
+
[PregelExecutableTask, int, Call | None],
|
|
370
|
+
Awaitable[PregelExecutableTask | None],
|
|
371
|
+
],
|
|
372
|
+
) -> AsyncIterator[None]:
|
|
373
|
+
try:
|
|
374
|
+
loop = asyncio.get_event_loop()
|
|
375
|
+
except RuntimeError:
|
|
376
|
+
loop = asyncio.new_event_loop()
|
|
377
|
+
asyncio.set_event_loop(loop)
|
|
378
|
+
tasks = tuple(tasks)
|
|
379
|
+
futures = FuturesDict(
|
|
380
|
+
callback=weakref.WeakMethod(self.commit),
|
|
381
|
+
event=asyncio.Event(),
|
|
382
|
+
should_stop=partial(
|
|
383
|
+
_should_stop_others, handled_exception_ids=self._handled_exception_ids
|
|
384
|
+
),
|
|
385
|
+
future_type=asyncio.Future,
|
|
386
|
+
)
|
|
387
|
+
# give control back to the caller
|
|
388
|
+
yield
|
|
389
|
+
# fast path if single task with no waiter and no timeout
|
|
390
|
+
if len(tasks) == 0:
|
|
391
|
+
return
|
|
392
|
+
elif len(tasks) == 1 and get_waiter is None and timeout is None:
|
|
393
|
+
t = tasks[0]
|
|
394
|
+
scheduled_error_handler = False
|
|
395
|
+
try:
|
|
396
|
+
await arun_with_retry(
|
|
397
|
+
t,
|
|
398
|
+
retry_policy,
|
|
399
|
+
stream=self.use_astream,
|
|
400
|
+
configurable={
|
|
401
|
+
CONFIG_KEY_CALL: partial(
|
|
402
|
+
_acall,
|
|
403
|
+
weakref.ref(t),
|
|
404
|
+
stream=self.use_astream,
|
|
405
|
+
retry_policy=retry_policy,
|
|
406
|
+
futures=weakref.ref(futures),
|
|
407
|
+
schedule_task=schedule_task,
|
|
408
|
+
submit=self.submit,
|
|
409
|
+
loop=loop,
|
|
410
|
+
),
|
|
411
|
+
},
|
|
412
|
+
)
|
|
413
|
+
self.commit(t, None)
|
|
414
|
+
except Exception as exc:
|
|
415
|
+
self.commit(t, exc)
|
|
416
|
+
if (
|
|
417
|
+
not isinstance(exc, GraphBubbleUp)
|
|
418
|
+
and self._should_route_to_error_handler(t)
|
|
419
|
+
and self.aschedule_error_handler is not None
|
|
420
|
+
):
|
|
421
|
+
self._handled_exception_ids.add(id(exc))
|
|
422
|
+
if handler_task := await self.aschedule_error_handler(t, exc):
|
|
423
|
+
tasks = (handler_task,)
|
|
424
|
+
scheduled_error_handler = True
|
|
425
|
+
if reraise and futures:
|
|
426
|
+
if id(exc) not in self._handled_exception_ids:
|
|
427
|
+
# will be re-raised after futures are done
|
|
428
|
+
fut: asyncio.Future = loop.create_future()
|
|
429
|
+
fut.set_exception(exc)
|
|
430
|
+
futures.done.add(fut)
|
|
431
|
+
elif reraise and id(exc) not in self._handled_exception_ids:
|
|
432
|
+
if tb := exc.__traceback__:
|
|
433
|
+
while tb.tb_next is not None and any(
|
|
434
|
+
tb.tb_frame.f_code.co_filename.endswith(name)
|
|
435
|
+
for name in EXCLUDED_FRAME_FNAMES
|
|
436
|
+
):
|
|
437
|
+
tb = tb.tb_next
|
|
438
|
+
exc.__traceback__ = tb
|
|
439
|
+
raise
|
|
440
|
+
if not futures and not scheduled_error_handler:
|
|
441
|
+
# maybe `t` scheduled another task
|
|
442
|
+
return
|
|
443
|
+
else:
|
|
444
|
+
if not scheduled_error_handler:
|
|
445
|
+
tasks = () # don't reschedule this task
|
|
446
|
+
# add waiter task if requested
|
|
447
|
+
if get_waiter is not None:
|
|
448
|
+
futures[get_waiter()] = None
|
|
449
|
+
# schedule tasks
|
|
450
|
+
for t in tasks:
|
|
451
|
+
fut = cast(
|
|
452
|
+
asyncio.Future,
|
|
453
|
+
self.submit()( # type: ignore[misc]
|
|
454
|
+
arun_with_retry,
|
|
455
|
+
t,
|
|
456
|
+
retry_policy,
|
|
457
|
+
stream=self.use_astream,
|
|
458
|
+
configurable={
|
|
459
|
+
CONFIG_KEY_CALL: partial(
|
|
460
|
+
_acall,
|
|
461
|
+
weakref.ref(t),
|
|
462
|
+
retry_policy=retry_policy,
|
|
463
|
+
stream=self.use_astream,
|
|
464
|
+
futures=weakref.ref(futures),
|
|
465
|
+
schedule_task=schedule_task,
|
|
466
|
+
submit=self.submit,
|
|
467
|
+
loop=loop,
|
|
468
|
+
),
|
|
469
|
+
},
|
|
470
|
+
__name__=t.name,
|
|
471
|
+
__cancel_on_exit__=True,
|
|
472
|
+
__reraise_on_exit__=reraise,
|
|
473
|
+
),
|
|
474
|
+
)
|
|
475
|
+
futures[fut] = t
|
|
476
|
+
# execute tasks, and wait for one to fail or all to finish.
|
|
477
|
+
# each task is independent from all other concurrent tasks
|
|
478
|
+
# yield updates/debug output as each task finishes
|
|
479
|
+
end_time = timeout + loop.time() if timeout else None
|
|
480
|
+
handled_futures: set[asyncio.Future[Any]] = set()
|
|
481
|
+
while len(futures) > (1 if get_waiter is not None else 0):
|
|
482
|
+
done, inflight = await asyncio.wait(
|
|
483
|
+
futures,
|
|
484
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
485
|
+
timeout=(max(0, end_time - loop.time()) if end_time else None),
|
|
486
|
+
)
|
|
487
|
+
if not done:
|
|
488
|
+
break # timed out
|
|
489
|
+
done_for_stop: set[asyncio.Future[Any]] = set()
|
|
490
|
+
for fut in done:
|
|
491
|
+
task = futures.pop(fut)
|
|
492
|
+
if task is None:
|
|
493
|
+
# waiter task finished, schedule another
|
|
494
|
+
if inflight and get_waiter is not None:
|
|
495
|
+
futures[get_waiter()] = None
|
|
496
|
+
elif (
|
|
497
|
+
(task_exc := _exception(fut))
|
|
498
|
+
and self._should_route_to_error_handler(task)
|
|
499
|
+
and not isinstance(task_exc, GraphBubbleUp)
|
|
500
|
+
):
|
|
501
|
+
self._handled_exception_ids.add(id(task_exc))
|
|
502
|
+
SKIP_RERAISE_SET.add(fut)
|
|
503
|
+
handled_futures.add(fut)
|
|
504
|
+
if self.aschedule_error_handler is not None:
|
|
505
|
+
if handler_task := await self.aschedule_error_handler(
|
|
506
|
+
task, task_exc
|
|
507
|
+
):
|
|
508
|
+
handler_fut = cast(
|
|
509
|
+
asyncio.Future,
|
|
510
|
+
self.submit()( # type: ignore[misc]
|
|
511
|
+
arun_with_retry,
|
|
512
|
+
handler_task,
|
|
513
|
+
retry_policy,
|
|
514
|
+
stream=self.use_astream,
|
|
515
|
+
configurable={
|
|
516
|
+
CONFIG_KEY_CALL: partial(
|
|
517
|
+
_acall,
|
|
518
|
+
weakref.ref(handler_task),
|
|
519
|
+
retry_policy=retry_policy,
|
|
520
|
+
stream=self.use_astream,
|
|
521
|
+
futures=weakref.ref(futures),
|
|
522
|
+
schedule_task=schedule_task,
|
|
523
|
+
submit=self.submit,
|
|
524
|
+
loop=loop,
|
|
525
|
+
),
|
|
526
|
+
},
|
|
527
|
+
__name__=handler_task.name,
|
|
528
|
+
__cancel_on_exit__=True,
|
|
529
|
+
__reraise_on_exit__=reraise,
|
|
530
|
+
),
|
|
531
|
+
)
|
|
532
|
+
futures[handler_fut] = handler_task
|
|
533
|
+
else:
|
|
534
|
+
done_for_stop.add(fut)
|
|
535
|
+
else:
|
|
536
|
+
# remove references to loop vars
|
|
537
|
+
del fut, task
|
|
538
|
+
# maybe stop other tasks
|
|
539
|
+
if _should_stop_others(
|
|
540
|
+
done_for_stop, handled_exception_ids=self._handled_exception_ids
|
|
541
|
+
):
|
|
542
|
+
break
|
|
543
|
+
# give control back to the caller
|
|
544
|
+
yield
|
|
545
|
+
# wait for done callbacks
|
|
546
|
+
await asyncio.wait_for(
|
|
547
|
+
futures.event.wait(),
|
|
548
|
+
timeout=(max(0, end_time - loop.time()) if end_time else None),
|
|
549
|
+
)
|
|
550
|
+
# give control back to the caller
|
|
551
|
+
yield
|
|
552
|
+
# cancel waiter task
|
|
553
|
+
for fut in futures:
|
|
554
|
+
fut.cancel()
|
|
555
|
+
# panic on failure or timeout
|
|
556
|
+
try:
|
|
557
|
+
_panic_or_proceed(
|
|
558
|
+
futures.done.union(f for f, t in futures.items() if t is not None),
|
|
559
|
+
timeout_exc_cls=asyncio.TimeoutError,
|
|
560
|
+
panic=reraise,
|
|
561
|
+
handled_exception_ids=self._handled_exception_ids,
|
|
562
|
+
handled_futures=handled_futures,
|
|
563
|
+
)
|
|
564
|
+
except Exception as exc:
|
|
565
|
+
if tb := exc.__traceback__:
|
|
566
|
+
while tb.tb_next is not None and any(
|
|
567
|
+
tb.tb_frame.f_code.co_filename.endswith(name)
|
|
568
|
+
for name in EXCLUDED_FRAME_FNAMES
|
|
569
|
+
):
|
|
570
|
+
tb = tb.tb_next
|
|
571
|
+
exc.__traceback__ = tb
|
|
572
|
+
raise
|
|
573
|
+
|
|
574
|
+
def commit(
|
|
575
|
+
self,
|
|
576
|
+
task: PregelExecutableTask,
|
|
577
|
+
exception: BaseException | None,
|
|
578
|
+
) -> None:
|
|
579
|
+
if isinstance(exception, asyncio.CancelledError):
|
|
580
|
+
# for cancelled tasks, also save error in task,
|
|
581
|
+
# so loop can finish super-step
|
|
582
|
+
task.writes.append((ERROR, exception))
|
|
583
|
+
self.put_writes()(task.id, task.writes) # type: ignore[misc]
|
|
584
|
+
elif exception:
|
|
585
|
+
if isinstance(exception, GraphInterrupt):
|
|
586
|
+
# save interrupt to checkpointer
|
|
587
|
+
if exception.args[0]:
|
|
588
|
+
writes = [(INTERRUPT, exception.args[0])]
|
|
589
|
+
if resumes := [w for w in task.writes if w[0] == RESUME]:
|
|
590
|
+
writes.extend(resumes)
|
|
591
|
+
self.put_writes()(task.id, writes) # type: ignore[misc]
|
|
592
|
+
elif isinstance(exception, GraphBubbleUp):
|
|
593
|
+
# exception will be raised in _panic_or_proceed
|
|
594
|
+
pass
|
|
595
|
+
else:
|
|
596
|
+
# save error to checkpointer
|
|
597
|
+
task.writes.append((ERROR, exception))
|
|
598
|
+
if self._should_route_to_error_handler(task) and not isinstance(
|
|
599
|
+
exception, GraphBubbleUp
|
|
600
|
+
):
|
|
601
|
+
task.writes.append((ERROR_SOURCE_NODE, task.name))
|
|
602
|
+
self._handled_exception_ids.add(id(exception))
|
|
603
|
+
self.put_writes()(task.id, task.writes) # type: ignore[misc]
|
|
604
|
+
else:
|
|
605
|
+
if self.node_finished and (
|
|
606
|
+
task.config is None or TAG_HIDDEN not in task.config.get("tags", [])
|
|
607
|
+
):
|
|
608
|
+
self.node_finished(task.name)
|
|
609
|
+
if not task.writes:
|
|
610
|
+
# add no writes marker
|
|
611
|
+
task.writes.append((NO_WRITES, None))
|
|
612
|
+
# save task writes to checkpointer
|
|
613
|
+
self.put_writes()(task.id, task.writes) # type: ignore[misc]
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _should_stop_others(
|
|
617
|
+
done: set[F],
|
|
618
|
+
*,
|
|
619
|
+
handled_exception_ids: set[int] | None = None,
|
|
620
|
+
) -> bool:
|
|
621
|
+
"""Check if any task failed, if so, cancel all other tasks.
|
|
622
|
+
GraphInterrupts are not considered failures."""
|
|
623
|
+
for fut in done:
|
|
624
|
+
if fut.cancelled():
|
|
625
|
+
continue
|
|
626
|
+
elif exc := fut.exception():
|
|
627
|
+
if (
|
|
628
|
+
id(exc) not in (handled_exception_ids or set())
|
|
629
|
+
and not isinstance(exc, GraphBubbleUp)
|
|
630
|
+
and fut not in SKIP_RERAISE_SET
|
|
631
|
+
):
|
|
632
|
+
return True
|
|
633
|
+
|
|
634
|
+
return False
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _exception(
|
|
638
|
+
fut: concurrent.futures.Future[Any] | asyncio.Future[Any],
|
|
639
|
+
) -> BaseException | None:
|
|
640
|
+
"""Return the exception from a future, without raising CancelledError."""
|
|
641
|
+
if fut.cancelled():
|
|
642
|
+
if isinstance(fut, asyncio.Future):
|
|
643
|
+
return asyncio.CancelledError()
|
|
644
|
+
else:
|
|
645
|
+
return concurrent.futures.CancelledError()
|
|
646
|
+
else:
|
|
647
|
+
return fut.exception()
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _panic_or_proceed(
|
|
651
|
+
futs: set[concurrent.futures.Future] | set[asyncio.Future],
|
|
652
|
+
*,
|
|
653
|
+
timeout_exc_cls: type[Exception] = TimeoutError,
|
|
654
|
+
panic: bool = True,
|
|
655
|
+
handled_exception_ids: set[int] | None = None,
|
|
656
|
+
handled_futures: Collection[concurrent.futures.Future[Any] | asyncio.Future[Any]]
|
|
657
|
+
| None = None,
|
|
658
|
+
) -> None:
|
|
659
|
+
"""Cancel remaining tasks if any failed, re-raise exception if panic is True."""
|
|
660
|
+
done: set[concurrent.futures.Future[Any] | asyncio.Future[Any]] = set()
|
|
661
|
+
inflight: set[concurrent.futures.Future[Any] | asyncio.Future[Any]] = set()
|
|
662
|
+
for fut in futs:
|
|
663
|
+
if fut.cancelled():
|
|
664
|
+
continue
|
|
665
|
+
elif fut.done():
|
|
666
|
+
done.add(fut)
|
|
667
|
+
else:
|
|
668
|
+
inflight.add(fut)
|
|
669
|
+
interrupts: list[GraphInterrupt] = []
|
|
670
|
+
while done:
|
|
671
|
+
# if any task failed
|
|
672
|
+
fut = done.pop()
|
|
673
|
+
if exc := _exception(fut):
|
|
674
|
+
if fut in (handled_futures or set()):
|
|
675
|
+
continue
|
|
676
|
+
if id(exc) in (handled_exception_ids or set()):
|
|
677
|
+
continue
|
|
678
|
+
# cancel all pending tasks
|
|
679
|
+
while inflight:
|
|
680
|
+
inflight.pop().cancel()
|
|
681
|
+
# raise the exception
|
|
682
|
+
if panic:
|
|
683
|
+
if isinstance(exc, GraphInterrupt):
|
|
684
|
+
# collect interrupts
|
|
685
|
+
interrupts.append(exc)
|
|
686
|
+
elif fut not in SKIP_RERAISE_SET:
|
|
687
|
+
raise exc
|
|
688
|
+
# raise combined interrupts
|
|
689
|
+
if interrupts:
|
|
690
|
+
raise GraphInterrupt(tuple(i for exc in interrupts for i in exc.args[0]))
|
|
691
|
+
if inflight:
|
|
692
|
+
# if we got here means we timed out
|
|
693
|
+
while inflight:
|
|
694
|
+
# cancel all pending tasks
|
|
695
|
+
inflight.pop().cancel()
|
|
696
|
+
# raise timeout error
|
|
697
|
+
raise timeout_exc_cls("Timed out")
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _call(
|
|
701
|
+
task: weakref.ref[PregelExecutableTask],
|
|
702
|
+
func: Callable[[Any], Awaitable[Any] | Any],
|
|
703
|
+
input: Any,
|
|
704
|
+
*,
|
|
705
|
+
retry_policy: Sequence[RetryPolicy] | None = None,
|
|
706
|
+
cache_policy: CachePolicy | None = None,
|
|
707
|
+
timeout: TimeoutPolicy | None = None,
|
|
708
|
+
callbacks: Callbacks = None,
|
|
709
|
+
futures: weakref.ref[FuturesDict],
|
|
710
|
+
schedule_task: Callable[
|
|
711
|
+
[PregelExecutableTask, int, Call | None], PregelExecutableTask | None
|
|
712
|
+
],
|
|
713
|
+
submit: weakref.ref[Submit],
|
|
714
|
+
) -> concurrent.futures.Future[Any]:
|
|
715
|
+
if inspect.iscoroutinefunction(func):
|
|
716
|
+
raise RuntimeError("In an sync context async tasks cannot be called")
|
|
717
|
+
|
|
718
|
+
fut: concurrent.futures.Future | None = None
|
|
719
|
+
# schedule PUSH tasks, collect futures
|
|
720
|
+
scratchpad: PregelScratchpad = task().config[CONF][CONFIG_KEY_SCRATCHPAD] # type: ignore[union-attr]
|
|
721
|
+
# schedule the next task, if the callback returns one
|
|
722
|
+
if next_task := schedule_task(
|
|
723
|
+
task(), # type: ignore[arg-type]
|
|
724
|
+
scratchpad.call_counter(),
|
|
725
|
+
Call(
|
|
726
|
+
func,
|
|
727
|
+
input,
|
|
728
|
+
retry_policy=retry_policy,
|
|
729
|
+
cache_policy=cache_policy,
|
|
730
|
+
callbacks=callbacks,
|
|
731
|
+
timeout=timeout,
|
|
732
|
+
),
|
|
733
|
+
):
|
|
734
|
+
if fut := next(
|
|
735
|
+
(
|
|
736
|
+
f
|
|
737
|
+
for f, t in list(futures().items()) # type: ignore[union-attr]
|
|
738
|
+
if t is not None and t == next_task.id
|
|
739
|
+
),
|
|
740
|
+
None,
|
|
741
|
+
):
|
|
742
|
+
# if the parent task was retried,
|
|
743
|
+
# the next task might already be running
|
|
744
|
+
pass
|
|
745
|
+
elif next_task.writes:
|
|
746
|
+
# if it already ran, return the result
|
|
747
|
+
fut = concurrent.futures.Future()
|
|
748
|
+
ret = next((v for c, v in next_task.writes if c == RETURN), MISSING)
|
|
749
|
+
if ret is not MISSING:
|
|
750
|
+
fut.set_result(ret)
|
|
751
|
+
elif exc := next((v for c, v in next_task.writes if c == ERROR), None):
|
|
752
|
+
fut.set_exception(
|
|
753
|
+
exc if isinstance(exc, BaseException) else Exception(exc)
|
|
754
|
+
)
|
|
755
|
+
else:
|
|
756
|
+
fut.set_result(None)
|
|
757
|
+
else:
|
|
758
|
+
# schedule the next task
|
|
759
|
+
fut = submit()( # type: ignore[misc]
|
|
760
|
+
run_with_retry,
|
|
761
|
+
next_task,
|
|
762
|
+
retry_policy,
|
|
763
|
+
configurable={
|
|
764
|
+
CONFIG_KEY_CALL: partial(
|
|
765
|
+
_call,
|
|
766
|
+
weakref.ref(next_task),
|
|
767
|
+
futures=futures,
|
|
768
|
+
retry_policy=retry_policy,
|
|
769
|
+
callbacks=callbacks,
|
|
770
|
+
schedule_task=schedule_task,
|
|
771
|
+
submit=submit,
|
|
772
|
+
),
|
|
773
|
+
},
|
|
774
|
+
__reraise_on_exit__=False,
|
|
775
|
+
# starting a new task in the next tick ensures
|
|
776
|
+
# updates from this tick are committed/streamed first
|
|
777
|
+
__next_tick__=True,
|
|
778
|
+
)
|
|
779
|
+
# exceptions for call() tasks are raised into the parent task
|
|
780
|
+
# so we should not re-raise at the end of the tick
|
|
781
|
+
SKIP_RERAISE_SET.add(fut)
|
|
782
|
+
futures()[fut] = next_task # type: ignore[index]
|
|
783
|
+
fut = cast(asyncio.Future | concurrent.futures.Future, fut)
|
|
784
|
+
# return a chained future to ensure commit() callback is called
|
|
785
|
+
# before the returned future is resolved, to ensure stream order etc
|
|
786
|
+
return chain_future(fut, concurrent.futures.Future())
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _acall(
|
|
790
|
+
task: weakref.ref[PregelExecutableTask],
|
|
791
|
+
func: Callable[[Any], Awaitable[Any] | Any],
|
|
792
|
+
input: Any,
|
|
793
|
+
*,
|
|
794
|
+
retry_policy: Sequence[RetryPolicy] | None = None,
|
|
795
|
+
cache_policy: CachePolicy | None = None,
|
|
796
|
+
timeout: TimeoutPolicy | None = None,
|
|
797
|
+
callbacks: Callbacks = None,
|
|
798
|
+
# injected dependencies
|
|
799
|
+
futures: weakref.ref[FuturesDict],
|
|
800
|
+
schedule_task: Callable[
|
|
801
|
+
[PregelExecutableTask, int, Call | None],
|
|
802
|
+
Awaitable[PregelExecutableTask | None],
|
|
803
|
+
],
|
|
804
|
+
submit: weakref.ref[Submit],
|
|
805
|
+
loop: asyncio.AbstractEventLoop,
|
|
806
|
+
stream: bool = False,
|
|
807
|
+
) -> asyncio.Future[Any] | concurrent.futures.Future[Any]:
|
|
808
|
+
# return a chained future to ensure commit() callback is called
|
|
809
|
+
# before the returned future is resolved, to ensure stream order etc
|
|
810
|
+
try:
|
|
811
|
+
in_async = asyncio.current_task() is not None
|
|
812
|
+
except RuntimeError:
|
|
813
|
+
in_async = False
|
|
814
|
+
# if in async context return an async future, otherwise return a sync future
|
|
815
|
+
if in_async:
|
|
816
|
+
fut: asyncio.Future[Any] | concurrent.futures.Future[Any] = asyncio.Future(
|
|
817
|
+
loop=loop
|
|
818
|
+
)
|
|
819
|
+
else:
|
|
820
|
+
fut = concurrent.futures.Future()
|
|
821
|
+
# schedule the next task
|
|
822
|
+
run_coroutine_threadsafe(
|
|
823
|
+
_acall_impl(
|
|
824
|
+
fut,
|
|
825
|
+
task,
|
|
826
|
+
func,
|
|
827
|
+
input,
|
|
828
|
+
retry_policy=retry_policy,
|
|
829
|
+
cache_policy=cache_policy,
|
|
830
|
+
timeout=timeout,
|
|
831
|
+
callbacks=callbacks,
|
|
832
|
+
futures=futures,
|
|
833
|
+
schedule_task=schedule_task,
|
|
834
|
+
submit=submit,
|
|
835
|
+
loop=loop,
|
|
836
|
+
stream=stream,
|
|
837
|
+
),
|
|
838
|
+
loop,
|
|
839
|
+
lazy=False,
|
|
840
|
+
)
|
|
841
|
+
return fut
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
async def _acall_impl(
|
|
845
|
+
destination: asyncio.Future[Any] | concurrent.futures.Future[Any],
|
|
846
|
+
task: weakref.ref[PregelExecutableTask],
|
|
847
|
+
func: Callable[[Any], Awaitable[Any] | Any],
|
|
848
|
+
input: Any,
|
|
849
|
+
*,
|
|
850
|
+
retry_policy: Sequence[RetryPolicy] | None = None,
|
|
851
|
+
cache_policy: CachePolicy | None = None,
|
|
852
|
+
timeout: TimeoutPolicy | None = None,
|
|
853
|
+
callbacks: Callbacks = None,
|
|
854
|
+
# injected dependencies
|
|
855
|
+
futures: weakref.ref[FuturesDict[asyncio.Future, asyncio.Event]],
|
|
856
|
+
schedule_task: Callable[
|
|
857
|
+
[PregelExecutableTask, int, Call | None],
|
|
858
|
+
Awaitable[PregelExecutableTask | None],
|
|
859
|
+
],
|
|
860
|
+
submit: weakref.ref[Submit],
|
|
861
|
+
loop: asyncio.AbstractEventLoop,
|
|
862
|
+
stream: bool = False,
|
|
863
|
+
) -> None:
|
|
864
|
+
try:
|
|
865
|
+
fut: asyncio.Future | None = None
|
|
866
|
+
# schedule PUSH tasks, collect futures
|
|
867
|
+
scratchpad: PregelScratchpad = task().config[CONF][CONFIG_KEY_SCRATCHPAD] # type: ignore[union-attr]
|
|
868
|
+
# schedule the next task, if the callback returns one
|
|
869
|
+
if next_task := await schedule_task(
|
|
870
|
+
task(), # type: ignore[arg-type]
|
|
871
|
+
scratchpad.call_counter(),
|
|
872
|
+
Call(
|
|
873
|
+
func,
|
|
874
|
+
input,
|
|
875
|
+
retry_policy=retry_policy,
|
|
876
|
+
cache_policy=cache_policy,
|
|
877
|
+
callbacks=callbacks,
|
|
878
|
+
timeout=timeout,
|
|
879
|
+
),
|
|
880
|
+
):
|
|
881
|
+
if fut := next(
|
|
882
|
+
(
|
|
883
|
+
f
|
|
884
|
+
for f, t in list(futures().items()) # type: ignore[union-attr]
|
|
885
|
+
if t is not None and t == next_task.id
|
|
886
|
+
),
|
|
887
|
+
None,
|
|
888
|
+
):
|
|
889
|
+
# if the parent task was retried,
|
|
890
|
+
# the next task might already be running
|
|
891
|
+
pass
|
|
892
|
+
elif next_task.writes:
|
|
893
|
+
# if it already ran, return the result
|
|
894
|
+
fut = asyncio.Future(loop=loop)
|
|
895
|
+
ret = next((v for c, v in next_task.writes if c == RETURN), MISSING)
|
|
896
|
+
if ret is not MISSING:
|
|
897
|
+
fut.set_result(ret)
|
|
898
|
+
elif exc := next((v for c, v in next_task.writes if c == ERROR), None):
|
|
899
|
+
fut.set_exception(
|
|
900
|
+
exc if isinstance(exc, BaseException) else Exception(exc)
|
|
901
|
+
)
|
|
902
|
+
else:
|
|
903
|
+
fut.set_result(None)
|
|
904
|
+
else:
|
|
905
|
+
# schedule the next task
|
|
906
|
+
fut = cast(
|
|
907
|
+
asyncio.Future,
|
|
908
|
+
submit()( # type: ignore[misc]
|
|
909
|
+
arun_with_retry,
|
|
910
|
+
next_task,
|
|
911
|
+
retry_policy,
|
|
912
|
+
stream=stream,
|
|
913
|
+
configurable={
|
|
914
|
+
CONFIG_KEY_CALL: partial(
|
|
915
|
+
_acall,
|
|
916
|
+
weakref.ref(next_task),
|
|
917
|
+
stream=stream,
|
|
918
|
+
futures=futures,
|
|
919
|
+
schedule_task=schedule_task,
|
|
920
|
+
submit=submit,
|
|
921
|
+
loop=loop,
|
|
922
|
+
),
|
|
923
|
+
},
|
|
924
|
+
__name__=next_task.name,
|
|
925
|
+
__cancel_on_exit__=True,
|
|
926
|
+
__reraise_on_exit__=False,
|
|
927
|
+
# starting a new task in the next tick ensures
|
|
928
|
+
# updates from this tick are committed/streamed first
|
|
929
|
+
__next_tick__=True,
|
|
930
|
+
),
|
|
931
|
+
)
|
|
932
|
+
# exceptions for call() tasks are raised into the parent task
|
|
933
|
+
# so we should not re-raise at the end of the tick
|
|
934
|
+
SKIP_RERAISE_SET.add(fut)
|
|
935
|
+
futures()[fut] = next_task # type: ignore[index]
|
|
936
|
+
if fut is not None:
|
|
937
|
+
chain_future(fut, destination)
|
|
938
|
+
else:
|
|
939
|
+
destination.set_exception(RuntimeError("Task not scheduled"))
|
|
940
|
+
except Exception as exc:
|
|
941
|
+
destination.set_exception(exc)
|