phronomy 0.26.0 → 0.27.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -0
- data/README.md +4 -0
- data/VERIFY.sh +27 -27
- data/benchmark/bench_agent_invoke.rb +26 -22
- data/benchmark/bench_context_assembler.rb +4 -5
- data/docs/architecture/agent-chat-and-state-ownership.md +147 -0
- data/docs/architecture/agent-configuration-and-tool-binding.md +139 -0
- data/docs/architecture/agent-context.md +4 -2
- data/docs/architecture/agent-transition-ownership.md +89 -0
- data/docs/architecture/before-llm-input.md +6 -0
- data/docs/architecture/context-management.md +30 -0
- data/docs/architecture/context-preparation-steps.md +90 -0
- data/docs/architecture/entry-action-and-team-wording.md +72 -0
- data/docs/architecture/execution-metadata-and-values.md +80 -0
- data/docs/architecture/generator-verifier-ownership.md +111 -0
- data/docs/architecture/multi-agent-handoff.md +8 -2
- data/docs/architecture/persistence-refactoring-plan.md +50 -0
- data/docs/architecture/persistence.md +27 -4
- data/docs/architecture/refactoring-closure.md +334 -0
- data/docs/architecture/remaining-refactoring-plan.md +374 -0
- data/docs/architecture/rubyllm-2-token-ownership.md +82 -0
- data/docs/architecture/tool-schema-recording-gap.md +50 -0
- data/docs/architecture/tracing.md +1 -1
- data/docs/architecture/workflow-terminal-ownership-design.md +104 -0
- data/docs/architecture.md +209 -0
- data/docs/decisions/014-unified-persistence-durable-state.md +11 -0
- data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +5 -1
- data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +13 -3
- data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +5 -1
- data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +5 -0
- data/docs/decisions/031-durable-multi-agent-coordination.md +5 -0
- data/docs/decisions/032-storage-backend-composition.md +80 -0
- data/docs/decisions/033-domain-persistence-ownership.md +79 -0
- data/docs/decisions/034-handoff-runner-coordination-ownership.md +76 -0
- data/docs/decisions/035-tool-executor-capability-ownership.md +68 -0
- data/docs/decisions/036-context-contract-ownership.md +86 -0
- data/docs/decisions/037-common-definition-ownership.md +62 -0
- data/docs/decisions/038-responsibility-based-source-layout.md +131 -0
- data/docs/decisions/039-runtime-configuration-lifecycle-ownership.md +74 -0
- data/docs/decisions/040-configuration-default-composition.md +94 -0
- data/docs/decisions/041-feature-owned-identity-registries.md +100 -0
- data/docs/decisions/042-feature-owned-execution-state.md +106 -0
- data/docs/decisions/043-storage-execution-constraint-notifications.md +75 -0
- data/docs/decisions/044-agent-default-and-one-shot-composition.md +106 -0
- data/docs/decisions/045-worker-input-restriction-ownership.md +100 -0
- data/docs/decisions/046-agent-responsibility-layout-and-shared-records.md +110 -0
- data/docs/decisions/047-recovered-execution-continuation-contract.md +116 -0
- data/docs/decisions/048-dispatch-preparation-worker-ownership.md +120 -0
- data/docs/decisions/049-initial-preparation-worker-ownership.md +111 -0
- data/docs/decisions/050-approval-resume-snapshot-and-commit-ownership.md +98 -0
- data/docs/decisions/051-execution-outcome-worker-ownership.md +151 -0
- data/docs/decisions/052-tool-invocation-restoration-ownership.md +92 -0
- data/docs/decisions/053-shared-state-coordination-ownership.md +71 -0
- data/docs/decisions/054-workflow-terminal-save-single-owner.md +76 -0
- data/docs/decisions/055-terminal-observer-failure-settlement.md +60 -0
- data/docs/decisions/056-workflow-terminal-policy-ownership.md +87 -0
- data/docs/decisions/057-storage-transaction-boundaries.md +73 -0
- data/docs/decisions/058-neutral-storage-primitives.md +78 -0
- data/docs/decisions/README.md +35 -6
- data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +6 -0
- data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +6 -0
- data/docs/features.md +9 -3
- data/docs/getting-started.md +4 -4
- data/docs/migrations/0.15.md +5 -0
- data/docs/migrations/durable-semantic-coordination-v2.md +6 -0
- data/docs/migrations/handoff-runner-multi-agent.md +35 -0
- data/docs/migrations/neutral-storage-spi.md +43 -0
- data/docs/migrations/parallel-tool-chat-removal.md +49 -0
- data/docs/migrations/shared-state-multi-agent.md +44 -0
- data/docs/migrations/storage-backend-composition.md +117 -0
- data/docs/migrations/storage-transaction-boundaries.md +74 -0
- data/docs/persistence-backends.md +162 -204
- data/docs/runtime-and-concurrency.md +117 -32
- data/lib/phronomy/agent/api/agent.rb +17 -0
- data/lib/phronomy/agent/async_event_api.rb +2 -2
- data/lib/phronomy/agent/base.rb +51 -241
- data/lib/phronomy/{agent.rb → agent/composition/run_once.rb} +4 -13
- data/lib/phronomy/agent/context/capability/base.rb +67 -26
- data/lib/phronomy/agent/context/capability/tool_executor.rb +62 -0
- data/lib/phronomy/agent/{context_assembler.rb → context_assembly/context_assembler.rb} +143 -88
- data/lib/phronomy/agent/{context_importer.rb → context_assembly/context_importer.rb} +2 -2
- data/lib/phronomy/agent/{ruby_llm_materializer.rb → context_assembly/ruby_llm_materializer.rb} +4 -7
- data/lib/phronomy/agent/context_assembly/runtime_chat_builder.rb +36 -0
- data/lib/phronomy/agent/context_assembly/saved_context_reader.rb +53 -0
- data/lib/phronomy/agent/context_assembly/state_writer.rb +165 -0
- data/lib/phronomy/agent/context_assembly/token_budget_resolver.rb +22 -0
- data/lib/phronomy/agent/{context_plan.rb → context_contract/context_plan.rb} +1 -1
- data/lib/phronomy/agent/{context_policy_input.rb → context_contract/context_policy_input.rb} +6 -6
- data/lib/phronomy/agent/{llm_input_build_context.rb → context_contract/llm_input_build_context.rb} +1 -1
- data/lib/phronomy/agent/{llm_input_manifest.rb → context_contract/llm_input_manifest.rb} +20 -20
- data/lib/phronomy/agent/{llm_input_patch.rb → context_contract/llm_input_patch.rb} +2 -2
- data/lib/phronomy/agent/{agent_execution.rb → execution/agent_execution.rb} +8 -4
- data/lib/phronomy/agent/{agent_invocation.rb → execution/agent_invocation.rb} +8 -13
- data/lib/phronomy/agent/{agent_invocation_session_builder.rb → execution/agent_invocation_session_builder.rb} +19 -95
- data/lib/phronomy/agent/execution/approval_resume_commit.rb +108 -0
- data/lib/phronomy/agent/execution/dispatch_preparation.rb +305 -0
- data/lib/phronomy/agent/{exact_execution.rb → execution/exact_execution.rb} +9 -10
- data/lib/phronomy/agent/{execution_cancellation.rb → execution/execution_cancellation.rb} +2 -3
- data/lib/phronomy/agent/execution/execution_coordinator.rb +1925 -0
- data/lib/phronomy/agent/execution/execution_failure.rb +30 -0
- data/lib/phronomy/agent/execution/execution_metadata.rb +53 -0
- data/lib/phronomy/agent/execution/execution_outcome_committer.rb +344 -0
- data/lib/phronomy/agent/execution/execution_registry.rb +459 -0
- data/lib/phronomy/agent/execution/execution_session_runner.rb +118 -0
- data/lib/phronomy/agent/execution/initial_preparation.rb +421 -0
- data/lib/phronomy/agent/execution/invocation_transitions.rb +86 -0
- data/lib/phronomy/agent/{phase_machine_builder.rb → execution/phase_machine_builder.rb} +20 -71
- data/lib/phronomy/agent/{provider_call_outcome.rb → execution/provider_call_outcome.rb} +9 -9
- data/lib/phronomy/agent/execution/runtime_record_encoder.rb +210 -0
- data/lib/phronomy/agent/{handoff_context.rb → handoff/handoff_context.rb} +2 -2
- data/lib/phronomy/agent/handoff/handoff_execution_coordinator.rb +15 -0
- data/lib/phronomy/agent/handoff/handoff_outcome_committer.rb +131 -0
- data/lib/phronomy/agent/{handoff_state.rb → handoff/handoff_state.rb} +1 -1
- data/lib/phronomy/agent/{journal_projection.rb → journal/journal_projection.rb} +4 -0
- data/lib/phronomy/agent/{journal_record.rb → journal/journal_record.rb} +3 -3
- data/lib/phronomy/agent/{llm_call_record.rb → journal/llm_call_record.rb} +2 -2
- data/lib/phronomy/agent/{agent_root.rb → lifecycle/agent_root.rb} +6 -2
- data/lib/phronomy/agent/lifecycle/default_persistence.rb +29 -0
- data/lib/phronomy/{engine/runtime/agent_ownership_registry.rb → agent/lifecycle/ownership_registry.rb} +24 -10
- data/lib/phronomy/{agent_already_exists_error.rb → agent/lifecycle_contract/agent_already_exists_error.rb} +2 -0
- data/lib/phronomy/{agent_busy_error.rb → agent/lifecycle_contract/agent_busy_error.rb} +2 -0
- data/lib/phronomy/{agent_purged_error.rb → agent/lifecycle_contract/agent_purged_error.rb} +2 -0
- data/lib/phronomy/agent/lifecycle_contract/handoff_error.rb +7 -0
- data/lib/phronomy/{stream_callback_error.rb → agent/lifecycle_contract/stream_callback_error.rb} +2 -0
- data/lib/phronomy/agent/persistence/agent_repository.rb +61 -0
- data/lib/phronomy/agent/persistence/codec.rb +358 -0
- data/lib/phronomy/agent/persistence/execution_repository.rb +108 -0
- data/lib/phronomy/agent/persistence/handoff_state_repository.rb +58 -0
- data/lib/phronomy/agent/persistence/journal_repository.rb +54 -0
- data/lib/phronomy/agent/persistence/queries.rb +61 -0
- data/lib/phronomy/agent/persistence/storage_schema.rb +24 -0
- data/lib/phronomy/agent/persistence/watermark.rb +27 -0
- data/lib/phronomy/agent/recovery/invocation_restorer.rb +132 -0
- data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/continuation.rb +21 -45
- data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/installation.rb +32 -37
- data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/resolution.rb +37 -40
- data/lib/phronomy/agent/{recovery_coordinator.rb → recovery/recovery_coordinator.rb} +6 -10
- data/lib/phronomy/agent/recovery/recovery_support.rb +227 -0
- data/lib/phronomy/agent/selection/candidate.rb +1 -1
- data/lib/phronomy/agent/{approval_evaluation_request.rb → tool_execution/approval_evaluation_request.rb} +1 -12
- data/lib/phronomy/agent/{tool_approval_request.rb → tool_execution/tool_approval_request.rb} +1 -10
- data/lib/phronomy/agent/tool_execution/tool_binding.rb +90 -0
- data/lib/phronomy/agent/{tool_call_intercepted.rb → tool_execution/tool_call_intercepted.rb} +2 -2
- data/lib/phronomy/agent/{tool_definition_set.rb → tool_execution/tool_definition_set.rb} +9 -4
- data/lib/phronomy/agent/{tool_invocation.rb → tool_execution/tool_invocation.rb} +69 -34
- data/lib/phronomy/common/configuration_error.rb +7 -0
- data/lib/phronomy/common/error.rb +5 -0
- data/lib/phronomy/{agent → common/values}/immutable.rb +9 -1
- data/lib/phronomy/common/values/serializable.rb +32 -0
- data/lib/phronomy/{configuration.rb → configuration/configuration.rb} +14 -5
- data/lib/phronomy/configuration/global_configuration.rb +26 -0
- data/lib/phronomy/content_store/storage_schema.rb +11 -0
- data/lib/phronomy/content_store/stored_contents.rb +43 -0
- data/lib/phronomy/engine/backpressure_error.rb +7 -0
- data/lib/phronomy/{blocking.rb → engine/blocking.rb} +1 -1
- data/lib/phronomy/engine/cancellation_error.rb +7 -0
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +4 -0
- data/lib/phronomy/engine/concurrency/offload_pool.rb +4 -0
- data/lib/phronomy/engine/concurrency/operation_binding.rb +1 -1
- data/lib/phronomy/engine/concurrency/worker_input_restricted.rb +14 -0
- data/lib/phronomy/engine/event_loop.rb +158 -624
- data/lib/phronomy/engine/event_loop_reentrancy_error.rb +8 -0
- data/lib/phronomy/{execution.rb → engine/execution.rb} +0 -7
- data/lib/phronomy/{execution_cancellation_error.rb → engine/execution_cancellation_error.rb} +2 -0
- data/lib/phronomy/engine/execution_receiver.rb +65 -0
- data/lib/phronomy/{execution_timeout_error.rb → engine/execution_timeout_error.rb} +2 -0
- data/lib/phronomy/engine/fsm_protocol.rb +14 -0
- data/lib/phronomy/engine/fsm_session.rb +35 -28
- data/lib/phronomy/{invalid_async_entry_action_error.rb → engine/invalid_async_entry_action_error.rb} +2 -0
- data/lib/phronomy/{invalid_async_transition_action_error.rb → engine/invalid_async_transition_action_error.rb} +2 -0
- data/lib/phronomy/{invalid_async_workflow_action_error.rb → engine/invalid_async_workflow_action_error.rb} +2 -0
- data/lib/phronomy/engine/pool_shutdown_error.rb +7 -0
- data/lib/phronomy/engine/recursion_limit_error.rb +7 -0
- data/lib/phronomy/engine/runtime.rb +80 -84
- data/lib/phronomy/engine/runtime_shutdown_error.rb +7 -0
- data/lib/phronomy/engine/runtime_shutdown_reentrancy_error.rb +7 -0
- data/lib/phronomy/engine/scheduler_reentrancy_error.rb +9 -0
- data/lib/phronomy/engine/task_result.rb +4 -0
- data/lib/phronomy/engine/timeout_error.rb +7 -0
- data/lib/phronomy/filter/contract/filter_block_error.rb +14 -0
- data/lib/phronomy/generation/generator_verifier/agent_result_receiver.rb +89 -0
- data/lib/phronomy/generation/generator_verifier/pipeline_state.rb +57 -0
- data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +112 -0
- data/lib/phronomy/generation/generator_verifier.rb +118 -0
- data/lib/phronomy/generation/low_confidence_error.rb +14 -0
- data/lib/phronomy/llm_context_window/token_budget.rb +6 -7
- data/lib/phronomy/llm_contract/authentication_error.rb +7 -0
- data/lib/phronomy/{context_budget_exceeded_error.rb → llm_contract/context_budget_exceeded_error.rb} +2 -0
- data/lib/phronomy/llm_contract/context_length_error.rb +7 -0
- data/lib/phronomy/llm_contract/rate_limit_error.rb +7 -0
- data/lib/phronomy/{token_usage.rb → llm_contract/token_usage.rb} +2 -2
- data/lib/phronomy/llm_contract/transport_error.rb +7 -0
- data/lib/phronomy/multi_agent/admission_registry.rb +22 -2
- data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +5 -5
- data/lib/phronomy/{agent → multi_agent}/handoff_runner.rb +18 -17
- data/lib/phronomy/multi_agent/orchestrator.rb +2 -2
- data/lib/phronomy/multi_agent/persistence/codec.rb +55 -0
- data/lib/phronomy/multi_agent/persistence/queries.rb +30 -0
- data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +108 -0
- data/lib/phronomy/multi_agent/persistence/team_repository.rb +61 -0
- data/lib/phronomy/{agent → multi_agent}/shared_state.rb +56 -39
- data/lib/phronomy/multi_agent/storage_contract/team_storage_schema.rb +15 -0
- data/lib/phronomy/multi_agent/team_coordinator.rb +21 -18
- data/lib/phronomy/multi_agent/team_execution.rb +1 -1
- data/lib/phronomy/{engine/runtime → multi_agent}/team_ownership_registry.rb +14 -4
- data/lib/phronomy/multi_agent/team_root.rb +1 -1
- data/lib/phronomy/output_parser/contract/parse_error.rb +7 -0
- data/lib/phronomy/persistence/api/persistence.rb +140 -0
- data/lib/phronomy/persistence/migration/initial_format_migration.rb +19 -19
- data/lib/phronomy/persistence_composition/repositories.rb +77 -0
- data/lib/phronomy/persistence_composition/storage_schema.rb +24 -0
- data/lib/phronomy/{execution_rehydration_required_error.rb → recovery/execution_rehydration_required_error.rb} +2 -0
- data/lib/phronomy/{recovery.rb → recovery/recovery.rb} +1 -1
- data/lib/phronomy/runtime_composition/agent_defaults.rb +7 -0
- data/lib/phronomy/runtime_composition/configuration_defaults.rb +9 -0
- data/lib/phronomy/runtime_composition/global_runtime.rb +19 -0
- data/lib/phronomy/storage/backend.rb +101 -0
- data/lib/phronomy/storage/backends/in_memory.rb +157 -0
- data/lib/phronomy/storage/blob_conflict_error.rb +10 -0
- data/lib/phronomy/storage/blobs.rb +31 -0
- data/lib/phronomy/storage/condition.rb +25 -0
- data/lib/phronomy/storage/condition_failed_error.rb +16 -0
- data/lib/phronomy/storage/conflict_error.rb +9 -0
- data/lib/phronomy/{persistence → storage}/durable_record.rb +12 -12
- data/lib/phronomy/storage/entry.rb +33 -0
- data/lib/phronomy/storage/guard_ref.rb +13 -0
- data/lib/phronomy/storage/not_found_error.rb +9 -0
- data/lib/phronomy/storage/record_codec.rb +177 -0
- data/lib/phronomy/storage/records.rb +61 -0
- data/lib/phronomy/storage/resource.rb +126 -0
- data/lib/phronomy/storage/scope.rb +25 -0
- data/lib/phronomy/storage/serialization_error.rb +9 -0
- data/lib/phronomy/storage/streams.rb +44 -0
- data/lib/phronomy/storage/transaction_error.rb +10 -0
- data/lib/phronomy/storage/unique_constraint_error.rb +17 -0
- data/lib/phronomy/storage/unsupported_backend_error.rb +9 -0
- data/lib/phronomy/storage/validation.rb +53 -0
- data/lib/phronomy/storage/view.rb +131 -0
- data/lib/phronomy/testing/persistence_contract/a_content_store.rb +1 -1
- data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +4 -4
- data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +10 -7
- data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +2 -2
- data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +6 -6
- data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +6 -6
- data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +8 -8
- data/lib/phronomy/testing/persistence_contract/neutral_storage_primitives.rb +263 -0
- data/lib/phronomy/testing/persistence_contract/storage_transaction_boundaries.rb +123 -0
- data/lib/phronomy/testing/persistence_contract.rb +4 -0
- data/lib/phronomy/tool/contract/tool_error.rb +7 -0
- data/lib/phronomy/tools/agent.rb +1 -1
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/{workflow.rb → workflow/execution/workflow.rb} +8 -6
- data/lib/phronomy/{workflow_context.rb → workflow/execution/workflow_context.rb} +4 -0
- data/lib/phronomy/workflow/execution/workflow_context_ownership_error.rb +7 -0
- data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +188 -0
- data/lib/phronomy/{workflow_runner.rb → workflow/execution/workflow_runner.rb} +122 -71
- data/lib/phronomy/workflow/execution/workflow_terminal_policy.rb +40 -0
- data/lib/phronomy/workflow/persistence/codec.rb +153 -0
- data/lib/phronomy/workflow/persistence/state_repository.rb +57 -0
- data/lib/phronomy/workflow/phase_machine_builder.rb +2 -2
- data/lib/phronomy/workflow/storage_contract/workflow_storage_schema.rb +9 -0
- data/lib/phronomy.rb +52 -94
- data/scripts/api_snapshot.rb +1 -1
- data/scripts/storage_spi_snapshot.rb +36 -0
- data/sig/phronomy/agent.rbs +0 -1
- data/sig/phronomy/execution_receiver.rbs +34 -0
- data/sig/phronomy/handoff.rbs +4 -2
- data/sig/phronomy/multi_agent.rbs +18 -0
- data/sig/phronomy/persistence.rbs +7 -91
- data/sig/phronomy/storage.rbs +174 -0
- data/sig/phronomy/tool.rbs +10 -1
- metadata +230 -99
- data/lib/phronomy/agent/execution_coordinator.rb +0 -3151
- data/lib/phronomy/agent/handoff_execution_coordinator.rb +0 -143
- data/lib/phronomy/agent/recovery_support.rb +0 -504
- data/lib/phronomy/agent/token_budget_resolver.rb +0 -70
- data/lib/phronomy/agent/tool_executor.rb +0 -55
- data/lib/phronomy/generator_verifier.rb +0 -369
- data/lib/phronomy/invalid_context_budget_configuration_error.rb +0 -8
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +0 -116
- data/lib/phronomy/persistence/durable_codec.rb +0 -706
- data/lib/phronomy/persistence/in_memory.rb +0 -690
- data/lib/phronomy/persistence/repository_facades.rb +0 -535
- data/lib/phronomy/persistence.rb +0 -276
- data/lib/phronomy/ruby_llm_patches.rb +0 -24
- data/lib/phronomy/workflow_recovery.rb +0 -123
- /data/lib/phronomy/agent/{context_candidate_resolver.rb → context_assembly/context_candidate_resolver.rb} +0 -0
- /data/lib/phronomy/agent/{context_policy_input_builder.rb → context_assembly/context_policy_input_builder.rb} +0 -0
- /data/lib/phronomy/agent/{context_plan_validator.rb → context_contract/context_plan_validator.rb} +0 -0
- /data/lib/phronomy/agent/{context_policy.rb → context_contract/context_policy.rb} +0 -0
- /data/lib/phronomy/agent/{llm_operation_result.rb → execution/llm_operation_result.rb} +0 -0
- /data/lib/phronomy/agent/{handoff.rb → handoff/handoff.rb} +0 -0
- /data/lib/phronomy/agent/{handoff_capability_factory.rb → handoff/handoff_capability_factory.rb} +0 -0
- /data/lib/phronomy/agent/{handoff_policy.rb → handoff/handoff_policy.rb} +0 -0
- /data/lib/phronomy/agent/{handoff_projection.rb → handoff/handoff_projection.rb} +0 -0
- /data/lib/phronomy/agent/{handoff_request.rb → handoff/handoff_request.rb} +0 -0
- /data/lib/phronomy/agent/{tool_invocation_session_builder.rb → tool_execution/tool_invocation_session_builder.rb} +0 -0
- /data/lib/phronomy/{canonical_json.rb → common/canonical_json.rb} +0 -0
- /data/lib/phronomy/{diagnostics.rb → engine/diagnostics.rb} +0 -0
- /data/lib/phronomy/{event.rb → engine/event.rb} +0 -0
- /data/lib/phronomy/{invocation_context.rb → engine/invocation_context.rb} +0 -0
- /data/lib/phronomy/{metrics.rb → engine/metrics.rb} +0 -0
- /data/lib/phronomy/{runnable.rb → engine/runnable.rb} +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Storage
|
|
5
|
+
# Compare-and-write records and declared equality indexes.
|
|
6
|
+
# @api public
|
|
7
|
+
class Records
|
|
8
|
+
UNCHECKED = Object.new.freeze
|
|
9
|
+
attr_reader :resource
|
|
10
|
+
def initialize(view, resource) = (@view, @resource = view, resource)
|
|
11
|
+
|
|
12
|
+
# @api public
|
|
13
|
+
def insert(key:, revision:, attributes:, record:)
|
|
14
|
+
@view.validate!
|
|
15
|
+
entry = Entry::Record.new(key: key, revision: revision, attributes: resource.validate_attributes(attributes), record: record)
|
|
16
|
+
@view.perform(:insert_record, resource, entry: entry)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @api public
|
|
20
|
+
def read(key)
|
|
21
|
+
@view.validate!
|
|
22
|
+
@view.perform(:read_record, resource, key: Validation.key(key))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @api public
|
|
26
|
+
def fetch(key)
|
|
27
|
+
read(key) || raise(NotFoundError, "record not found: #{resource.id}/#{key}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @api public
|
|
31
|
+
def replace(key:, expected_revision:, next_revision:, attributes:, record:, expected_attributes: {})
|
|
32
|
+
@view.validate!
|
|
33
|
+
expected = Validation.revision(expected_revision)
|
|
34
|
+
entry = Entry::Record.new(key: key, revision: next_revision, attributes: resource.validate_attributes(attributes), record: record)
|
|
35
|
+
raise ConflictError, "replace must advance revision exactly once" unless next_revision == expected + 1
|
|
36
|
+
expected_values = resource.validate_attributes(expected_attributes, partial: true)
|
|
37
|
+
@view.perform(:replace_record, resource, entry: entry, expected_revision: expected, expected_attributes: expected_values)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @api public
|
|
41
|
+
def delete(key:, expected_revision: UNCHECKED)
|
|
42
|
+
@view.validate!
|
|
43
|
+
expected = expected_revision.equal?(UNCHECKED) ? UNCHECKED : Validation.revision(expected_revision)
|
|
44
|
+
@view.perform(:delete_record, resource, key: Validation.key(key), expected_revision: expected)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @api public
|
|
48
|
+
def scan(index:, equals:, after: nil, limit: nil)
|
|
49
|
+
@view.validate!
|
|
50
|
+
values = resource.index_values(index, equals)
|
|
51
|
+
@view.perform(:scan_records, resource, equals: values, after: after.nil? ? nil : Validation.key(after), limit: Validation.limit(limit))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @api public
|
|
55
|
+
def delete_matching(index:, equals:)
|
|
56
|
+
@view.validate!
|
|
57
|
+
@view.perform(:delete_matching_records, resource, equals: resource.index_values(index, equals))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Storage
|
|
5
|
+
# Immutable logical schema. Physical drivers receive a separate mapping.
|
|
6
|
+
# @api public
|
|
7
|
+
class Resource
|
|
8
|
+
TYPES = [:string, :integer, :boolean, :nullable_string, :nullable_integer, :nullable_boolean].freeze
|
|
9
|
+
attr_reader :id, :kind, :attributes, :immutable_attributes, :indexes, :unique, :guard
|
|
10
|
+
|
|
11
|
+
# Normalize a schema object or ID before constructing guards and conditions.
|
|
12
|
+
# @api private
|
|
13
|
+
def self.normalize_reference(value)
|
|
14
|
+
value.is_a?(Resource) ? value : Validation.key(value)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @api public
|
|
18
|
+
def initialize(id:, kind:, attributes: {}, immutable_attributes: [], indexes: {}, unique: [], guard: nil)
|
|
19
|
+
@id = Validation.key(id)
|
|
20
|
+
@kind = kind
|
|
21
|
+
@attributes = Validation.immutable(attributes)
|
|
22
|
+
@immutable_attributes = Validation.immutable(immutable_attributes)
|
|
23
|
+
@indexes = Validation.immutable(indexes)
|
|
24
|
+
@unique = Validation.immutable(unique)
|
|
25
|
+
@guard = Validation.immutable(guard)
|
|
26
|
+
validate_shape!
|
|
27
|
+
validate_indexes!
|
|
28
|
+
validate_guard!
|
|
29
|
+
validate_unique!
|
|
30
|
+
freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @api public
|
|
34
|
+
def validate_attributes(values, partial: false)
|
|
35
|
+
valid = values.is_a?(Hash) && (values.keys - attributes.keys).empty? && (partial || values.keys.sort == attributes.keys.sort)
|
|
36
|
+
unless valid
|
|
37
|
+
raise ArgumentError, "attributes do not match #{id}"
|
|
38
|
+
end
|
|
39
|
+
values = values.to_h do |name, value|
|
|
40
|
+
type = attributes.fetch(name)
|
|
41
|
+
if type.to_s.start_with?("nullable_") && value.nil?
|
|
42
|
+
[name, nil]
|
|
43
|
+
else
|
|
44
|
+
[name, typed_value(name, type, value)]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
if guard && values.key?(guard[:via])
|
|
48
|
+
values[guard[:via]] = Validation.key(values.fetch(guard[:via]))
|
|
49
|
+
end
|
|
50
|
+
Validation.attributes(values)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @api public
|
|
54
|
+
def index_values(index, equals)
|
|
55
|
+
fields = indexes.fetch(index) { raise ArgumentError, "unknown index: #{index}" }
|
|
56
|
+
raise ArgumentError, "index attributes must match exactly" unless equals.is_a?(Hash) && equals.keys.sort == fields.sort
|
|
57
|
+
validate_attributes(equals, partial: true)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def validate_shape!
|
|
63
|
+
raise ArgumentError, "unsupported resource kind" unless [:records, :streams, :blobs].include?(kind)
|
|
64
|
+
unless attributes.is_a?(Hash) && attributes.keys.all? { |key| key.is_a?(Symbol) } && attributes.values.all? { |type| TYPES.include?(type) }
|
|
65
|
+
raise ArgumentError, "invalid attribute declarations"
|
|
66
|
+
end
|
|
67
|
+
unless field_list?(immutable_attributes, empty: true) && indexes.is_a?(Hash) && unique.is_a?(Array)
|
|
68
|
+
raise ArgumentError, "invalid resource declarations"
|
|
69
|
+
end
|
|
70
|
+
if kind != :records && (!immutable_attributes.empty? || !indexes.empty? || !unique.empty?)
|
|
71
|
+
raise ArgumentError, "only records declare indexes, uniqueness and immutable attributes"
|
|
72
|
+
end
|
|
73
|
+
raise ArgumentError, "streams have no indexed attributes" if kind == :streams && !attributes.empty?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def validate_indexes!
|
|
77
|
+
unless indexes.all? { |name, fields| name.is_a?(Symbol) && field_list?(fields) }
|
|
78
|
+
raise ArgumentError, "indexes need names and known distinct fields"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def validate_unique!
|
|
83
|
+
names = unique.map do |constraint|
|
|
84
|
+
unless constraint.is_a?(Hash) && constraint.keys.sort == [:fields, :name, :where] &&
|
|
85
|
+
constraint[:name].is_a?(Symbol) && field_list?(constraint[:fields])
|
|
86
|
+
raise ArgumentError, "invalid unique constraint"
|
|
87
|
+
end
|
|
88
|
+
validate_attributes(constraint.fetch(:where), partial: true)
|
|
89
|
+
constraint[:name]
|
|
90
|
+
end
|
|
91
|
+
raise ArgumentError, "unique constraints need distinct names" unless names.uniq == names
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validate_guard!
|
|
95
|
+
return unless guard
|
|
96
|
+
unless guard.is_a?(Hash) && guard.keys.sort == [:resource, :via] && guard[:resource].is_a?(String)
|
|
97
|
+
raise ArgumentError, "invalid guard declaration"
|
|
98
|
+
end
|
|
99
|
+
Validation.key(guard[:resource])
|
|
100
|
+
allowed = case kind
|
|
101
|
+
when :records then [:key, *attributes.select { |_, type| type == :string }.keys]
|
|
102
|
+
when :streams then [:stream]
|
|
103
|
+
else []
|
|
104
|
+
end
|
|
105
|
+
raise ArgumentError, "invalid guard key" unless allowed.include?(guard[:via])
|
|
106
|
+
if kind == :records && guard[:via] != :key && !immutable_attributes.include?(guard[:via])
|
|
107
|
+
raise ArgumentError, "guard attributes must be immutable"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def field_list?(values, empty: false)
|
|
112
|
+
values.is_a?(Array) && (empty || !values.empty?) && values.uniq == values && (values - attributes.keys).empty?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def typed_value(name, type, value)
|
|
116
|
+
valid = case type.to_s.delete_prefix("nullable_")
|
|
117
|
+
when "string" then value.is_a?(String) && value.valid_encoding? && !value.include?("\0")
|
|
118
|
+
when "integer" then value.is_a?(Integer)
|
|
119
|
+
when "boolean" then value.equal?(true) || value.equal?(false)
|
|
120
|
+
end
|
|
121
|
+
raise ArgumentError, "invalid #{id}.#{name}" unless valid
|
|
122
|
+
value.is_a?(String) ? value.encode(Encoding::UTF_8) : value
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Storage
|
|
5
|
+
# One synchronous transaction lifetime, independent of physical rollback.
|
|
6
|
+
# @api private
|
|
7
|
+
class Scope
|
|
8
|
+
def initialize
|
|
9
|
+
@thread = Thread.current
|
|
10
|
+
@active = true
|
|
11
|
+
@failed = false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def check!
|
|
15
|
+
raise TransactionError, "transaction belongs to another execution context" unless @thread.equal?(Thread.current)
|
|
16
|
+
raise TransactionError, "transaction scope is closed" unless @active
|
|
17
|
+
raise TransactionError, "transaction scope has failed; leave it before continuing" if @failed
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def fail! = @failed = true
|
|
22
|
+
def close! = @active = false
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Storage
|
|
5
|
+
# Atomic ordered append with a per-stream head and entry identity.
|
|
6
|
+
# @api public
|
|
7
|
+
class Streams
|
|
8
|
+
attr_reader :resource
|
|
9
|
+
def initialize(view, resource) = (@view, @resource = view, resource)
|
|
10
|
+
|
|
11
|
+
# @api public
|
|
12
|
+
def append(stream:, expected_head:, entries:)
|
|
13
|
+
@view.validate!
|
|
14
|
+
key = Validation.key(stream)
|
|
15
|
+
expected = Validation.revision(expected_head)
|
|
16
|
+
raise ArgumentError, "entries must be an Array" unless entries.is_a?(Array)
|
|
17
|
+
values = entries.map do |entry|
|
|
18
|
+
raise SerializationError, "expected Entry::Append" unless entry.is_a?(Entry::Append)
|
|
19
|
+
Entry::Append.new(id: entry.id, record: entry.record)
|
|
20
|
+
end
|
|
21
|
+
raise ConflictError, "duplicate stream entry identity" unless values.map(&:id).uniq.length == values.length
|
|
22
|
+
@view.perform(:append_stream, resource, stream: key, expected_head: expected, entries: values.freeze)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @api public
|
|
26
|
+
def read(stream:, after: 0, limit: nil)
|
|
27
|
+
@view.validate!
|
|
28
|
+
@view.perform(:read_stream, resource, stream: Validation.key(stream), after: Validation.revision(after), limit: Validation.limit(limit))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @api public
|
|
32
|
+
def head(stream:)
|
|
33
|
+
@view.validate!
|
|
34
|
+
@view.perform(:stream_head, resource, stream: Validation.key(stream))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @api public
|
|
38
|
+
def delete(stream:)
|
|
39
|
+
@view.validate!
|
|
40
|
+
@view.perform(:delete_stream, resource, stream: Validation.key(stream))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Storage
|
|
5
|
+
# A neutral storage contract failure.
|
|
6
|
+
# @api public
|
|
7
|
+
class UniqueConstraintError < ConflictError
|
|
8
|
+
attr_reader :resource, :constraint
|
|
9
|
+
# @api public
|
|
10
|
+
def initialize(resource:, constraint:)
|
|
11
|
+
@resource = resource.id
|
|
12
|
+
@constraint = constraint
|
|
13
|
+
super("unique constraint #{resource.id}.#{constraint} failed")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Storage
|
|
5
|
+
# Input validation shared by storage handles; no physical work happens here.
|
|
6
|
+
# @api private
|
|
7
|
+
module Validation
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def key(value)
|
|
11
|
+
raise ArgumentError, "key must be a non-empty String" unless value.is_a?(String) && !value.empty?
|
|
12
|
+
result = value.encode(Encoding::UTF_8)
|
|
13
|
+
raise ArgumentError, "key must be valid UTF-8" unless result.valid_encoding? && !result.include?("\0")
|
|
14
|
+
result.dup.freeze
|
|
15
|
+
rescue EncodingError
|
|
16
|
+
raise ArgumentError, "key must be valid UTF-8"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def revision(value)
|
|
20
|
+
raise ArgumentError, "revision must be a non-negative Integer" unless value.is_a?(Integer) && value >= 0
|
|
21
|
+
value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def limit(value)
|
|
25
|
+
return nil if value.nil?
|
|
26
|
+
raise ArgumentError, "limit must be a positive Integer" unless value.is_a?(Integer) && value.positive?
|
|
27
|
+
value
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def record(value)
|
|
31
|
+
raise SerializationError, "expected Storage::DurableRecord" unless value.is_a?(DurableRecord)
|
|
32
|
+
value.copy
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def attributes(value)
|
|
36
|
+
unless value.is_a?(Hash) && value.all? { |name, item| name.is_a?(Symbol) && [String, Integer, TrueClass, FalseClass, NilClass].any? { |type| item.is_a?(type) } }
|
|
37
|
+
raise ArgumentError, "attributes must contain named scalar values"
|
|
38
|
+
end
|
|
39
|
+
immutable(value)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def immutable(value)
|
|
43
|
+
case value
|
|
44
|
+
when Hash then value.to_h { |k, v| [immutable(k), immutable(v)] }.freeze
|
|
45
|
+
when Array then value.map { |v| immutable(v) }.freeze
|
|
46
|
+
when String then value.dup.freeze
|
|
47
|
+
when Symbol, Integer, TrueClass, FalseClass, NilClass then value
|
|
48
|
+
else raise ArgumentError, "declarations must contain immutable scalar values"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Storage
|
|
5
|
+
# Root handles route to the current scope; bound handles cannot outlive it.
|
|
6
|
+
# @api public
|
|
7
|
+
class View
|
|
8
|
+
def initialize(backend, scope: nil, context: nil)
|
|
9
|
+
@backend, @scope, @context = backend, scope, context
|
|
10
|
+
@handles = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @api public
|
|
14
|
+
def records(resource) = handle(resource, :records, Records)
|
|
15
|
+
# @api public
|
|
16
|
+
def streams(resource) = handle(resource, :streams, Streams)
|
|
17
|
+
# @api public
|
|
18
|
+
def blobs(resource) = handle(resource, :blobs, Blobs)
|
|
19
|
+
|
|
20
|
+
# Acquire stable anchors before evaluating conditions in the same view.
|
|
21
|
+
# @api public
|
|
22
|
+
def check!(guards:, conditions:)
|
|
23
|
+
validate!
|
|
24
|
+
raise ArgumentError, "guards and conditions must be arrays" unless guards.is_a?(Array) && conditions.is_a?(Array)
|
|
25
|
+
guards.each do |guard|
|
|
26
|
+
raise ArgumentError, "expected GuardRef" unless guard.is_a?(GuardRef)
|
|
27
|
+
resolve(guard.resource, :records)
|
|
28
|
+
end
|
|
29
|
+
conditions.each do |condition|
|
|
30
|
+
case condition
|
|
31
|
+
when Condition::RevisionIs then resolve(condition.resource, :records)
|
|
32
|
+
when Condition::StreamHeadIs then resolve(condition.resource, :streams)
|
|
33
|
+
when Condition::NoRows then resolve(condition.resource, :records).index_values(condition.index, condition.equals)
|
|
34
|
+
else raise ArgumentError, "unsupported condition"
|
|
35
|
+
end
|
|
36
|
+
require_guard!(condition, guards)
|
|
37
|
+
end
|
|
38
|
+
atomic do |bound|
|
|
39
|
+
guards.uniq.sort_by { |g| [resolve(g.resource, :records).id.b, g.key.b] }.each do |guard|
|
|
40
|
+
bound.perform(:lock_guard, resolve(guard.resource, :records), key: guard.key)
|
|
41
|
+
end
|
|
42
|
+
conditions.each do |condition|
|
|
43
|
+
matched = case condition
|
|
44
|
+
when Condition::RevisionIs then bound.records(condition.resource).read(condition.key)&.revision == condition.expected
|
|
45
|
+
when Condition::StreamHeadIs then bound.streams(condition.resource).head(stream: condition.stream) == condition.expected
|
|
46
|
+
when Condition::NoRows then bound.records(condition.resource).scan(index: condition.index, equals: condition.equals, limit: 1).empty?
|
|
47
|
+
end
|
|
48
|
+
raise ConditionFailedError, condition unless matched
|
|
49
|
+
end
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Keeps domain encoding/response validation inside the physical transaction.
|
|
55
|
+
# @api private
|
|
56
|
+
def atomic
|
|
57
|
+
validate!
|
|
58
|
+
unless @scope
|
|
59
|
+
current = @backend.current_view
|
|
60
|
+
return current.atomic { |bound| yield bound } if current
|
|
61
|
+
return @backend.transaction { |bound| bound.atomic { yield bound } }
|
|
62
|
+
end
|
|
63
|
+
completed = false
|
|
64
|
+
begin
|
|
65
|
+
result = yield self
|
|
66
|
+
@scope.check!
|
|
67
|
+
completed = true
|
|
68
|
+
result
|
|
69
|
+
ensure
|
|
70
|
+
@scope.fail! unless completed
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @api private
|
|
75
|
+
def perform(operation, resource, **arguments)
|
|
76
|
+
atomic { |bound| bound.dispatch(operation, resource, **arguments) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @api private
|
|
80
|
+
def dispatch(operation, resource, **arguments)
|
|
81
|
+
validate!
|
|
82
|
+
@backend.execute(@context, operation, resource, **arguments)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @api private
|
|
86
|
+
def validate!
|
|
87
|
+
@scope&.check!
|
|
88
|
+
true
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def require_guard!(condition, guards)
|
|
94
|
+
resource = @backend.resources.fetch(condition.resource.is_a?(Resource) ? condition.resource.id : condition.resource)
|
|
95
|
+
guard = resource.guard
|
|
96
|
+
return unless guard
|
|
97
|
+
key = case condition
|
|
98
|
+
when Condition::RevisionIs
|
|
99
|
+
if guard[:via] == :key
|
|
100
|
+
condition.key
|
|
101
|
+
else
|
|
102
|
+
raise ArgumentError, "revision checks require a key-based guard"
|
|
103
|
+
end
|
|
104
|
+
when Condition::StreamHeadIs then condition.stream
|
|
105
|
+
when Condition::NoRows
|
|
106
|
+
condition.equals.fetch(guard[:via]) { raise ArgumentError, "condition must constrain its guard attribute" }
|
|
107
|
+
end
|
|
108
|
+
covered = guards.any? do |candidate|
|
|
109
|
+
resolve(candidate.resource, :records).id == guard[:resource] && candidate.key == key
|
|
110
|
+
end
|
|
111
|
+
raise ArgumentError, "condition requires its declared guard" unless covered
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def resolve(resource, kind)
|
|
115
|
+
id = resource.is_a?(Resource) ? resource.id : resource
|
|
116
|
+
registered = @backend.resources[id]
|
|
117
|
+
valid = registered && registered.kind == kind && (!resource.is_a?(Resource) || registered.equal?(resource))
|
|
118
|
+
unless valid
|
|
119
|
+
raise UnsupportedBackendError, "unregistered or mismatched #{kind} resource: #{id}"
|
|
120
|
+
end
|
|
121
|
+
registered
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def handle(resource, kind, type)
|
|
125
|
+
validate!
|
|
126
|
+
registered = resolve(resource, kind)
|
|
127
|
+
@handles[registered.id] ||= type.new(self, registered)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -21,7 +21,7 @@ RSpec.shared_examples "a persistence content store" do
|
|
|
21
21
|
it "raises NotFoundError for a missing content_id" do
|
|
22
22
|
expect do
|
|
23
23
|
content_store.fetch("sha256:#{"0" * 64}")
|
|
24
|
-
end.to raise_error(Phronomy::
|
|
24
|
+
end.to raise_error(Phronomy::Storage::NotFoundError)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
it "isolates durable bytes from mutation of fetched values" do
|
|
@@ -51,7 +51,7 @@ RSpec.shared_examples "a Journal repository" do
|
|
|
51
51
|
expected_position: 1,
|
|
52
52
|
records: [record]
|
|
53
53
|
)
|
|
54
|
-
end.to raise_error(Phronomy::
|
|
54
|
+
end.to raise_error(Phronomy::Storage::ConflictError)
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
it "rejects a duplicate record_id already present in the Journal" do
|
|
@@ -76,7 +76,7 @@ RSpec.shared_examples "a Journal repository" do
|
|
|
76
76
|
expected_position: 1,
|
|
77
77
|
records: [duplicate]
|
|
78
78
|
)
|
|
79
|
-
end.to raise_error(Phronomy::
|
|
79
|
+
end.to raise_error(Phronomy::Storage::ConflictError)
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
it "rejects duplicate record_ids within one append" do
|
|
@@ -94,7 +94,7 @@ RSpec.shared_examples "a Journal repository" do
|
|
|
94
94
|
expected_position: 0,
|
|
95
95
|
records: records
|
|
96
96
|
)
|
|
97
|
-
end.to raise_error(Phronomy::
|
|
97
|
+
end.to raise_error(Phronomy::Storage::ConflictError)
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
it "rejects records belonging to another Agent" do
|
|
@@ -106,7 +106,7 @@ RSpec.shared_examples "a Journal repository" do
|
|
|
106
106
|
expected_position: 0,
|
|
107
107
|
records: [record]
|
|
108
108
|
)
|
|
109
|
-
end.to raise_error(Phronomy::
|
|
109
|
+
end.to raise_error(Phronomy::Storage::SerializationError)
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
it "reads records in sequence order and supports after/limit" do
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
|
|
5
5
|
RSpec.shared_examples "a Persistence backend" do
|
|
6
|
+
include_examples "neutral storage primitives"
|
|
7
|
+
include_examples "storage transaction boundaries"
|
|
8
|
+
|
|
6
9
|
let(:backend_agent_root) do
|
|
7
10
|
Phronomy::Agent::AgentRoot.create(
|
|
8
11
|
agent_id: "backend-agent-#{SecureRandom.uuid}",
|
|
@@ -23,8 +26,8 @@ RSpec.shared_examples "a Persistence backend" do
|
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
it "advertises every required capability" do
|
|
26
|
-
Phronomy::
|
|
27
|
-
expect(persistence.capabilities[name]).to eq(required_value)
|
|
29
|
+
Phronomy::Storage::Backend::REQUIRED_CAPABILITIES.each do |name, required_value|
|
|
30
|
+
expect(persistence.backend.capabilities[name]).to eq(required_value)
|
|
28
31
|
end
|
|
29
32
|
end
|
|
30
33
|
|
|
@@ -129,11 +132,11 @@ RSpec.shared_examples "a Persistence backend" do
|
|
|
129
132
|
expect(persistence.contents.exist?(content_id)).to be(false)
|
|
130
133
|
expect do
|
|
131
134
|
persistence.agents.load(root.agent_id)
|
|
132
|
-
end.to raise_error(Phronomy::
|
|
135
|
+
end.to raise_error(Phronomy::Storage::NotFoundError)
|
|
133
136
|
expect(persistence.journals.head(root.agent_id)).to eq(0)
|
|
134
137
|
expect do
|
|
135
138
|
persistence.executions.load(execution.execution_id)
|
|
136
|
-
end.to raise_error(Phronomy::
|
|
139
|
+
end.to raise_error(Phronomy::Storage::NotFoundError)
|
|
137
140
|
expect(persistence.workflow_states.load(workflow_id)).to be_nil
|
|
138
141
|
end
|
|
139
142
|
|
|
@@ -162,7 +165,7 @@ RSpec.shared_examples "a Persistence backend" do
|
|
|
162
165
|
agent_revision: 0,
|
|
163
166
|
journal_position: 0
|
|
164
167
|
)
|
|
165
|
-
end.to raise_error(Phronomy::
|
|
168
|
+
end.to raise_error(Phronomy::Storage::ConflictError)
|
|
166
169
|
end
|
|
167
170
|
|
|
168
171
|
it "raises ConflictError when the durable Journal position has advanced" do
|
|
@@ -188,7 +191,7 @@ RSpec.shared_examples "a Persistence backend" do
|
|
|
188
191
|
agent_revision: 0,
|
|
189
192
|
journal_position: 0
|
|
190
193
|
)
|
|
191
|
-
end.to raise_error(Phronomy::
|
|
194
|
+
end.to raise_error(Phronomy::Storage::ConflictError)
|
|
192
195
|
end
|
|
193
196
|
|
|
194
197
|
it "rolls back earlier writes when a watermark precondition fails" do
|
|
@@ -209,7 +212,7 @@ RSpec.shared_examples "a Persistence backend" do
|
|
|
209
212
|
journal_position: 0
|
|
210
213
|
)
|
|
211
214
|
end
|
|
212
|
-
end.to raise_error(Phronomy::
|
|
215
|
+
end.to raise_error(Phronomy::Storage::ConflictError)
|
|
213
216
|
|
|
214
217
|
expect(persistence.contents.exist?(temporary_content_id)).to be(false)
|
|
215
218
|
end
|
|
@@ -54,7 +54,7 @@ RSpec.shared_examples "a workflow state repository" do
|
|
|
54
54
|
expected_revision: nil,
|
|
55
55
|
snapshot: {fields: {value: 2}, phase: "__end__"}
|
|
56
56
|
)
|
|
57
|
-
end.to raise_error(Phronomy::
|
|
57
|
+
end.to raise_error(Phronomy::Storage::ConflictError)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
it "returns a snapshot representation accepted by WorkflowRunner" do
|
|
@@ -111,7 +111,7 @@ RSpec.shared_examples "a workflow state repository" do
|
|
|
111
111
|
|
|
112
112
|
expect do
|
|
113
113
|
repository.delete(workflow_instance_id, expected_revision: 99)
|
|
114
|
-
end.to raise_error(Phronomy::
|
|
114
|
+
end.to raise_error(Phronomy::Storage::ConflictError)
|
|
115
115
|
|
|
116
116
|
repository.delete(workflow_instance_id, expected_revision: 1)
|
|
117
117
|
expect(repository.load(workflow_instance_id)).to be_nil
|
|
@@ -22,13 +22,13 @@ RSpec.shared_examples "an Agent repository" do
|
|
|
22
22
|
|
|
23
23
|
expect do
|
|
24
24
|
agent_repository.create(agent_root)
|
|
25
|
-
end.to raise_error(Phronomy::
|
|
25
|
+
end.to raise_error(Phronomy::Storage::ConflictError)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
it "raises NotFoundError for a missing Agent" do
|
|
29
29
|
expect do
|
|
30
30
|
agent_repository.load("missing-#{SecureRandom.uuid}")
|
|
31
|
-
end.to raise_error(Phronomy::
|
|
31
|
+
end.to raise_error(Phronomy::Storage::NotFoundError)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
it "saves only at the expected revision" do
|
|
@@ -54,7 +54,7 @@ RSpec.shared_examples "an Agent repository" do
|
|
|
54
54
|
|
|
55
55
|
expect do
|
|
56
56
|
agent_repository.save(agent_root.agent_id, expected_revision: 0, root: stale)
|
|
57
|
-
end.to raise_error(Phronomy::
|
|
57
|
+
end.to raise_error(Phronomy::Storage::ConflictError)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
it "rejects an Agent identity mismatch" do
|
|
@@ -71,7 +71,7 @@ RSpec.shared_examples "an Agent repository" do
|
|
|
71
71
|
expected_revision: 0,
|
|
72
72
|
root: other
|
|
73
73
|
)
|
|
74
|
-
end.to raise_error(Phronomy::
|
|
74
|
+
end.to raise_error(Phronomy::Storage::SerializationError)
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
it "requires revision to advance exactly once" do
|
|
@@ -84,7 +84,7 @@ RSpec.shared_examples "an Agent repository" do
|
|
|
84
84
|
expected_revision: 0,
|
|
85
85
|
root: skipped
|
|
86
86
|
)
|
|
87
|
-
end.to raise_error(Phronomy::
|
|
87
|
+
end.to raise_error(Phronomy::Storage::ConflictError)
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
it "deletes idempotently" do
|
|
@@ -94,6 +94,6 @@ RSpec.shared_examples "an Agent repository" do
|
|
|
94
94
|
|
|
95
95
|
expect do
|
|
96
96
|
agent_repository.load(agent_root.agent_id)
|
|
97
|
-
end.to raise_error(Phronomy::
|
|
97
|
+
end.to raise_error(Phronomy::Storage::NotFoundError)
|
|
98
98
|
end
|
|
99
99
|
end
|