openframe-core 3.3.0__tar.gz → 3.4.0__tar.gz
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.
- {openframe_core-3.3.0 → openframe_core-3.4.0}/.github/CHANGELOG.md +42 -1
- {openframe_core-3.3.0 → openframe_core-3.4.0}/PKG-INFO +1 -1
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/plugins/__init__.py +3 -3
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/plugins/registry.py +9 -5
- openframe_core-3.4.0/openframe/core/resilience/__init__.py +44 -0
- openframe_core-3.4.0/openframe/core/resilience/circuit_breaker.py +260 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/runtime/__init__.py +4 -1
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/runtime/bootstrap.py +9 -5
- {openframe_core-3.3.0 → openframe_core-3.4.0}/pyproject.toml +1 -1
- openframe_core-3.4.0/tests/test_resilience.py +233 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/.github/requirements/docs.txt +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/.github/requirements/test.txt +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/.github/workflows/app-test.yml +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/.github/workflows/auto-docs.yml +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/.github/workflows/python-build.yml +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/.gitignore +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/.vscode/settings.json +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/LICENSE +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/README.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/assets/images/banner.png +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/assets/images/favicon.png +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/assets/images/logo-dark.png +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/assets/images/logo-light.png +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/assets/javascripts/extra.js +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/assets/stylesheets/extra.css +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/code/cicd/pipeline.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/code/getting-started/index.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/code/modules/config.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/code/modules/exceptions.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/code/modules/health.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/code/modules/inbound.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/code/modules/index.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/code/modules/middleware.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/code/modules/plugins.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/code/modules/ports.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/code/modules/runtime.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/code/modules/telemetry.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/code/modules/testing.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/code/modules/tracing.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/developer-guide/debugging.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/developer-guide/explore.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/developer-guide/first-change.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/developer-guide/how-it-works.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/developer-guide/local-dev.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/developer-guide/quick-start/index.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/index.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/architecture/adrs/adr-001-namespace.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/architecture/adrs/adr-002-hexagonal.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/architecture/adrs/adr-003-async.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/architecture/adrs/adr-004-pydantic.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/architecture/adrs/adr-005-middleware.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/architecture/adrs/adr-006-unified-port-lifecycle.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/architecture/adrs/adr-007-schema-contracts.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/architecture/adrs/index.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/architecture/capability-taxonomy.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/architecture/component-lifecycle.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/architecture/data-flows.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/architecture/design-decisions.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/architecture/error-taxonomy.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/architecture/package-journey/adapter-lifecycle.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/architecture/package-journey/error-propagation.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/architecture/package-journey/index.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/architecture/package-journey/template-wiring.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/architecture/package-journey/tracing-flow.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/architecture/system-architecture.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/deployment/cicd.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/deployment/environments.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/deployment/secrets-and-config.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/infrastructure/networking.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/infrastructure/platform.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/infrastructure/storage.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/operations/index.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/operations/monitoring.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/operations/runbooks/adapter-connection-failure.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/operations/runbooks/modal-cold-start-spike.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/operations/runbooks/postgres-pool-exhausted.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/operations/runbooks/queue-consumer-stuck.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/operations/runbooks/redis-connection-lost.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/operations/runbooks/telemetry-export-failing.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/overview/glossary.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/overview/roadmap.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/docs/technical/overview/system-overview.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/mkdocs.yml +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/config/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/config/settings.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/exceptions/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/exceptions/adapter.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/exceptions/base.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/exceptions/codes.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/exceptions/plugin.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/inbound/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/inbound/command.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/inbound/context.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/inbound/query.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/inbound/usecase.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/middleware/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/middleware/telemetry.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/middleware/types.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/ports/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/ports/capability.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/ports/context.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/ports/health.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/ports/identity.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/ports/lifecycle.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/ports/outbound/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/ports/outbound/consumer.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/ports/outbound/producer.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/ports/outbound/repository.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/ports/port.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/schemas/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/telemetry/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/telemetry/setup.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/testing/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/testing/contracts/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/testing/contracts/consumer.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/testing/contracts/inbound.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/testing/contracts/lifecycle.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/testing/contracts/port.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/testing/contracts/producer.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/testing/contracts/repository.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/testing/fakes/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/testing/fakes/consumer.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/testing/fakes/inbound.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/testing/fakes/producer.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/testing/fakes/repository.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/testing/fixtures.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/tracing/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/tracing/propagation.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/core/tracing/proxy.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/openframe/py.typed +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/tests/conftest.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/tests/test_config.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/tests/test_exceptions.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/tests/test_inbound.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/tests/test_inbound_contracts.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/tests/test_middleware.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/tests/test_middleware_types.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/tests/test_plugins.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/tests/test_ports.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/tests/test_ports_primitives.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/tests/test_record_error.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/tests/test_runtime.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/tests/test_schemas.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/tests/test_shutdown_telemetry.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/tests/test_telemetry_sidecar.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/tests/test_testing_fakes.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/tests/test_tracing.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.4.0}/tests/test_tracing_propagation.py +0 -0
|
@@ -11,6 +11,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## v3.4.0 — 2026-09-28
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`openframe.core.resilience.CircuitBreakerProxy`** (new module,
|
|
19
|
+
experimental) — zero-code async circuit breaker. Wraps any object and
|
|
20
|
+
short-circuits its async methods after `failure_threshold` consecutive
|
|
21
|
+
failures, raising `AdapterConnectionError` ("circuit open", retryable)
|
|
22
|
+
immediately instead of letting every caller block until
|
|
23
|
+
`operation_timeout` on every single call during a sustained outage.
|
|
24
|
+
Standard closed → open → half-open → closed/open state machine.
|
|
25
|
+
Mirrors `TracingProxy`'s exact wrapping mechanism (`__getattr__`
|
|
26
|
+
interception, fresh `getattr` resolution of the wrapped method on every
|
|
27
|
+
call, sync methods pass through unwrapped) so the two proxies compose
|
|
28
|
+
predictably — see the module docstring for the recommended composition
|
|
29
|
+
order (`CircuitBreakerProxy(TracingProxy(repo, ...), ...)`, not the
|
|
30
|
+
reverse, so a short-circuited call never produces a misleading adapter
|
|
31
|
+
span). Addresses a real gap: none of the four `openframe-adapters`
|
|
32
|
+
packages (or anything else in the ecosystem) previously had any
|
|
33
|
+
resilience pattern beyond "block until timeout, then raise."
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **Graduated `PluginRegistry` and `ApplicationBootstrap` from `experimental`
|
|
38
|
+
to `stable`** (`openframe.core.plugins.registry`, `openframe.core.runtime.bootstrap`,
|
|
39
|
+
and their respective package `__init__.py` re-exports). Graduation
|
|
40
|
+
criteria: (1) all 7 `openframe-local-validation-framework` services
|
|
41
|
+
already depend on both in production-shaped code across multiple
|
|
42
|
+
releases with no breaking change required of them; (2) v3.3.0's
|
|
43
|
+
`ApplicationBootstrap.compose()`/`get_all()`/`registry` additions landed
|
|
44
|
+
as pure additive extensions on top of the existing contract — no
|
|
45
|
+
breaking change was needed to add meaningful new functionality, which is
|
|
46
|
+
itself evidence the contract shape is right. This is the first module in
|
|
47
|
+
`openframe-core` to graduate via an explicit, written criteria set
|
|
48
|
+
(documented in each module's own docstring) rather than an unstated
|
|
49
|
+
judgment call — future modules going through the same
|
|
50
|
+
experimental→stable path should record their own criteria the same way.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
14
54
|
## v3.3.0 — 2026-09-28
|
|
15
55
|
|
|
16
56
|
### Added
|
|
@@ -385,7 +425,8 @@ Dev dependencies: `pytest>=8.0`, `pytest-asyncio>=0.23`, `httpx>=0.27`.
|
|
|
385
425
|
|
|
386
426
|
---
|
|
387
427
|
|
|
388
|
-
[Unreleased]: https://github.com/openframe-org/openframe-core/compare/v3.
|
|
428
|
+
[Unreleased]: https://github.com/openframe-org/openframe-core/compare/v3.4.0...HEAD
|
|
429
|
+
[3.4.0]: https://github.com/openframe-org/openframe-core/compare/v3.3.0...v3.4.0
|
|
389
430
|
[3.3.0]: https://github.com/openframe-org/openframe-core/compare/v3.2.1...v3.3.0
|
|
390
431
|
[3.2.1]: https://github.com/openframe-org/openframe-core/compare/v3.2.0...v3.2.1
|
|
391
432
|
[3.2.0]: https://github.com/openframe-org/openframe-core/compare/v3.1.0...v3.2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: openframe-core
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.0
|
|
4
4
|
Summary: OpenFrame Microservice Suite - platform kernel. Port contracts, testing utilities, plugin registry, and composition root.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Furious-Meteors/openframe-core
|
|
6
6
|
Project-URL: Documentation, https://furious-meteors.github.io/openframe-core/
|
|
@@ -9,10 +9,10 @@ plugin protocol. This package re-exports the ``ports`` types
|
|
|
9
9
|
:class:`~openframe.core.plugins.registry.PluginRegistry` operates on for
|
|
10
10
|
convenience, plus the registry itself.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
Stable as of v3.3.0 — see ``PluginRegistry``'s own module docstring
|
|
13
|
+
(``openframe.core.plugins.registry``) for the graduation rationale.
|
|
14
14
|
|
|
15
|
-
.. stability:
|
|
15
|
+
.. stability: stable
|
|
16
16
|
"""
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
@@ -17,10 +17,14 @@ correct shutdown ordering (ports then ``shutdown_telemetry()``). Use
|
|
|
17
17
|
initialization order or are building a framework layer on top of
|
|
18
18
|
openframe-core.
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Stable as of v3.3.0 — graduated from experimental after all 7
|
|
21
|
+
``openframe-local-validation-framework`` services proved it in
|
|
22
|
+
production-shaped code across multiple releases with no breaking change,
|
|
23
|
+
and the v3.3.0 ``ApplicationBootstrap.compose()``/``get_all()``/``registry``
|
|
24
|
+
additions landed as pure additive extensions on top of this contract,
|
|
25
|
+
confirming the shape was already right.
|
|
21
26
|
|
|
22
|
-
.. stability:
|
|
23
|
-
Experimental — API may change or be removed in any version.
|
|
27
|
+
.. stability: stable
|
|
24
28
|
|
|
25
29
|
Dependency order:
|
|
26
30
|
ports → (apex; no plugins/registry-side imports)
|
|
@@ -52,7 +56,7 @@ if TYPE_CHECKING:
|
|
|
52
56
|
|
|
53
57
|
__all__ = ["PluginRegistry"]
|
|
54
58
|
|
|
55
|
-
__stability__ = "
|
|
59
|
+
__stability__ = "stable"
|
|
56
60
|
|
|
57
61
|
_log = logging.getLogger(__name__)
|
|
58
62
|
|
|
@@ -75,7 +79,7 @@ class PluginRegistry:
|
|
|
75
79
|
control over initialization order, or when building a framework layer
|
|
76
80
|
that manages the registry lifecycle itself.
|
|
77
81
|
|
|
78
|
-
.. stability:
|
|
82
|
+
.. stability: stable
|
|
79
83
|
|
|
80
84
|
Direct usage::
|
|
81
85
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
openframe/core/resilience/
|
|
3
|
+
============================
|
|
4
|
+
Resilience patterns for the OpenFrame ecosystem.
|
|
5
|
+
|
|
6
|
+
Modules
|
|
7
|
+
-------
|
|
8
|
+
``CircuitBreakerProxy``
|
|
9
|
+
Zero-code async circuit breaker. Wraps any object and short-circuits
|
|
10
|
+
its async methods after a run of consecutive failures, without
|
|
11
|
+
requiring the adapter or service layer to know it exists. Mirrors
|
|
12
|
+
``openframe.core.tracing.TracingProxy``'s wrapping mechanism, so the
|
|
13
|
+
two proxies compose predictably.
|
|
14
|
+
|
|
15
|
+
Usage::
|
|
16
|
+
|
|
17
|
+
from openframe.core.resilience import CircuitBreakerProxy
|
|
18
|
+
|
|
19
|
+
raw_repo = PostgresRepository(settings)
|
|
20
|
+
guarded_repo = CircuitBreakerProxy(raw_repo, failure_threshold=5, reset_timeout=30.0)
|
|
21
|
+
# After 5 consecutive failures, calls raise AdapterConnectionError
|
|
22
|
+
# ("circuit open") immediately instead of blocking on operation_timeout.
|
|
23
|
+
|
|
24
|
+
Composing with ``TracingProxy`` — prefer wrapping the traced object, not
|
|
25
|
+
the other way around, so a short-circuited call never produces a
|
|
26
|
+
misleading adapter span for a call that never reached the adapter::
|
|
27
|
+
|
|
28
|
+
from openframe.core.tracing import TracingProxy
|
|
29
|
+
|
|
30
|
+
traced = TracingProxy(raw_repo, prefix="repository.item")
|
|
31
|
+
guarded = CircuitBreakerProxy(traced, failure_threshold=5, reset_timeout=30.0)
|
|
32
|
+
|
|
33
|
+
.. stability: experimental
|
|
34
|
+
New in v3.4.0. Experimental — API may change based on real usage,
|
|
35
|
+
consistent with how ``ApplicationBootstrap``/``PluginRegistry`` started
|
|
36
|
+
experimental and graduated once proven (see their own module docstrings).
|
|
37
|
+
"""
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
from openframe.core.resilience.circuit_breaker import CircuitBreakerProxy, CircuitState
|
|
41
|
+
|
|
42
|
+
__all__ = ["CircuitBreakerProxy", "CircuitState"]
|
|
43
|
+
|
|
44
|
+
__stability__ = "experimental"
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""
|
|
2
|
+
openframe/core/resilience/circuit_breaker.py
|
|
3
|
+
===============================================
|
|
4
|
+
Generic async circuit breaker for the OpenFrame ecosystem.
|
|
5
|
+
|
|
6
|
+
``CircuitBreakerProxy`` wraps any object and short-circuits its async
|
|
7
|
+
methods after a run of consecutive failures, giving a struggling backend
|
|
8
|
+
time to recover instead of every caller blocking until ``operation_timeout``
|
|
9
|
+
on every single call. Neither the wrapped adapter nor the service layer
|
|
10
|
+
need to know the circuit breaker exists.
|
|
11
|
+
|
|
12
|
+
Mirrors :class:`~openframe.core.tracing.proxy.TracingProxy`'s exact
|
|
13
|
+
wrapping mechanism (``__getattr__`` interception, ``object.__setattr__``
|
|
14
|
+
for the proxy's own state, fresh ``getattr`` resolution of the wrapped
|
|
15
|
+
method on every call — never a stale snapshot) so the two proxies compose
|
|
16
|
+
predictably and a reader who understands one understands the other.
|
|
17
|
+
|
|
18
|
+
State machine (per proxy instance, per wrapped object — not global)::
|
|
19
|
+
|
|
20
|
+
CLOSED --[failure_threshold consecutive failures]--> OPEN
|
|
21
|
+
OPEN --[reset_timeout elapsed]--> HALF_OPEN
|
|
22
|
+
HALF_OPEN --[probe call succeeds]--> CLOSED
|
|
23
|
+
HALF_OPEN --[probe call fails]--> OPEN (timer restarts)
|
|
24
|
+
|
|
25
|
+
Known simplification: multiple concurrent callers arriving right at the
|
|
26
|
+
``reset_timeout`` boundary can all be let through as "probes" during the
|
|
27
|
+
same half-open window (this implementation does not cap in-flight probes
|
|
28
|
+
at exactly one). In practice this only matters under high concurrent call
|
|
29
|
+
volume at the exact moment the circuit would reopen or close — it does not
|
|
30
|
+
affect the CLOSED/OPEN transitions, which are strictly threshold- and
|
|
31
|
+
timeout-gated.
|
|
32
|
+
|
|
33
|
+
Dependency order: imports ``openframe.core.exceptions`` (lowest layer) only.
|
|
34
|
+
No dependency on ``telemetry``/``tracing`` — composes with ``TracingProxy``
|
|
35
|
+
by wrapping, not by sharing state.
|
|
36
|
+
|
|
37
|
+
.. stability: experimental
|
|
38
|
+
New in v3.4.0. API may change based on real usage.
|
|
39
|
+
"""
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import asyncio
|
|
43
|
+
import inspect
|
|
44
|
+
import logging
|
|
45
|
+
import time
|
|
46
|
+
from collections.abc import Callable
|
|
47
|
+
from enum import Enum, auto
|
|
48
|
+
from typing import Any
|
|
49
|
+
|
|
50
|
+
from openframe.core.exceptions import AdapterConnectionError
|
|
51
|
+
|
|
52
|
+
__all__ = ["CircuitBreakerProxy", "CircuitState"]
|
|
53
|
+
|
|
54
|
+
__stability__ = "experimental"
|
|
55
|
+
|
|
56
|
+
_log = logging.getLogger(__name__)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class CircuitState(Enum):
|
|
60
|
+
"""Lifecycle state of a single :class:`CircuitBreakerProxy` instance."""
|
|
61
|
+
|
|
62
|
+
CLOSED = auto()
|
|
63
|
+
OPEN = auto()
|
|
64
|
+
HALF_OPEN = auto()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class CircuitBreakerProxy:
|
|
68
|
+
"""
|
|
69
|
+
Generic async circuit breaker.
|
|
70
|
+
|
|
71
|
+
Wraps any object and intercepts every async method call. After
|
|
72
|
+
``failure_threshold`` consecutive failures, the circuit opens: further
|
|
73
|
+
calls raise :class:`~openframe.core.exceptions.AdapterConnectionError`
|
|
74
|
+
immediately (retryable) without invoking the wrapped method, for
|
|
75
|
+
``reset_timeout`` seconds. After that window elapses, the next call is
|
|
76
|
+
let through as a probe (half-open); if it succeeds the circuit closes
|
|
77
|
+
and failures reset to zero, if it fails the circuit reopens and the
|
|
78
|
+
timer restarts.
|
|
79
|
+
|
|
80
|
+
Reconnect safety:
|
|
81
|
+
Like ``TracingProxy``, the wrapped method is resolved fresh via
|
|
82
|
+
``getattr`` on every invocation — never captured as a snapshot.
|
|
83
|
+
|
|
84
|
+
Sync methods:
|
|
85
|
+
Pass through unwrapped, uncounted — the circuit breaker only
|
|
86
|
+
applies to async methods, matching ``TracingProxy``'s convention.
|
|
87
|
+
|
|
88
|
+
Composing with ``TracingProxy``:
|
|
89
|
+
Order matters. ``TracingProxy(CircuitBreakerProxy(repo, ...), ...)``
|
|
90
|
+
traces the circuit breaker's own short-circuit raises as failed
|
|
91
|
+
spans. ``CircuitBreakerProxy(TracingProxy(repo, ...), ...)`` counts
|
|
92
|
+
failures on the traced calls but never creates a span for a
|
|
93
|
+
short-circuited call (the wrapped ``TracingProxy`` is never
|
|
94
|
+
reached while open). Prefer the latter — a call that never reached
|
|
95
|
+
the adapter is not an adapter span.
|
|
96
|
+
|
|
97
|
+
.. stability: experimental
|
|
98
|
+
|
|
99
|
+
Usage::
|
|
100
|
+
|
|
101
|
+
raw = PostgresRepository(settings)
|
|
102
|
+
guarded = CircuitBreakerProxy(raw, failure_threshold=5, reset_timeout=30.0)
|
|
103
|
+
# After 5 consecutive failures, calls raise AdapterConnectionError
|
|
104
|
+
# ("circuit open") immediately for 30s instead of blocking on
|
|
105
|
+
# operation_timeout each time.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
wrapped: object,
|
|
111
|
+
*,
|
|
112
|
+
failure_threshold: int = 5,
|
|
113
|
+
reset_timeout: float = 30.0,
|
|
114
|
+
name: str = "circuit-breaker",
|
|
115
|
+
) -> None:
|
|
116
|
+
"""
|
|
117
|
+
Initialise the proxy.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
wrapped: The object whose async methods will be guarded.
|
|
121
|
+
failure_threshold: Consecutive failures (per method call,
|
|
122
|
+
across all methods on this proxy) before the circuit opens.
|
|
123
|
+
Must be >= 1.
|
|
124
|
+
reset_timeout: Seconds the circuit stays open before a probe
|
|
125
|
+
call is allowed through (half-open). Must be > 0.
|
|
126
|
+
name: Used as the ``adapter`` field on the
|
|
127
|
+
``AdapterConnectionError`` raised while open, and in log
|
|
128
|
+
messages — e.g. ``"postgres-primary"``.
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
ValueError: ``failure_threshold`` < 1 or ``reset_timeout`` <= 0.
|
|
132
|
+
"""
|
|
133
|
+
if failure_threshold < 1:
|
|
134
|
+
raise ValueError(f"failure_threshold must be >= 1, got {failure_threshold!r}")
|
|
135
|
+
if reset_timeout <= 0:
|
|
136
|
+
raise ValueError(f"reset_timeout must be > 0, got {reset_timeout!r}")
|
|
137
|
+
|
|
138
|
+
object.__setattr__(self, "_wrapped", wrapped)
|
|
139
|
+
object.__setattr__(self, "_failure_threshold", failure_threshold)
|
|
140
|
+
object.__setattr__(self, "_reset_timeout", reset_timeout)
|
|
141
|
+
object.__setattr__(self, "_name", name)
|
|
142
|
+
object.__setattr__(self, "_cache", {})
|
|
143
|
+
object.__setattr__(self, "_state", CircuitState.CLOSED)
|
|
144
|
+
object.__setattr__(self, "_consecutive_failures", 0)
|
|
145
|
+
object.__setattr__(self, "_opened_at", 0.0)
|
|
146
|
+
# Guards state transitions against concurrent callers racing on
|
|
147
|
+
# the same proxy instance (e.g. two coroutines calling .get()
|
|
148
|
+
# concurrently while the circuit is deciding whether to open).
|
|
149
|
+
object.__setattr__(self, "_lock", asyncio.Lock())
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def state(self) -> CircuitState:
|
|
153
|
+
"""Current circuit state. Read-only — for observability/tests."""
|
|
154
|
+
return object.__getattribute__(self, "_state")
|
|
155
|
+
|
|
156
|
+
def __getattr__(self, name: str) -> Any:
|
|
157
|
+
"""
|
|
158
|
+
Intercept attribute access.
|
|
159
|
+
|
|
160
|
+
Returns a cached guarded closure for async methods, or the raw
|
|
161
|
+
attribute for sync methods and non-callable attributes.
|
|
162
|
+
"""
|
|
163
|
+
cache: dict[str, Callable[..., Any]] = object.__getattribute__(self, "_cache")
|
|
164
|
+
if name in cache:
|
|
165
|
+
return cache[name]
|
|
166
|
+
|
|
167
|
+
wrapped = object.__getattribute__(self, "_wrapped")
|
|
168
|
+
probe = getattr(wrapped, name)
|
|
169
|
+
|
|
170
|
+
if not inspect.iscoroutinefunction(probe):
|
|
171
|
+
return probe
|
|
172
|
+
|
|
173
|
+
async def _guarded(*args: Any, **kwargs: Any) -> Any:
|
|
174
|
+
await self._before_call(name)
|
|
175
|
+
try:
|
|
176
|
+
current = getattr(object.__getattribute__(self, "_wrapped"), name)
|
|
177
|
+
result = await current(*args, **kwargs)
|
|
178
|
+
except Exception:
|
|
179
|
+
await self._on_failure(name)
|
|
180
|
+
raise
|
|
181
|
+
else:
|
|
182
|
+
await self._on_success(name)
|
|
183
|
+
return result
|
|
184
|
+
|
|
185
|
+
cache[name] = _guarded
|
|
186
|
+
return _guarded
|
|
187
|
+
|
|
188
|
+
async def _before_call(self, method_name: str) -> None:
|
|
189
|
+
"""
|
|
190
|
+
Called before every guarded invocation.
|
|
191
|
+
|
|
192
|
+
Raises ``AdapterConnectionError`` immediately if the circuit is
|
|
193
|
+
open and ``reset_timeout`` has not yet elapsed. If the timeout has
|
|
194
|
+
elapsed, transitions to half-open and lets exactly this one call
|
|
195
|
+
through as a probe.
|
|
196
|
+
"""
|
|
197
|
+
lock: asyncio.Lock = object.__getattribute__(self, "_lock")
|
|
198
|
+
async with lock:
|
|
199
|
+
state: CircuitState = object.__getattribute__(self, "_state")
|
|
200
|
+
if state is not CircuitState.OPEN:
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
opened_at: float = object.__getattribute__(self, "_opened_at")
|
|
204
|
+
reset_timeout: float = object.__getattribute__(self, "_reset_timeout")
|
|
205
|
+
if time.monotonic() - opened_at < reset_timeout:
|
|
206
|
+
name: str = object.__getattribute__(self, "_name")
|
|
207
|
+
raise AdapterConnectionError(
|
|
208
|
+
f"circuit open — {name}.{method_name} short-circuited "
|
|
209
|
+
f"after repeated failures; retry after "
|
|
210
|
+
f"{reset_timeout - (time.monotonic() - opened_at):.1f}s",
|
|
211
|
+
adapter=name,
|
|
212
|
+
operation=method_name,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# reset_timeout elapsed — allow exactly this call through as a probe.
|
|
216
|
+
object.__setattr__(self, "_state", CircuitState.HALF_OPEN)
|
|
217
|
+
_log.info("CircuitBreakerProxy %r half-open — probing %r", object.__getattribute__(self, "_name"), method_name)
|
|
218
|
+
|
|
219
|
+
async def _on_success(self, method_name: str) -> None:
|
|
220
|
+
"""Reset failure count and close the circuit on any success."""
|
|
221
|
+
lock: asyncio.Lock = object.__getattribute__(self, "_lock")
|
|
222
|
+
async with lock:
|
|
223
|
+
object.__setattr__(self, "_consecutive_failures", 0)
|
|
224
|
+
if object.__getattribute__(self, "_state") is not CircuitState.CLOSED:
|
|
225
|
+
object.__setattr__(self, "_state", CircuitState.CLOSED)
|
|
226
|
+
_log.info(
|
|
227
|
+
"CircuitBreakerProxy %r closed — %r succeeded",
|
|
228
|
+
object.__getattribute__(self, "_name"),
|
|
229
|
+
method_name,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
async def _on_failure(self, method_name: str) -> None:
|
|
233
|
+
"""
|
|
234
|
+
Count the failure. Opens the circuit if the threshold is reached
|
|
235
|
+
(from closed) or immediately re-opens it (from half-open, where a
|
|
236
|
+
single probe failure is sufficient — the backend is still down).
|
|
237
|
+
"""
|
|
238
|
+
lock: asyncio.Lock = object.__getattribute__(self, "_lock")
|
|
239
|
+
async with lock:
|
|
240
|
+
state: CircuitState = object.__getattribute__(self, "_state")
|
|
241
|
+
name: str = object.__getattribute__(self, "_name")
|
|
242
|
+
|
|
243
|
+
if state is CircuitState.HALF_OPEN:
|
|
244
|
+
object.__setattr__(self, "_state", CircuitState.OPEN)
|
|
245
|
+
object.__setattr__(self, "_opened_at", time.monotonic())
|
|
246
|
+
_log.warning(
|
|
247
|
+
"CircuitBreakerProxy %r re-opened — probe %r failed", name, method_name
|
|
248
|
+
)
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
failures = object.__getattribute__(self, "_consecutive_failures") + 1
|
|
252
|
+
object.__setattr__(self, "_consecutive_failures", failures)
|
|
253
|
+
threshold: int = object.__getattribute__(self, "_failure_threshold")
|
|
254
|
+
if failures >= threshold:
|
|
255
|
+
object.__setattr__(self, "_state", CircuitState.OPEN)
|
|
256
|
+
object.__setattr__(self, "_opened_at", time.monotonic())
|
|
257
|
+
_log.warning(
|
|
258
|
+
"CircuitBreakerProxy %r opened — %d consecutive failures (last: %r)",
|
|
259
|
+
name, failures, method_name,
|
|
260
|
+
)
|
|
@@ -12,7 +12,10 @@ See ``openframe.core.plugins.PluginRegistry`` if you need fine-grained
|
|
|
12
12
|
control over initialization order or want to manage the registry lifecycle
|
|
13
13
|
directly without the composition abstraction.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Stable as of v3.3.0 — see ``ApplicationBootstrap``'s own module docstring
|
|
16
|
+
(``openframe.core.runtime.bootstrap``) for the graduation rationale.
|
|
17
|
+
|
|
18
|
+
.. stability: stable
|
|
16
19
|
"""
|
|
17
20
|
from __future__ import annotations
|
|
18
21
|
|
|
@@ -26,10 +26,14 @@ intentionally no separate ``PluginRegistry``-direct or ``deps.py``+``lru_cache``
|
|
|
26
26
|
like when hand-rolled, minus the correct shutdown ordering tier 1-3 give you
|
|
27
27
|
for free.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Stable as of v3.3.0 — graduated from experimental after all 7
|
|
30
|
+
``openframe-local-validation-framework`` services proved it in
|
|
31
|
+
production-shaped code across multiple releases with no breaking change,
|
|
32
|
+
and the v3.3.0 ``compose()``/``get_all()``/``registry`` additions landed
|
|
33
|
+
as pure additive extensions on top of this contract, confirming the shape
|
|
34
|
+
was already right.
|
|
30
35
|
|
|
31
|
-
.. stability:
|
|
32
|
-
Experimental — API may change or be removed in any version.
|
|
36
|
+
.. stability: stable
|
|
33
37
|
|
|
34
38
|
Dependency order:
|
|
35
39
|
ports → (apex)
|
|
@@ -49,7 +53,7 @@ if TYPE_CHECKING:
|
|
|
49
53
|
|
|
50
54
|
__all__ = ["ApplicationBootstrap"]
|
|
51
55
|
|
|
52
|
-
__stability__ = "
|
|
56
|
+
__stability__ = "stable"
|
|
53
57
|
|
|
54
58
|
|
|
55
59
|
class ApplicationBootstrap:
|
|
@@ -63,7 +67,7 @@ class ApplicationBootstrap:
|
|
|
63
67
|
the OTel SDK is flushed via ``shutdown_telemetry()`` so no spans are
|
|
64
68
|
silently dropped at process exit.
|
|
65
69
|
|
|
66
|
-
.. stability:
|
|
70
|
+
.. stability: stable
|
|
67
71
|
|
|
68
72
|
**Tier 1 — no subclass, one or a few ports**::
|
|
69
73
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "openframe-core"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.4.0"
|
|
8
8
|
description = "OpenFrame Microservice Suite - platform kernel. Port contracts, testing utilities, plugin registry, and composition root."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|