pyfect-dev 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.
- pyfect/__init__.py +729 -0
- pyfect/_all.py +242 -0
- pyfect/_background.py +28 -0
- pyfect/_binary_codecs.py +124 -0
- pyfect/_cached.py +165 -0
- pyfect/_catch.py +128 -0
- pyfect/_catch_reason.py +574 -0
- pyfect/_concurrency.py +291 -0
- pyfect/_context_effects.py +163 -0
- pyfect/_context_managers.py +76 -0
- pyfect/_core.py +368 -0
- pyfect/_digits.py +71 -0
- pyfect/_effect_args.py +46 -0
- pyfect/_effect_base.py +71 -0
- pyfect/_effect_concurrency.py +133 -0
- pyfect/_effect_eager.py +103 -0
- pyfect/_effect_errors.py +344 -0
- pyfect/_effect_folding.py +110 -0
- pyfect/_effect_hash.py +140 -0
- pyfect/_effect_logging.py +57 -0
- pyfect/_effect_metrics.py +102 -0
- pyfect/_effect_resources.py +179 -0
- pyfect/_effect_services.py +25 -0
- pyfect/_effect_timing.py +199 -0
- pyfect/_effect_tracing.py +99 -0
- pyfect/_equal.py +195 -0
- pyfect/_extras.py +43 -0
- pyfect/_fiber.py +969 -0
- pyfect/_fiber_all.py +134 -0
- pyfect/_fiber_collections.py +106 -0
- pyfect/_fn.py +706 -0
- pyfect/_hamt.py +174 -0
- pyfect/_interrupt_reason.py +33 -0
- pyfect/_isaac.py +149 -0
- pyfect/_iterables.py +231 -0
- pyfect/_js.py +263 -0
- pyfect/_js_date.py +87 -0
- pyfect/_json_text.py +286 -0
- pyfect/_json_value.py +9 -0
- pyfect/_layer_core.py +403 -0
- pyfect/_layer_for_life.py +33 -0
- pyfect/_layer_tracing.py +70 -0
- pyfect/_locked_ref.py +147 -0
- pyfect/_loop_thread.py +152 -0
- pyfect/_metric_ref.py +29 -0
- pyfect/_one_or_many.py +14 -0
- pyfect/_os_wait.py +118 -0
- pyfect/_path_posix.py +129 -0
- pyfect/_primitives.py +434 -0
- pyfect/_public.py +36 -0
- pyfect/_reasons.py +64 -0
- pyfect/_released.py +67 -0
- pyfect/_root.py +87 -0
- pyfect/_round_robin.py +73 -0
- pyfect/_some.py +17 -0
- pyfect/_stack.py +52 -0
- pyfect/_stack_frame.py +114 -0
- pyfect/_string_hash.py +35 -0
- pyfect/_subscriber_queue.py +47 -0
- pyfect/_tags.py +14 -0
- pyfect/_tracer_ref.py +21 -0
- pyfect/_tx_ops.py +77 -0
- pyfect/_unsubscripted.py +23 -0
- pyfect/_url.py +54 -0
- pyfect/_waiters.py +127 -0
- pyfect/_wake_tracking.py +98 -0
- pyfect/_wire_codec.py +123 -0
- pyfect/ai/__init__.py +73 -0
- pyfect/ai/_approvals.py +249 -0
- pyfect/ai/_decision_answers.py +185 -0
- pyfect/ai/_generated_client.py +298 -0
- pyfect/ai/_generated_support.py +84 -0
- pyfect/ai/_mcp/__init__.py +1 -0
- pyfect/ai/_mcp/batch.py +68 -0
- pyfect/ai/_mcp/client_transport.py +412 -0
- pyfect/ai/_mcp/client_wire.py +284 -0
- pyfect/ai/_mcp/core.py +490 -0
- pyfect/ai/_mcp/dispatch.py +1048 -0
- pyfect/ai/_mcp/http.py +462 -0
- pyfect/ai/_mcp/http_admission.py +269 -0
- pyfect/ai/_mcp/http_status.py +27 -0
- pyfect/ai/_mcp/input_wire.py +226 -0
- pyfect/ai/_mcp/log_levels.py +55 -0
- pyfect/ai/_mcp/param_headers.py +63 -0
- pyfect/ai/_mcp/projection.py +248 -0
- pyfect/ai/_mcp/register.py +683 -0
- pyfect/ai/_mcp/reverse_policy.py +155 -0
- pyfect/ai/_mcp/routing_header.py +24 -0
- pyfect/ai/_mcp/sessions.py +114 -0
- pyfect/ai/_mcp/stdio.py +110 -0
- pyfect/ai/_mcp/subscriptions.py +134 -0
- pyfect/ai/_mcp/tool_schema.py +35 -0
- pyfect/ai/_noul_answers.py +64 -0
- pyfect/ai/_openai_all_of.py +137 -0
- pyfect/ai/_openai_errors.py +233 -0
- pyfect/ai/_provider.py +439 -0
- pyfect/ai/_provider_tools.py +129 -0
- pyfect/ai/_stream_resolution.py +231 -0
- pyfect/ai/_structured_output_checks.py +138 -0
- pyfect/ai/_structured_output_codec.py +229 -0
- pyfect/ai/_structured_output_json.py +109 -0
- pyfect/ai/_tool_call_ids.py +80 -0
- pyfect/ai/ai_error.py +785 -0
- pyfect/ai/anthropic/__init__.py +28 -0
- pyfect/ai/anthropic/_errors.py +164 -0
- pyfect/ai/anthropic/_prompt.py +375 -0
- pyfect/ai/anthropic/_response.py +443 -0
- pyfect/ai/anthropic/_schema.py +206 -0
- pyfect/ai/anthropic/_stream.py +377 -0
- pyfect/ai/anthropic/_tools.py +248 -0
- pyfect/ai/anthropic/client.py +289 -0
- pyfect/ai/anthropic/config.py +32 -0
- pyfect/ai/anthropic/error.py +27 -0
- pyfect/ai/anthropic/language_model.py +366 -0
- pyfect/ai/anthropic/telemetry.py +51 -0
- pyfect/ai/anthropic/tool.py +570 -0
- pyfect/ai/anthropic_structured_output.py +138 -0
- pyfect/ai/chat.py +548 -0
- pyfect/ai/cloudflare/__init__.py +15 -0
- pyfect/ai/cloudflare/_errors.py +108 -0
- pyfect/ai/cloudflare/client.py +174 -0
- pyfect/ai/cloudflare/decision_model.py +117 -0
- pyfect/ai/cloudflare/schema.py +144 -0
- pyfect/ai/decision.py +290 -0
- pyfect/ai/decision_model.py +283 -0
- pyfect/ai/embedding_model.py +177 -0
- pyfect/ai/id_generator.py +79 -0
- pyfect/ai/language_model.py +1077 -0
- pyfect/ai/mcp_client.py +682 -0
- pyfect/ai/mcp_client_error.py +223 -0
- pyfect/ai/mcp_protocol.py +205 -0
- pyfect/ai/mcp_schema.py +1300 -0
- pyfect/ai/mcp_server.py +404 -0
- pyfect/ai/model.py +56 -0
- pyfect/ai/openai/__init__.py +49 -0
- pyfect/ai/openai/_errors.py +89 -0
- pyfect/ai/openai/_prompt.py +527 -0
- pyfect/ai/openai/_requests.py +34 -0
- pyfect/ai/openai/_response.py +569 -0
- pyfect/ai/openai/_schema.py +769 -0
- pyfect/ai/openai/_stream.py +771 -0
- pyfect/ai/openai/_tools.py +128 -0
- pyfect/ai/openai/_web_socket.py +295 -0
- pyfect/ai/openai/client.py +296 -0
- pyfect/ai/openai/client_generated.py +123 -0
- pyfect/ai/openai/config.py +34 -0
- pyfect/ai/openai/decision_model.py +177 -0
- pyfect/ai/openai/embedding_model.py +116 -0
- pyfect/ai/openai/error.py +8 -0
- pyfect/ai/openai/generated/__init__.py +7213 -0
- pyfect/ai/openai/generated/_recursive.py +18 -0
- pyfect/ai/openai/generated/_schemas_0.py +1015 -0
- pyfect/ai/openai/generated/_schemas_1.py +1016 -0
- pyfect/ai/openai/generated/_schemas_2.py +1017 -0
- pyfect/ai/openai/generated/_schemas_3.py +615 -0
- pyfect/ai/openai/generated/client.py +5298 -0
- pyfect/ai/openai/generated/types.py +8123 -0
- pyfect/ai/openai/language_model.py +304 -0
- pyfect/ai/openai/telemetry.py +19 -0
- pyfect/ai/openai/tool.py +391 -0
- pyfect/ai/openai_compat/__init__.py +25 -0
- pyfect/ai/openai_compat/_prompt.py +191 -0
- pyfect/ai/openai_compat/_request.py +153 -0
- pyfect/ai/openai_compat/_response.py +407 -0
- pyfect/ai/openai_compat/_schema.py +97 -0
- pyfect/ai/openai_compat/client.py +234 -0
- pyfect/ai/openai_compat/config.py +32 -0
- pyfect/ai/openai_compat/embedding_model.py +139 -0
- pyfect/ai/openai_compat/error.py +7 -0
- pyfect/ai/openai_compat/language_model.py +245 -0
- pyfect/ai/openai_compat/telemetry.py +51 -0
- pyfect/ai/openai_structured_output.py +118 -0
- pyfect/ai/openrouter/__init__.py +36 -0
- pyfect/ai/openrouter/_errors.py +186 -0
- pyfect/ai/openrouter/_prompt.py +272 -0
- pyfect/ai/openrouter/_response.py +226 -0
- pyfect/ai/openrouter/_stream.py +320 -0
- pyfect/ai/openrouter/client.py +283 -0
- pyfect/ai/openrouter/config.py +36 -0
- pyfect/ai/openrouter/decision_model.py +161 -0
- pyfect/ai/openrouter/error.py +23 -0
- pyfect/ai/openrouter/generated/__init__.py +3029 -0
- pyfect/ai/openrouter/generated/_recursive.py +15 -0
- pyfect/ai/openrouter/generated/_schemas_0.py +1015 -0
- pyfect/ai/openrouter/generated/_schemas_1.py +522 -0
- pyfect/ai/openrouter/generated/client.py +1445 -0
- pyfect/ai/openrouter/generated/types.py +4769 -0
- pyfect/ai/openrouter/language_model.py +345 -0
- pyfect/ai/openrouter/schema.py +165 -0
- pyfect/ai/prompt.py +664 -0
- pyfect/ai/response.py +768 -0
- pyfect/ai/response_id_tracker.py +82 -0
- pyfect/ai/telemetry.py +126 -0
- pyfect/ai/tokenizer.py +61 -0
- pyfect/ai/tool.py +536 -0
- pyfect/ai/toolkit.py +490 -0
- pyfect/ai/typesafe/__init__.py +17 -0
- pyfect/ai/typesafe/_errors.py +120 -0
- pyfect/ai/typesafe/client.py +146 -0
- pyfect/ai/typesafe/config.py +34 -0
- pyfect/ai/typesafe/decision_model.py +90 -0
- pyfect/ai/typesafe/schema.py +140 -0
- pyfect/batching/__init__.py +58 -0
- pyfect/batching/_api.py +71 -0
- pyfect/batching/_engine.py +229 -0
- pyfect/batching/_from_effect.py +76 -0
- pyfect/batching/_persisted.py +98 -0
- pyfect/batching/_request.py +133 -0
- pyfect/batching/_resolver.py +477 -0
- pyfect/batching/_with_cache.py +90 -0
- pyfect/big_decimal.py +500 -0
- pyfect/brand.py +114 -0
- pyfect/byte_size.py +354 -0
- pyfect/cache/__init__.py +29 -0
- pyfect/cache/_cache.py +426 -0
- pyfect/cache/_expiry.py +69 -0
- pyfect/cache/_scoped_cache.py +546 -0
- pyfect/case.py +174 -0
- pyfect/cause.py +462 -0
- pyfect/channel/__init__.py +155 -0
- pyfect/channel/_base.py +92 -0
- pyfect/channel/_catch.py +158 -0
- pyfect/channel/_catch_reason.py +572 -0
- pyfect/channel/_channel.py +158 -0
- pyfect/channel/_combinators.py +220 -0
- pyfect/channel/_concurrency_methods.py +217 -0
- pyfect/channel/_concurrent_map.py +118 -0
- pyfect/channel/_constructors.py +349 -0
- pyfect/channel/_context.py +232 -0
- pyfect/channel/_error_methods.py +336 -0
- pyfect/channel/_errors.py +180 -0
- pyfect/channel/_filters.py +225 -0
- pyfect/channel/_lifecycle.py +71 -0
- pyfect/channel/_lifecycle_methods.py +206 -0
- pyfect/channel/_loop.py +100 -0
- pyfect/channel/_merge.py +246 -0
- pyfect/channel/_repeat.py +154 -0
- pyfect/channel/_run.py +149 -0
- pyfect/channel/_run_methods.py +341 -0
- pyfect/channel/_sinks.py +88 -0
- pyfect/channel/_sources.py +238 -0
- pyfect/channel/_text.py +176 -0
- pyfect/channel/_transform_methods.py +415 -0
- pyfect/channel/_upstream.py +67 -0
- pyfect/channel_schema.py +131 -0
- pyfect/cli/__init__.py +79 -0
- pyfect/cli/_ansi.py +82 -0
- pyfect/cli/_command.py +469 -0
- pyfect/cli/_config.py +164 -0
- pyfect/cli/_environment.py +10 -0
- pyfect/cli/_help.py +195 -0
- pyfect/cli/_lexer.py +81 -0
- pyfect/cli/_parser.py +465 -0
- pyfect/cli/_settings.py +85 -0
- pyfect/cli/_suggest.py +33 -0
- pyfect/cli/_width.py +134 -0
- pyfect/cli/_wizard.py +317 -0
- pyfect/cli/argument.py +212 -0
- pyfect/cli/cli_config.py +40 -0
- pyfect/cli/cli_error.py +242 -0
- pyfect/cli/cli_output.py +305 -0
- pyfect/cli/command.py +440 -0
- pyfect/cli/completions/__init__.py +50 -0
- pyfect/cli/completions/_completer.ps1 +190 -0
- pyfect/cli/completions/_descriptor.py +182 -0
- pyfect/cli/completions/bash.py +300 -0
- pyfect/cli/completions/fish.py +164 -0
- pyfect/cli/completions/powershell.py +156 -0
- pyfect/cli/completions/zsh.py +189 -0
- pyfect/cli/flag.py +246 -0
- pyfect/cli/global_flag.py +99 -0
- pyfect/cli/help_doc.py +63 -0
- pyfect/cli/param.py +789 -0
- pyfect/cli/primitive.py +308 -0
- pyfect/cli/prompt/__init__.py +75 -0
- pyfect/cli/prompt/_auto_complete.py +205 -0
- pyfect/cli/prompt/_choices.py +104 -0
- pyfect/cli/prompt/_confirm.py +195 -0
- pyfect/cli/prompt/_core.py +512 -0
- pyfect/cli/prompt/_date.py +173 -0
- pyfect/cli/prompt/_date_parts.py +387 -0
- pyfect/cli/prompt/_file.py +372 -0
- pyfect/cli/prompt/_multi_select.py +204 -0
- pyfect/cli/prompt/_number.py +257 -0
- pyfect/cli/prompt/_select.py +117 -0
- pyfect/cli/prompt/_text.py +239 -0
- pyfect/cli/terminal_capabilities.py +118 -0
- pyfect/clock.py +150 -0
- pyfect/cluster/__init__.py +155 -0
- pyfect/cluster/_cluster_abandon.py +36 -0
- pyfect/cluster/_cluster_layer.py +122 -0
- pyfect/cluster/_entity_activation.py +16 -0
- pyfect/cluster/_entity_manager/__init__.py +22 -0
- pyfect/cluster/_entity_manager/_activation.py +540 -0
- pyfect/cluster/_entity_manager/_core.py +186 -0
- pyfect/cluster/_entity_manager/_decode.py +60 -0
- pyfect/cluster/_entity_manager/_manager.py +423 -0
- pyfect/cluster/_entity_manager/_state.py +113 -0
- pyfect/cluster/_entity_reaper.py +94 -0
- pyfect/cluster/_hash.py +13 -0
- pyfect/cluster/_interruptors.py +88 -0
- pyfect/cluster/_resource_map.py +179 -0
- pyfect/cluster/_resource_ref.py +266 -0
- pyfect/cluster/_shard_lock.py +14 -0
- pyfect/cluster/cluster_cron.py +230 -0
- pyfect/cluster/cluster_error.py +81 -0
- pyfect/cluster/cluster_http.py +210 -0
- pyfect/cluster/cluster_metrics.py +54 -0
- pyfect/cluster/cluster_schema.py +137 -0
- pyfect/cluster/cluster_socket.py +197 -0
- pyfect/cluster/cluster_workflow_engine/__init__.py +24 -0
- pyfect/cluster/cluster_workflow_engine/_engine.py +1099 -0
- pyfect/cluster/cluster_workflow_engine/_entities.py +298 -0
- pyfect/cluster/deliver_at.py +31 -0
- pyfect/cluster/entity/__init__.py +39 -0
- pyfect/cluster/entity/_entity.py +332 -0
- pyfect/cluster/entity/_handlers.py +168 -0
- pyfect/cluster/entity/_keep_alive.py +74 -0
- pyfect/cluster/entity/_queue.py +103 -0
- pyfect/cluster/entity/_request.py +34 -0
- pyfect/cluster/entity/_services.py +23 -0
- pyfect/cluster/entity/_test_client.py +237 -0
- pyfect/cluster/entity_address.py +25 -0
- pyfect/cluster/entity_id.py +20 -0
- pyfect/cluster/entity_proxy.py +161 -0
- pyfect/cluster/entity_proxy_server.py +103 -0
- pyfect/cluster/entity_resource.py +122 -0
- pyfect/cluster/entity_type.py +20 -0
- pyfect/cluster/envelope.py +307 -0
- pyfect/cluster/http_runner.py +254 -0
- pyfect/cluster/k8s_http_client.py +281 -0
- pyfect/cluster/k8s_types.py +1394 -0
- pyfect/cluster/machine_id.py +21 -0
- pyfect/cluster/message.py +219 -0
- pyfect/cluster/message_storage/__init__.py +67 -0
- pyfect/cluster/message_storage/_encoded.py +393 -0
- pyfect/cluster/message_storage/_memory.py +386 -0
- pyfect/cluster/message_storage/_noop.py +109 -0
- pyfect/cluster/message_storage/_service.py +388 -0
- pyfect/cluster/reply.py +236 -0
- pyfect/cluster/runner.py +32 -0
- pyfect/cluster/runner_address.py +23 -0
- pyfect/cluster/runner_health.py +93 -0
- pyfect/cluster/runner_server.py +286 -0
- pyfect/cluster/runner_storage.py +212 -0
- pyfect/cluster/runners/__init__.py +55 -0
- pyfect/cluster/runners/_local.py +460 -0
- pyfect/cluster/runners/_noop.py +57 -0
- pyfect/cluster/runners/_rpc.py +290 -0
- pyfect/cluster/runners/_service.py +113 -0
- pyfect/cluster/shard_id.py +39 -0
- pyfect/cluster/sharding/__init__.py +12 -0
- pyfect/cluster/sharding/_acquisition.py +490 -0
- pyfect/cluster/sharding/_clients.py +271 -0
- pyfect/cluster/sharding/_make.py +388 -0
- pyfect/cluster/sharding/_routing.py +296 -0
- pyfect/cluster/sharding/_runners_sync.py +198 -0
- pyfect/cluster/sharding/_service.py +91 -0
- pyfect/cluster/sharding/_singletons.py +183 -0
- pyfect/cluster/sharding/_state.py +296 -0
- pyfect/cluster/sharding/_storage_loop.py +421 -0
- pyfect/cluster/sharding_config.py +230 -0
- pyfect/cluster/sharding_registration_event.py +28 -0
- pyfect/cluster/single_runner.py +49 -0
- pyfect/cluster/singleton.py +36 -0
- pyfect/cluster/singleton_address.py +20 -0
- pyfect/cluster/snowflake.py +133 -0
- pyfect/cluster/socket_runner.py +67 -0
- pyfect/cluster/sql_message_storage.py +1001 -0
- pyfect/cluster/sql_runner_storage.py +841 -0
- pyfect/cluster/test_runner.py +23 -0
- pyfect/config/__init__.py +93 -0
- pyfect/config/_all.py +96 -0
- pyfect/config/_collections.py +291 -0
- pyfect/config/_config.py +261 -0
- pyfect/config/_constructors.py +106 -0
- pyfect/config/_error.py +27 -0
- pyfect/config/_leaf.py +92 -0
- pyfect/config/_scalars.py +193 -0
- pyfect/config/_schema.py +340 -0
- pyfect/config_provider/__init__.py +60 -0
- pyfect/config_provider/_dir.py +84 -0
- pyfect/config_provider/_dotenv.py +127 -0
- pyfect/config_provider/_env.py +79 -0
- pyfect/config_provider/_layers.py +80 -0
- pyfect/config_provider/_node.py +90 -0
- pyfect/config_provider/_provider.py +118 -0
- pyfect/config_provider/_unknown.py +62 -0
- pyfect/console/__init__.py +63 -0
- pyfect/console/_effects.py +134 -0
- pyfect/console/_live.py +156 -0
- pyfect/console/_service.py +49 -0
- pyfect/console/_table.py +90 -0
- pyfect/context.py +414 -0
- pyfect/cron/__init__.py +30 -0
- pyfect/cron/_model.py +183 -0
- pyfect/cron/_search.py +268 -0
- pyfect/cron/_text.py +202 -0
- pyfect/cron/_wall.py +42 -0
- pyfect/crypto.py +257 -0
- pyfect/date_time/__init__.py +77 -0
- pyfect/date_time/_calendar.py +209 -0
- pyfect/date_time/_current.py +118 -0
- pyfect/date_time/_epoch.py +22 -0
- pyfect/date_time/_iso.py +80 -0
- pyfect/date_time/_zones.py +135 -0
- pyfect/date_time/intl.py +144 -0
- pyfect/deferred.py +145 -0
- pyfect/devtools/__init__.py +24 -0
- pyfect/devtools/client.py +323 -0
- pyfect/devtools/layers.py +32 -0
- pyfect/devtools/schema.py +301 -0
- pyfect/devtools/server.py +72 -0
- pyfect/duration.py +295 -0
- pyfect/effect.py +565 -0
- pyfect/effectable.py +37 -0
- pyfect/encoding/__init__.py +62 -0
- pyfect/encoding/_archive.py +197 -0
- pyfect/encoding/_json_array_scanner.py +477 -0
- pyfect/encoding/base64.py +23 -0
- pyfect/encoding/base64_url.py +19 -0
- pyfect/encoding/encoding_error.py +17 -0
- pyfect/encoding/hex.py +28 -0
- pyfect/encoding/ini.py +188 -0
- pyfect/encoding/json_array.py +246 -0
- pyfect/encoding/ndjson.py +461 -0
- pyfect/encoding/schema_binary/__init__.py +257 -0
- pyfect/encoding/schema_binary/_channels.py +173 -0
- pyfect/encoding/schema_binary/_compile.py +584 -0
- pyfect/encoding/schema_binary/_decode.py +848 -0
- pyfect/encoding/schema_binary/_dictionary.py +90 -0
- pyfect/encoding/schema_binary/_encode.py +838 -0
- pyfect/encoding/schema_binary/_fingerprint.py +168 -0
- pyfect/encoding/schema_binary/_layout.py +350 -0
- pyfect/encoding/schema_binary/_literals.py +47 -0
- pyfect/encoding/schema_binary/_parser.py +163 -0
- pyfect/encoding/schema_binary/_reader.py +148 -0
- pyfect/encoding/schema_binary/_target.py +310 -0
- pyfect/encoding/schema_binary/_values.py +133 -0
- pyfect/encoding/schema_binary/_wire.py +253 -0
- pyfect/encoding/sse.py +476 -0
- pyfect/encoding/tar/__init__.py +396 -0
- pyfect/encoding/tar/_decoder.py +220 -0
- pyfect/encoding/tar/_encoder.py +83 -0
- pyfect/encoding/tar/_header.py +494 -0
- pyfect/encoding/tar/_model.py +82 -0
- pyfect/encoding/toml.py +375 -0
- pyfect/encoding/xml/__init__.py +222 -0
- pyfect/encoding/xml/_chars.py +119 -0
- pyfect/encoding/xml/_collector.py +106 -0
- pyfect/encoding/xml/_inputs.py +126 -0
- pyfect/encoding/xml/_model.py +114 -0
- pyfect/encoding/xml/_parser.py +694 -0
- pyfect/encoding/xml/_scanner.py +180 -0
- pyfect/encoding/xml/_writer.py +238 -0
- pyfect/encoding/yaml.py +739 -0
- pyfect/encoding/zip/__init__.py +392 -0
- pyfect/encoding/zip/_decoder.py +378 -0
- pyfect/encoding/zip/_format.py +122 -0
- pyfect/encoding/zip/_inflate.py +65 -0
- pyfect/encoding/zip/_model.py +86 -0
- pyfect/encoding/zip/_writer.py +311 -0
- pyfect/error_reporter.py +240 -0
- pyfect/errors.py +321 -0
- pyfect/eventlog/__init__.py +77 -0
- pyfect/eventlog/_conflicts.py +94 -0
- pyfect/eventlog/_crypto.py +64 -0
- pyfect/eventlog/_identity.py +26 -0
- pyfect/eventlog/_identity_derivation.py +79 -0
- pyfect/eventlog/_transferable_bytes.py +55 -0
- pyfect/eventlog/_uuid.py +33 -0
- pyfect/eventlog/event.py +115 -0
- pyfect/eventlog/event_group.py +55 -0
- pyfect/eventlog/event_journal.py +630 -0
- pyfect/eventlog/event_log.py +1039 -0
- pyfect/eventlog/event_log_encryption.py +219 -0
- pyfect/eventlog/event_log_message.py +357 -0
- pyfect/eventlog/event_log_remote.py +465 -0
- pyfect/eventlog/event_log_server.py +442 -0
- pyfect/eventlog/event_log_server_encrypted.py +322 -0
- pyfect/eventlog/event_log_server_unencrypted.py +814 -0
- pyfect/eventlog/event_log_session_auth.py +250 -0
- pyfect/eventlog/event_row.py +423 -0
- pyfect/eventlog/sql_event_journal.py +692 -0
- pyfect/eventlog/sql_event_log_server_encrypted.py +440 -0
- pyfect/eventlog/sql_event_log_server_unencrypted.py +568 -0
- pyfect/execution_plan/__init__.py +42 -0
- pyfect/execution_plan/_events.py +128 -0
- pyfect/execution_plan/_model.py +108 -0
- pyfect/execution_plan/_run.py +176 -0
- pyfect/exit.py +205 -0
- pyfect/fiber_handle.py +305 -0
- pyfect/fiber_map.py +350 -0
- pyfect/fiber_set.py +232 -0
- pyfect/file_system/__init__.py +69 -0
- pyfect/file_system/_copy.py +116 -0
- pyfect/file_system/_errors.py +32 -0
- pyfect/file_system/_file.py +177 -0
- pyfect/file_system/_live.py +351 -0
- pyfect/file_system/_model.py +72 -0
- pyfect/file_system/_noop.py +189 -0
- pyfect/file_system/_service.py +336 -0
- pyfect/file_system/_stat.py +69 -0
- pyfect/file_system/_watch.py +107 -0
- pyfect/filters.py +245 -0
- pyfect/graph/__init__.py +101 -0
- pyfect/graph/_all_pairs.py +150 -0
- pyfect/graph/_combine.py +259 -0
- pyfect/graph/_compact.py +143 -0
- pyfect/graph/_components.py +200 -0
- pyfect/graph/_cycles.py +144 -0
- pyfect/graph/_enumerate.py +168 -0
- pyfect/graph/_errors.py +51 -0
- pyfect/graph/_flow.py +164 -0
- pyfect/graph/_forest.py +109 -0
- pyfect/graph/_graph.py +269 -0
- pyfect/graph/_lowlink.py +98 -0
- pyfect/graph/_matching.py +101 -0
- pyfect/graph/_mutate.py +211 -0
- pyfect/graph/_query.py +138 -0
- pyfect/graph/_records.py +119 -0
- pyfect/graph/_render.py +109 -0
- pyfect/graph/_shortest.py +274 -0
- pyfect/graph/_snapshot.py +50 -0
- pyfect/graph/_store.py +136 -0
- pyfect/graph/_traverse.py +289 -0
- pyfect/graph/_view.py +384 -0
- pyfect/graph/_walker.py +70 -0
- pyfect/graphql/__init__.py +87 -0
- pyfect/graphql/_codec.py +117 -0
- pyfect/graphql/_http.py +309 -0
- pyfect/graphql/_schemas.py +39 -0
- pyfect/graphql/_slices.py +110 -0
- pyfect/graphql/_template.py +111 -0
- pyfect/graphql/_types.py +115 -0
- pyfect/graphql/_ws.py +395 -0
- pyfect/graphql/_ws_messages.py +120 -0
- pyfect/graphql/client.py +428 -0
- pyfect/graphql/client_error.py +285 -0
- pyfect/graphql/group.py +142 -0
- pyfect/graphql/middleware.py +98 -0
- pyfect/graphql/operation.py +409 -0
- pyfect/graphql/protocol.py +119 -0
- pyfect/graphql/resolver.py +574 -0
- pyfect/graphql/template.py +61 -0
- pyfect/hash_map.py +317 -0
- pyfect/hash_ring.py +206 -0
- pyfect/hash_set.py +163 -0
- pyfect/http/__init__.py +125 -0
- pyfect/http/_asgi_listener.py +272 -0
- pyfect/http/_authority.py +63 -0
- pyfect/http/_brotli.py +27 -0
- pyfect/http/_compression.py +52 -0
- pyfect/http/_field.py +18 -0
- pyfect/http/_foreign_path.py +27 -0
- pyfect/http/_form_data.py +34 -0
- pyfect/http/_httpx_options.py +120 -0
- pyfect/http/_in_memory.py +76 -0
- pyfect/http/_request_state.py +59 -0
- pyfect/http/_vary.py +14 -0
- pyfect/http/abort.py +65 -0
- pyfect/http/asgi/__init__.py +59 -0
- pyfect/http/asgi/_app.py +155 -0
- pyfect/http/asgi/_http.py +201 -0
- pyfect/http/asgi/_mount.py +263 -0
- pyfect/http/asgi/_scope.py +86 -0
- pyfect/http/asgi/_serve.py +72 -0
- pyfect/http/asgi/_types.py +13 -0
- pyfect/http/asgi/_web_socket.py +325 -0
- pyfect/http/body.py +437 -0
- pyfect/http/client/__init__.py +111 -0
- pyfect/http/client/_circuit_breaker.py +113 -0
- pyfect/http/client/_cookie_jar.py +120 -0
- pyfect/http/client/_make.py +252 -0
- pyfect/http/client/_rate_limit.py +423 -0
- pyfect/http/client/_redirects.py +93 -0
- pyfect/http/client/_retry.py +115 -0
- pyfect/http/client/_service.py +497 -0
- pyfect/http/cookies.py +444 -0
- pyfect/http/error.py +188 -0
- pyfect/http/etag.py +96 -0
- pyfect/http/file_like.py +49 -0
- pyfect/http/find_my_way/__init__.py +10 -0
- pyfect/http/find_my_way/_nodes.py +186 -0
- pyfect/http/find_my_way/_paths.py +43 -0
- pyfect/http/find_my_way/_query_string.py +67 -0
- pyfect/http/find_my_way/_router.py +401 -0
- pyfect/http/headers.py +343 -0
- pyfect/http/http_effect.py +198 -0
- pyfect/http/httpx_client.py +291 -0
- pyfect/http/hypercorn_server.py +122 -0
- pyfect/http/incoming_message.py +140 -0
- pyfect/http/live_server/__init__.py +191 -0
- pyfect/http/live_server/_body.py +164 -0
- pyfect/http/live_server/_exchange.py +321 -0
- pyfect/http/live_server/_head.py +194 -0
- pyfect/http/live_server/_inbound.py +156 -0
- pyfect/http/live_server/_listener.py +211 -0
- pyfect/http/live_server/_serve.py +225 -0
- pyfect/http/live_server/_timers.py +20 -0
- pyfect/http/live_server/_upgrade.py +108 -0
- pyfect/http/live_server/_websocket.py +286 -0
- pyfect/http/live_server/_ws_frames.py +242 -0
- pyfect/http/method.py +30 -0
- pyfect/http/middleware/__init__.py +61 -0
- pyfect/http/middleware/_compression.py +156 -0
- pyfect/http/middleware/_cors.py +129 -0
- pyfect/http/middleware/_decompression.py +274 -0
- pyfect/http/middleware/_forwarded.py +113 -0
- pyfect/http/middleware/_logger.py +73 -0
- pyfect/http/middleware/_search_params.py +34 -0
- pyfect/http/middleware/_tracer.py +153 -0
- pyfect/http/middleware/_types.py +16 -0
- pyfect/http/mime/__init__.py +69 -0
- pyfect/http/mime/_types.py +361 -0
- pyfect/http/multipart.py +613 -0
- pyfect/http/multipart_parser/__init__.py +348 -0
- pyfect/http/multipart_parser/_content_type.py +109 -0
- pyfect/http/multipart_parser/_headers.py +206 -0
- pyfect/http/multipart_parser/_search.py +85 -0
- pyfect/http/origin.py +146 -0
- pyfect/http/platform.py +593 -0
- pyfect/http/prometheus.py +34 -0
- pyfect/http/request.py +384 -0
- pyfect/http/response.py +474 -0
- pyfect/http/router/__init__.py +82 -0
- pyfect/http/router/_api.py +462 -0
- pyfect/http/router/_in_memory.py +76 -0
- pyfect/http/router/_markers.py +47 -0
- pyfect/http/router/_middleware.py +181 -0
- pyfect/http/router/_middleware_stack.py +51 -0
- pyfect/http/router/_provided.py +31 -0
- pyfect/http/router/_route.py +114 -0
- pyfect/http/router/_router.py +308 -0
- pyfect/http/schema.py +276 -0
- pyfect/http/server.py +199 -0
- pyfect/http/server_error.py +261 -0
- pyfect/http/server_request.py +729 -0
- pyfect/http/server_respondable.py +77 -0
- pyfect/http/server_response.py +569 -0
- pyfect/http/static_server/__init__.py +257 -0
- pyfect/http/static_server/_conditional.py +72 -0
- pyfect/http/static_server/_mime.py +55 -0
- pyfect/http/static_server/_range.py +53 -0
- pyfect/http/status.py +51 -0
- pyfect/http/template.py +161 -0
- pyfect/http/trace_context.py +91 -0
- pyfect/http/url.py +442 -0
- pyfect/http/url_params.py +304 -0
- pyfect/http/uvicorn_server.py +119 -0
- pyfect/http/web.py +171 -0
- pyfect/http/web_handler.py +463 -0
- pyfect/http/wire.py +209 -0
- pyfect/http_api/__init__.py +83 -0
- pyfect/http_api/_assets/scalar.js +8 -0
- pyfect/http_api/_assets/swagger-ui.css +3 -0
- pyfect/http_api/_assets/swagger-ui.js +4 -0
- pyfect/http_api/_client_method.py +220 -0
- pyfect/http_api/_client_payload.py +144 -0
- pyfect/http_api/_client_response.py +377 -0
- pyfect/http_api/_handlers.py +282 -0
- pyfect/http_api/_html.py +38 -0
- pyfect/http_api/_media_type.py +6 -0
- pyfect/http_api/_openapi_responses.py +164 -0
- pyfect/http_api/_openapi_schemas.py +163 -0
- pyfect/http_api/_path.py +102 -0
- pyfect/http_api/_payload.py +260 -0
- pyfect/http_api/_response.py +363 -0
- pyfect/http_api/_security.py +174 -0
- pyfect/http_api/api.py +265 -0
- pyfect/http_api/builder.py +447 -0
- pyfect/http_api/client.py +474 -0
- pyfect/http_api/endpoint.py +969 -0
- pyfect/http_api/error.py +280 -0
- pyfect/http_api/group.py +215 -0
- pyfect/http_api/middleware.py +232 -0
- pyfect/http_api/openapi.py +396 -0
- pyfect/http_api/scalar.py +143 -0
- pyfect/http_api/schema.py +713 -0
- pyfect/http_api/security.py +91 -0
- pyfect/http_api/swagger.py +69 -0
- pyfect/http_api/test.py +105 -0
- pyfect/json_patch.py +255 -0
- pyfect/json_pointer.py +100 -0
- pyfect/json_schema/__init__.py +66 -0
- pyfect/json_schema/_api.py +96 -0
- pyfect/json_schema/_converter.py +258 -0
- pyfect/json_schema/_draft04.py +198 -0
- pyfect/json_schema/_draft07.py +203 -0
- pyfect/json_schema/_keywords.py +202 -0
- pyfect/json_schema/_model.py +201 -0
- pyfect/json_schema/_openapi.py +208 -0
- pyfect/latch.py +127 -0
- pyfect/layer.py +479 -0
- pyfect/layer_map.py +279 -0
- pyfect/layer_ref.py +208 -0
- pyfect/logger/__init__.py +91 -0
- pyfect/logger/_batched.py +49 -0
- pyfect/logger/_console.py +116 -0
- pyfect/logger/_file.py +31 -0
- pyfect/logger/_formats.py +104 -0
- pyfect/logger/_layer.py +50 -0
- pyfect/logger/_level.py +53 -0
- pyfect/logger/_log.py +142 -0
- pyfect/logger/_model.py +163 -0
- pyfect/logger/_refs.py +78 -0
- pyfect/logger/_stdlib.py +54 -0
- pyfect/logger/_text.py +263 -0
- pyfect/logger/_tracer.py +35 -0
- pyfect/managed_runtime.py +393 -0
- pyfect/metric/__init__.py +119 -0
- pyfect/metric/_boundaries.py +27 -0
- pyfect/metric/_kinds.py +417 -0
- pyfect/metric/_metric.py +302 -0
- pyfect/metric/_prometheus.py +150 -0
- pyfect/metric/_refs.py +78 -0
- pyfect/metric/_runtime.py +75 -0
- pyfect/metric/_snapshot.py +97 -0
- pyfect/metric/_state.py +74 -0
- pyfect/msgspec.py +98 -0
- pyfect/mutable_hash_map.py +140 -0
- pyfect/mutable_hash_set.py +95 -0
- pyfect/net/__init__.py +18 -0
- pyfect/net/_errors.py +29 -0
- pyfect/net/address_resolver.py +179 -0
- pyfect/net/dns/__init__.py +92 -0
- pyfect/net/dns/_error.py +43 -0
- pyfect/net/dns/_records.py +335 -0
- pyfect/net/dns/_service.py +287 -0
- pyfect/net/dns/_system.py +89 -0
- pyfect/net/host.py +311 -0
- pyfect/net/ip_interface.py +202 -0
- pyfect/net/ip_network.py +250 -0
- pyfect/net/net_address/__init__.py +245 -0
- pyfect/net/net_address/_inet.py +412 -0
- pyfect/net/net_address/_ip.py +348 -0
- pyfect/net/net_address/_mac.py +119 -0
- pyfect/optic/__init__.py +50 -0
- pyfect/optic/_builtins.py +90 -0
- pyfect/optic/_data.py +97 -0
- pyfect/optic/_optic.py +422 -0
- pyfect/optic/_steps.py +151 -0
- pyfect/option.py +362 -0
- pyfect/otel/__init__.py +22 -0
- pyfect/otel/_attributes.py +67 -0
- pyfect/otel/_producer.py +322 -0
- pyfect/otel/_shutdown.py +26 -0
- pyfect/otel/_span.py +292 -0
- pyfect/otel/_wrapper.py +92 -0
- pyfect/otel/logger.py +154 -0
- pyfect/otel/metrics.py +126 -0
- pyfect/otel/resource.py +111 -0
- pyfect/otel/sdk.py +195 -0
- pyfect/otel/tracer.py +157 -0
- pyfect/partitioned_semaphore.py +211 -0
- pyfect/path.py +304 -0
- pyfect/persistence/__init__.py +77 -0
- pyfect/persistence/_columns.py +32 -0
- pyfect/persistence/_error.py +14 -0
- pyfect/persistence/_seconds.py +68 -0
- pyfect/persistence/_sql_keys.py +47 -0
- pyfect/persistence/circuit_breaker/__init__.py +120 -0
- pyfect/persistence/circuit_breaker/_breaker.py +317 -0
- pyfect/persistence/circuit_breaker/_errors.py +67 -0
- pyfect/persistence/circuit_breaker/_memory.py +243 -0
- pyfect/persistence/circuit_breaker/_redis.py +509 -0
- pyfect/persistence/circuit_breaker/_store.py +60 -0
- pyfect/persistence/circuit_breaker/_types.py +191 -0
- pyfect/persistence/key_value_store/__init__.py +51 -0
- pyfect/persistence/key_value_store/_layers.py +216 -0
- pyfect/persistence/key_value_store/_schema_store.py +91 -0
- pyfect/persistence/key_value_store/_sql.py +197 -0
- pyfect/persistence/key_value_store/_store.py +212 -0
- pyfect/persistence/persistable.py +164 -0
- pyfect/persistence/persisted_cache.py +159 -0
- pyfect/persistence/persisted_queue/__init__.py +75 -0
- pyfect/persistence/persisted_queue/_error.py +42 -0
- pyfect/persistence/persisted_queue/_mailbox.py +101 -0
- pyfect/persistence/persisted_queue/_memory.py +182 -0
- pyfect/persistence/persisted_queue/_queue.py +272 -0
- pyfect/persistence/persisted_queue/_redis.py +455 -0
- pyfect/persistence/persisted_queue/_redis_scripts.py +395 -0
- pyfect/persistence/persisted_queue/_sql.py +334 -0
- pyfect/persistence/persisted_queue/_sql_migrations.py +319 -0
- pyfect/persistence/persisted_queue/_sql_statements.py +441 -0
- pyfect/persistence/persisted_queue/_store.py +147 -0
- pyfect/persistence/persistence/__init__.py +84 -0
- pyfect/persistence/persistence/_kvs.py +135 -0
- pyfect/persistence/persistence/_memory.py +62 -0
- pyfect/persistence/persistence/_redis.py +162 -0
- pyfect/persistence/persistence/_service.py +276 -0
- pyfect/persistence/persistence/_sql.py +499 -0
- pyfect/persistence/rate_limiter/__init__.py +82 -0
- pyfect/persistence/rate_limiter/_errors.py +59 -0
- pyfect/persistence/rate_limiter/_limiter.py +325 -0
- pyfect/persistence/rate_limiter/_memory.py +217 -0
- pyfect/persistence/rate_limiter/_redis.py +485 -0
- pyfect/persistence/rate_limiter/_store.py +89 -0
- pyfect/persistence/redis/__init__.py +52 -0
- pyfect/persistence/redis/_error.py +12 -0
- pyfect/persistence/redis/_make.py +234 -0
- pyfect/persistence/redis/_script.py +78 -0
- pyfect/persistence/redis/_service.py +104 -0
- pyfect/persistence/redis_py.py +303 -0
- pyfect/persistence/user_directories.py +195 -0
- pyfect/platform.py +26 -0
- pyfect/platform_error.py +256 -0
- pyfect/pool/__init__.py +36 -0
- pyfect/pool/_pool.py +707 -0
- pyfect/pool/_state.py +161 -0
- pyfect/pool/_ttl.py +105 -0
- pyfect/pool/_waiters.py +116 -0
- pyfect/primary_key.py +35 -0
- pyfect/process/__init__.py +82 -0
- pyfect/process/_child.py +213 -0
- pyfect/process/_command.py +358 -0
- pyfect/process/_handle.py +77 -0
- pyfect/process/_live.py +455 -0
- pyfect/process/_os.py +78 -0
- pyfect/process/_pipes.py +291 -0
- pyfect/process/_spawner.py +75 -0
- pyfect/pubsub/__init__.py +33 -0
- pyfect/pubsub/_atomic.py +181 -0
- pyfect/pubsub/_hub.py +214 -0
- pyfect/pubsub/_pubsub.py +214 -0
- pyfect/pubsub/_replay.py +107 -0
- pyfect/pubsub/_subscription.py +226 -0
- pyfect/pull.py +132 -0
- pyfect/py.typed +0 -0
- pyfect/pydantic.py +188 -0
- pyfect/queue/__init__.py +33 -0
- pyfect/queue/_queue.py +525 -0
- pyfect/queue/_state.py +111 -0
- pyfect/queue/_views.py +55 -0
- pyfect/random.py +146 -0
- pyfect/rc_map.py +363 -0
- pyfect/rc_ref.py +271 -0
- pyfect/reactivity/__init__.py +53 -0
- pyfect/reactivity/_async_result_schema.py +227 -0
- pyfect/reactivity/_hydration.py +18 -0
- pyfect/reactivity/_live.py +21 -0
- pyfect/reactivity/_reactivity.py +256 -0
- pyfect/reactivity/_result_effect.py +17 -0
- pyfect/reactivity/_time.py +11 -0
- pyfect/reactivity/async_result.py +679 -0
- pyfect/reactivity/atom/__init__.py +143 -0
- pyfect/reactivity/atom/_combinators.py +223 -0
- pyfect/reactivity/atom/_constructors.py +80 -0
- pyfect/reactivity/atom/_conversions.py +79 -0
- pyfect/reactivity/atom/_effect.py +252 -0
- pyfect/reactivity/atom/_fn.py +341 -0
- pyfect/reactivity/atom/_kvs.py +101 -0
- pyfect/reactivity/atom/_optimistic.py +162 -0
- pyfect/reactivity/atom/_runtime.py +323 -0
- pyfect/reactivity/atom/_serializable.py +65 -0
- pyfect/reactivity/atom/_subscription_ref.py +102 -0
- pyfect/reactivity/atom/_swr.py +69 -0
- pyfect/reactivity/atom/_types.py +435 -0
- pyfect/reactivity/atom_http_api.py +667 -0
- pyfect/reactivity/atom_ref.py +260 -0
- pyfect/reactivity/atom_registry/__init__.py +219 -0
- pyfect/reactivity/atom_registry/_batch.py +96 -0
- pyfect/reactivity/atom_registry/_dispatcher.py +60 -0
- pyfect/reactivity/atom_registry/_lifetime.py +202 -0
- pyfect/reactivity/atom_registry/_node.py +423 -0
- pyfect/reactivity/atom_registry/_registry.py +380 -0
- pyfect/reactivity/atom_rpc.py +283 -0
- pyfect/reactivity/hydration.py +200 -0
- pyfect/redacted.py +83 -0
- pyfect/ref.py +129 -0
- pyfect/resource.py +98 -0
- pyfect/result.py +306 -0
- pyfect/rpc/__init__.py +64 -0
- pyfect/rpc/_encoded_schema.py +86 -0
- pyfect/rpc/_json_rpc.py +246 -0
- pyfect/rpc/_methods.py +177 -0
- pyfect/rpc/_rpc.py +498 -0
- pyfect/rpc/client/__init__.py +100 -0
- pyfect/rpc/client/_core.py +449 -0
- pyfect/rpc/client/_http.py +257 -0
- pyfect/rpc/client/_protocol.py +171 -0
- pyfect/rpc/client/_serialized.py +321 -0
- pyfect/rpc/client/_transports.py +328 -0
- pyfect/rpc/client/_worker.py +283 -0
- pyfect/rpc/client_error.py +35 -0
- pyfect/rpc/group.py +296 -0
- pyfect/rpc/message.py +299 -0
- pyfect/rpc/middleware.py +182 -0
- pyfect/rpc/schema.py +89 -0
- pyfect/rpc/serialization.py +366 -0
- pyfect/rpc/server/__init__.py +181 -0
- pyfect/rpc/server/_core.py +549 -0
- pyfect/rpc/server/_protocol.py +123 -0
- pyfect/rpc/server/_serialized.py +363 -0
- pyfect/rpc/server/_stdio.py +101 -0
- pyfect/rpc/server/_transports.py +427 -0
- pyfect/rpc/server/_worker.py +108 -0
- pyfect/rpc/test.py +74 -0
- pyfect/rpc/worker.py +92 -0
- pyfect/run.py +393 -0
- pyfect/schedule/__init__.py +91 -0
- pyfect/schedule/_combinators.py +168 -0
- pyfect/schedule/_constructors.py +223 -0
- pyfect/schedule/_cron.py +49 -0
- pyfect/schedule/_drive.py +191 -0
- pyfect/schedule/_model.py +405 -0
- pyfect/scheduler.py +575 -0
- pyfect/schema/__init__.py +866 -0
- pyfect/schema/_adapters.py +39 -0
- pyfect/schema/_arbitrary_constraint.py +242 -0
- pyfect/schema/_arbitrary_graph.py +325 -0
- pyfect/schema/_arbitrary_links.py +179 -0
- pyfect/schema/_arbitrary_number.py +112 -0
- pyfect/schema/_arrays.py +287 -0
- pyfect/schema/_assignable.py +118 -0
- pyfect/schema/_boolean_literals.py +31 -0
- pyfect/schema/_checks_number.py +216 -0
- pyfect/schema/_checks_order.py +140 -0
- pyfect/schema/_checks_size.py +277 -0
- pyfect/schema/_checks_string.py +241 -0
- pyfect/schema/_class.py +569 -0
- pyfect/schema/_codec_derivation.py +227 -0
- pyfect/schema/_codec_links.py +221 -0
- pyfect/schema/_codecs.py +316 -0
- pyfect/schema/_collections.py +172 -0
- pyfect/schema/_constructor_default.py +89 -0
- pyfect/schema/_data.py +440 -0
- pyfect/schema/_data_codecs.py +145 -0
- pyfect/schema/_decimal_regexp.py +243 -0
- pyfect/schema/_declare.py +68 -0
- pyfect/schema/_derive.py +292 -0
- pyfect/schema/_duration_codecs.py +27 -0
- pyfect/schema/_ecma_pattern.py +416 -0
- pyfect/schema/_error.py +26 -0
- pyfect/schema/_error_instance.py +222 -0
- pyfect/schema/_graph.py +271 -0
- pyfect/schema/_instances.py +240 -0
- pyfect/schema/_jsonschema/__init__.py +90 -0
- pyfect/schema/_jsonschema/_annotations.py +114 -0
- pyfect/schema/_jsonschema/_checks.py +253 -0
- pyfect/schema/_jsonschema/_compile.py +346 -0
- pyfect/schema/_jsonschema/_exact.py +19 -0
- pyfect/schema/_jsonschema/_merge.py +211 -0
- pyfect/schema/_jsonschema/_objects.py +231 -0
- pyfect/schema/_jsonschema/_references.py +221 -0
- pyfect/schema/_jsonschema/_template.py +69 -0
- pyfect/schema/_literals.py +147 -0
- pyfect/schema/_middleware.py +209 -0
- pyfect/schema/_net.py +522 -0
- pyfect/schema/_options.py +49 -0
- pyfect/schema/_parse.py +351 -0
- pyfect/schema/_parsed_text.py +37 -0
- pyfect/schema/_primitives.py +48 -0
- pyfect/schema/_record.py +41 -0
- pyfect/schema/_refine.py +81 -0
- pyfect/schema/_schema.py +158 -0
- pyfect/schema/_semver.py +144 -0
- pyfect/schema/_strategy/__init__.py +258 -0
- pyfect/schema/_strategy/_big_decimal.py +203 -0
- pyfect/schema/_strategy/_composites.py +119 -0
- pyfect/schema/_strategy/_leaves.py +94 -0
- pyfect/schema/_strategy/_net.py +133 -0
- pyfect/schema/_struct.py +87 -0
- pyfect/schema/_struct_keys.py +128 -0
- pyfect/schema/_tagged.py +254 -0
- pyfect/schema/_template_literal.py +172 -0
- pyfect/schema/_to_codec_array_from_single.py +115 -0
- pyfect/schema/_to_codec_go_json.py +174 -0
- pyfect/schema/_to_codec_json.py +73 -0
- pyfect/schema/_to_codec_string_tree.py +115 -0
- pyfect/schema/_to_differ_json_patch.py +44 -0
- pyfect/schema/_to_encoder_xml.py +119 -0
- pyfect/schema/_to_equivalence.py +186 -0
- pyfect/schema/_to_formatter.py +180 -0
- pyfect/schema/_to_iso.py +73 -0
- pyfect/schema/_to_strategy.py +31 -0
- pyfect/schema/_transform.py +210 -0
- pyfect/schema/_tree.py +27 -0
- pyfect/schema/_type_walk.py +53 -0
- pyfect/schema/_undefined.py +58 -0
- pyfect/schema/_value_codecs.py +84 -0
- pyfect/schema/_values.py +123 -0
- pyfect/schema/_void.py +86 -0
- pyfect/schema/ast/__init__.py +227 -0
- pyfect/schema/ast/_annotations.py +43 -0
- pyfect/schema/ast/_arrays.py +210 -0
- pyfect/schema/ast/_base.py +295 -0
- pyfect/schema/ast/_candidates.py +309 -0
- pyfect/schema/ast/_check.py +181 -0
- pyfect/schema/ast/_class.py +159 -0
- pyfect/schema/ast/_constructors.py +65 -0
- pyfect/schema/ast/_declaration.py +97 -0
- pyfect/schema/ast/_encoding.py +111 -0
- pyfect/schema/ast/_json.py +129 -0
- pyfect/schema/ast/_keywords.py +125 -0
- pyfect/schema/ast/_literal.py +157 -0
- pyfect/schema/ast/_objects.py +356 -0
- pyfect/schema/ast/_record.py +36 -0
- pyfect/schema/ast/_result.py +116 -0
- pyfect/schema/ast/_steps.py +209 -0
- pyfect/schema/ast/_suspend.py +65 -0
- pyfect/schema/ast/_template_literal.py +256 -0
- pyfect/schema/ast/_union.py +172 -0
- pyfect/schema/getter/__init__.py +136 -0
- pyfect/schema/getter/_binary.py +104 -0
- pyfect/schema/getter/_coerce.py +196 -0
- pyfect/schema/getter/_getter.py +243 -0
- pyfect/schema/getter/_make.py +151 -0
- pyfect/schema/getter/_text.py +141 -0
- pyfect/schema/getter/_tree_record.py +170 -0
- pyfect/schema/issue.py +420 -0
- pyfect/schema/model/__init__.py +120 -0
- pyfect/schema/model/_class.py +245 -0
- pyfect/schema/model/_dates.py +103 -0
- pyfect/schema/model/_fields.py +77 -0
- pyfect/schema/model/_toolkit.py +114 -0
- pyfect/schema/model/_uuid.py +69 -0
- pyfect/schema/representation/__init__.py +424 -0
- pyfect/schema/representation/_builtin_revivers.py +331 -0
- pyfect/schema/representation/_code.py +106 -0
- pyfect/schema/representation/_code_builtins.py +281 -0
- pyfect/schema/representation/_code_document.py +563 -0
- pyfect/schema/representation/_code_sort.py +111 -0
- pyfect/schema/representation/_from.py +278 -0
- pyfect/schema/representation/_import/__init__.py +78 -0
- pyfect/schema/representation/_import/_finalize.py +121 -0
- pyfect/schema/representation/_import/_intersect.py +467 -0
- pyfect/schema/representation/_import/_js_order.py +66 -0
- pyfect/schema/representation/_import/_keywords.py +224 -0
- pyfect/schema/representation/_import/_nodes.py +199 -0
- pyfect/schema/representation/_import/_translate.py +455 -0
- pyfect/schema/representation/_json.py +520 -0
- pyfect/schema/representation/_json_schema.py +178 -0
- pyfect/schema/representation/_model.py +284 -0
- pyfect/schema/representation/_revivers.py +89 -0
- pyfect/schema/representation/_to.py +249 -0
- pyfect/schema/transformation/__init__.py +99 -0
- pyfect/schema/transformation/_builtins.py +314 -0
- pyfect/schema/transformation/_durations.py +150 -0
- pyfect/schema/transformation/_transformation.py +177 -0
- pyfect/schema/variant_schema/__init__.py +69 -0
- pyfect/schema/variant_schema/_class.py +188 -0
- pyfect/schema/variant_schema/_defaults.py +30 -0
- pyfect/schema/variant_schema/_field.py +32 -0
- pyfect/schema/variant_schema/_struct.py +130 -0
- pyfect/schema/variant_schema/_toolkit.py +160 -0
- pyfect/scope.py +374 -0
- pyfect/scoped_ref.py +124 -0
- pyfect/semaphore.py +276 -0
- pyfect/semver.py +403 -0
- pyfect/semver_range.py +420 -0
- pyfect/sink/__init__.py +131 -0
- pyfect/sink/_combinators.py +151 -0
- pyfect/sink/_constructors.py +187 -0
- pyfect/sink/_context.py +87 -0
- pyfect/sink/_folds.py +336 -0
- pyfect/sink/_for_each.py +87 -0
- pyfect/sink/_sink.py +265 -0
- pyfect/sink/_steps.py +34 -0
- pyfect/sink/_taking.py +183 -0
- pyfect/socket/__init__.py +146 -0
- pyfect/socket/_accepted.py +65 -0
- pyfect/socket/_connection.py +384 -0
- pyfect/socket/_errors.py +135 -0
- pyfect/socket/_loop.py +66 -0
- pyfect/socket/_net.py +281 -0
- pyfect/socket/_socket.py +245 -0
- pyfect/socket/_tls.py +88 -0
- pyfect/socket/_tls_session.py +117 -0
- pyfect/socket/_web_socket.py +555 -0
- pyfect/socket/datagram/__init__.py +110 -0
- pyfect/socket/datagram/_core.py +460 -0
- pyfect/socket/datagram/_errors.py +102 -0
- pyfect/socket/datagram/_live.py +675 -0
- pyfect/socket/datagram/_model.py +217 -0
- pyfect/socket/server.py +258 -0
- pyfect/socket/ws.py +484 -0
- pyfect/sql/__init__.py +60 -0
- pyfect/sql/_loop.py +134 -0
- pyfect/sql/clickhouse/__init__.py +74 -0
- pyfect/sql/clickhouse/_connection.py +237 -0
- pyfect/sql/clickhouse/_rows.py +87 -0
- pyfect/sql/clickhouse/client.py +322 -0
- pyfect/sql/clickhouse/config.py +241 -0
- pyfect/sql/clickhouse/driver.py +434 -0
- pyfect/sql/clickhouse/error.py +106 -0
- pyfect/sql/clickhouse/migrator.py +47 -0
- pyfect/sql/clickhouse/params.py +168 -0
- pyfect/sql/client/__init__.py +35 -0
- pyfect/sql/client/_client.py +309 -0
- pyfect/sql/client/_sqlite_acquirers.py +130 -0
- pyfect/sql/client/_transaction.py +410 -0
- pyfect/sql/connection.py +87 -0
- pyfect/sql/d1/__init__.py +48 -0
- pyfect/sql/d1/api.py +270 -0
- pyfect/sql/d1/client.py +344 -0
- pyfect/sql/d1/config.py +55 -0
- pyfect/sql/error.py +311 -0
- pyfect/sql/libsql/__init__.py +58 -0
- pyfect/sql/libsql/_connection.py +110 -0
- pyfect/sql/libsql/_hrana.py +258 -0
- pyfect/sql/libsql/_local.py +251 -0
- pyfect/sql/libsql/_targets.py +28 -0
- pyfect/sql/libsql/_values.py +46 -0
- pyfect/sql/libsql/client.py +225 -0
- pyfect/sql/libsql/config.py +170 -0
- pyfect/sql/libsql/migrator.py +43 -0
- pyfect/sql/libsql/sdk.py +123 -0
- pyfect/sql/migrator.py +434 -0
- pyfect/sql/model.py +324 -0
- pyfect/sql/mssql/__init__.py +79 -0
- pyfect/sql/mssql/_connection.py +169 -0
- pyfect/sql/mssql/_driver.py +103 -0
- pyfect/sql/mssql/_names.py +9 -0
- pyfect/sql/mssql/_pool.py +125 -0
- pyfect/sql/mssql/_tds.py +447 -0
- pyfect/sql/mssql/_tds_params.py +155 -0
- pyfect/sql/mssql/_worker.py +155 -0
- pyfect/sql/mssql/client.py +344 -0
- pyfect/sql/mssql/config.py +118 -0
- pyfect/sql/mssql/error.py +111 -0
- pyfect/sql/mssql/migrator.py +43 -0
- pyfect/sql/mssql/parameter.py +44 -0
- pyfect/sql/mssql/procedure.py +119 -0
- pyfect/sql/mssql/types.py +137 -0
- pyfect/sql/mysql/__init__.py +60 -0
- pyfect/sql/mysql/_asyncmy.py +168 -0
- pyfect/sql/mysql/_bind.py +76 -0
- pyfect/sql/mysql/_connection.py +156 -0
- pyfect/sql/mysql/_pool.py +63 -0
- pyfect/sql/mysql/_session.py +262 -0
- pyfect/sql/mysql/client.py +244 -0
- pyfect/sql/mysql/config.py +108 -0
- pyfect/sql/mysql/error.py +104 -0
- pyfect/sql/mysql/migrator.py +44 -0
- pyfect/sql/pg/__init__.py +82 -0
- pyfect/sql/pg/_resolve.py +284 -0
- pyfect/sql/pg/_session.py +380 -0
- pyfect/sql/pg/client.py +398 -0
- pyfect/sql/pg/config.py +142 -0
- pyfect/sql/pg/connection.py +585 -0
- pyfect/sql/pg/error.py +131 -0
- pyfect/sql/pg/migrator.py +139 -0
- pyfect/sql/pg/pool.py +265 -0
- pyfect/sql/resolver.py +294 -0
- pyfect/sql/result.py +24 -0
- pyfect/sql/schema.py +116 -0
- pyfect/sql/sqlite/__init__.py +26 -0
- pyfect/sql/sqlite/_connection.py +230 -0
- pyfect/sql/sqlite/_worker.py +71 -0
- pyfect/sql/sqlite/client.py +248 -0
- pyfect/sql/sqlite/migrator.py +43 -0
- pyfect/sql/statement/__init__.py +144 -0
- pyfect/sql/statement/_compiler.py +384 -0
- pyfect/sql/statement/_segments.py +487 -0
- pyfect/sql/statement/_statement.py +526 -0
- pyfect/sql/statement/_transforms.py +93 -0
- pyfect/sql/stream.py +88 -0
- pyfect/stdio.py +206 -0
- pyfect/stream/__init__.py +151 -0
- pyfect/stream/_async_iterable.py +129 -0
- pyfect/stream/_base.py +69 -0
- pyfect/stream/_bridge_methods.py +139 -0
- pyfect/stream/_catch.py +135 -0
- pyfect/stream/_catch_reason.py +558 -0
- pyfect/stream/_channel_methods.py +61 -0
- pyfect/stream/_chunk_methods.py +169 -0
- pyfect/stream/_chunking.py +310 -0
- pyfect/stream/_concat.py +65 -0
- pyfect/stream/_constructors.py +341 -0
- pyfect/stream/_context.py +147 -0
- pyfect/stream/_error_methods.py +254 -0
- pyfect/stream/_execution_plan.py +199 -0
- pyfect/stream/_fanout.py +154 -0
- pyfect/stream/_fanout_methods.py +183 -0
- pyfect/stream/_filter_methods.py +203 -0
- pyfect/stream/_grouping.py +168 -0
- pyfect/stream/_many.py +65 -0
- pyfect/stream/_map_methods.py +233 -0
- pyfect/stream/_mapping.py +126 -0
- pyfect/stream/_run.py +114 -0
- pyfect/stream/_run_methods.py +114 -0
- pyfect/stream/_service_methods.py +36 -0
- pyfect/stream/_sink_methods.py +151 -0
- pyfect/stream/_sinking.py +277 -0
- pyfect/stream/_sources.py +124 -0
- pyfect/stream/_stream.py +162 -0
- pyfect/stream/_taking.py +330 -0
- pyfect/stream/_text.py +43 -0
- pyfect/stream/_timing.py +294 -0
- pyfect/stream/_timing_methods.py +252 -0
- pyfect/stream/_zip_methods.py +279 -0
- pyfect/stream/_zipping.py +213 -0
- pyfect/subscription_ref.py +66 -0
- pyfect/synchronized_ref.py +38 -0
- pyfect/take.py +36 -0
- pyfect/terminal/__init__.py +41 -0
- pyfect/terminal/_keys.py +113 -0
- pyfect/terminal/_live.py +223 -0
- pyfect/terminal/_service.py +73 -0
- pyfect/testing/__init__.py +122 -0
- pyfect/testing/_assertions.py +92 -0
- pyfect/testing/_clock.py +309 -0
- pyfect/testing/_console.py +164 -0
- pyfect/testing/_crypto.py +44 -0
- pyfect/testing/_decorators.py +211 -0
- pyfect/testing/_flaky.py +34 -0
- pyfect/testing/_layers.py +153 -0
- pyfect/testing/_prop.py +255 -0
- pyfect/testing/_runners.py +84 -0
- pyfect/testing/_runtime.py +174 -0
- pyfect/testing/_schema.py +238 -0
- pyfect/testing/plugin.py +59 -0
- pyfect/tracer/__init__.py +107 -0
- pyfect/tracer/_model.py +176 -0
- pyfect/tracer/_native.py +172 -0
- pyfect/tracer/_refs.py +87 -0
- pyfect/tracer/_spans.py +269 -0
- pyfect/transaction.py +340 -0
- pyfect/trie.py +317 -0
- pyfect/tx_chunk.py +127 -0
- pyfect/tx_deferred.py +75 -0
- pyfect/tx_hash_map.py +265 -0
- pyfect/tx_hash_set.py +151 -0
- pyfect/tx_priority_queue.py +224 -0
- pyfect/tx_pubsub.py +194 -0
- pyfect/tx_queue/__init__.py +27 -0
- pyfect/tx_queue/_queue.py +359 -0
- pyfect/tx_queue/_views.py +60 -0
- pyfect/tx_reentrant_lock.py +201 -0
- pyfect/tx_ref.py +79 -0
- pyfect/tx_semaphore.py +136 -0
- pyfect/tx_subscription_ref.py +118 -0
- pyfect/workers/__init__.py +62 -0
- pyfect/workers/_frames.py +82 -0
- pyfect/workers/_mailbox.py +51 -0
- pyfect/workers/_port.py +106 -0
- pyfect/workers/_process.py +155 -0
- pyfect/workers/_thread.py +100 -0
- pyfect/workers/python_worker.py +133 -0
- pyfect/workers/python_worker_runner.py +112 -0
- pyfect/workers/transferable.py +103 -0
- pyfect/workers/worker.py +273 -0
- pyfect/workers/worker_error.py +86 -0
- pyfect/workers/worker_runner.py +85 -0
- pyfect/workflow/__init__.py +78 -0
- pyfect/workflow/_crypto.py +23 -0
- pyfect/workflow/_encoded.py +381 -0
- pyfect/workflow/_memory.py +424 -0
- pyfect/workflow/_result.py +43 -0
- pyfect/workflow/_services.py +241 -0
- pyfect/workflow/_suspension.py +153 -0
- pyfect/workflow/activity.py +427 -0
- pyfect/workflow/durable_clock.py +72 -0
- pyfect/workflow/durable_deferred.py +403 -0
- pyfect/workflow/durable_queue.py +259 -0
- pyfect/workflow/workflow.py +509 -0
- pyfect/workflow/workflow_engine.py +42 -0
- pyfect/workflow/workflow_proxy.py +161 -0
- pyfect/workflow/workflow_proxy_server.py +146 -0
- pyfect_dev-0.1.0.dist-info/METADATA +296 -0
- pyfect_dev-0.1.0.dist-info/RECORD +1265 -0
- pyfect_dev-0.1.0.dist-info/WHEEL +4 -0
- pyfect_dev-0.1.0.dist-info/entry_points.txt +3 -0
- pyfect_dev-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1099 @@
|
|
|
1
|
+
"""The cluster workflow engine (Effect's ``ClusterWorkflowEngine.make``):
|
|
2
|
+
workflow executions, activities, deferred completions, resumes, interrupts
|
|
3
|
+
and durable clocks are persisted entity messages, so a durable workflow
|
|
4
|
+
survives the runner that started it."""
|
|
5
|
+
# pyright: reportPrivateUsage=false
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import builtins
|
|
10
|
+
from collections.abc import Callable, Generator
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import UTC, datetime, timedelta
|
|
13
|
+
from math import ceil
|
|
14
|
+
from typing import Any, Literal, Never, cast
|
|
15
|
+
|
|
16
|
+
from pyfect._catch import catch_tag
|
|
17
|
+
from pyfect._core import Effect, _WithFiber
|
|
18
|
+
from pyfect._fiber import Fiber
|
|
19
|
+
from pyfect._json_text import stringify
|
|
20
|
+
from pyfect.cause import Cause, Interrupt
|
|
21
|
+
from pyfect.cluster import _cluster_abandon as cluster_abandon
|
|
22
|
+
from pyfect.cluster._entity_activation import CurrentActivationScope
|
|
23
|
+
from pyfect.cluster.cluster_error import EntityNotAssignedToRunner, PersistenceError
|
|
24
|
+
from pyfect.cluster.cluster_schema import Dynamic, ShardGroup, WithTransaction
|
|
25
|
+
from pyfect.cluster.cluster_workflow_engine._entities import (
|
|
26
|
+
ActivityPayload,
|
|
27
|
+
AnyWorkflow,
|
|
28
|
+
ClockEntity,
|
|
29
|
+
ClockPayload,
|
|
30
|
+
ClockRpc,
|
|
31
|
+
DeferredPayload,
|
|
32
|
+
DeferredRpc,
|
|
33
|
+
Parent,
|
|
34
|
+
ResumePayload,
|
|
35
|
+
RunPayload,
|
|
36
|
+
activity_primary_key,
|
|
37
|
+
make_partial_workflow_entity,
|
|
38
|
+
make_workflow_entity,
|
|
39
|
+
run_payload,
|
|
40
|
+
run_rpc_of,
|
|
41
|
+
workflow_payload,
|
|
42
|
+
)
|
|
43
|
+
from pyfect.cluster.entity import CurrentAddress, Entity, EntityHandlers, EntityOptions, Request
|
|
44
|
+
from pyfect.cluster.entity_address import EntityAddress
|
|
45
|
+
from pyfect.cluster.entity_id import EntityId
|
|
46
|
+
from pyfect.cluster.entity_type import EntityType
|
|
47
|
+
from pyfect.cluster.envelope import Interrupt as InterruptEnvelope
|
|
48
|
+
from pyfect.cluster.envelope import Request as EnvelopeRequest
|
|
49
|
+
from pyfect.cluster.envelope import make_request
|
|
50
|
+
from pyfect.cluster.message import OutgoingEnvelope, OutgoingRequest
|
|
51
|
+
from pyfect.cluster.message_storage import MessageStorage, MessageStorageService
|
|
52
|
+
from pyfect.cluster.reply import with_exit_schema
|
|
53
|
+
from pyfect.cluster.sharding import Sharding, ShardingService
|
|
54
|
+
from pyfect.cluster.snowflake import Snowflake
|
|
55
|
+
from pyfect.context import Context
|
|
56
|
+
from pyfect.date_time import now
|
|
57
|
+
from pyfect.deferred import Deferred
|
|
58
|
+
from pyfect.duration import Seconds, to_millis
|
|
59
|
+
from pyfect.effect import (
|
|
60
|
+
context,
|
|
61
|
+
die,
|
|
62
|
+
fail,
|
|
63
|
+
fail_cause,
|
|
64
|
+
from_exit,
|
|
65
|
+
gen,
|
|
66
|
+
never,
|
|
67
|
+
provide_context,
|
|
68
|
+
provide_service,
|
|
69
|
+
service_option,
|
|
70
|
+
succeed,
|
|
71
|
+
suspend,
|
|
72
|
+
sync,
|
|
73
|
+
void,
|
|
74
|
+
)
|
|
75
|
+
from pyfect.exit import Exit, Failure, Success
|
|
76
|
+
from pyfect.logger import log_warning
|
|
77
|
+
from pyfect.rc_map import RcMap
|
|
78
|
+
from pyfect.rpc import wrap as rpc_wrap
|
|
79
|
+
from pyfect.rpc._rpc import AnyRpc, exit_schema
|
|
80
|
+
from pyfect.rpc.schema import ClientAbort
|
|
81
|
+
from pyfect.schedule import exponential, min, spaced
|
|
82
|
+
from pyfect.schema import decode_unknown_effect, decode_unknown_sync, to_codec_json
|
|
83
|
+
from pyfect.schema import to_json_schema_document as json_schema_document
|
|
84
|
+
from pyfect.scope import CloseableScope, Scope, scoped
|
|
85
|
+
from pyfect.semaphore import Semaphore
|
|
86
|
+
from pyfect.tracer import annotate_current_span, current_span
|
|
87
|
+
from pyfect.workflow._encoded import DeferredState, Encoded, Handler, make_deferred_state
|
|
88
|
+
from pyfect.workflow._encoded import make_unsafe as make_engine_unsafe
|
|
89
|
+
from pyfect.workflow._result import Result, Suspended
|
|
90
|
+
from pyfect.workflow._services import (
|
|
91
|
+
WorkflowEngine,
|
|
92
|
+
WorkflowEngineService,
|
|
93
|
+
WorkflowInstance,
|
|
94
|
+
WorkflowInstanceService,
|
|
95
|
+
)
|
|
96
|
+
from pyfect.workflow.activity import Activity, CurrentAttempt
|
|
97
|
+
from pyfect.workflow.durable_clock import DurableClock
|
|
98
|
+
from pyfect.workflow.durable_clock import make as make_clock
|
|
99
|
+
from pyfect.workflow.durable_deferred import DurableDeferred
|
|
100
|
+
from pyfect.workflow.durable_deferred import make as make_deferred
|
|
101
|
+
from pyfect.workflow.workflow import SuspendOnFailure, into_result
|
|
102
|
+
|
|
103
|
+
type WakeMode = Literal["explicit", "automatic"]
|
|
104
|
+
type ReplyEncoded = dict[str, Any]
|
|
105
|
+
|
|
106
|
+
INTERRUPT_SIGNAL: DurableDeferred[None, Never] = make_deferred("Workflow/InterruptSignal")
|
|
107
|
+
|
|
108
|
+
# Workflow state is durable, so an idle entity (completed or suspended) can be
|
|
109
|
+
# released quickly and is rebuilt from storage when the next message arrives.
|
|
110
|
+
# This keeps completed and suspended executions from holding
|
|
111
|
+
# ``max_resident_entities`` slots for the full ``entity_max_idle_time``.
|
|
112
|
+
ENTITY_MAX_IDLE_TIME = Seconds(10)
|
|
113
|
+
|
|
114
|
+
_EPOCH = datetime(1970, 1, 1, tzinfo=UTC)
|
|
115
|
+
|
|
116
|
+
_retry_policy = min(exponential(0.2, 1.5), spaced(60))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def ensure_success[A](effect: Effect[A, Any, Any]) -> Effect[A, Never, Any]:
|
|
120
|
+
"""Retry ``effect`` until it succeeds. A cluster abandonment means this
|
|
121
|
+
owner can no longer complete it, so retrying would only wait on a
|
|
122
|
+
retired run: it is raised again as an annotated interrupt, so the
|
|
123
|
+
activation can unwind and its shard can be released."""
|
|
124
|
+
|
|
125
|
+
def retryable(cause: Cause[Any]) -> bool:
|
|
126
|
+
return not cluster_abandon.is_cause(cause)
|
|
127
|
+
|
|
128
|
+
def abandon(_: Cause[Any]) -> Effect[Never]:
|
|
129
|
+
return cluster_abandon.interrupt
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
effect.sandbox()
|
|
133
|
+
.retry(schedule=_retry_policy, while_=retryable)
|
|
134
|
+
.catch_if(cluster_abandon.is_cause, abandon)
|
|
135
|
+
.or_die()
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _retry_persistence[A](effect: Effect[A, Any, Any]) -> Effect[A, Never, Any]:
|
|
140
|
+
return effect.retry(
|
|
141
|
+
times=3,
|
|
142
|
+
schedule=exponential(0.25),
|
|
143
|
+
while_=lambda e: isinstance(e, PersistenceError),
|
|
144
|
+
).or_die()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _is_suspended_reply(reply: ReplyEncoded) -> bool:
|
|
148
|
+
exit = reply["exit"]
|
|
149
|
+
return exit.get("_tag") == "Success" and exit["value"].get("_tag") == "Suspended"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _is_failed_reply(reply: ReplyEncoded) -> bool:
|
|
153
|
+
return _is_suspended_reply(reply) and reply["exit"]["value"].get("failed") is True
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
_decode_deferred_with_exit = decode_unknown_sync(
|
|
157
|
+
to_codec_json(with_exit_schema(exit_schema(DeferredRpc)))
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _deferred_exit(reply: ReplyEncoded) -> Exit[Any, Any]:
|
|
162
|
+
decoded = _decode_deferred_with_exit(reply)
|
|
163
|
+
exit = decoded.exit
|
|
164
|
+
return cast("Exit[Any, Any]", exit.value) if isinstance(exit, Success) else exit
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def payload_shape(workflow: AnyWorkflow) -> str:
|
|
168
|
+
"""The JSON Schema of ``workflow``'s payload, for a warning."""
|
|
169
|
+
try:
|
|
170
|
+
document = json_schema_document(workflow.payload_schema)
|
|
171
|
+
schema: dict[str, object] = dict(document.schema)
|
|
172
|
+
definitions = document.definitions
|
|
173
|
+
shown = schema if not definitions else {**schema, "$defs": definitions}
|
|
174
|
+
return stringify(shown)
|
|
175
|
+
except Exception:
|
|
176
|
+
return "<unavailable>"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass(slots=True, eq=False)
|
|
180
|
+
class _ActivationState:
|
|
181
|
+
workflow_name: str
|
|
182
|
+
execution_id: str
|
|
183
|
+
results: dict[str, Exit[Any, Any]]
|
|
184
|
+
# Every run forks its scope from here, so runs that suspended keep their
|
|
185
|
+
# resources and durable finalizers until the execution finishes.
|
|
186
|
+
scope: CloseableScope
|
|
187
|
+
instances: set[WorkflowInstanceService] = field(default_factory=set[WorkflowInstanceService])
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@dataclass(slots=True, eq=False)
|
|
191
|
+
class _CurrentRun:
|
|
192
|
+
request: Request[Any]
|
|
193
|
+
instance: WorkflowInstanceService
|
|
194
|
+
# Activity awaits must wake the run after suspension without preempting it.
|
|
195
|
+
activity_awaits: dict[str, int]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@dataclass(frozen=True, slots=True)
|
|
199
|
+
class _ActivityEntry:
|
|
200
|
+
activity: Activity[Any, Any, Any]
|
|
201
|
+
context: Context[Any]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class ClusterEngine:
|
|
205
|
+
"""The state of one cluster workflow engine and its ``Encoded``
|
|
206
|
+
operations."""
|
|
207
|
+
|
|
208
|
+
def __init__(
|
|
209
|
+
self,
|
|
210
|
+
sharding: ShardingService,
|
|
211
|
+
storage: MessageStorageService,
|
|
212
|
+
clients: RcMap[str, Callable[[str], Any], Never],
|
|
213
|
+
clients_partial: RcMap[str, Callable[[str], Any], Never],
|
|
214
|
+
holder: dict[str, ClusterEngine],
|
|
215
|
+
) -> None:
|
|
216
|
+
self.sharding = sharding
|
|
217
|
+
self.storage = storage
|
|
218
|
+
self.clients = clients
|
|
219
|
+
self.clients_partial = clients_partial
|
|
220
|
+
holder["engine"] = self
|
|
221
|
+
self.workflows: dict[str, AnyWorkflow] = {}
|
|
222
|
+
self.entities: dict[str, builtins.type[Entity]] = {}
|
|
223
|
+
self.partial_entities: dict[str, builtins.type[Entity]] = {}
|
|
224
|
+
self.activities: dict[str, _ActivityEntry] = {}
|
|
225
|
+
self.interrupted_activities: set[str] = set()
|
|
226
|
+
self.deferred_state: DeferredState = make_deferred_state()
|
|
227
|
+
# Rebuilds share an activation; overlapping activations have separate
|
|
228
|
+
# results. Removed once the activation has closed its runs.
|
|
229
|
+
self.pending_by_activation: dict[int, tuple[CloseableScope, _ActivationState]] = {}
|
|
230
|
+
self.service: WorkflowEngineService = make_engine_unsafe(
|
|
231
|
+
Encoded(
|
|
232
|
+
register=self.register,
|
|
233
|
+
execute=self.execute,
|
|
234
|
+
poll=self.poll,
|
|
235
|
+
interrupt=lambda workflow, execution_id: self.interrupt(
|
|
236
|
+
workflow, execution_id
|
|
237
|
+
).as_void(),
|
|
238
|
+
interrupt_unsafe=self.interrupt_unsafe,
|
|
239
|
+
resume=lambda workflow, execution_id: ensure_success(
|
|
240
|
+
self.resume(workflow, execution_id, "explicit")
|
|
241
|
+
),
|
|
242
|
+
activity_execute=self.activity_execute,
|
|
243
|
+
deferred_result=self.deferred_result,
|
|
244
|
+
deferred_done=self.deferred_done,
|
|
245
|
+
schedule_clock=self.schedule_clock,
|
|
246
|
+
await_complete=self.await_complete,
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# --- entities and addresses ----------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
def ensure_entity(self, workflow: AnyWorkflow) -> builtins.type[Entity]:
|
|
253
|
+
entity = self.entities.get(workflow.tag)
|
|
254
|
+
if entity is None:
|
|
255
|
+
entity = make_workflow_entity(workflow)
|
|
256
|
+
self.workflows[workflow.tag] = workflow
|
|
257
|
+
self.entities[workflow.tag] = entity
|
|
258
|
+
return entity
|
|
259
|
+
|
|
260
|
+
def ensure_partial_entity(self, workflow_name: str) -> builtins.type[Entity]:
|
|
261
|
+
entity = self.partial_entities.get(workflow_name)
|
|
262
|
+
if entity is None:
|
|
263
|
+
entity = make_partial_workflow_entity(workflow_name)
|
|
264
|
+
self.partial_entities[workflow_name] = entity
|
|
265
|
+
return entity
|
|
266
|
+
|
|
267
|
+
def entity_address_for(
|
|
268
|
+
self, workflow: AnyWorkflow, entity_type: str, execution_id: str
|
|
269
|
+
) -> EntityAddress:
|
|
270
|
+
entity_id = EntityId(execution_id)
|
|
271
|
+
shard_group = workflow.annotations.get(ShardGroup)(entity_id)
|
|
272
|
+
return EntityAddress(
|
|
273
|
+
entity_type=EntityType(entity_type),
|
|
274
|
+
entity_id=entity_id,
|
|
275
|
+
shard_id=self.sharding.get_shard_id(entity_id, shard_group),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
def run_address(self, workflow: AnyWorkflow, execution_id: str) -> EntityAddress:
|
|
279
|
+
return self.entity_address_for(workflow, f"Workflow/{workflow.tag}", execution_id)
|
|
280
|
+
|
|
281
|
+
def activation_state(self, activation: CloseableScope | None) -> _ActivationState | None:
|
|
282
|
+
if activation is None:
|
|
283
|
+
return None
|
|
284
|
+
entry = self.pending_by_activation.get(id(activation))
|
|
285
|
+
return entry[1] if entry is not None and entry[0] is activation else None
|
|
286
|
+
|
|
287
|
+
def current_state(self) -> Effect[_ActivationState | None]:
|
|
288
|
+
return service_option(CurrentActivationScope).map(
|
|
289
|
+
lambda found: None if found is None else self.activation_state(found.value)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# --- storage ------------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
def send_discard(
|
|
295
|
+
self, rpc: AnyRpc, address: EntityAddress, payload: object
|
|
296
|
+
) -> Effect[None, Any]:
|
|
297
|
+
def body() -> Generator[Effect[Any, Any, Any], Any]:
|
|
298
|
+
span = yield from current_span().or_die()
|
|
299
|
+
envelope = make_request(
|
|
300
|
+
request_id=(yield from self.sharding.get_snowflake),
|
|
301
|
+
address=address,
|
|
302
|
+
tag=rpc.tag,
|
|
303
|
+
payload=payload,
|
|
304
|
+
trace_id=span.trace_id,
|
|
305
|
+
span_id=span.span_id,
|
|
306
|
+
sampled=span.sampled,
|
|
307
|
+
)
|
|
308
|
+
message = OutgoingRequest(
|
|
309
|
+
envelope=envelope,
|
|
310
|
+
context=Context.empty(),
|
|
311
|
+
last_received_reply=None,
|
|
312
|
+
rpc=rpc,
|
|
313
|
+
respond=lambda _: void,
|
|
314
|
+
annotations=rpc.annotations.get(Dynamic)(rpc.annotations, envelope),
|
|
315
|
+
)
|
|
316
|
+
yield from self.sharding.send_outgoing(message, True)
|
|
317
|
+
|
|
318
|
+
return gen(body)
|
|
319
|
+
|
|
320
|
+
def request_id_for(
|
|
321
|
+
self, workflow: AnyWorkflow, entity_type: str, execution_id: str, tag: str, id: str
|
|
322
|
+
) -> Effect[Snowflake | None, PersistenceError]:
|
|
323
|
+
address = self.entity_address_for(workflow, entity_type, execution_id)
|
|
324
|
+
return self.storage.request_id_for_primary_key(address=address, tag=tag, id=id)
|
|
325
|
+
|
|
326
|
+
def reply_for_request_id(self, request_id: Snowflake) -> Effect[ReplyEncoded | None, Any]:
|
|
327
|
+
def last(replies: list[Any]) -> ReplyEncoded | None:
|
|
328
|
+
if replies and replies[-1].get("_tag") == "WithExit":
|
|
329
|
+
return cast("ReplyEncoded", replies[-1])
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
return self.storage.replies_for_unfiltered([request_id]).map(last)
|
|
333
|
+
|
|
334
|
+
def request_reply(
|
|
335
|
+
self, workflow: AnyWorkflow, entity_type: str, execution_id: str, tag: str, id: str
|
|
336
|
+
) -> Effect[ReplyEncoded | None, Any]:
|
|
337
|
+
def found(request_id: Snowflake | None) -> Effect[ReplyEncoded | None, Any]:
|
|
338
|
+
if request_id is None:
|
|
339
|
+
return succeed(None)
|
|
340
|
+
return self.reply_for_request_id(request_id)
|
|
341
|
+
|
|
342
|
+
return self.request_id_for(workflow, entity_type, execution_id, tag, id).flat_map(found)
|
|
343
|
+
|
|
344
|
+
def run_reply(
|
|
345
|
+
self, workflow: AnyWorkflow, execution_id: str
|
|
346
|
+
) -> Effect[ReplyEncoded | None, Any]:
|
|
347
|
+
return self.request_reply(workflow, f"Workflow/{workflow.tag}", execution_id, "run", "")
|
|
348
|
+
|
|
349
|
+
def reset_activity_attempt(
|
|
350
|
+
self,
|
|
351
|
+
workflow: AnyWorkflow,
|
|
352
|
+
execution_id: str,
|
|
353
|
+
activity: Activity[Any, Any, Any],
|
|
354
|
+
attempt: int,
|
|
355
|
+
) -> Effect[None]:
|
|
356
|
+
def body() -> Generator[Effect[Any, Any, Any], Any]:
|
|
357
|
+
request_id = yield from self.request_id_for(
|
|
358
|
+
workflow,
|
|
359
|
+
f"Workflow/{workflow.tag}",
|
|
360
|
+
execution_id,
|
|
361
|
+
"activity",
|
|
362
|
+
activity_primary_key(activity.name, attempt),
|
|
363
|
+
)
|
|
364
|
+
if request_id is None:
|
|
365
|
+
return
|
|
366
|
+
yield from self.sharding.reset(request_id)
|
|
367
|
+
|
|
368
|
+
return gen(body).retry(times=3, schedule=exponential(0.25)).or_die()
|
|
369
|
+
|
|
370
|
+
def clear_clock(self, workflow: AnyWorkflow, execution_id: str) -> Effect[None, Any]:
|
|
371
|
+
address = self.entity_address_for(workflow, ClockEntity.type, execution_id)
|
|
372
|
+
return self.storage.clear_address(address)
|
|
373
|
+
|
|
374
|
+
def resume(self, workflow: AnyWorkflow, execution_id: str, mode: WakeMode) -> Effect[None, Any]:
|
|
375
|
+
def body() -> Generator[Effect[Any, Any, Any], Any]:
|
|
376
|
+
reply = yield from self.run_reply(workflow, execution_id)
|
|
377
|
+
if reply is None or not _is_suspended_reply(reply):
|
|
378
|
+
return
|
|
379
|
+
# Replies without ``failed`` retain automatic wakes for compatibility.
|
|
380
|
+
if mode == "automatic" and _is_failed_reply(reply):
|
|
381
|
+
return
|
|
382
|
+
reset = yield from self.sharding.reset(
|
|
383
|
+
Snowflake(int(reply["requestId"])),
|
|
384
|
+
expected_reply_id=Snowflake(int(reply["id"])),
|
|
385
|
+
)
|
|
386
|
+
if not reset:
|
|
387
|
+
return (yield from fail(RuntimeError("Failed to reset workflow run")))
|
|
388
|
+
yield from self.sharding.poll_storage
|
|
389
|
+
|
|
390
|
+
return gen(body)
|
|
391
|
+
|
|
392
|
+
def interrupt_pending(self, instance: WorkflowInstanceService) -> Effect[bool]:
|
|
393
|
+
"""Whether a persisted interrupt is pending for ``instance``:
|
|
394
|
+
activations load it when they start, so this reads only memory."""
|
|
395
|
+
|
|
396
|
+
def check(state: _ActivationState | None) -> bool:
|
|
397
|
+
return (
|
|
398
|
+
state is not None
|
|
399
|
+
and state.workflow_name == instance.workflow.tag
|
|
400
|
+
and state.execution_id == instance.execution_id
|
|
401
|
+
and INTERRUPT_SIGNAL.name in state.results
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
return self.current_state().map(check)
|
|
405
|
+
|
|
406
|
+
def send_resume_parent(self, parent: Parent, child_execution_id: str) -> Effect[None, Any]:
|
|
407
|
+
def body() -> Generator[Effect[Any, Any, Any], Any]:
|
|
408
|
+
workflow_name = parent["workflowName"]
|
|
409
|
+
request_id = yield from self.request_id_for(
|
|
410
|
+
self.workflows[workflow_name],
|
|
411
|
+
f"Workflow/{workflow_name}",
|
|
412
|
+
parent["executionId"],
|
|
413
|
+
"resume",
|
|
414
|
+
child_execution_id,
|
|
415
|
+
)
|
|
416
|
+
if request_id is None:
|
|
417
|
+
make_client = yield from self.clients_partial.get(workflow_name)
|
|
418
|
+
client: Any = make_client(parent["executionId"])
|
|
419
|
+
discard: Effect[None, Any] = client.resume.discard(
|
|
420
|
+
ResumePayload(childExecutionId=child_execution_id)
|
|
421
|
+
)
|
|
422
|
+
return (yield from discard)
|
|
423
|
+
reply = yield from self.reply_for_request_id(request_id)
|
|
424
|
+
if reply is None:
|
|
425
|
+
return
|
|
426
|
+
yield from self.sharding.reset(request_id)
|
|
427
|
+
|
|
428
|
+
return scoped(gen(body))
|
|
429
|
+
|
|
430
|
+
def await_run_reply(
|
|
431
|
+
self,
|
|
432
|
+
workflow: AnyWorkflow,
|
|
433
|
+
request: EnvelopeRequest[Any],
|
|
434
|
+
is_settled: Callable[[ReplyEncoded], bool],
|
|
435
|
+
) -> Effect[bool, Any]:
|
|
436
|
+
"""Wait for the run's next reply, subscribing before reading
|
|
437
|
+
storage, so a reply cannot fall between the read and the
|
|
438
|
+
subscription: ``True`` when the stored reply already settles the
|
|
439
|
+
wait, ``False`` when a new reply arrived."""
|
|
440
|
+
|
|
441
|
+
def body() -> Generator[Effect[Any, Any, Any], Any, bool]:
|
|
442
|
+
waiter: Fiber[None, Any] = yield from self.storage.register_reply_handler(
|
|
443
|
+
OutgoingRequest(
|
|
444
|
+
envelope=request,
|
|
445
|
+
context=Context.empty(),
|
|
446
|
+
rpc=run_rpc_of(self.ensure_entity(workflow)),
|
|
447
|
+
last_received_reply=None,
|
|
448
|
+
respond=lambda _: void,
|
|
449
|
+
annotations=Context.empty(),
|
|
450
|
+
)
|
|
451
|
+
).fork_scoped(start_immediately=True)
|
|
452
|
+
reply = yield from self.reply_for_request_id(request.request_id)
|
|
453
|
+
if reply is not None and is_settled(reply):
|
|
454
|
+
return True
|
|
455
|
+
yield from waiter.join()
|
|
456
|
+
return False
|
|
457
|
+
|
|
458
|
+
return scoped(gen(body))
|
|
459
|
+
|
|
460
|
+
def wait_for_run_reply(self, workflow: AnyWorkflow, request: Request[Any]) -> Effect[None]:
|
|
461
|
+
"""A child or deferred may complete while a run is still unwinding:
|
|
462
|
+
wait for the run's reply, so the wake cannot fall between the run's
|
|
463
|
+
last read and its suspended reply."""
|
|
464
|
+
|
|
465
|
+
def on_cause(cause: Cause[Any]) -> Effect[None, Any]:
|
|
466
|
+
if cluster_abandon.is_cause(cause):
|
|
467
|
+
return cluster_abandon.interrupt
|
|
468
|
+
return fail_cause(cause)
|
|
469
|
+
|
|
470
|
+
return cast(
|
|
471
|
+
"Effect[None]",
|
|
472
|
+
self.await_run_reply(workflow, request, lambda _: True)
|
|
473
|
+
.as_void()
|
|
474
|
+
.pipe(catch_tag(EntityNotAssignedToRunner, lambda _: cluster_abandon.interrupt))
|
|
475
|
+
.catch_cause(on_cause),
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
def await_complete(self, workflow: AnyWorkflow, execution_id: str) -> Effect[None]:
|
|
479
|
+
"""Wake an external caller once its run publishes a reply other than
|
|
480
|
+
``Suspended``. Replies are only observed on the runner that saves
|
|
481
|
+
them, so callers on other runners, and any failure here, leave
|
|
482
|
+
delivery to the caller's retry schedule."""
|
|
483
|
+
|
|
484
|
+
def body() -> Generator[Effect[Any, Any, Any], Any]:
|
|
485
|
+
address = self.run_address(workflow, execution_id)
|
|
486
|
+
if not self.sharding.has_shard_id(address.shard_id):
|
|
487
|
+
return (yield from never)
|
|
488
|
+
request_id = yield from self.storage.request_id_for_primary_key(
|
|
489
|
+
address=address, tag="run", id=""
|
|
490
|
+
)
|
|
491
|
+
if request_id is None:
|
|
492
|
+
return (yield from never)
|
|
493
|
+
request = make_request(
|
|
494
|
+
request_id=request_id, address=address, tag="run", payload=RunPayload()
|
|
495
|
+
)
|
|
496
|
+
# A replay that suspends again re-arms the wait, so it does not
|
|
497
|
+
# spend a step of the caller's retry schedule.
|
|
498
|
+
settled = False
|
|
499
|
+
while not settled:
|
|
500
|
+
settled = yield from self.await_run_reply(
|
|
501
|
+
workflow, request, lambda reply: not _is_suspended_reply(reply)
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
return gen(body).catch_cause(lambda _: never)
|
|
505
|
+
|
|
506
|
+
def interrupt(self, workflow: AnyWorkflow, execution_id: str) -> Effect[Snowflake | None]:
|
|
507
|
+
def body() -> Generator[Effect[Any, Any, Any], Any, Snowflake | None]:
|
|
508
|
+
self.ensure_entity(workflow)
|
|
509
|
+
request_id = yield from self.request_id_for(
|
|
510
|
+
workflow, f"Workflow/{workflow.tag}", execution_id, "run", ""
|
|
511
|
+
)
|
|
512
|
+
if request_id is None:
|
|
513
|
+
return None
|
|
514
|
+
reply = yield from self.reply_for_request_id(request_id)
|
|
515
|
+
if reply is not None and not _is_suspended_reply(reply):
|
|
516
|
+
return None
|
|
517
|
+
yield from self.service.deferred_done(
|
|
518
|
+
INTERRUPT_SIGNAL,
|
|
519
|
+
workflow_name=workflow.tag,
|
|
520
|
+
execution_id=execution_id,
|
|
521
|
+
deferred_name=INTERRUPT_SIGNAL.name,
|
|
522
|
+
exit=Success(None),
|
|
523
|
+
)
|
|
524
|
+
return request_id
|
|
525
|
+
|
|
526
|
+
return _retry_persistence(gen(body))
|
|
527
|
+
|
|
528
|
+
def interrupt_unsafe(self, workflow: AnyWorkflow, execution_id: str) -> Effect[None]:
|
|
529
|
+
def body() -> Generator[Effect[Any, Any, Any], Any]:
|
|
530
|
+
request_id = yield from self.interrupt(workflow, execution_id)
|
|
531
|
+
if request_id is None:
|
|
532
|
+
return
|
|
533
|
+
entity = self.ensure_entity(workflow)
|
|
534
|
+
envelope = InterruptEnvelope(
|
|
535
|
+
id=(yield from self.sharding.get_snowflake),
|
|
536
|
+
address=self.run_address(workflow, execution_id),
|
|
537
|
+
request_id=request_id,
|
|
538
|
+
)
|
|
539
|
+
yield from self.sharding.send_outgoing(
|
|
540
|
+
OutgoingEnvelope(envelope=envelope, rpc=run_rpc_of(entity)), False
|
|
541
|
+
).or_die()
|
|
542
|
+
|
|
543
|
+
return gen(body)
|
|
544
|
+
|
|
545
|
+
# --- registration -------------------------------------------------------------------
|
|
546
|
+
|
|
547
|
+
def register(self, workflow: AnyWorkflow, execute: Handler, /) -> Effect[None, Never, Scope]:
|
|
548
|
+
def body() -> Effect[None, Never, Scope]:
|
|
549
|
+
existing = self.workflows.get(workflow.tag)
|
|
550
|
+
registration = self.sharding.register_entity(
|
|
551
|
+
self.ensure_entity(workflow),
|
|
552
|
+
gen(lambda: self._build_handlers(workflow, execute)),
|
|
553
|
+
# Alongside the run, let deferred handlers briefly acquire a
|
|
554
|
+
# permit to fork their wake and return an asynchronous reply.
|
|
555
|
+
EntityOptions(concurrency=2, max_idle_time=ENTITY_MAX_IDLE_TIME),
|
|
556
|
+
)
|
|
557
|
+
if existing is None or existing is workflow:
|
|
558
|
+
return registration
|
|
559
|
+
return log_warning(
|
|
560
|
+
f'Workflow "{workflow.tag}" is already registered with payload shape '
|
|
561
|
+
f"{payload_shape(existing)}; ignoring duplicate definition with payload "
|
|
562
|
+
f"shape {payload_shape(workflow)}"
|
|
563
|
+
).and_then(registration)
|
|
564
|
+
|
|
565
|
+
return suspend(body)
|
|
566
|
+
|
|
567
|
+
def _load_activation(
|
|
568
|
+
self, workflow: AnyWorkflow, execution_id: str, activation: CloseableScope
|
|
569
|
+
) -> Generator[Effect[Any, Any, Any], Any, _ActivationState]:
|
|
570
|
+
pending = self.activation_state(activation)
|
|
571
|
+
if pending is not None:
|
|
572
|
+
return pending
|
|
573
|
+
results: dict[str, Exit[Any, Any]] = {}
|
|
574
|
+
# A new owner must see an interrupt persisted before it took over.
|
|
575
|
+
# Publish the cache only once loaded, so a retried build repeats a
|
|
576
|
+
# failed lookup.
|
|
577
|
+
interrupt = yield from self.request_reply(
|
|
578
|
+
workflow,
|
|
579
|
+
f"Workflow/{workflow.tag}",
|
|
580
|
+
execution_id,
|
|
581
|
+
"deferred",
|
|
582
|
+
INTERRUPT_SIGNAL.name,
|
|
583
|
+
).or_die()
|
|
584
|
+
if interrupt is not None:
|
|
585
|
+
results[INTERRUPT_SIGNAL.name] = _deferred_exit(interrupt)
|
|
586
|
+
pending = _ActivationState(workflow.tag, execution_id, results, CloseableScope())
|
|
587
|
+
key = id(activation)
|
|
588
|
+
|
|
589
|
+
def forget() -> None:
|
|
590
|
+
entry = self.pending_by_activation.get(key)
|
|
591
|
+
if entry is not None and entry[1] is pending:
|
|
592
|
+
del self.pending_by_activation[key]
|
|
593
|
+
|
|
594
|
+
yield from activation.add_finalizer(
|
|
595
|
+
lambda _: self.close_runs(pending, abandoned=True).and_then(sync(forget))
|
|
596
|
+
)
|
|
597
|
+
self.pending_by_activation[key] = (activation, pending)
|
|
598
|
+
return pending
|
|
599
|
+
|
|
600
|
+
def close_runs(self, state: _ActivationState, *, abandoned: bool) -> Effect[None]:
|
|
601
|
+
"""Close the scopes retained by suspended runs. The latest run
|
|
602
|
+
already closed its own scope with its real exit, so earlier
|
|
603
|
+
finalizers observe success and skip superseded compensations.
|
|
604
|
+
Abandonment releases owner-local resources without running durable
|
|
605
|
+
finalizers, so the run can replay elsewhere."""
|
|
606
|
+
|
|
607
|
+
def run() -> Effect[None]:
|
|
608
|
+
scope = state.scope
|
|
609
|
+
if abandoned:
|
|
610
|
+
for instance in state.instances:
|
|
611
|
+
instance.abandoned = True
|
|
612
|
+
state.instances.clear()
|
|
613
|
+
state.scope = CloseableScope()
|
|
614
|
+
return scope.close(Success(None))
|
|
615
|
+
|
|
616
|
+
return suspend(run)
|
|
617
|
+
|
|
618
|
+
def _build_handlers(
|
|
619
|
+
self, workflow: AnyWorkflow, execute: Handler
|
|
620
|
+
) -> Generator[Effect[Any, Any, Any], Any, EntityHandlers[Any, Any]]:
|
|
621
|
+
address = yield from CurrentAddress()
|
|
622
|
+
execution_id = str(address.entity_id)
|
|
623
|
+
activation = yield from CurrentActivationScope()
|
|
624
|
+
pending = yield from self._load_activation(workflow, execution_id, activation)
|
|
625
|
+
return _Handlers(
|
|
626
|
+
self,
|
|
627
|
+
workflow=workflow,
|
|
628
|
+
execute=execute,
|
|
629
|
+
execution_id=execution_id,
|
|
630
|
+
activation=activation,
|
|
631
|
+
pending=pending,
|
|
632
|
+
).build()
|
|
633
|
+
|
|
634
|
+
# --- execution ----------------------------------------------------------------------
|
|
635
|
+
|
|
636
|
+
def execute(
|
|
637
|
+
self,
|
|
638
|
+
workflow: AnyWorkflow,
|
|
639
|
+
/,
|
|
640
|
+
*,
|
|
641
|
+
execution_id: str,
|
|
642
|
+
payload: object,
|
|
643
|
+
discard: bool,
|
|
644
|
+
parent: WorkflowInstanceService | None,
|
|
645
|
+
detached: bool,
|
|
646
|
+
) -> Effect[Result[Any, Any] | None]:
|
|
647
|
+
self.ensure_entity(workflow)
|
|
648
|
+
parent_key: Parent | None = None
|
|
649
|
+
if parent is not None:
|
|
650
|
+
# A detached child keeps its parent for observability only.
|
|
651
|
+
parent_key = Parent(workflowName=parent.workflow.tag, executionId=parent.execution_id)
|
|
652
|
+
if detached:
|
|
653
|
+
parent_key["detached"] = True
|
|
654
|
+
run = run_payload(workflow, payload, parent_key)
|
|
655
|
+
|
|
656
|
+
def call(make_client: Callable[[str], Any]) -> Effect[Any, Any]:
|
|
657
|
+
client = make_client(execution_id)
|
|
658
|
+
return client.run.discard(run) if discard else client.run(run)
|
|
659
|
+
|
|
660
|
+
return scoped(self.clients.get(workflow.tag).flat_map(call).or_die())
|
|
661
|
+
|
|
662
|
+
def poll(self, workflow: AnyWorkflow, execution_id: str) -> Effect[Result[Any, Any] | None]:
|
|
663
|
+
def body() -> Generator[Effect[Any, Any, Any], Any, Result[Any, Any] | None]:
|
|
664
|
+
entity = self.ensure_entity(workflow)
|
|
665
|
+
schema = to_codec_json(exit_schema(run_rpc_of(entity)))
|
|
666
|
+
reply = yield from self.run_reply(workflow, execution_id)
|
|
667
|
+
if reply is None:
|
|
668
|
+
return None
|
|
669
|
+
exit = yield from decode_unknown_effect(schema)(reply["exit"])
|
|
670
|
+
return (yield from from_exit(cast("Exit[Result[Any, Any], Any]", exit)))
|
|
671
|
+
|
|
672
|
+
return gen(body).or_die()
|
|
673
|
+
|
|
674
|
+
def activity_execute(
|
|
675
|
+
self, activity: Activity[Any, Any, Any], attempt: int
|
|
676
|
+
) -> Effect[Result[Any, Any], Never, WorkflowInstance]:
|
|
677
|
+
def body() -> Generator[Effect[Any, Any, Any], Any, Result[Any, Any]]:
|
|
678
|
+
services = yield from context()
|
|
679
|
+
instance = services.get_unsafe(WorkflowInstance)
|
|
680
|
+
yield from annotate_current_span("executionId", instance.execution_id)
|
|
681
|
+
activity_id = f"{instance.execution_id}/{activity.name}"
|
|
682
|
+
# A pending interrupt replays only to rebuild compensations, so it
|
|
683
|
+
# must not run new activities. Finalizers run once ``interrupted``
|
|
684
|
+
# is set.
|
|
685
|
+
if not instance.interrupted and (yield from self.interrupt_pending(instance)):
|
|
686
|
+
reply = yield from self.request_reply(
|
|
687
|
+
instance.workflow,
|
|
688
|
+
f"Workflow/{instance.workflow.tag}",
|
|
689
|
+
instance.execution_id,
|
|
690
|
+
"activity",
|
|
691
|
+
activity_primary_key(activity.name, attempt),
|
|
692
|
+
).or_die()
|
|
693
|
+
# Suspend so ``on_exit`` applies the interrupt to the run.
|
|
694
|
+
if reply is None or _is_suspended_reply(reply):
|
|
695
|
+
return Suspended()
|
|
696
|
+
make_client = yield from self.clients_partial.get(instance.workflow.tag)
|
|
697
|
+
client: Any = make_client(instance.execution_id)
|
|
698
|
+
while True:
|
|
699
|
+
if activity_id not in self.activities:
|
|
700
|
+
self.activities[activity_id] = _ActivityEntry(activity, services)
|
|
701
|
+
call: Effect[Result[Any, Any], Any] = client.activity(
|
|
702
|
+
ActivityPayload(
|
|
703
|
+
name=activity.name,
|
|
704
|
+
attempt=attempt,
|
|
705
|
+
withTransaction=activity.annotations.get(WithTransaction),
|
|
706
|
+
)
|
|
707
|
+
)
|
|
708
|
+
result = yield from call.or_die()
|
|
709
|
+
# If the activity has suspended and did not execute, resume it
|
|
710
|
+
# by resetting the attempt and executing it again.
|
|
711
|
+
if isinstance(result, Suspended) and (
|
|
712
|
+
activity_id in self.activities or activity_id in self.interrupted_activities
|
|
713
|
+
):
|
|
714
|
+
yield from self.reset_activity_attempt(
|
|
715
|
+
instance.workflow, instance.execution_id, activity, attempt
|
|
716
|
+
)
|
|
717
|
+
continue
|
|
718
|
+
self.activities.pop(activity_id, None)
|
|
719
|
+
return result
|
|
720
|
+
|
|
721
|
+
return cast("Effect[Result[Any, Any], Never, WorkflowInstance]", scoped(gen(body)))
|
|
722
|
+
|
|
723
|
+
def deferred_result(
|
|
724
|
+
self, deferred: DurableDeferred[Any, Any]
|
|
725
|
+
) -> Effect[Exit[Any, Any] | None, Never, WorkflowInstance]:
|
|
726
|
+
def body() -> Generator[Effect[Any, Any, Any], Any, Exit[Any, Any] | None]:
|
|
727
|
+
instance = yield from WorkflowInstance()
|
|
728
|
+
state = yield from self.current_state()
|
|
729
|
+
# Captured contexts can cross workflow boundaries. Never use another
|
|
730
|
+
# execution's completion, even if its deferred has the same name.
|
|
731
|
+
if (
|
|
732
|
+
state is not None
|
|
733
|
+
and state.workflow_name == instance.workflow.tag
|
|
734
|
+
and state.execution_id == instance.execution_id
|
|
735
|
+
):
|
|
736
|
+
found = state.results.get(deferred.name)
|
|
737
|
+
if found is not None:
|
|
738
|
+
return found
|
|
739
|
+
reply = yield from self.request_reply(
|
|
740
|
+
instance.workflow,
|
|
741
|
+
f"Workflow/{instance.workflow.tag}",
|
|
742
|
+
instance.execution_id,
|
|
743
|
+
"deferred",
|
|
744
|
+
deferred.name,
|
|
745
|
+
)
|
|
746
|
+
return None if reply is None else _deferred_exit(reply)
|
|
747
|
+
|
|
748
|
+
return cast(
|
|
749
|
+
"Effect[Exit[Any, Any] | None, Never, WorkflowInstance]",
|
|
750
|
+
_retry_persistence(gen(body)),
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
def deferred_done(
|
|
754
|
+
self, *, workflow_name: str, execution_id: str, deferred_name: str, exit: Exit[Any, Any]
|
|
755
|
+
) -> Effect[None]:
|
|
756
|
+
def body() -> Generator[Effect[Any, Any, Any], Any]:
|
|
757
|
+
found = yield from service_option(WorkflowInstance)
|
|
758
|
+
# Activities run with their own instance, so their completions
|
|
759
|
+
# still wake a run that suspended before reaching the await. An
|
|
760
|
+
# interrupt must wake the run even when it signals itself.
|
|
761
|
+
if (
|
|
762
|
+
found is not None
|
|
763
|
+
and found.value.execution_id == execution_id
|
|
764
|
+
and deferred_name != INTERRUPT_SIGNAL.name
|
|
765
|
+
):
|
|
766
|
+
found.value.completed_deferreds.add(deferred_name)
|
|
767
|
+
workflow = self.workflows.get(workflow_name)
|
|
768
|
+
payload = DeferredPayload(name=deferred_name, exit=exit)
|
|
769
|
+
if workflow is not None:
|
|
770
|
+
address = self.entity_address_for(
|
|
771
|
+
workflow, f"Workflow/{workflow_name}", execution_id
|
|
772
|
+
)
|
|
773
|
+
return (yield from self.send_discard(DeferredRpc, address, payload).or_die())
|
|
774
|
+
make_client = yield from self.clients_partial.get(workflow_name)
|
|
775
|
+
yield from make_client(execution_id).deferred.discard(payload).or_die()
|
|
776
|
+
|
|
777
|
+
return scoped(gen(body))
|
|
778
|
+
|
|
779
|
+
def schedule_clock(
|
|
780
|
+
self, workflow: AnyWorkflow, execution_id: str, clock: DurableClock
|
|
781
|
+
) -> Effect[None]:
|
|
782
|
+
def send(at: Any) -> Effect[None, Any]:
|
|
783
|
+
millis = (at - _EPOCH) / timedelta(milliseconds=1) + to_millis(clock.duration)
|
|
784
|
+
wake_up = _EPOCH + timedelta(milliseconds=ceil(millis))
|
|
785
|
+
payload = ClockPayload(name=clock.name, workflowName=workflow.tag, wakeUp=wake_up)
|
|
786
|
+
address = self.entity_address_for(workflow, ClockEntity.type, execution_id)
|
|
787
|
+
return self.send_discard(ClockRpc, address, payload)
|
|
788
|
+
|
|
789
|
+
return now().flat_map(send).or_die()
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
class _Handlers:
|
|
793
|
+
"""The handlers of one workflow entity activation (Effect's
|
|
794
|
+
``register`` build)."""
|
|
795
|
+
|
|
796
|
+
def __init__(
|
|
797
|
+
self,
|
|
798
|
+
engine: ClusterEngine,
|
|
799
|
+
*,
|
|
800
|
+
workflow: AnyWorkflow,
|
|
801
|
+
execute: Handler,
|
|
802
|
+
execution_id: str,
|
|
803
|
+
activation: CloseableScope,
|
|
804
|
+
pending: _ActivationState,
|
|
805
|
+
) -> None:
|
|
806
|
+
self.engine = engine
|
|
807
|
+
self.workflow = workflow
|
|
808
|
+
self.execute = execute
|
|
809
|
+
self.execution_id = execution_id
|
|
810
|
+
self.activation = activation
|
|
811
|
+
self.pending = pending
|
|
812
|
+
# Retain awaited names through reply persistence, including
|
|
813
|
+
# suspension. Replays reuse the request id but track a fresh set of
|
|
814
|
+
# awaits.
|
|
815
|
+
self.current_run: _CurrentRun | None = None
|
|
816
|
+
# Concurrent automatic wakes share one wait for the current run to
|
|
817
|
+
# publish its reply. Explicit wakes cannot be dropped, because an
|
|
818
|
+
# automatic wake skips the reset of a failure suspension, so they
|
|
819
|
+
# wait outside the gate. Operator resume does not use the gate.
|
|
820
|
+
self.resume_gate = Semaphore(1)
|
|
821
|
+
|
|
822
|
+
def build(self) -> EntityHandlers[Any, Any]:
|
|
823
|
+
entity = self.engine.ensure_entity(self.workflow)
|
|
824
|
+
rpcs = entity.rpcs
|
|
825
|
+
handlers: Any = entity.handlers()
|
|
826
|
+
handlers = handlers.handle(rpcs["run"])(self.run)
|
|
827
|
+
handlers = handlers.handle(rpcs["activity"])(self.activity)
|
|
828
|
+
handlers = handlers.handle(rpcs["deferred"])(self.deferred)
|
|
829
|
+
handlers = handlers.handle(rpcs["resume"])(self.resume)
|
|
830
|
+
return cast("EntityHandlers[Any, Any]", handlers)
|
|
831
|
+
|
|
832
|
+
def wait_for_current_run_reply(self) -> Effect[None]:
|
|
833
|
+
return suspend(
|
|
834
|
+
lambda: (
|
|
835
|
+
void
|
|
836
|
+
if self.current_run is None
|
|
837
|
+
else self.engine.wait_for_run_reply(self.workflow, self.current_run.request)
|
|
838
|
+
)
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
def resume_current_run(self, mode: WakeMode) -> Effect[None]:
|
|
842
|
+
engine = self.engine
|
|
843
|
+
if mode == "explicit":
|
|
844
|
+
return ensure_success(
|
|
845
|
+
self.wait_for_current_run_reply().and_then(
|
|
846
|
+
engine.resume(self.workflow, self.execution_id, mode)
|
|
847
|
+
)
|
|
848
|
+
)
|
|
849
|
+
# Release the gate before reset can start another run, so later
|
|
850
|
+
# completions can wake that replay.
|
|
851
|
+
return ensure_success(
|
|
852
|
+
self.resume_gate.with_permits_if_available(self.wait_for_current_run_reply()).flat_map(
|
|
853
|
+
lambda waited: (
|
|
854
|
+
void
|
|
855
|
+
if waited is None
|
|
856
|
+
else engine.resume(self.workflow, self.execution_id, mode)
|
|
857
|
+
)
|
|
858
|
+
)
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
def run(self, request: Request[RunPayload]) -> Effect[Any, Any, Any]:
|
|
862
|
+
engine = self.engine
|
|
863
|
+
workflow = self.workflow
|
|
864
|
+
execution_id = self.execution_id
|
|
865
|
+
pending = self.pending
|
|
866
|
+
# Each run gets a fresh scope under the activation's parent, so
|
|
867
|
+
# replays rebuild compensations while suspended runs keep theirs.
|
|
868
|
+
instance = WorkflowInstanceService.initial(
|
|
869
|
+
workflow, execution_id, scope=pending.scope.fork_unsafe()
|
|
870
|
+
)
|
|
871
|
+
pending.instances.add(instance)
|
|
872
|
+
self.current_run = _CurrentRun(request, instance, {})
|
|
873
|
+
parent = request.payload.parent
|
|
874
|
+
suspend_on_failure = workflow.annotations.get_unsafe(SuspendOnFailure)
|
|
875
|
+
|
|
876
|
+
def on_run_exit(exit: Exit[Any, Any]) -> Effect[None, Never, Any]:
|
|
877
|
+
# An annotated cluster abandonment means this owner can no longer
|
|
878
|
+
# finish the run. Do not resume the parent, but still allow a
|
|
879
|
+
# pending durable interrupt to take effect.
|
|
880
|
+
instance.abandoned = instance.abandoned or (
|
|
881
|
+
isinstance(exit, Failure) and cluster_abandon.is_cause(exit.cause)
|
|
882
|
+
)
|
|
883
|
+
if instance.abandoned:
|
|
884
|
+
instance.suspended = False
|
|
885
|
+
if (
|
|
886
|
+
not instance.suspended
|
|
887
|
+
and not instance.abandoned
|
|
888
|
+
and not (suspend_on_failure and isinstance(exit, Failure))
|
|
889
|
+
):
|
|
890
|
+
resume_parent: Effect[None] = (
|
|
891
|
+
ensure_success(engine.send_resume_parent(parent, execution_id))
|
|
892
|
+
if parent is not None and not parent.get("detached", False)
|
|
893
|
+
else void
|
|
894
|
+
)
|
|
895
|
+
if isinstance(exit, Success) or not exit.cause.has_interrupts_only:
|
|
896
|
+
return resume_parent
|
|
897
|
+
|
|
898
|
+
# ``interrupt_unsafe`` cuts the run off without a result and its
|
|
899
|
+
# replay applies the interrupt. Retain the scope like a
|
|
900
|
+
# suspended run, so ``close_runs`` supersedes it and only the
|
|
901
|
+
# final run's compensations observe the interrupt.
|
|
902
|
+
def after_interrupt(found: Exit[Any, Any] | None) -> Effect[None]:
|
|
903
|
+
if found is None:
|
|
904
|
+
return resume_parent
|
|
905
|
+
instance.suspended = True
|
|
906
|
+
return void
|
|
907
|
+
|
|
908
|
+
return engine.service.deferred_result(INTERRUPT_SIGNAL).flat_map(after_interrupt)
|
|
909
|
+
|
|
910
|
+
def apply_interrupt(found: Exit[Any, Any] | None) -> Effect[None]:
|
|
911
|
+
if found is None:
|
|
912
|
+
return void
|
|
913
|
+
instance.suspended = False
|
|
914
|
+
instance.interrupted = True
|
|
915
|
+
return (
|
|
916
|
+
engine.clear_clock(workflow, execution_id)
|
|
917
|
+
.ignore()
|
|
918
|
+
.and_then(_WithFiber(lambda fiber: fiber.interrupt().interruptible()))
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
return engine.service.deferred_result(INTERRUPT_SIGNAL).flat_map(apply_interrupt)
|
|
922
|
+
|
|
923
|
+
def close(exit: Exit[Result[Any, Any], Any]) -> Effect[None]:
|
|
924
|
+
if isinstance(exit, Success) and isinstance(exit.value, Suspended):
|
|
925
|
+
return void
|
|
926
|
+
return engine.close_runs(pending, abandoned=instance.abandoned)
|
|
927
|
+
|
|
928
|
+
body = into_result(
|
|
929
|
+
self.execute(workflow_payload(workflow, request.payload), execution_id).on_exit(
|
|
930
|
+
on_run_exit
|
|
931
|
+
)
|
|
932
|
+
).on_exit(close)
|
|
933
|
+
return engine.deferred_state.track_run(instance, body).pipe(
|
|
934
|
+
# Request middleware and local callers may carry another entity's context.
|
|
935
|
+
provide_service(CurrentActivationScope, self.activation)
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
def activity(self, request: Request[ActivityPayload]) -> Effect[Any, Any, Any]:
|
|
939
|
+
engine = self.engine
|
|
940
|
+
payload = request.payload
|
|
941
|
+
activity_id = f"{self.execution_id}/{payload.name}"
|
|
942
|
+
current = self.current_run
|
|
943
|
+
instance = WorkflowInstanceService.initial(
|
|
944
|
+
self.workflow,
|
|
945
|
+
self.execution_id,
|
|
946
|
+
awaited_deferreds=None if current is None else current.activity_awaits,
|
|
947
|
+
)
|
|
948
|
+
engine.interrupted_activities.discard(activity_id)
|
|
949
|
+
entry = engine.activities.get(activity_id)
|
|
950
|
+
if entry is None:
|
|
951
|
+
# Replay may never register a losing race activity. Answer it
|
|
952
|
+
# without waiting; a later registration can reset this reply.
|
|
953
|
+
return rpc_wrap(fork=True)(succeed(Suspended()))
|
|
954
|
+
services = (
|
|
955
|
+
entry.context.add(WorkflowInstance, instance)
|
|
956
|
+
.add(CurrentActivationScope, self.activation)
|
|
957
|
+
.add(CurrentAttempt, payload.attempt)
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
def on_cause(cause: Cause[Any]) -> Effect[Result[Any, Any], Any]:
|
|
961
|
+
# Store interrupts as suspensions only when the client asked.
|
|
962
|
+
if any(
|
|
963
|
+
isinstance(reason, Interrupt) and ClientAbort in reason.annotations
|
|
964
|
+
for reason in cause.reasons
|
|
965
|
+
):
|
|
966
|
+
engine.interrupted_activities.add(activity_id)
|
|
967
|
+
return succeed(Suspended())
|
|
968
|
+
return fail_cause(cause)
|
|
969
|
+
|
|
970
|
+
def forget() -> None:
|
|
971
|
+
engine.activities.pop(activity_id, None)
|
|
972
|
+
|
|
973
|
+
effect = (
|
|
974
|
+
into_result(entry.activity.execute_encoded.pipe(provide_context(services)))
|
|
975
|
+
.catch_cause(on_cause)
|
|
976
|
+
.pipe(provide_service(WorkflowInstance, instance))
|
|
977
|
+
.pipe(provide_service(CurrentAttempt, payload.attempt))
|
|
978
|
+
.ensuring(sync(forget))
|
|
979
|
+
)
|
|
980
|
+
return rpc_wrap(fork=True, uninterruptible=True)(effect)
|
|
981
|
+
|
|
982
|
+
def deferred(self, request: Request[DeferredPayload]) -> Effect[Any, Any, Any]:
|
|
983
|
+
payload = request.payload
|
|
984
|
+
name = payload.name
|
|
985
|
+
self.pending.results[name] = payload.exit
|
|
986
|
+
mode: WakeMode = "explicit" if name == INTERRUPT_SIGNAL.name else "automatic"
|
|
987
|
+
engine = self.engine
|
|
988
|
+
|
|
989
|
+
# Await registers before reading the cache. If this run has not
|
|
990
|
+
# registered yet, a later await will see the result just stored.
|
|
991
|
+
# Check when the wake runs and skip preemption too: interrupting a
|
|
992
|
+
# later await would require waiting for its reply before reset. A
|
|
993
|
+
# run completing its own deferred also reads it from the cache when
|
|
994
|
+
# it awaits later, unless an activity is already parked on it. A run
|
|
995
|
+
# only reads ``INTERRUPT_SIGNAL`` while suspending, so an interrupt
|
|
996
|
+
# always waits for the current run's reply before it resets the run.
|
|
997
|
+
def wake() -> Effect[None]:
|
|
998
|
+
current = self.current_run
|
|
999
|
+
if (
|
|
1000
|
+
current is not None
|
|
1001
|
+
and mode == "automatic"
|
|
1002
|
+
and name not in current.instance.awaited_deferreds
|
|
1003
|
+
):
|
|
1004
|
+
if name in current.activity_awaits:
|
|
1005
|
+
# Hold an activity wake until the run has published its reply.
|
|
1006
|
+
return self.resume_current_run(mode)
|
|
1007
|
+
if name in current.instance.completed_deferreds:
|
|
1008
|
+
return void
|
|
1009
|
+
return ensure_success(engine.resume(self.workflow, self.execution_id, mode))
|
|
1010
|
+
return engine.deferred_state.deferred_done(self.execution_id, name).and_then(
|
|
1011
|
+
self.resume_current_run(mode)
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
# An asynchronous reply releases the RPC concurrency permit while the
|
|
1015
|
+
# wake waits; the pending request still occupies mailbox capacity.
|
|
1016
|
+
# The activation owns the wake so it survives handler rebuilds, but
|
|
1017
|
+
# activation shutdown still interrupts it for durable redelivery.
|
|
1018
|
+
reply = Deferred[Any, Any]()
|
|
1019
|
+
return (
|
|
1020
|
+
reply.into(suspend(wake).as_(payload.exit))
|
|
1021
|
+
.fork_in(self.activation, start_immediately=True)
|
|
1022
|
+
.as_(reply)
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
def resume(self, _: Request[ResumePayload]) -> Effect[Any, Any, Any]:
|
|
1026
|
+
return rpc_wrap(fork=True, uninterruptible=True)(self.resume_current_run("automatic"))
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
# --- the durable clock entity -----------------------------------------------------------
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
def _clock_handlers() -> Generator[Effect[Any, Any, Any], Any, EntityHandlers[Any, Any]]:
|
|
1033
|
+
engine = yield from WorkflowEngine()
|
|
1034
|
+
address = yield from CurrentAddress()
|
|
1035
|
+
execution_id = str(address.entity_id)
|
|
1036
|
+
|
|
1037
|
+
def run(request: Request[ClockPayload]) -> Effect[None]:
|
|
1038
|
+
deferred = make_clock(name=request.payload.name, duration=0).deferred
|
|
1039
|
+
return ensure_success(
|
|
1040
|
+
engine.deferred_done(
|
|
1041
|
+
deferred,
|
|
1042
|
+
workflow_name=request.payload.workflowName,
|
|
1043
|
+
execution_id=execution_id,
|
|
1044
|
+
deferred_name=deferred.name,
|
|
1045
|
+
exit=Success(None),
|
|
1046
|
+
)
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
return ClockEntity.handlers().handle(ClockEntity.run)(run)
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
clock_entity_layer = ClockEntity.to_layer_effect(
|
|
1053
|
+
gen(_clock_handlers), max_idle_time=ENTITY_MAX_IDLE_TIME
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def make() -> Effect[WorkflowEngineService, Never, Sharding | MessageStorage | Scope]:
|
|
1058
|
+
"""A ``WorkflowEngineService`` backed by cluster sharding and message
|
|
1059
|
+
storage (Effect's ``ClusterWorkflowEngine.make``)."""
|
|
1060
|
+
|
|
1061
|
+
def body() -> Generator[Effect[Any, Any, Any], Any, WorkflowEngineService]:
|
|
1062
|
+
sharding = yield from Sharding()
|
|
1063
|
+
storage = yield from MessageStorage()
|
|
1064
|
+
holder: dict[str, ClusterEngine] = {}
|
|
1065
|
+
|
|
1066
|
+
def lookup(workflow_name: str) -> Effect[Callable[[str], Any]]:
|
|
1067
|
+
engine = holder["engine"]
|
|
1068
|
+
entity = engine.entities.get(workflow_name)
|
|
1069
|
+
if entity is None:
|
|
1070
|
+
return die(RuntimeError(f"Workflow {workflow_name} not registered"))
|
|
1071
|
+
return engine.clients_partial.invalidate(workflow_name).and_then(
|
|
1072
|
+
cast("Effect[Callable[[str], Any]]", entity.client())
|
|
1073
|
+
)
|
|
1074
|
+
|
|
1075
|
+
def lookup_partial(workflow_name: str) -> Effect[Callable[[str], Any]]:
|
|
1076
|
+
engine = holder["engine"]
|
|
1077
|
+
entity = engine.entities.get(workflow_name) or engine.ensure_partial_entity(
|
|
1078
|
+
workflow_name
|
|
1079
|
+
)
|
|
1080
|
+
return cast("Effect[Callable[[str], Any]]", entity.client())
|
|
1081
|
+
|
|
1082
|
+
clients = yield from RcMap.make(lookup, idle_time_to_live=300)
|
|
1083
|
+
clients_partial = yield from RcMap.make(lookup_partial, idle_time_to_live=300)
|
|
1084
|
+
return ClusterEngine(sharding, storage, clients, clients_partial, holder).service
|
|
1085
|
+
|
|
1086
|
+
return cast(
|
|
1087
|
+
"Effect[WorkflowEngineService, Never, Sharding | MessageStorage | Scope]", gen(body)
|
|
1088
|
+
)
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
__all__ = [
|
|
1092
|
+
"ENTITY_MAX_IDLE_TIME",
|
|
1093
|
+
"INTERRUPT_SIGNAL",
|
|
1094
|
+
"ClusterEngine",
|
|
1095
|
+
"clock_entity_layer",
|
|
1096
|
+
"ensure_success",
|
|
1097
|
+
"make",
|
|
1098
|
+
"payload_shape",
|
|
1099
|
+
]
|