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,1460 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import binascii
|
|
4
|
+
import itertools
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
from collections import defaultdict, deque
|
|
8
|
+
from collections.abc import Callable, Iterable, Mapping, Sequence
|
|
9
|
+
from copy import copy
|
|
10
|
+
from functools import partial
|
|
11
|
+
from hashlib import sha1
|
|
12
|
+
from typing import (
|
|
13
|
+
Any,
|
|
14
|
+
Literal,
|
|
15
|
+
NamedTuple,
|
|
16
|
+
Protocol,
|
|
17
|
+
cast,
|
|
18
|
+
overload,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from coco_core.callbacks import Callbacks
|
|
22
|
+
from coco_core.callbacks.manager import AsyncParentRunManager, ParentRunManager
|
|
23
|
+
from coco_core.runnables.config import RunnableConfig
|
|
24
|
+
from coco_graph.checkpoint.base import (
|
|
25
|
+
BaseCheckpointSaver,
|
|
26
|
+
ChannelVersions,
|
|
27
|
+
Checkpoint,
|
|
28
|
+
PendingWrite,
|
|
29
|
+
V,
|
|
30
|
+
)
|
|
31
|
+
from coco_graph.store.base import BaseStore
|
|
32
|
+
from xxhash import xxh3_128_hexdigest
|
|
33
|
+
|
|
34
|
+
from coco_graph._internal._config import merge_configs, patch_config
|
|
35
|
+
from coco_graph._internal._constants import (
|
|
36
|
+
CACHE_NS_WRITES,
|
|
37
|
+
CONF,
|
|
38
|
+
CONFIG_KEY_CHECKPOINT_ID,
|
|
39
|
+
CONFIG_KEY_CHECKPOINT_MAP,
|
|
40
|
+
CONFIG_KEY_CHECKPOINT_NS,
|
|
41
|
+
CONFIG_KEY_CHECKPOINTER,
|
|
42
|
+
CONFIG_KEY_NODE_ERROR,
|
|
43
|
+
CONFIG_KEY_READ,
|
|
44
|
+
CONFIG_KEY_RESUME_MAP,
|
|
45
|
+
CONFIG_KEY_RUNTIME,
|
|
46
|
+
CONFIG_KEY_SCRATCHPAD,
|
|
47
|
+
CONFIG_KEY_SEND,
|
|
48
|
+
CONFIG_KEY_TASK_ID,
|
|
49
|
+
CONFIG_KEY_THREAD_ID,
|
|
50
|
+
ERROR,
|
|
51
|
+
ERROR_SOURCE_NODE,
|
|
52
|
+
INTERRUPT,
|
|
53
|
+
NO_WRITES,
|
|
54
|
+
NS_END,
|
|
55
|
+
NS_SEP,
|
|
56
|
+
NULL_TASK_ID,
|
|
57
|
+
PREVIOUS,
|
|
58
|
+
PULL,
|
|
59
|
+
PUSH,
|
|
60
|
+
RESERVED,
|
|
61
|
+
RESUME,
|
|
62
|
+
RETURN,
|
|
63
|
+
TASKS,
|
|
64
|
+
)
|
|
65
|
+
from coco_graph._internal._scratchpad import PregelScratchpad
|
|
66
|
+
from coco_graph._internal._typing import EMPTY_SEQ, MISSING
|
|
67
|
+
from coco_graph.channels.base import BaseChannel
|
|
68
|
+
from coco_graph.channels.topic import Topic
|
|
69
|
+
from coco_graph.channels.untracked_value import UntrackedValue
|
|
70
|
+
from coco_graph.constants import TAG_HIDDEN
|
|
71
|
+
from coco_graph.errors import NodeError
|
|
72
|
+
from coco_graph.managed.base import ManagedValueMapping
|
|
73
|
+
from coco_graph.pregel._call import get_runnable_for_task, identifier
|
|
74
|
+
from coco_graph.pregel._io import read_channels
|
|
75
|
+
from coco_graph.pregel._log import logger
|
|
76
|
+
from coco_graph.pregel._read import INPUT_CACHE_KEY_TYPE, PregelNode
|
|
77
|
+
from coco_graph.runtime import DEFAULT_RUNTIME, ExecutionInfo, Runtime
|
|
78
|
+
from coco_graph.types import (
|
|
79
|
+
All,
|
|
80
|
+
CacheKey,
|
|
81
|
+
CachePolicy,
|
|
82
|
+
PregelExecutableTask,
|
|
83
|
+
PregelTask,
|
|
84
|
+
RetryPolicy,
|
|
85
|
+
Send,
|
|
86
|
+
TimeoutPolicy,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
GetNextVersion = Callable[[V | None, None], V]
|
|
90
|
+
SUPPORTS_EXC_NOTES = sys.version_info >= (3, 11)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class WritesProtocol(Protocol):
|
|
94
|
+
"""Protocol for objects containing writes to be applied to checkpoint.
|
|
95
|
+
Implemented by PregelTaskWrites and PregelExecutableTask."""
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def path(self) -> tuple[str | int | tuple, ...]: ...
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def name(self) -> str: ...
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def writes(self) -> Sequence[tuple[str, Any]]: ...
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def triggers(self) -> Sequence[str]: ...
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class PregelTaskWrites(NamedTuple):
|
|
111
|
+
"""Simplest implementation of WritesProtocol, for usage with writes that
|
|
112
|
+
don't originate from a runnable task, eg. graph input, update_state, etc."""
|
|
113
|
+
|
|
114
|
+
path: tuple[str | int | tuple, ...]
|
|
115
|
+
name: str
|
|
116
|
+
writes: Sequence[tuple[str, Any]]
|
|
117
|
+
triggers: Sequence[str]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Call:
|
|
121
|
+
__slots__ = (
|
|
122
|
+
"func",
|
|
123
|
+
"input",
|
|
124
|
+
"retry_policy",
|
|
125
|
+
"cache_policy",
|
|
126
|
+
"callbacks",
|
|
127
|
+
"timeout",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
func: Callable
|
|
131
|
+
input: tuple[tuple[Any, ...], dict[str, Any]]
|
|
132
|
+
retry_policy: Sequence[RetryPolicy] | None
|
|
133
|
+
cache_policy: CachePolicy | None
|
|
134
|
+
callbacks: Callbacks
|
|
135
|
+
timeout: TimeoutPolicy | None
|
|
136
|
+
|
|
137
|
+
def __init__(
|
|
138
|
+
self,
|
|
139
|
+
func: Callable,
|
|
140
|
+
input: tuple[tuple[Any, ...], dict[str, Any]],
|
|
141
|
+
*,
|
|
142
|
+
retry_policy: Sequence[RetryPolicy] | None,
|
|
143
|
+
cache_policy: CachePolicy | None,
|
|
144
|
+
callbacks: Callbacks,
|
|
145
|
+
timeout: TimeoutPolicy | None = None,
|
|
146
|
+
) -> None:
|
|
147
|
+
self.func = func
|
|
148
|
+
self.input = input
|
|
149
|
+
self.retry_policy = retry_policy
|
|
150
|
+
self.cache_policy = cache_policy
|
|
151
|
+
self.callbacks = callbacks
|
|
152
|
+
self.timeout = timeout
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def should_interrupt(
|
|
156
|
+
checkpoint: Checkpoint,
|
|
157
|
+
interrupt_nodes: All | Sequence[str],
|
|
158
|
+
tasks: Iterable[PregelExecutableTask],
|
|
159
|
+
) -> list[PregelExecutableTask]:
|
|
160
|
+
"""Check if the graph should be interrupted based on current state."""
|
|
161
|
+
version_type = type(next(iter(checkpoint["channel_versions"].values()), None))
|
|
162
|
+
null_version = version_type() # type: ignore[misc]
|
|
163
|
+
seen = checkpoint["versions_seen"].get(INTERRUPT, {})
|
|
164
|
+
# interrupt if any channel has been updated since last interrupt
|
|
165
|
+
any_updates_since_prev_interrupt = any(
|
|
166
|
+
version > seen.get(chan, null_version) # type: ignore[operator]
|
|
167
|
+
for chan, version in checkpoint["channel_versions"].items()
|
|
168
|
+
)
|
|
169
|
+
# and any triggered node is in interrupt_nodes list
|
|
170
|
+
return (
|
|
171
|
+
[
|
|
172
|
+
task
|
|
173
|
+
for task in tasks
|
|
174
|
+
if (
|
|
175
|
+
(
|
|
176
|
+
not task.config
|
|
177
|
+
or TAG_HIDDEN not in task.config.get("tags", EMPTY_SEQ)
|
|
178
|
+
)
|
|
179
|
+
if interrupt_nodes == "*"
|
|
180
|
+
else task.name in interrupt_nodes
|
|
181
|
+
)
|
|
182
|
+
]
|
|
183
|
+
if any_updates_since_prev_interrupt
|
|
184
|
+
else []
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def local_read(
|
|
189
|
+
scratchpad: PregelScratchpad,
|
|
190
|
+
channels: Mapping[str, BaseChannel],
|
|
191
|
+
managed: ManagedValueMapping,
|
|
192
|
+
task: WritesProtocol,
|
|
193
|
+
select: list[str] | str,
|
|
194
|
+
fresh: bool = False,
|
|
195
|
+
) -> dict[str, Any] | Any:
|
|
196
|
+
"""Function injected under CONFIG_KEY_READ in task config, to read current state.
|
|
197
|
+
Used by conditional edges to read a copy of the state with reflecting the writes
|
|
198
|
+
from that node only."""
|
|
199
|
+
updated: dict[str, list[Any]] = defaultdict(list)
|
|
200
|
+
if isinstance(select, str):
|
|
201
|
+
managed_keys = []
|
|
202
|
+
for c, v in task.writes:
|
|
203
|
+
if c == select:
|
|
204
|
+
updated[c].append(v)
|
|
205
|
+
else:
|
|
206
|
+
managed_keys = [k for k in select if k in managed]
|
|
207
|
+
select = [k for k in select if k not in managed]
|
|
208
|
+
for c, v in task.writes:
|
|
209
|
+
if c in select:
|
|
210
|
+
updated[c].append(v)
|
|
211
|
+
if fresh:
|
|
212
|
+
# apply writes
|
|
213
|
+
local_channels: dict[str, BaseChannel] = {}
|
|
214
|
+
for k in channels:
|
|
215
|
+
cc = channels[k].copy()
|
|
216
|
+
cc.update(updated[k])
|
|
217
|
+
local_channels[k] = cc
|
|
218
|
+
# read fresh values
|
|
219
|
+
values = read_channels(local_channels, select)
|
|
220
|
+
else:
|
|
221
|
+
values = read_channels(channels, select)
|
|
222
|
+
if managed_keys:
|
|
223
|
+
values.update({k: managed[k].get(scratchpad) for k in managed_keys})
|
|
224
|
+
return values
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def increment(current: int | None, channel: None) -> int:
|
|
228
|
+
"""Default channel versioning function, increments the current int version."""
|
|
229
|
+
return current + 1 if current is not None else 1
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def apply_writes(
|
|
233
|
+
checkpoint: Checkpoint,
|
|
234
|
+
channels: Mapping[str, BaseChannel],
|
|
235
|
+
tasks: Iterable[WritesProtocol],
|
|
236
|
+
get_next_version: GetNextVersion | None,
|
|
237
|
+
trigger_to_nodes: Mapping[str, Sequence[str]],
|
|
238
|
+
) -> set[str]:
|
|
239
|
+
"""Apply writes from a set of tasks (usually the tasks from a Pregel step)
|
|
240
|
+
to the checkpoint and channels, and return managed values writes to be applied
|
|
241
|
+
externally.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
checkpoint: The checkpoint to update.
|
|
245
|
+
channels: The channels to update.
|
|
246
|
+
tasks: The tasks to apply writes from.
|
|
247
|
+
get_next_version: Optional function to determine the next version of a channel.
|
|
248
|
+
trigger_to_nodes: Mapping of channel names to the set of nodes that can be triggered by updates to that channel.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Set of channels that were updated in this step.
|
|
252
|
+
"""
|
|
253
|
+
# sort tasks on path, to ensure deterministic order for update application
|
|
254
|
+
# any path parts after the 3rd are ignored for sorting
|
|
255
|
+
# (we use them for eg. task ids which aren't good for sorting)
|
|
256
|
+
tasks = sorted(tasks, key=lambda t: task_path_str(t.path[:3]))
|
|
257
|
+
# if no task has triggers this is applying writes from the null task only
|
|
258
|
+
# so we don't do anything other than update the channels written to
|
|
259
|
+
bump_step = any(t.triggers for t in tasks)
|
|
260
|
+
|
|
261
|
+
# update seen versions
|
|
262
|
+
for task in tasks:
|
|
263
|
+
checkpoint["versions_seen"].setdefault(task.name, {}).update(
|
|
264
|
+
{
|
|
265
|
+
chan: checkpoint["channel_versions"][chan]
|
|
266
|
+
for chan in task.triggers
|
|
267
|
+
if chan in checkpoint["channel_versions"]
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Find the highest version of all channels
|
|
272
|
+
if get_next_version is None:
|
|
273
|
+
next_version = None
|
|
274
|
+
else:
|
|
275
|
+
next_version = get_next_version(
|
|
276
|
+
(
|
|
277
|
+
max(checkpoint["channel_versions"].values())
|
|
278
|
+
if checkpoint["channel_versions"]
|
|
279
|
+
else None
|
|
280
|
+
),
|
|
281
|
+
None,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# Consume all channels that were read
|
|
285
|
+
for chan in {
|
|
286
|
+
chan
|
|
287
|
+
for task in tasks
|
|
288
|
+
for chan in task.triggers
|
|
289
|
+
if chan not in RESERVED and chan in channels
|
|
290
|
+
}:
|
|
291
|
+
if channels[chan].consume() and next_version is not None:
|
|
292
|
+
checkpoint["channel_versions"][chan] = next_version
|
|
293
|
+
|
|
294
|
+
# Group writes by channel
|
|
295
|
+
pending_writes_by_channel: dict[str, list[Any]] = defaultdict(list)
|
|
296
|
+
for task in tasks:
|
|
297
|
+
for chan, val in task.writes:
|
|
298
|
+
if chan in (
|
|
299
|
+
NO_WRITES,
|
|
300
|
+
PUSH,
|
|
301
|
+
RESUME,
|
|
302
|
+
INTERRUPT,
|
|
303
|
+
RETURN,
|
|
304
|
+
ERROR,
|
|
305
|
+
ERROR_SOURCE_NODE,
|
|
306
|
+
):
|
|
307
|
+
pass
|
|
308
|
+
elif chan in channels:
|
|
309
|
+
pending_writes_by_channel[chan].append(val)
|
|
310
|
+
else:
|
|
311
|
+
logger.warning(
|
|
312
|
+
f"Task {task.name} with path {task.path} wrote to unknown channel {chan}, ignoring it."
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Apply writes to channels
|
|
316
|
+
updated_channels: set[str] = set()
|
|
317
|
+
for chan, vals in pending_writes_by_channel.items():
|
|
318
|
+
if chan in channels:
|
|
319
|
+
if channels[chan].update(vals) and next_version is not None:
|
|
320
|
+
checkpoint["channel_versions"][chan] = next_version
|
|
321
|
+
# unavailable channels can't trigger tasks, so don't add them
|
|
322
|
+
if channels[chan].is_available():
|
|
323
|
+
updated_channels.add(chan)
|
|
324
|
+
|
|
325
|
+
# Channels that weren't updated in this step are notified of a new step
|
|
326
|
+
if bump_step:
|
|
327
|
+
for chan in channels:
|
|
328
|
+
if channels[chan].is_available() and chan not in updated_channels:
|
|
329
|
+
if channels[chan].update(EMPTY_SEQ) and next_version is not None:
|
|
330
|
+
checkpoint["channel_versions"][chan] = next_version
|
|
331
|
+
# unavailable channels can't trigger tasks, so don't add them
|
|
332
|
+
if channels[chan].is_available():
|
|
333
|
+
updated_channels.add(chan)
|
|
334
|
+
|
|
335
|
+
# If this is (tentatively) the last superstep, notify all channels of finish
|
|
336
|
+
if bump_step and updated_channels.isdisjoint(trigger_to_nodes):
|
|
337
|
+
for chan in channels:
|
|
338
|
+
if channels[chan].finish() and next_version is not None:
|
|
339
|
+
checkpoint["channel_versions"][chan] = next_version
|
|
340
|
+
# unavailable channels can't trigger tasks, so don't add them
|
|
341
|
+
if channels[chan].is_available():
|
|
342
|
+
updated_channels.add(chan)
|
|
343
|
+
|
|
344
|
+
# Return managed values writes to be applied externally
|
|
345
|
+
return updated_channels
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@overload
|
|
349
|
+
def prepare_next_tasks(
|
|
350
|
+
checkpoint: Checkpoint,
|
|
351
|
+
pending_writes: list[PendingWrite],
|
|
352
|
+
processes: Mapping[str, PregelNode],
|
|
353
|
+
channels: Mapping[str, BaseChannel],
|
|
354
|
+
managed: ManagedValueMapping,
|
|
355
|
+
config: RunnableConfig,
|
|
356
|
+
step: int,
|
|
357
|
+
stop: int,
|
|
358
|
+
*,
|
|
359
|
+
for_execution: Literal[False],
|
|
360
|
+
store: Literal[None] = None,
|
|
361
|
+
checkpointer: Literal[None] = None,
|
|
362
|
+
manager: Literal[None] = None,
|
|
363
|
+
trigger_to_nodes: Mapping[str, Sequence[str]] | None = None,
|
|
364
|
+
updated_channels: set[str] | None = None,
|
|
365
|
+
retry_policy: Sequence[RetryPolicy] = (),
|
|
366
|
+
cache_policy: Literal[None] = None,
|
|
367
|
+
) -> dict[str, PregelTask]: ...
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@overload
|
|
371
|
+
def prepare_next_tasks(
|
|
372
|
+
checkpoint: Checkpoint,
|
|
373
|
+
pending_writes: list[PendingWrite],
|
|
374
|
+
processes: Mapping[str, PregelNode],
|
|
375
|
+
channels: Mapping[str, BaseChannel],
|
|
376
|
+
managed: ManagedValueMapping,
|
|
377
|
+
config: RunnableConfig,
|
|
378
|
+
step: int,
|
|
379
|
+
stop: int,
|
|
380
|
+
*,
|
|
381
|
+
for_execution: Literal[True],
|
|
382
|
+
store: BaseStore | None,
|
|
383
|
+
checkpointer: BaseCheckpointSaver | None,
|
|
384
|
+
manager: None | ParentRunManager | AsyncParentRunManager,
|
|
385
|
+
trigger_to_nodes: Mapping[str, Sequence[str]] | None = None,
|
|
386
|
+
updated_channels: set[str] | None = None,
|
|
387
|
+
retry_policy: Sequence[RetryPolicy] = (),
|
|
388
|
+
cache_policy: CachePolicy | None = None,
|
|
389
|
+
) -> dict[str, PregelExecutableTask]: ...
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def prepare_next_tasks(
|
|
393
|
+
checkpoint: Checkpoint,
|
|
394
|
+
pending_writes: list[PendingWrite],
|
|
395
|
+
processes: Mapping[str, PregelNode],
|
|
396
|
+
channels: Mapping[str, BaseChannel],
|
|
397
|
+
managed: ManagedValueMapping,
|
|
398
|
+
config: RunnableConfig,
|
|
399
|
+
step: int,
|
|
400
|
+
stop: int,
|
|
401
|
+
*,
|
|
402
|
+
for_execution: bool,
|
|
403
|
+
store: BaseStore | None = None,
|
|
404
|
+
checkpointer: BaseCheckpointSaver | None = None,
|
|
405
|
+
manager: None | ParentRunManager | AsyncParentRunManager = None,
|
|
406
|
+
trigger_to_nodes: Mapping[str, Sequence[str]] | None = None,
|
|
407
|
+
updated_channels: set[str] | None = None,
|
|
408
|
+
retry_policy: Sequence[RetryPolicy] = (),
|
|
409
|
+
cache_policy: CachePolicy | None = None,
|
|
410
|
+
) -> dict[str, PregelTask] | dict[str, PregelExecutableTask]:
|
|
411
|
+
"""Prepare the set of tasks that will make up the next Pregel step.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
checkpoint: The current checkpoint.
|
|
415
|
+
pending_writes: The list of pending writes.
|
|
416
|
+
processes: The mapping of process names to PregelNode instances.
|
|
417
|
+
channels: The mapping of channel names to BaseChannel instances.
|
|
418
|
+
managed: The mapping of managed value names to functions.
|
|
419
|
+
config: The `Runnable` configuration.
|
|
420
|
+
step: The current step.
|
|
421
|
+
for_execution: Whether the tasks are being prepared for execution.
|
|
422
|
+
store: An instance of BaseStore to make it available for usage within tasks.
|
|
423
|
+
checkpointer: `Checkpointer` instance used for saving checkpoints.
|
|
424
|
+
manager: The parent run manager to use for the tasks.
|
|
425
|
+
trigger_to_nodes: Optional: Mapping of channel names to the set of nodes
|
|
426
|
+
that are can be triggered by that channel.
|
|
427
|
+
updated_channels: Optional. Set of channel names that have been updated during
|
|
428
|
+
the previous step. Using in conjunction with trigger_to_nodes to speed
|
|
429
|
+
up the process of determining which nodes should be triggered in the next
|
|
430
|
+
step.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
A dictionary of tasks to be executed. The keys are the task ids and the values
|
|
434
|
+
are the tasks themselves. This is the union of all PUSH tasks (Sends)
|
|
435
|
+
and PULL tasks (nodes triggered by edges).
|
|
436
|
+
"""
|
|
437
|
+
input_cache: dict[INPUT_CACHE_KEY_TYPE, Any] = {}
|
|
438
|
+
checkpoint_id_bytes = binascii.unhexlify(checkpoint["id"].replace("-", ""))
|
|
439
|
+
null_version = checkpoint_null_version(checkpoint)
|
|
440
|
+
tasks: list[PregelTask | PregelExecutableTask] = []
|
|
441
|
+
# Consume pending tasks
|
|
442
|
+
tasks_channel = cast(Topic[Send] | None, channels.get(TASKS))
|
|
443
|
+
if tasks_channel and tasks_channel.is_available():
|
|
444
|
+
for idx, _ in enumerate(tasks_channel.get()):
|
|
445
|
+
if task := prepare_single_task(
|
|
446
|
+
(PUSH, idx),
|
|
447
|
+
None,
|
|
448
|
+
checkpoint=checkpoint,
|
|
449
|
+
checkpoint_id_bytes=checkpoint_id_bytes,
|
|
450
|
+
checkpoint_null_version=null_version,
|
|
451
|
+
pending_writes=pending_writes,
|
|
452
|
+
processes=processes,
|
|
453
|
+
channels=channels,
|
|
454
|
+
managed=managed,
|
|
455
|
+
config=config,
|
|
456
|
+
step=step,
|
|
457
|
+
stop=stop,
|
|
458
|
+
for_execution=for_execution,
|
|
459
|
+
store=store,
|
|
460
|
+
checkpointer=checkpointer,
|
|
461
|
+
manager=manager,
|
|
462
|
+
input_cache=input_cache,
|
|
463
|
+
cache_policy=cache_policy,
|
|
464
|
+
retry_policy=retry_policy,
|
|
465
|
+
):
|
|
466
|
+
tasks.append(task)
|
|
467
|
+
|
|
468
|
+
# This section is an optimization that allows which nodes will be active
|
|
469
|
+
# during the next step.
|
|
470
|
+
# When there's information about:
|
|
471
|
+
# 1. Which channels were updated in the previous step
|
|
472
|
+
# 2. Which nodes are triggered by which channels
|
|
473
|
+
# Then we can determine which nodes should be triggered in the next step
|
|
474
|
+
# without having to cycle through all nodes.
|
|
475
|
+
if updated_channels and trigger_to_nodes:
|
|
476
|
+
triggered_nodes: set[str] = set()
|
|
477
|
+
# Get all nodes that have triggers associated with an updated channel
|
|
478
|
+
for channel in updated_channels:
|
|
479
|
+
if node_ids := trigger_to_nodes.get(channel):
|
|
480
|
+
triggered_nodes.update(node_ids)
|
|
481
|
+
# Sort the nodes to ensure deterministic order
|
|
482
|
+
candidate_nodes: Iterable[str] = sorted(triggered_nodes)
|
|
483
|
+
elif not checkpoint["channel_versions"]:
|
|
484
|
+
candidate_nodes = ()
|
|
485
|
+
else:
|
|
486
|
+
candidate_nodes = processes.keys()
|
|
487
|
+
|
|
488
|
+
# Check if any processes should be run in next step
|
|
489
|
+
# If so, prepare the values to be passed to them
|
|
490
|
+
for name in candidate_nodes:
|
|
491
|
+
if task := prepare_single_task(
|
|
492
|
+
(PULL, name),
|
|
493
|
+
None,
|
|
494
|
+
checkpoint=checkpoint,
|
|
495
|
+
checkpoint_id_bytes=checkpoint_id_bytes,
|
|
496
|
+
checkpoint_null_version=null_version,
|
|
497
|
+
pending_writes=pending_writes,
|
|
498
|
+
processes=processes,
|
|
499
|
+
channels=channels,
|
|
500
|
+
managed=managed,
|
|
501
|
+
config=config,
|
|
502
|
+
step=step,
|
|
503
|
+
stop=stop,
|
|
504
|
+
for_execution=for_execution,
|
|
505
|
+
store=store,
|
|
506
|
+
checkpointer=checkpointer,
|
|
507
|
+
manager=manager,
|
|
508
|
+
input_cache=input_cache,
|
|
509
|
+
cache_policy=cache_policy,
|
|
510
|
+
retry_policy=retry_policy,
|
|
511
|
+
):
|
|
512
|
+
tasks.append(task)
|
|
513
|
+
return {t.id: t for t in tasks}
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
PUSH_TRIGGER = (PUSH,)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
class _TaskIDFn(Protocol):
|
|
520
|
+
def __call__(self, namespace: bytes, *parts: str | bytes) -> str:
|
|
521
|
+
pass
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def prepare_single_task(
|
|
525
|
+
task_path: tuple[Any, ...],
|
|
526
|
+
task_id_checksum: str | None,
|
|
527
|
+
*,
|
|
528
|
+
checkpoint: Checkpoint,
|
|
529
|
+
checkpoint_id_bytes: bytes,
|
|
530
|
+
checkpoint_null_version: V | None,
|
|
531
|
+
pending_writes: list[PendingWrite],
|
|
532
|
+
processes: Mapping[str, PregelNode],
|
|
533
|
+
channels: Mapping[str, BaseChannel],
|
|
534
|
+
managed: ManagedValueMapping,
|
|
535
|
+
config: RunnableConfig,
|
|
536
|
+
step: int,
|
|
537
|
+
stop: int,
|
|
538
|
+
for_execution: bool,
|
|
539
|
+
store: BaseStore | None = None,
|
|
540
|
+
checkpointer: BaseCheckpointSaver | None = None,
|
|
541
|
+
manager: None | ParentRunManager | AsyncParentRunManager = None,
|
|
542
|
+
input_cache: dict[INPUT_CACHE_KEY_TYPE, Any] | None = None,
|
|
543
|
+
cache_policy: CachePolicy | None = None,
|
|
544
|
+
retry_policy: Sequence[RetryPolicy] = (),
|
|
545
|
+
) -> None | PregelTask | PregelExecutableTask:
|
|
546
|
+
"""Prepares a single task for the next Pregel step, given a task path, which
|
|
547
|
+
uniquely identifies a PUSH or PULL task within the graph."""
|
|
548
|
+
configurable = config.get(CONF, {})
|
|
549
|
+
parent_ns = configurable.get(CONFIG_KEY_CHECKPOINT_NS, "")
|
|
550
|
+
task_id_func = _xxhash_str if checkpoint["v"] > 1 else _uuid5_str
|
|
551
|
+
|
|
552
|
+
if task_path[0] == PUSH and isinstance(task_path[-1], Call):
|
|
553
|
+
return prepare_push_task_functional(
|
|
554
|
+
cast(tuple[str, tuple, int, str, Call], task_path),
|
|
555
|
+
task_id_checksum,
|
|
556
|
+
checkpoint=checkpoint,
|
|
557
|
+
checkpoint_id_bytes=checkpoint_id_bytes,
|
|
558
|
+
pending_writes=pending_writes,
|
|
559
|
+
channels=channels,
|
|
560
|
+
managed=managed,
|
|
561
|
+
config=config,
|
|
562
|
+
step=step,
|
|
563
|
+
stop=stop,
|
|
564
|
+
for_execution=for_execution,
|
|
565
|
+
store=store,
|
|
566
|
+
checkpointer=checkpointer,
|
|
567
|
+
manager=manager,
|
|
568
|
+
cache_policy=cache_policy,
|
|
569
|
+
retry_policy=retry_policy,
|
|
570
|
+
parent_ns=parent_ns,
|
|
571
|
+
task_id_func=task_id_func,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
elif task_path[0] == PUSH:
|
|
575
|
+
return prepare_push_task_send(
|
|
576
|
+
cast(tuple[str, tuple], task_path),
|
|
577
|
+
task_id_checksum,
|
|
578
|
+
checkpoint=checkpoint,
|
|
579
|
+
checkpoint_id_bytes=checkpoint_id_bytes,
|
|
580
|
+
pending_writes=pending_writes,
|
|
581
|
+
channels=channels,
|
|
582
|
+
managed=managed,
|
|
583
|
+
config=config,
|
|
584
|
+
step=step,
|
|
585
|
+
processes=processes,
|
|
586
|
+
stop=stop,
|
|
587
|
+
for_execution=for_execution,
|
|
588
|
+
store=store,
|
|
589
|
+
checkpointer=checkpointer,
|
|
590
|
+
manager=manager,
|
|
591
|
+
cache_policy=cache_policy,
|
|
592
|
+
retry_policy=retry_policy,
|
|
593
|
+
parent_ns=parent_ns,
|
|
594
|
+
task_id_func=task_id_func,
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
elif task_path[0] == PULL:
|
|
598
|
+
# (PULL, node name)
|
|
599
|
+
name = cast(str, task_path[1])
|
|
600
|
+
if name not in processes:
|
|
601
|
+
return
|
|
602
|
+
proc = processes[name]
|
|
603
|
+
if checkpoint_null_version is None:
|
|
604
|
+
return
|
|
605
|
+
# If any of the channels read by this process were updated
|
|
606
|
+
if _triggers(
|
|
607
|
+
channels,
|
|
608
|
+
checkpoint["channel_versions"],
|
|
609
|
+
checkpoint["versions_seen"].get(name),
|
|
610
|
+
checkpoint_null_version,
|
|
611
|
+
proc,
|
|
612
|
+
):
|
|
613
|
+
triggers = tuple(sorted(proc.triggers))
|
|
614
|
+
# create task id
|
|
615
|
+
checkpoint_ns = f"{parent_ns}{NS_SEP}{name}" if parent_ns else name
|
|
616
|
+
task_id = task_id_func(
|
|
617
|
+
checkpoint_id_bytes,
|
|
618
|
+
checkpoint_ns,
|
|
619
|
+
str(step),
|
|
620
|
+
name,
|
|
621
|
+
PULL,
|
|
622
|
+
*triggers,
|
|
623
|
+
)
|
|
624
|
+
task_checkpoint_ns = f"{checkpoint_ns}{NS_END}{task_id}"
|
|
625
|
+
# create scratchpad
|
|
626
|
+
scratchpad = _scratchpad(
|
|
627
|
+
config[CONF].get(CONFIG_KEY_SCRATCHPAD),
|
|
628
|
+
pending_writes,
|
|
629
|
+
task_id,
|
|
630
|
+
xxh3_128_hexdigest(task_checkpoint_ns.encode()),
|
|
631
|
+
config[CONF].get(CONFIG_KEY_RESUME_MAP),
|
|
632
|
+
step,
|
|
633
|
+
stop,
|
|
634
|
+
)
|
|
635
|
+
# create task input
|
|
636
|
+
try:
|
|
637
|
+
val = _proc_input(
|
|
638
|
+
proc,
|
|
639
|
+
managed,
|
|
640
|
+
channels,
|
|
641
|
+
for_execution=for_execution,
|
|
642
|
+
input_cache=input_cache,
|
|
643
|
+
scratchpad=scratchpad,
|
|
644
|
+
)
|
|
645
|
+
if val is MISSING:
|
|
646
|
+
return
|
|
647
|
+
except Exception as exc:
|
|
648
|
+
if SUPPORTS_EXC_NOTES:
|
|
649
|
+
exc.add_note(
|
|
650
|
+
f"Before task with name '{name}' and path '{task_path[:3]}'"
|
|
651
|
+
)
|
|
652
|
+
raise
|
|
653
|
+
|
|
654
|
+
metadata = {
|
|
655
|
+
"coco_graph_step": step,
|
|
656
|
+
"coco_graph_node": name,
|
|
657
|
+
"coco_graph_triggers": triggers,
|
|
658
|
+
"coco_graph_path": task_path[:3],
|
|
659
|
+
"coco_graph_checkpoint_ns": task_checkpoint_ns,
|
|
660
|
+
}
|
|
661
|
+
if task_id_checksum is not None:
|
|
662
|
+
assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
|
|
663
|
+
if for_execution:
|
|
664
|
+
if node := proc.node:
|
|
665
|
+
if proc.metadata:
|
|
666
|
+
metadata.update(proc.metadata)
|
|
667
|
+
writes: deque[tuple[str, Any]] = deque()
|
|
668
|
+
cache_policy = proc.cache_policy or cache_policy
|
|
669
|
+
if cache_policy:
|
|
670
|
+
args_key = cache_policy.key_func(val)
|
|
671
|
+
cache_key = CacheKey(
|
|
672
|
+
(
|
|
673
|
+
CACHE_NS_WRITES,
|
|
674
|
+
(identifier(proc) or "__dynamic__"),
|
|
675
|
+
name,
|
|
676
|
+
),
|
|
677
|
+
xxh3_128_hexdigest(
|
|
678
|
+
(
|
|
679
|
+
args_key.encode()
|
|
680
|
+
if isinstance(args_key, str)
|
|
681
|
+
else args_key
|
|
682
|
+
),
|
|
683
|
+
),
|
|
684
|
+
cache_policy.ttl,
|
|
685
|
+
)
|
|
686
|
+
else:
|
|
687
|
+
cache_key = None
|
|
688
|
+
runtime = cast(
|
|
689
|
+
Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME)
|
|
690
|
+
)
|
|
691
|
+
runtime = runtime.override(
|
|
692
|
+
previous=checkpoint["channel_values"].get(PREVIOUS, None),
|
|
693
|
+
store=store,
|
|
694
|
+
execution_info=ExecutionInfo(
|
|
695
|
+
checkpoint_id=checkpoint["id"],
|
|
696
|
+
checkpoint_ns=task_checkpoint_ns,
|
|
697
|
+
task_id=task_id,
|
|
698
|
+
thread_id=configurable.get(CONFIG_KEY_THREAD_ID),
|
|
699
|
+
run_id=str(rid) if (rid := config.get("run_id")) else None,
|
|
700
|
+
),
|
|
701
|
+
)
|
|
702
|
+
additional_config = {
|
|
703
|
+
"metadata": metadata,
|
|
704
|
+
"tags": proc.tags,
|
|
705
|
+
}
|
|
706
|
+
return PregelExecutableTask(
|
|
707
|
+
name,
|
|
708
|
+
val,
|
|
709
|
+
node,
|
|
710
|
+
writes,
|
|
711
|
+
patch_config(
|
|
712
|
+
merge_configs(
|
|
713
|
+
config, cast(RunnableConfig, additional_config)
|
|
714
|
+
),
|
|
715
|
+
run_name=name,
|
|
716
|
+
callbacks=(
|
|
717
|
+
manager.get_child(f"graph:step:{step}")
|
|
718
|
+
if manager
|
|
719
|
+
else None
|
|
720
|
+
),
|
|
721
|
+
configurable={
|
|
722
|
+
CONFIG_KEY_TASK_ID: task_id,
|
|
723
|
+
# deque.extend is thread-safe
|
|
724
|
+
CONFIG_KEY_SEND: writes.extend,
|
|
725
|
+
CONFIG_KEY_READ: partial(
|
|
726
|
+
local_read,
|
|
727
|
+
scratchpad,
|
|
728
|
+
channels,
|
|
729
|
+
managed,
|
|
730
|
+
PregelTaskWrites(
|
|
731
|
+
task_path[:3],
|
|
732
|
+
name,
|
|
733
|
+
writes,
|
|
734
|
+
triggers,
|
|
735
|
+
),
|
|
736
|
+
),
|
|
737
|
+
CONFIG_KEY_CHECKPOINTER: (
|
|
738
|
+
checkpointer
|
|
739
|
+
or configurable.get(CONFIG_KEY_CHECKPOINTER)
|
|
740
|
+
),
|
|
741
|
+
CONFIG_KEY_CHECKPOINT_MAP: {
|
|
742
|
+
**configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
|
|
743
|
+
parent_ns: checkpoint["id"],
|
|
744
|
+
},
|
|
745
|
+
CONFIG_KEY_CHECKPOINT_ID: None,
|
|
746
|
+
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
|
|
747
|
+
CONFIG_KEY_SCRATCHPAD: scratchpad,
|
|
748
|
+
CONFIG_KEY_RUNTIME: runtime,
|
|
749
|
+
},
|
|
750
|
+
),
|
|
751
|
+
triggers,
|
|
752
|
+
proc.retry_policy or retry_policy,
|
|
753
|
+
cache_key,
|
|
754
|
+
task_id,
|
|
755
|
+
task_path[:3],
|
|
756
|
+
writers=proc.flat_writers,
|
|
757
|
+
subgraphs=proc.subgraphs,
|
|
758
|
+
timeout=proc.timeout,
|
|
759
|
+
)
|
|
760
|
+
else:
|
|
761
|
+
return PregelTask(task_id, name, task_path[:3])
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _coerce_pending_error(value: Any) -> BaseException:
|
|
765
|
+
if isinstance(value, BaseException):
|
|
766
|
+
return value
|
|
767
|
+
return Exception(str(value))
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def _read_errors_from_pending_writes(
|
|
771
|
+
pending_writes: list[PendingWrite],
|
|
772
|
+
) -> list[BaseException]:
|
|
773
|
+
errors: list[BaseException] = []
|
|
774
|
+
for _, channel, value in pending_writes:
|
|
775
|
+
if channel == ERROR:
|
|
776
|
+
errors.append(_coerce_pending_error(value))
|
|
777
|
+
return errors
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _read_error_for_task_id_from_pending_writes(
|
|
781
|
+
pending_writes: list[PendingWrite], task_id: str
|
|
782
|
+
) -> BaseException | None:
|
|
783
|
+
for pending_task_id, channel, value in reversed(pending_writes):
|
|
784
|
+
if pending_task_id == task_id and channel == ERROR:
|
|
785
|
+
return _coerce_pending_error(value)
|
|
786
|
+
return None
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _read_error_source_node_from_pending_writes(
|
|
790
|
+
pending_writes: list[PendingWrite], task_id: str
|
|
791
|
+
) -> str | None:
|
|
792
|
+
for pending_task_id, channel, value in reversed(pending_writes):
|
|
793
|
+
if pending_task_id == task_id and channel == ERROR_SOURCE_NODE:
|
|
794
|
+
if isinstance(value, str):
|
|
795
|
+
return value
|
|
796
|
+
return str(value)
|
|
797
|
+
return None
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def prepare_push_task_functional(
|
|
801
|
+
task_path: tuple[str, tuple, int, str, Call],
|
|
802
|
+
# (PUSH, parent task path, idx of PUSH write, id of parent task, Call)
|
|
803
|
+
task_id_checksum: str | None,
|
|
804
|
+
*,
|
|
805
|
+
checkpoint: Checkpoint,
|
|
806
|
+
checkpoint_id_bytes: bytes,
|
|
807
|
+
pending_writes: list[PendingWrite],
|
|
808
|
+
channels: Mapping[str, BaseChannel],
|
|
809
|
+
managed: ManagedValueMapping,
|
|
810
|
+
config: RunnableConfig,
|
|
811
|
+
step: int,
|
|
812
|
+
stop: int,
|
|
813
|
+
for_execution: bool,
|
|
814
|
+
store: BaseStore | None = None,
|
|
815
|
+
checkpointer: BaseCheckpointSaver | None = None,
|
|
816
|
+
manager: None | ParentRunManager | AsyncParentRunManager = None,
|
|
817
|
+
cache_policy: CachePolicy | None = None,
|
|
818
|
+
retry_policy: Sequence[RetryPolicy] = (),
|
|
819
|
+
parent_ns: str,
|
|
820
|
+
# namespace: bytes, *parts: str | bytes
|
|
821
|
+
task_id_func: _TaskIDFn,
|
|
822
|
+
) -> PregelTask | PregelExecutableTask:
|
|
823
|
+
"""Prepare a push task with an attached caller. Used for the functional API."""
|
|
824
|
+
configurable = config.get(CONF, {})
|
|
825
|
+
|
|
826
|
+
call = task_path[-1]
|
|
827
|
+
proc_ = get_runnable_for_task(call.func)
|
|
828
|
+
name = proc_.name
|
|
829
|
+
if name is None:
|
|
830
|
+
raise ValueError("`call` functions must have a `__name__` attribute")
|
|
831
|
+
# create task id
|
|
832
|
+
triggers: Sequence[str] = PUSH_TRIGGER
|
|
833
|
+
checkpoint_ns = f"{parent_ns}{NS_SEP}{name}" if parent_ns else name
|
|
834
|
+
task_id = task_id_func(
|
|
835
|
+
checkpoint_id_bytes,
|
|
836
|
+
checkpoint_ns,
|
|
837
|
+
str(step),
|
|
838
|
+
name,
|
|
839
|
+
PUSH,
|
|
840
|
+
task_path_str(task_path[1]),
|
|
841
|
+
str(task_path[2]),
|
|
842
|
+
)
|
|
843
|
+
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
|
|
844
|
+
# we append True to the task path to indicate that a call is being
|
|
845
|
+
# made, so we should not return interrupts from this task (responsibility lies with the parent)
|
|
846
|
+
in_progress_task_path = (*task_path[:3], True)
|
|
847
|
+
metadata = {
|
|
848
|
+
"coco_graph_step": step,
|
|
849
|
+
"coco_graph_node": name,
|
|
850
|
+
"coco_graph_triggers": triggers,
|
|
851
|
+
"coco_graph_path": in_progress_task_path,
|
|
852
|
+
"coco_graph_checkpoint_ns": task_checkpoint_ns,
|
|
853
|
+
}
|
|
854
|
+
if task_id_checksum is not None:
|
|
855
|
+
assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
|
|
856
|
+
if for_execution:
|
|
857
|
+
writes: deque[tuple[str, Any]] = deque()
|
|
858
|
+
cache_policy = call.cache_policy or cache_policy
|
|
859
|
+
if cache_policy:
|
|
860
|
+
args_key = cache_policy.key_func(*call.input[0], **call.input[1])
|
|
861
|
+
cache_key: CacheKey | None = CacheKey(
|
|
862
|
+
(
|
|
863
|
+
CACHE_NS_WRITES,
|
|
864
|
+
(identifier(call.func) or "__dynamic__"),
|
|
865
|
+
),
|
|
866
|
+
xxh3_128_hexdigest(
|
|
867
|
+
args_key.encode() if isinstance(args_key, str) else args_key,
|
|
868
|
+
),
|
|
869
|
+
cache_policy.ttl,
|
|
870
|
+
)
|
|
871
|
+
else:
|
|
872
|
+
cache_key = None
|
|
873
|
+
scratchpad = _scratchpad(
|
|
874
|
+
configurable.get(CONFIG_KEY_SCRATCHPAD),
|
|
875
|
+
pending_writes,
|
|
876
|
+
task_id,
|
|
877
|
+
xxh3_128_hexdigest(task_checkpoint_ns.encode()),
|
|
878
|
+
configurable.get(CONFIG_KEY_RESUME_MAP),
|
|
879
|
+
step,
|
|
880
|
+
stop,
|
|
881
|
+
)
|
|
882
|
+
runtime = cast(Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME))
|
|
883
|
+
runtime = runtime.override(
|
|
884
|
+
store=store,
|
|
885
|
+
execution_info=ExecutionInfo(
|
|
886
|
+
checkpoint_id=checkpoint["id"],
|
|
887
|
+
checkpoint_ns=task_checkpoint_ns,
|
|
888
|
+
task_id=task_id,
|
|
889
|
+
thread_id=configurable.get(CONFIG_KEY_THREAD_ID),
|
|
890
|
+
run_id=str(rid) if (rid := config.get("run_id")) else None,
|
|
891
|
+
),
|
|
892
|
+
)
|
|
893
|
+
return PregelExecutableTask(
|
|
894
|
+
name,
|
|
895
|
+
call.input,
|
|
896
|
+
proc_,
|
|
897
|
+
writes,
|
|
898
|
+
patch_config(
|
|
899
|
+
merge_configs(config, {"metadata": metadata}),
|
|
900
|
+
run_name=name,
|
|
901
|
+
callbacks=call.callbacks
|
|
902
|
+
or (manager.get_child(f"graph:step:{step}") if manager else None),
|
|
903
|
+
configurable={
|
|
904
|
+
CONFIG_KEY_TASK_ID: task_id,
|
|
905
|
+
# deque.extend is thread-safe
|
|
906
|
+
CONFIG_KEY_SEND: writes.extend,
|
|
907
|
+
CONFIG_KEY_READ: partial(
|
|
908
|
+
local_read,
|
|
909
|
+
scratchpad,
|
|
910
|
+
channels,
|
|
911
|
+
managed,
|
|
912
|
+
PregelTaskWrites(in_progress_task_path, name, writes, triggers),
|
|
913
|
+
),
|
|
914
|
+
CONFIG_KEY_CHECKPOINTER: (
|
|
915
|
+
checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
|
|
916
|
+
),
|
|
917
|
+
CONFIG_KEY_CHECKPOINT_MAP: {
|
|
918
|
+
**configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
|
|
919
|
+
parent_ns: checkpoint["id"],
|
|
920
|
+
},
|
|
921
|
+
CONFIG_KEY_CHECKPOINT_ID: None,
|
|
922
|
+
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
|
|
923
|
+
CONFIG_KEY_SCRATCHPAD: scratchpad,
|
|
924
|
+
CONFIG_KEY_RUNTIME: runtime,
|
|
925
|
+
},
|
|
926
|
+
),
|
|
927
|
+
triggers,
|
|
928
|
+
call.retry_policy or retry_policy,
|
|
929
|
+
cache_key,
|
|
930
|
+
task_id,
|
|
931
|
+
in_progress_task_path,
|
|
932
|
+
timeout=call.timeout,
|
|
933
|
+
)
|
|
934
|
+
else:
|
|
935
|
+
return PregelTask(task_id, name, in_progress_task_path)
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def prepare_push_task_send(
|
|
939
|
+
task_path: tuple[str, tuple],
|
|
940
|
+
# (PUSH, parent task path)
|
|
941
|
+
task_id_checksum: str | None,
|
|
942
|
+
*,
|
|
943
|
+
checkpoint: Checkpoint,
|
|
944
|
+
checkpoint_id_bytes: bytes,
|
|
945
|
+
pending_writes: list[PendingWrite],
|
|
946
|
+
channels: Mapping[str, BaseChannel],
|
|
947
|
+
managed: ManagedValueMapping,
|
|
948
|
+
config: RunnableConfig,
|
|
949
|
+
step: int,
|
|
950
|
+
stop: int,
|
|
951
|
+
for_execution: bool,
|
|
952
|
+
store: BaseStore | None = None,
|
|
953
|
+
checkpointer: BaseCheckpointSaver | None = None,
|
|
954
|
+
manager: None | ParentRunManager | AsyncParentRunManager = None,
|
|
955
|
+
cache_policy: CachePolicy | None = None,
|
|
956
|
+
retry_policy: Sequence[RetryPolicy] = (),
|
|
957
|
+
parent_ns: str,
|
|
958
|
+
task_id_func: _TaskIDFn,
|
|
959
|
+
processes: Mapping[str, PregelNode],
|
|
960
|
+
) -> PregelTask | PregelExecutableTask | None:
|
|
961
|
+
if len(task_path) == 2:
|
|
962
|
+
# SEND tasks, executed in superstep n+1
|
|
963
|
+
# (PUSH, idx of pending send)
|
|
964
|
+
idx = cast(int, task_path[1])
|
|
965
|
+
if not channels[TASKS].is_available():
|
|
966
|
+
return
|
|
967
|
+
sends: Sequence[Send] = channels[TASKS].get()
|
|
968
|
+
if idx < 0 or idx >= len(sends):
|
|
969
|
+
return
|
|
970
|
+
packet = sends[idx]
|
|
971
|
+
if not isinstance(packet, Send):
|
|
972
|
+
logger.warning(
|
|
973
|
+
f"Ignoring invalid packet type {type(packet)} in pending sends"
|
|
974
|
+
)
|
|
975
|
+
return
|
|
976
|
+
|
|
977
|
+
if packet.node not in processes:
|
|
978
|
+
logger.warning(f"Ignoring unknown node name {packet.node} in pending sends")
|
|
979
|
+
return
|
|
980
|
+
# find process
|
|
981
|
+
proc = processes[packet.node]
|
|
982
|
+
proc_node = proc.node
|
|
983
|
+
if proc_node is None:
|
|
984
|
+
return
|
|
985
|
+
# create task id
|
|
986
|
+
triggers = PUSH_TRIGGER
|
|
987
|
+
checkpoint_ns = (
|
|
988
|
+
f"{parent_ns}{NS_SEP}{packet.node}" if parent_ns else packet.node
|
|
989
|
+
)
|
|
990
|
+
task_id = task_id_func(
|
|
991
|
+
checkpoint_id_bytes,
|
|
992
|
+
checkpoint_ns,
|
|
993
|
+
str(step),
|
|
994
|
+
packet.node,
|
|
995
|
+
PUSH,
|
|
996
|
+
str(idx),
|
|
997
|
+
)
|
|
998
|
+
else:
|
|
999
|
+
logger.warning(f"Ignoring invalid PUSH task path {task_path}")
|
|
1000
|
+
return
|
|
1001
|
+
configurable = config.get(CONF, {})
|
|
1002
|
+
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
|
|
1003
|
+
# we append False to the task path to indicate that a call is not being made
|
|
1004
|
+
# so we should return interrupts from this task
|
|
1005
|
+
translated_task_path = (*task_path[:3], False)
|
|
1006
|
+
metadata = {
|
|
1007
|
+
"coco_graph_step": step,
|
|
1008
|
+
"coco_graph_node": packet.node,
|
|
1009
|
+
"coco_graph_triggers": triggers,
|
|
1010
|
+
"coco_graph_path": translated_task_path,
|
|
1011
|
+
"coco_graph_checkpoint_ns": task_checkpoint_ns,
|
|
1012
|
+
}
|
|
1013
|
+
if task_id_checksum is not None:
|
|
1014
|
+
assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
|
|
1015
|
+
if for_execution:
|
|
1016
|
+
if proc.metadata:
|
|
1017
|
+
metadata.update(proc.metadata)
|
|
1018
|
+
writes: deque[tuple[str, Any]] = deque()
|
|
1019
|
+
cache_policy = proc.cache_policy or cache_policy
|
|
1020
|
+
if cache_policy:
|
|
1021
|
+
args_key = cache_policy.key_func(packet.arg)
|
|
1022
|
+
cache_key = CacheKey(
|
|
1023
|
+
(
|
|
1024
|
+
CACHE_NS_WRITES,
|
|
1025
|
+
(identifier(proc) or "__dynamic__"),
|
|
1026
|
+
packet.node,
|
|
1027
|
+
),
|
|
1028
|
+
xxh3_128_hexdigest(
|
|
1029
|
+
args_key.encode() if isinstance(args_key, str) else args_key,
|
|
1030
|
+
),
|
|
1031
|
+
cache_policy.ttl,
|
|
1032
|
+
)
|
|
1033
|
+
else:
|
|
1034
|
+
cache_key = None
|
|
1035
|
+
scratchpad = _scratchpad(
|
|
1036
|
+
config[CONF].get(CONFIG_KEY_SCRATCHPAD),
|
|
1037
|
+
pending_writes,
|
|
1038
|
+
task_id,
|
|
1039
|
+
xxh3_128_hexdigest(task_checkpoint_ns.encode()),
|
|
1040
|
+
config[CONF].get(CONFIG_KEY_RESUME_MAP),
|
|
1041
|
+
step,
|
|
1042
|
+
stop,
|
|
1043
|
+
)
|
|
1044
|
+
runtime = cast(Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME))
|
|
1045
|
+
runtime = runtime.override(
|
|
1046
|
+
store=store,
|
|
1047
|
+
previous=checkpoint["channel_values"].get(PREVIOUS, None),
|
|
1048
|
+
execution_info=ExecutionInfo(
|
|
1049
|
+
checkpoint_id=checkpoint["id"],
|
|
1050
|
+
checkpoint_ns=task_checkpoint_ns,
|
|
1051
|
+
task_id=task_id,
|
|
1052
|
+
thread_id=configurable.get(CONFIG_KEY_THREAD_ID),
|
|
1053
|
+
run_id=str(rid) if (rid := config.get("run_id")) else None,
|
|
1054
|
+
),
|
|
1055
|
+
)
|
|
1056
|
+
additional_config: RunnableConfig = {
|
|
1057
|
+
"metadata": metadata,
|
|
1058
|
+
"tags": proc.tags,
|
|
1059
|
+
}
|
|
1060
|
+
return PregelExecutableTask(
|
|
1061
|
+
packet.node,
|
|
1062
|
+
packet.arg,
|
|
1063
|
+
proc_node,
|
|
1064
|
+
writes,
|
|
1065
|
+
patch_config(
|
|
1066
|
+
merge_configs(config, additional_config),
|
|
1067
|
+
run_name=packet.node,
|
|
1068
|
+
callbacks=(
|
|
1069
|
+
manager.get_child(f"graph:step:{step}") if manager else None
|
|
1070
|
+
),
|
|
1071
|
+
configurable={
|
|
1072
|
+
CONFIG_KEY_TASK_ID: task_id,
|
|
1073
|
+
# deque.extend is thread-safe
|
|
1074
|
+
CONFIG_KEY_SEND: writes.extend,
|
|
1075
|
+
CONFIG_KEY_READ: partial(
|
|
1076
|
+
local_read,
|
|
1077
|
+
scratchpad,
|
|
1078
|
+
channels,
|
|
1079
|
+
managed,
|
|
1080
|
+
PregelTaskWrites(
|
|
1081
|
+
translated_task_path, packet.node, writes, triggers
|
|
1082
|
+
),
|
|
1083
|
+
),
|
|
1084
|
+
CONFIG_KEY_CHECKPOINTER: (
|
|
1085
|
+
checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
|
|
1086
|
+
),
|
|
1087
|
+
CONFIG_KEY_CHECKPOINT_MAP: {
|
|
1088
|
+
**configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
|
|
1089
|
+
parent_ns: checkpoint["id"],
|
|
1090
|
+
},
|
|
1091
|
+
CONFIG_KEY_CHECKPOINT_ID: None,
|
|
1092
|
+
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
|
|
1093
|
+
CONFIG_KEY_SCRATCHPAD: scratchpad,
|
|
1094
|
+
CONFIG_KEY_RUNTIME: runtime,
|
|
1095
|
+
},
|
|
1096
|
+
),
|
|
1097
|
+
triggers,
|
|
1098
|
+
proc.retry_policy or retry_policy,
|
|
1099
|
+
cache_key,
|
|
1100
|
+
task_id,
|
|
1101
|
+
translated_task_path,
|
|
1102
|
+
writers=proc.flat_writers,
|
|
1103
|
+
subgraphs=proc.subgraphs,
|
|
1104
|
+
timeout=packet.timeout if packet.timeout is not None else proc.timeout,
|
|
1105
|
+
)
|
|
1106
|
+
else:
|
|
1107
|
+
return PregelTask(task_id, packet.node, translated_task_path)
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def prepare_node_error_handler_task(
|
|
1111
|
+
failed_task: PregelExecutableTask,
|
|
1112
|
+
*,
|
|
1113
|
+
handler_node_name: str,
|
|
1114
|
+
failed_error: BaseException,
|
|
1115
|
+
checkpoint: Checkpoint,
|
|
1116
|
+
pending_writes: list[PendingWrite],
|
|
1117
|
+
processes: Mapping[str, PregelNode],
|
|
1118
|
+
channels: Mapping[str, BaseChannel],
|
|
1119
|
+
managed: ManagedValueMapping,
|
|
1120
|
+
config: RunnableConfig,
|
|
1121
|
+
step: int,
|
|
1122
|
+
stop: int,
|
|
1123
|
+
store: BaseStore | None = None,
|
|
1124
|
+
checkpointer: BaseCheckpointSaver | None = None,
|
|
1125
|
+
manager: None | ParentRunManager | AsyncParentRunManager = None,
|
|
1126
|
+
cache_policy: CachePolicy | None = None,
|
|
1127
|
+
retry_policy: Sequence[RetryPolicy] = (),
|
|
1128
|
+
) -> PregelExecutableTask | None:
|
|
1129
|
+
"""Prepare an immediate node-level error handler task for a failed task."""
|
|
1130
|
+
if handler_node_name not in processes:
|
|
1131
|
+
return None
|
|
1132
|
+
proc = processes[handler_node_name]
|
|
1133
|
+
proc_node = proc.node
|
|
1134
|
+
if proc_node is None:
|
|
1135
|
+
return None
|
|
1136
|
+
|
|
1137
|
+
checkpoint_id_bytes = binascii.unhexlify(checkpoint["id"].replace("-", ""))
|
|
1138
|
+
task_id_func = _xxhash_str if checkpoint["v"] > 1 else _uuid5_str
|
|
1139
|
+
configurable = config.get(CONF, {})
|
|
1140
|
+
parent_ns = configurable.get(CONFIG_KEY_CHECKPOINT_NS, "")
|
|
1141
|
+
checkpoint_ns = (
|
|
1142
|
+
f"{parent_ns}{NS_SEP}{handler_node_name}" if parent_ns else handler_node_name
|
|
1143
|
+
)
|
|
1144
|
+
task_id = task_id_func(
|
|
1145
|
+
checkpoint_id_bytes,
|
|
1146
|
+
checkpoint_ns,
|
|
1147
|
+
str(step),
|
|
1148
|
+
handler_node_name,
|
|
1149
|
+
PUSH,
|
|
1150
|
+
"node_error_handler",
|
|
1151
|
+
failed_task.id,
|
|
1152
|
+
)
|
|
1153
|
+
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
|
|
1154
|
+
translated_task_path = (*failed_task.path[:3], "node_error_handler", False)
|
|
1155
|
+
metadata = {
|
|
1156
|
+
"coco_graph_step": step,
|
|
1157
|
+
"coco_graph_node": handler_node_name,
|
|
1158
|
+
"coco_graph_triggers": PUSH_TRIGGER,
|
|
1159
|
+
"coco_graph_path": translated_task_path,
|
|
1160
|
+
"coco_graph_checkpoint_ns": task_checkpoint_ns,
|
|
1161
|
+
}
|
|
1162
|
+
if proc.metadata:
|
|
1163
|
+
metadata.update(proc.metadata)
|
|
1164
|
+
writes: deque[tuple[str, Any]] = deque()
|
|
1165
|
+
|
|
1166
|
+
effective_retry_policy = proc.retry_policy or retry_policy
|
|
1167
|
+
effective_cache_policy = proc.cache_policy or cache_policy
|
|
1168
|
+
if effective_cache_policy:
|
|
1169
|
+
args_key = effective_cache_policy.key_func(failed_task.input)
|
|
1170
|
+
cache_key = CacheKey(
|
|
1171
|
+
(
|
|
1172
|
+
CACHE_NS_WRITES,
|
|
1173
|
+
(identifier(proc) or "__dynamic__"),
|
|
1174
|
+
handler_node_name,
|
|
1175
|
+
),
|
|
1176
|
+
xxh3_128_hexdigest(
|
|
1177
|
+
args_key.encode() if isinstance(args_key, str) else args_key
|
|
1178
|
+
),
|
|
1179
|
+
effective_cache_policy.ttl,
|
|
1180
|
+
)
|
|
1181
|
+
else:
|
|
1182
|
+
cache_key = None
|
|
1183
|
+
|
|
1184
|
+
scratchpad = _scratchpad(
|
|
1185
|
+
config[CONF].get(CONFIG_KEY_SCRATCHPAD),
|
|
1186
|
+
pending_writes,
|
|
1187
|
+
task_id,
|
|
1188
|
+
xxh3_128_hexdigest(task_checkpoint_ns.encode()),
|
|
1189
|
+
config[CONF].get(CONFIG_KEY_RESUME_MAP),
|
|
1190
|
+
step,
|
|
1191
|
+
stop,
|
|
1192
|
+
)
|
|
1193
|
+
runtime = cast(Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME))
|
|
1194
|
+
runtime = runtime.override(
|
|
1195
|
+
store=store, previous=checkpoint["channel_values"].get(PREVIOUS, None)
|
|
1196
|
+
)
|
|
1197
|
+
additional_config: RunnableConfig = {
|
|
1198
|
+
"metadata": metadata,
|
|
1199
|
+
"tags": proc.tags,
|
|
1200
|
+
}
|
|
1201
|
+
return PregelExecutableTask(
|
|
1202
|
+
handler_node_name,
|
|
1203
|
+
failed_task.input,
|
|
1204
|
+
proc_node,
|
|
1205
|
+
writes,
|
|
1206
|
+
patch_config(
|
|
1207
|
+
merge_configs(config, additional_config),
|
|
1208
|
+
run_name=handler_node_name,
|
|
1209
|
+
callbacks=manager.get_child(f"graph:step:{step}") if manager else None,
|
|
1210
|
+
configurable={
|
|
1211
|
+
CONFIG_KEY_TASK_ID: task_id,
|
|
1212
|
+
CONFIG_KEY_SEND: writes.extend,
|
|
1213
|
+
CONFIG_KEY_READ: partial(
|
|
1214
|
+
local_read,
|
|
1215
|
+
scratchpad,
|
|
1216
|
+
channels,
|
|
1217
|
+
managed,
|
|
1218
|
+
PregelTaskWrites(
|
|
1219
|
+
translated_task_path,
|
|
1220
|
+
handler_node_name,
|
|
1221
|
+
writes,
|
|
1222
|
+
PUSH_TRIGGER,
|
|
1223
|
+
),
|
|
1224
|
+
),
|
|
1225
|
+
CONFIG_KEY_CHECKPOINTER: (
|
|
1226
|
+
checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
|
|
1227
|
+
),
|
|
1228
|
+
CONFIG_KEY_CHECKPOINT_MAP: {
|
|
1229
|
+
**configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
|
|
1230
|
+
parent_ns: checkpoint["id"],
|
|
1231
|
+
},
|
|
1232
|
+
CONFIG_KEY_CHECKPOINT_ID: None,
|
|
1233
|
+
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
|
|
1234
|
+
CONFIG_KEY_SCRATCHPAD: scratchpad,
|
|
1235
|
+
CONFIG_KEY_RUNTIME: runtime,
|
|
1236
|
+
CONFIG_KEY_NODE_ERROR: NodeError(
|
|
1237
|
+
node=failed_task.name, error=failed_error
|
|
1238
|
+
),
|
|
1239
|
+
},
|
|
1240
|
+
),
|
|
1241
|
+
PUSH_TRIGGER,
|
|
1242
|
+
effective_retry_policy,
|
|
1243
|
+
cache_key,
|
|
1244
|
+
task_id,
|
|
1245
|
+
translated_task_path,
|
|
1246
|
+
writers=proc.flat_writers,
|
|
1247
|
+
subgraphs=proc.subgraphs,
|
|
1248
|
+
)
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
def checkpoint_null_version(
|
|
1252
|
+
checkpoint: Checkpoint,
|
|
1253
|
+
) -> V | None:
|
|
1254
|
+
"""Get the null version for the checkpoint, if available."""
|
|
1255
|
+
for version in checkpoint["channel_versions"].values():
|
|
1256
|
+
return type(version)()
|
|
1257
|
+
return None
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
def _triggers(
|
|
1261
|
+
channels: Mapping[str, BaseChannel],
|
|
1262
|
+
versions: ChannelVersions,
|
|
1263
|
+
seen: ChannelVersions | None,
|
|
1264
|
+
null_version: V,
|
|
1265
|
+
proc: PregelNode,
|
|
1266
|
+
) -> bool:
|
|
1267
|
+
if seen is None:
|
|
1268
|
+
for chan in proc.triggers:
|
|
1269
|
+
if channels[chan].is_available():
|
|
1270
|
+
return True
|
|
1271
|
+
else:
|
|
1272
|
+
for chan in proc.triggers:
|
|
1273
|
+
if channels[chan].is_available() and versions.get( # type: ignore[operator]
|
|
1274
|
+
chan, null_version
|
|
1275
|
+
) > seen.get(chan, null_version):
|
|
1276
|
+
return True
|
|
1277
|
+
return False
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
def _scratchpad(
|
|
1281
|
+
parent_scratchpad: PregelScratchpad | None,
|
|
1282
|
+
pending_writes: list[PendingWrite],
|
|
1283
|
+
task_id: str,
|
|
1284
|
+
namespace_hash: str,
|
|
1285
|
+
resume_map: dict[str, Any] | None,
|
|
1286
|
+
step: int,
|
|
1287
|
+
stop: int,
|
|
1288
|
+
) -> PregelScratchpad:
|
|
1289
|
+
if len(pending_writes) > 0:
|
|
1290
|
+
# find global resume value
|
|
1291
|
+
for w in pending_writes:
|
|
1292
|
+
if w[0] == NULL_TASK_ID and w[1] == RESUME:
|
|
1293
|
+
null_resume_write = w
|
|
1294
|
+
break
|
|
1295
|
+
else:
|
|
1296
|
+
# None cannot be used as a resume value, because it would be difficult to
|
|
1297
|
+
# distinguish from missing when used over http
|
|
1298
|
+
null_resume_write = None
|
|
1299
|
+
|
|
1300
|
+
# find task-specific resume value
|
|
1301
|
+
for w in pending_writes:
|
|
1302
|
+
if w[0] == task_id and w[1] == RESUME:
|
|
1303
|
+
task_resume_write = w[2]
|
|
1304
|
+
if not isinstance(task_resume_write, list):
|
|
1305
|
+
task_resume_write = [task_resume_write]
|
|
1306
|
+
break
|
|
1307
|
+
else:
|
|
1308
|
+
task_resume_write = []
|
|
1309
|
+
del w
|
|
1310
|
+
|
|
1311
|
+
# find namespace and task-specific resume value
|
|
1312
|
+
if resume_map and namespace_hash in resume_map:
|
|
1313
|
+
mapped_resume_write = resume_map[namespace_hash]
|
|
1314
|
+
task_resume_write.append(mapped_resume_write)
|
|
1315
|
+
|
|
1316
|
+
else:
|
|
1317
|
+
null_resume_write = None
|
|
1318
|
+
task_resume_write = []
|
|
1319
|
+
|
|
1320
|
+
def get_null_resume(consume: bool = False) -> Any:
|
|
1321
|
+
if null_resume_write is None:
|
|
1322
|
+
if parent_scratchpad is not None:
|
|
1323
|
+
return parent_scratchpad.get_null_resume(consume)
|
|
1324
|
+
return None
|
|
1325
|
+
if consume:
|
|
1326
|
+
try:
|
|
1327
|
+
pending_writes.remove(null_resume_write)
|
|
1328
|
+
return null_resume_write[2]
|
|
1329
|
+
except ValueError:
|
|
1330
|
+
return None
|
|
1331
|
+
return null_resume_write[2]
|
|
1332
|
+
|
|
1333
|
+
# using itertools.count as an atomic counter (+= 1 is not thread-safe)
|
|
1334
|
+
return PregelScratchpad(
|
|
1335
|
+
step=step,
|
|
1336
|
+
stop=stop,
|
|
1337
|
+
# call
|
|
1338
|
+
call_counter=LazyAtomicCounter(),
|
|
1339
|
+
# interrupt
|
|
1340
|
+
interrupt_counter=LazyAtomicCounter(),
|
|
1341
|
+
resume=task_resume_write,
|
|
1342
|
+
get_null_resume=get_null_resume,
|
|
1343
|
+
# subgraph
|
|
1344
|
+
subgraph_counter=LazyAtomicCounter(),
|
|
1345
|
+
)
|
|
1346
|
+
|
|
1347
|
+
|
|
1348
|
+
def _proc_input(
|
|
1349
|
+
proc: PregelNode,
|
|
1350
|
+
managed: ManagedValueMapping,
|
|
1351
|
+
channels: Mapping[str, BaseChannel],
|
|
1352
|
+
*,
|
|
1353
|
+
for_execution: bool,
|
|
1354
|
+
scratchpad: PregelScratchpad,
|
|
1355
|
+
input_cache: dict[INPUT_CACHE_KEY_TYPE, Any] | None,
|
|
1356
|
+
) -> Any:
|
|
1357
|
+
"""Prepare input for a PULL task, based on the process's channels and triggers."""
|
|
1358
|
+
# if in cache return shallow copy
|
|
1359
|
+
if input_cache is not None and proc.input_cache_key in input_cache:
|
|
1360
|
+
return copy(input_cache[proc.input_cache_key])
|
|
1361
|
+
# If all trigger channels subscribed by this process are not empty
|
|
1362
|
+
# then invoke the process with the values of all non-empty channels
|
|
1363
|
+
if isinstance(proc.channels, list):
|
|
1364
|
+
val: dict[str, Any] = {}
|
|
1365
|
+
for chan in proc.channels:
|
|
1366
|
+
if chan in channels:
|
|
1367
|
+
if channels[chan].is_available():
|
|
1368
|
+
val[chan] = channels[chan].get()
|
|
1369
|
+
else:
|
|
1370
|
+
val[chan] = managed[chan].get(scratchpad)
|
|
1371
|
+
elif isinstance(proc.channels, str):
|
|
1372
|
+
if proc.channels in channels:
|
|
1373
|
+
if channels[proc.channels].is_available():
|
|
1374
|
+
val = channels[proc.channels].get()
|
|
1375
|
+
else:
|
|
1376
|
+
return MISSING
|
|
1377
|
+
else:
|
|
1378
|
+
return MISSING
|
|
1379
|
+
else:
|
|
1380
|
+
raise RuntimeError(
|
|
1381
|
+
f"Invalid channels type, expected list or dict, got {proc.channels}"
|
|
1382
|
+
)
|
|
1383
|
+
|
|
1384
|
+
# If the process has a mapper, apply it to the value
|
|
1385
|
+
if for_execution and proc.mapper is not None:
|
|
1386
|
+
val = proc.mapper(val)
|
|
1387
|
+
|
|
1388
|
+
# Cache the input value
|
|
1389
|
+
if input_cache is not None:
|
|
1390
|
+
input_cache[proc.input_cache_key] = val
|
|
1391
|
+
|
|
1392
|
+
return val
|
|
1393
|
+
|
|
1394
|
+
|
|
1395
|
+
def _uuid5_str(namespace: bytes, *parts: str | bytes) -> str:
|
|
1396
|
+
"""Generate a UUID from the SHA-1 hash of a namespace and str parts."""
|
|
1397
|
+
|
|
1398
|
+
sha = sha1(namespace, usedforsecurity=False)
|
|
1399
|
+
sha.update(b"".join(p.encode() if isinstance(p, str) else p for p in parts))
|
|
1400
|
+
hex = sha.hexdigest()
|
|
1401
|
+
return f"{hex[:8]}-{hex[8:12]}-{hex[12:16]}-{hex[16:20]}-{hex[20:32]}"
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
def _xxhash_str(namespace: bytes, *parts: str | bytes) -> str:
|
|
1405
|
+
"""Generate a UUID from the XXH3 hash of a namespace and str parts."""
|
|
1406
|
+
hex = xxh3_128_hexdigest(
|
|
1407
|
+
namespace + b"".join(p.encode() if isinstance(p, str) else p for p in parts)
|
|
1408
|
+
)
|
|
1409
|
+
return f"{hex[:8]}-{hex[8:12]}-{hex[12:16]}-{hex[16:20]}-{hex[20:32]}"
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
def task_path_str(tup: str | int | tuple) -> str:
|
|
1413
|
+
"""Generate a string representation of the task path."""
|
|
1414
|
+
return (
|
|
1415
|
+
f"~{', '.join(task_path_str(x) for x in tup)}"
|
|
1416
|
+
if isinstance(tup, (tuple, list))
|
|
1417
|
+
else f"{tup:010d}"
|
|
1418
|
+
if isinstance(tup, int)
|
|
1419
|
+
else str(tup)
|
|
1420
|
+
)
|
|
1421
|
+
|
|
1422
|
+
|
|
1423
|
+
LAZY_ATOMIC_COUNTER_LOCK = threading.Lock()
|
|
1424
|
+
|
|
1425
|
+
|
|
1426
|
+
class LazyAtomicCounter:
|
|
1427
|
+
__slots__ = ("_counter",)
|
|
1428
|
+
|
|
1429
|
+
_counter: Callable[[], int] | None
|
|
1430
|
+
|
|
1431
|
+
def __init__(self) -> None:
|
|
1432
|
+
self._counter = None
|
|
1433
|
+
|
|
1434
|
+
def __call__(self) -> int:
|
|
1435
|
+
if self._counter is None:
|
|
1436
|
+
with LAZY_ATOMIC_COUNTER_LOCK:
|
|
1437
|
+
if self._counter is None:
|
|
1438
|
+
self._counter = itertools.count(0).__next__
|
|
1439
|
+
return self._counter()
|
|
1440
|
+
|
|
1441
|
+
|
|
1442
|
+
def sanitize_untracked_values_in_send(
|
|
1443
|
+
packet: Send, channels: Mapping[str, BaseChannel]
|
|
1444
|
+
) -> Send:
|
|
1445
|
+
"""Pop any values belonging to UntrackedValue channels in Send.arg for safe checkpointing.
|
|
1446
|
+
|
|
1447
|
+
Send is often called with state to be passed to the dest node, which may contain
|
|
1448
|
+
UntrackedValues at the top level. Send is not typed and arg may be a nested dict."""
|
|
1449
|
+
|
|
1450
|
+
if not isinstance(packet.arg, dict):
|
|
1451
|
+
# Command
|
|
1452
|
+
return packet
|
|
1453
|
+
|
|
1454
|
+
# top level keys should be the channel names
|
|
1455
|
+
sanitized_arg = {
|
|
1456
|
+
k: v
|
|
1457
|
+
for k, v in packet.arg.items()
|
|
1458
|
+
if not isinstance(channels.get(k), UntrackedValue)
|
|
1459
|
+
}
|
|
1460
|
+
return Send(node=packet.node, arg=sanitized_arg, timeout=packet.timeout)
|