matrx-orm 3.1.71__tar.gz → 3.1.72__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.
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/PKG-INFO +2 -2
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/__init__.py +11 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/FEATURE.md +20 -1
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/async_db_manager.py +117 -16
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/pool_maintenance.py +12 -0
- matrx_orm-3.1.72/matrx_orm/core/pool_watch.py +318 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/dynamic_admin.py +9 -9
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/staging_load.py +15 -10
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/pyproject.toml +2 -2
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_connection_poison_guard.py +24 -11
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_insert_ignore_and_admin_primitives.py +2 -1
- matrx_orm-3.1.72/tests/level1/test_pool_holder_census.py +138 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_pool_maintenance.py +6 -1
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/.gitignore +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/AGENTS.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/CLAUDE.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/MODEL_API.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/README.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/docs/migrations.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/adapters/postgresql.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/admin/router.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/catalog.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/catalog_sql.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/base.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/connections.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/db_function.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/expressions.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/one_database.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/tls.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/transaction.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/exceptions.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/identity_rule.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/conflict_writes.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/db_functions.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/dynamic_crud.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/graph_walk.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/latest_rows.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/relational_backfill.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/update.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/platform_db.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/query/FEATURE.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/query/builder.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/query/executor.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/codegen_writer.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/package_wiring.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/secrets_battery/FEATURE.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/secrets_battery/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/secrets_battery/attachments.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/secrets_battery/crypto.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/secrets_battery/items.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/secrets_battery/model.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/secrets_battery/org_service.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/secrets_battery/service.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/README.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/errors.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/fallback.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/flush.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/lifecycle.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/session.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/scripts/release.sh +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/conftest.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_admin_db_columns.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_admin_search_negation.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_bulk_hydration_offload.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_catalog.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_composite_pk_write_paths.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_conflict_writes.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_connection_primitives.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_derived_table.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_dynamic_crud.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_expression_primitives.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_graph_walk.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_join_and_aggregate.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_latest_rows.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_managed_write_guard.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_optimistic_concurrency.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_parameter_name_hydration.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_pool_loop_recreation.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_pydantic_bridge.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_query_builder.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_query_timeout.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_relational_backfill.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_rls_session_scope.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_tls.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_typed_join_projection.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_typed_predicates.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_update_with_rebase.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_upsert_default_leak.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_composite_unique_generation.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_dto_identity_generation.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_entity_capability_generation.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_junction_analysis.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_sql_expression_defaults.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_table_relationships_query.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_view_generation.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema_builder/test_focused_generation_plan.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema_builder/test_host_models_aggregator.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_cache_no_downgrade.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/test_db_functions.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/test_no_blocking_asyncpg_tls.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/test_one_database.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/test_secrets_battery.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/test_sql_param_casts.py +0 -0
- {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/test_sync_twins_guard.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: matrx-orm
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.72
|
|
4
4
|
Summary: Async-first PostgreSQL ORM with bidirectional migrations, schema introspection, many-to-many relationships, and built-in state caching
|
|
5
5
|
Project-URL: Homepage, https://github.com/AI-Matrix-Engine/aidream-current
|
|
6
6
|
Project-URL: Repository, https://github.com/AI-Matrix-Engine/aidream-current
|
|
@@ -25,7 +25,7 @@ Requires-Python: >=3.13
|
|
|
25
25
|
Requires-Dist: asyncpg>=0.31.0
|
|
26
26
|
Requires-Dist: cryptography>=43.0
|
|
27
27
|
Requires-Dist: gitpython>=3.1.40
|
|
28
|
-
Requires-Dist: matrx-utils>=2.0.
|
|
28
|
+
Requires-Dist: matrx-utils>=2.0.15
|
|
29
29
|
Requires-Dist: psycopg-pool>=3.2.5
|
|
30
30
|
Requires-Dist: psycopg[binary]>=3.2.5
|
|
31
31
|
Requires-Dist: python-dotenv>=1.0
|
|
@@ -197,6 +197,12 @@ from .core.pydantic_bridge import (
|
|
|
197
197
|
clear_schema_cache,
|
|
198
198
|
model_json_schema,
|
|
199
199
|
)
|
|
200
|
+
from .core.pool_watch import (
|
|
201
|
+
PoolHolderSnapshot,
|
|
202
|
+
PoolSaturationEvent,
|
|
203
|
+
connection_holder_census,
|
|
204
|
+
register_pool_saturation_sink,
|
|
205
|
+
)
|
|
200
206
|
from .core.pydantic_bridge import (
|
|
201
207
|
validate_input as validate_model_input,
|
|
202
208
|
)
|
|
@@ -562,6 +568,11 @@ __all__ = [
|
|
|
562
568
|
# ── Pagination ─────────────────────────────────────────────────────────────
|
|
563
569
|
"Paginator",
|
|
564
570
|
"Page",
|
|
571
|
+
# ── Pool saturation observability ───────────────────────────────────────────
|
|
572
|
+
"PoolHolderSnapshot",
|
|
573
|
+
"PoolSaturationEvent",
|
|
574
|
+
"connection_holder_census",
|
|
575
|
+
"register_pool_saturation_sink",
|
|
565
576
|
# ── Signals ────────────────────────────────────────────────────────────────
|
|
566
577
|
"Signal",
|
|
567
578
|
"pre_create",
|
|
@@ -53,7 +53,25 @@ connection and can strand old holders in release/reset.
|
|
|
53
53
|
|
|
54
54
|
Pinned by `tests/level1/test_pool_maintenance.py`.
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
**Every checkout is attributable.** `AsyncDatabaseManager.get_connection` and
|
|
57
|
+
the sanctioned caller-supplied-pool helper register the holder's asyncio task,
|
|
58
|
+
checkout call site/stack, current suspended call site/stack, and age. On the
|
|
59
|
+
first acquire timeout in an incident, `pool_watch` emits a critical red census
|
|
60
|
+
and invokes host sinks; subsequent retry timeouts do not spam another incident.
|
|
61
|
+
Once a release returns an idle slot, the alarm re-arms. The final
|
|
62
|
+
`PoolAcquireTimeoutError.details` carries both the first snapshot and the current
|
|
63
|
+
census, so retry/persistence casualties preserve the diagnosis.
|
|
64
|
+
|
|
65
|
+
**Release is bounded and cancellation-safe.** Return/reset runs in a dedicated,
|
|
66
|
+
shielded task with a 10-second asyncpg release timeout. A holder remains visible
|
|
67
|
+
as `releasing` until reset really finishes. If reset exceeds the bound, asyncpg
|
|
68
|
+
discards the physical connection and the failure is logged loudly rather than
|
|
69
|
+
leaving an invisible checkout forever.
|
|
70
|
+
|
|
71
|
+
Pinned by `tests/level1/test_pool_holder_census.py` and
|
|
72
|
+
`tests/level1/test_pool_maintenance.py`.
|
|
73
|
+
|
|
74
|
+
Verified against code: 2026-08-15.
|
|
57
75
|
|
|
58
76
|
## Transaction-pooled role scope
|
|
59
77
|
|
|
@@ -73,6 +91,7 @@ Verified against code: 2026-08-15.
|
|
|
73
91
|
|
|
74
92
|
- 2026-08-15: Pinned transaction-local RLS role cleanup across success,
|
|
75
93
|
exception, and early-return paths.
|
|
94
|
+
- 2026-08-15: Added first-timeout pool holder census, host alarm seam, tracked raw-pool helper, and bounded cancellation-safe release.
|
|
76
95
|
- 2026-08-14: Public models now render `public.table` SQL targets; bare public
|
|
77
96
|
relation names can no longer disable application logs or write-failure capture.
|
|
78
97
|
- 2026-08-13: Registry, admin, recovery, catalog, and diagnostic identities are now exact `schema.table`; bare names are accepted only as uncached discovery terms and ambiguity fails loudly.
|
|
@@ -91,6 +91,7 @@ def _integrity_constraint_kind(exc: asyncpg.exceptions.IntegrityConstraintViolat
|
|
|
91
91
|
# read path survives the same burst.
|
|
92
92
|
POOL_ACQUIRE_RETRIES = 2 # total acquire attempts = 1 + POOL_ACQUIRE_RETRIES
|
|
93
93
|
POOL_ACQUIRE_RETRY_BASE_BACKOFF = 0.25 # seconds; doubled per attempt
|
|
94
|
+
POOL_RELEASE_TIMEOUT_SECONDS = 10.0
|
|
94
95
|
|
|
95
96
|
# ── read-statement retry (command-timeout self-heal) ──────────────────────────
|
|
96
97
|
# A ``command_timeout`` on a READ (SELECT) is provably side-effect-free to retry
|
|
@@ -576,8 +577,7 @@ class AsyncDatabaseManager:
|
|
|
576
577
|
functions, so `call_function` cannot reach them; this is the sanctioned
|
|
577
578
|
accessor.
|
|
578
579
|
"""
|
|
579
|
-
|
|
580
|
-
async with pool.acquire() as conn:
|
|
580
|
+
async with cls.get_connection(config_name) as conn:
|
|
581
581
|
row = await conn.fetchrow(
|
|
582
582
|
"SELECT current_user AS current_user, session_user AS session_user, "
|
|
583
583
|
"current_database() AS database, "
|
|
@@ -643,7 +643,9 @@ class AsyncDatabaseManager:
|
|
|
643
643
|
|
|
644
644
|
growth_lock = cls._growth_locks.setdefault(config_name, asyncio.Lock())
|
|
645
645
|
async with growth_lock:
|
|
646
|
-
await maintain_one_connection(
|
|
646
|
+
await maintain_one_connection(
|
|
647
|
+
pool, target_size=pool_min, config_name=config_name
|
|
648
|
+
)
|
|
647
649
|
failures = 0
|
|
648
650
|
sweep_count = max(pool.get_size(), pool_min, 1)
|
|
649
651
|
interval = max(
|
|
@@ -718,6 +720,7 @@ class AsyncDatabaseManager:
|
|
|
718
720
|
config_name = resolve_database_name(config_name)
|
|
719
721
|
pool = await cls.get_pool(config_name)
|
|
720
722
|
conn = None
|
|
723
|
+
first_timeout_event = None
|
|
721
724
|
try:
|
|
722
725
|
# Bounded acquire-retry: a timeout here means NO SQL was sent (the
|
|
723
726
|
# connection was never handed out), so retrying is provably
|
|
@@ -731,13 +734,42 @@ class AsyncDatabaseManager:
|
|
|
731
734
|
conn = await cls._acquire_bounded_growth(config_name, pool)
|
|
732
735
|
break
|
|
733
736
|
except TimeoutError:
|
|
734
|
-
|
|
735
|
-
|
|
737
|
+
from matrx_orm.core.pool_watch import emit_first_pool_timeout
|
|
738
|
+
|
|
739
|
+
try:
|
|
740
|
+
_snap_size = pool.get_size()
|
|
741
|
+
except Exception:
|
|
742
|
+
_snap_size = -1
|
|
743
|
+
try:
|
|
744
|
+
_snap_idle = pool.get_idle_size()
|
|
745
|
+
except Exception:
|
|
746
|
+
_snap_idle = -1
|
|
747
|
+
try:
|
|
748
|
+
_snap_max = pool.get_max_size()
|
|
749
|
+
except Exception:
|
|
750
|
+
_snap_max = -1
|
|
751
|
+
try:
|
|
752
|
+
_snap_min = get_database_config(config_name).get(
|
|
753
|
+
"pool_min", pool.get_min_size()
|
|
754
|
+
)
|
|
755
|
+
except Exception:
|
|
736
756
|
try:
|
|
737
|
-
|
|
738
|
-
_snap_idle = pool.get_idle_size()
|
|
757
|
+
_snap_min = pool.get_min_size()
|
|
739
758
|
except Exception:
|
|
740
|
-
|
|
759
|
+
_snap_min = -1
|
|
760
|
+
timeout_event = emit_first_pool_timeout(
|
|
761
|
+
config_name=config_name,
|
|
762
|
+
timeout_seconds=timeout,
|
|
763
|
+
acquire_attempt=_acquire_attempt + 1,
|
|
764
|
+
pool_size=_snap_size,
|
|
765
|
+
pool_max=_snap_max,
|
|
766
|
+
pool_idle=_snap_idle,
|
|
767
|
+
pool_min=_snap_min,
|
|
768
|
+
)
|
|
769
|
+
if first_timeout_event is None:
|
|
770
|
+
first_timeout_event = timeout_event
|
|
771
|
+
if _acquire_attempt < POOL_ACQUIRE_RETRIES:
|
|
772
|
+
backoff = POOL_ACQUIRE_RETRY_BASE_BACKOFF * (2**_acquire_attempt)
|
|
741
773
|
logger.warning(
|
|
742
774
|
"[matrx_orm] Acquire timeout for '%s' (attempt %d/%d, "
|
|
743
775
|
"size=%s idle=%s) — retrying in %.2fs (no SQL sent, safe).",
|
|
@@ -782,8 +814,7 @@ class AsyncDatabaseManager:
|
|
|
782
814
|
|
|
783
815
|
if conn is not None:
|
|
784
816
|
logger.warning(
|
|
785
|
-
"[matrx_orm] Acquire healed for '%s' after replacing a "
|
|
786
|
-
"zero-connection pool.",
|
|
817
|
+
"[matrx_orm] Acquire healed for '%s' after replacing a zero-connection pool.",
|
|
787
818
|
config_name,
|
|
788
819
|
)
|
|
789
820
|
else:
|
|
@@ -791,13 +822,28 @@ class AsyncDatabaseManager:
|
|
|
791
822
|
|
|
792
823
|
try:
|
|
793
824
|
_pool_size = pool.get_size()
|
|
825
|
+
except Exception:
|
|
826
|
+
_pool_size = -1
|
|
827
|
+
try:
|
|
794
828
|
_pool_idle = pool.get_idle_size()
|
|
829
|
+
except Exception:
|
|
830
|
+
_pool_idle = -1
|
|
831
|
+
try:
|
|
795
832
|
_pool_max = pool.get_max_size()
|
|
833
|
+
except Exception:
|
|
834
|
+
_pool_max = -1
|
|
835
|
+
try:
|
|
796
836
|
_pool_min = get_database_config(config_name).get(
|
|
797
837
|
"pool_min", pool.get_min_size()
|
|
798
838
|
)
|
|
799
839
|
except Exception:
|
|
800
|
-
|
|
840
|
+
try:
|
|
841
|
+
_pool_min = pool.get_min_size()
|
|
842
|
+
except Exception:
|
|
843
|
+
_pool_min = -1
|
|
844
|
+
from matrx_orm.core.pool_watch import connection_holder_census
|
|
845
|
+
|
|
846
|
+
holders = connection_holder_census(config_name)
|
|
801
847
|
_timeout_err = PoolAcquireTimeoutError(
|
|
802
848
|
model=None,
|
|
803
849
|
config_name=config_name,
|
|
@@ -808,10 +854,14 @@ class AsyncDatabaseManager:
|
|
|
808
854
|
"pool_idle": _pool_idle,
|
|
809
855
|
"pool_min": _pool_min,
|
|
810
856
|
"acquire_attempts": (
|
|
811
|
-
2 + POOL_ACQUIRE_RETRIES
|
|
812
|
-
|
|
813
|
-
|
|
857
|
+
2 + POOL_ACQUIRE_RETRIES if rebuilt else 1 + POOL_ACQUIRE_RETRIES
|
|
858
|
+
),
|
|
859
|
+
"first_timeout": (
|
|
860
|
+
first_timeout_event.model_dump(mode="json")
|
|
861
|
+
if first_timeout_event is not None
|
|
862
|
+
else None
|
|
814
863
|
),
|
|
864
|
+
"holders": [holder.model_dump(mode="json") for holder in holders],
|
|
815
865
|
},
|
|
816
866
|
)
|
|
817
867
|
maybe_activate_queue(config_name, _timeout_err)
|
|
@@ -852,6 +902,17 @@ class AsyncDatabaseManager:
|
|
|
852
902
|
if classified is None:
|
|
853
903
|
raise
|
|
854
904
|
raise classified from None
|
|
905
|
+
from matrx_orm.core.pool_watch import (
|
|
906
|
+
mark_connection_releasing,
|
|
907
|
+
note_pool_recovered,
|
|
908
|
+
track_connection_holder,
|
|
909
|
+
untrack_connection_holder,
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
# Attribute the checkout immediately. Poison cleanup below is database
|
|
913
|
+
# work too; if it stalls, the holder census must show who owns the slot.
|
|
914
|
+
track_connection_holder(config_name, conn)
|
|
915
|
+
|
|
855
916
|
# Acquired successfully — yield outside the timeout scope so the
|
|
856
917
|
# user's work isn't bounded by it, and release in finally so leaks
|
|
857
918
|
# are impossible regardless of how the consumer exits.
|
|
@@ -888,7 +949,11 @@ class AsyncDatabaseManager:
|
|
|
888
949
|
conn.terminate()
|
|
889
950
|
except Exception:
|
|
890
951
|
pass
|
|
891
|
-
|
|
952
|
+
mark_connection_releasing(config_name, conn)
|
|
953
|
+
try:
|
|
954
|
+
await pool.release(conn, timeout=POOL_RELEASE_TIMEOUT_SECONDS)
|
|
955
|
+
finally:
|
|
956
|
+
untrack_connection_holder(config_name, conn)
|
|
892
957
|
raise StateError(
|
|
893
958
|
model=None,
|
|
894
959
|
operation="acquire_connection",
|
|
@@ -900,7 +965,43 @@ class AsyncDatabaseManager:
|
|
|
900
965
|
try:
|
|
901
966
|
yield conn
|
|
902
967
|
finally:
|
|
903
|
-
|
|
968
|
+
mark_connection_releasing(config_name, conn)
|
|
969
|
+
|
|
970
|
+
async def _release() -> None:
|
|
971
|
+
try:
|
|
972
|
+
await pool.release(conn, timeout=POOL_RELEASE_TIMEOUT_SECONDS)
|
|
973
|
+
finally:
|
|
974
|
+
untrack_connection_holder(config_name, conn)
|
|
975
|
+
|
|
976
|
+
# A caller cancellation must not strand a checkout while asyncpg
|
|
977
|
+
# resets it. The release has its own hard bound; asyncpg terminates
|
|
978
|
+
# the physical connection when reset exceeds that bound.
|
|
979
|
+
release_task = asyncio.create_task(_release(), name=f"matrx-orm-release:{config_name}")
|
|
980
|
+
|
|
981
|
+
def _release_done(task: asyncio.Task[None]) -> None:
|
|
982
|
+
if task.cancelled():
|
|
983
|
+
return
|
|
984
|
+
release_exc = task.exception()
|
|
985
|
+
if release_exc is not None:
|
|
986
|
+
logger.error(
|
|
987
|
+
"[matrx_orm] Connection release failed for '%s': %s",
|
|
988
|
+
config_name,
|
|
989
|
+
release_exc,
|
|
990
|
+
)
|
|
991
|
+
return
|
|
992
|
+
try:
|
|
993
|
+
if pool.get_idle_size() > 0:
|
|
994
|
+
note_pool_recovered(config_name)
|
|
995
|
+
except Exception:
|
|
996
|
+
pass
|
|
997
|
+
|
|
998
|
+
release_task.add_done_callback(_release_done)
|
|
999
|
+
try:
|
|
1000
|
+
await asyncio.shield(release_task)
|
|
1001
|
+
except asyncio.CancelledError:
|
|
1002
|
+
# The dedicated release task keeps running to completion and its
|
|
1003
|
+
# body/callback own census cleanup and recovery-latch reset.
|
|
1004
|
+
raise
|
|
904
1005
|
|
|
905
1006
|
@classmethod
|
|
906
1007
|
async def execute_query(
|
|
@@ -59,6 +59,7 @@ async def maintain_one_connection(
|
|
|
59
59
|
*,
|
|
60
60
|
target_size: int,
|
|
61
61
|
timeout_seconds: float = 5.0,
|
|
62
|
+
config_name: str = "<pool-maintenance>",
|
|
62
63
|
) -> bool:
|
|
63
64
|
"""Probe one idle connection or grow one disconnected holder, never more."""
|
|
64
65
|
queue, raw_queue = _pool_queue(pool)
|
|
@@ -83,10 +84,19 @@ async def maintain_one_connection(
|
|
|
83
84
|
try:
|
|
84
85
|
async with asyncio.timeout(timeout_seconds):
|
|
85
86
|
proxy = await holder.acquire()
|
|
87
|
+
from matrx_orm.core.pool_watch import track_connection_holder
|
|
88
|
+
|
|
89
|
+
track_connection_holder(config_name, proxy)
|
|
86
90
|
await proxy.fetchval("SELECT 1")
|
|
87
91
|
return True
|
|
88
92
|
finally:
|
|
89
93
|
if proxy is not None:
|
|
94
|
+
from matrx_orm.core.pool_watch import (
|
|
95
|
+
mark_connection_releasing,
|
|
96
|
+
untrack_connection_holder,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
mark_connection_releasing(config_name, proxy)
|
|
90
100
|
try:
|
|
91
101
|
await pool.release(proxy, timeout=timeout_seconds)
|
|
92
102
|
except Exception:
|
|
@@ -95,6 +105,8 @@ async def maintain_one_connection(
|
|
|
95
105
|
if holder not in raw_queue and getattr(holder, "_in_use", None) is None:
|
|
96
106
|
queue.put_nowait(holder)
|
|
97
107
|
raise
|
|
108
|
+
finally:
|
|
109
|
+
untrack_connection_holder(config_name, proxy)
|
|
98
110
|
elif holder not in raw_queue:
|
|
99
111
|
# connect/setup failed before a proxy was established.
|
|
100
112
|
queue.put_nowait(holder)
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
import traceback
|
|
7
|
+
from collections.abc import AsyncIterator, Callable
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from typing import Any, Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict
|
|
14
|
+
|
|
15
|
+
from matrx_utils import vcprint
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("matrx_orm.pool_watch")
|
|
18
|
+
|
|
19
|
+
_STACK_LIMIT = 24
|
|
20
|
+
_DISPLAY_STACK_LIMIT = 8
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PoolHolderSnapshot(BaseModel):
|
|
24
|
+
"""One connection currently checked out from a watched ORM pool."""
|
|
25
|
+
|
|
26
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
27
|
+
|
|
28
|
+
task_name: str
|
|
29
|
+
task_id: int
|
|
30
|
+
state: Literal["held", "releasing"]
|
|
31
|
+
age_seconds: float
|
|
32
|
+
acquired_at: datetime
|
|
33
|
+
checkout_site: str
|
|
34
|
+
checkout_stack: tuple[str, ...]
|
|
35
|
+
current_site: str | None
|
|
36
|
+
current_stack: tuple[str, ...]
|
|
37
|
+
task_done: bool
|
|
38
|
+
task_cancelled: bool
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PoolSaturationEvent(BaseModel):
|
|
42
|
+
"""Diagnostic snapshot captured on a pool's first acquire timeout."""
|
|
43
|
+
|
|
44
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
45
|
+
|
|
46
|
+
config_name: str
|
|
47
|
+
timeout_seconds: float
|
|
48
|
+
acquire_attempt: int
|
|
49
|
+
occurred_at: datetime
|
|
50
|
+
pool_size: int
|
|
51
|
+
pool_max: int
|
|
52
|
+
pool_idle: int
|
|
53
|
+
pool_min: int
|
|
54
|
+
waiter_task_name: str
|
|
55
|
+
waiter_site: str
|
|
56
|
+
holders: tuple[PoolHolderSnapshot, ...]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
PoolSaturationSink = Callable[[PoolSaturationEvent], None]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(slots=True)
|
|
63
|
+
class _TrackedHolder:
|
|
64
|
+
config_name: str
|
|
65
|
+
connection: Any
|
|
66
|
+
task: asyncio.Task[Any]
|
|
67
|
+
acquired_monotonic: float
|
|
68
|
+
acquired_at: datetime
|
|
69
|
+
checkout_site: str
|
|
70
|
+
checkout_stack: tuple[str, ...]
|
|
71
|
+
state: Literal["held", "releasing"] = "held"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
_holders: dict[str, dict[int, _TrackedHolder]] = {}
|
|
75
|
+
_sinks: list[PoolSaturationSink] = []
|
|
76
|
+
_open_saturation_incidents: set[str] = set()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _format_frame(frame: traceback.FrameSummary) -> str:
|
|
80
|
+
return f"{frame.filename}:{frame.lineno} in {frame.name}"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _is_orm_frame(frame: traceback.FrameSummary) -> bool:
|
|
84
|
+
normalized = frame.filename.replace("\\", "/")
|
|
85
|
+
return "/matrx_orm/" in normalized
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _is_context_wrapper_frame(frame: traceback.FrameSummary) -> bool:
|
|
89
|
+
normalized = frame.filename.replace("\\", "/")
|
|
90
|
+
return normalized.endswith("/contextlib.py") or "/asyncio/" in normalized
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _capture_checkout_stack() -> tuple[str, tuple[str, ...]]:
|
|
94
|
+
frames = traceback.extract_stack(limit=_STACK_LIMIT)[:-1]
|
|
95
|
+
formatted = tuple(_format_frame(frame) for frame in frames[-_DISPLAY_STACK_LIMIT:])
|
|
96
|
+
external = next(
|
|
97
|
+
(
|
|
98
|
+
frame
|
|
99
|
+
for frame in reversed(frames)
|
|
100
|
+
if not _is_orm_frame(frame) and not _is_context_wrapper_frame(frame)
|
|
101
|
+
),
|
|
102
|
+
None,
|
|
103
|
+
)
|
|
104
|
+
site = _format_frame(external or frames[-1]) if frames else "<stack unavailable>"
|
|
105
|
+
return site, formatted
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _task_stack(task: asyncio.Task[Any]) -> tuple[str | None, tuple[str, ...]]:
|
|
109
|
+
try:
|
|
110
|
+
frames = task.get_stack(limit=_DISPLAY_STACK_LIMIT)
|
|
111
|
+
except Exception:
|
|
112
|
+
return None, ()
|
|
113
|
+
formatted = tuple(
|
|
114
|
+
f"{frame.f_code.co_filename}:{frame.f_lineno} in {frame.f_code.co_name}" for frame in frames
|
|
115
|
+
)
|
|
116
|
+
return (formatted[-1] if formatted else None), formatted
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def track_connection_holder(config_name: str, connection: Any) -> None:
|
|
120
|
+
"""Register the current task as the holder of ``connection``."""
|
|
121
|
+
task = asyncio.current_task()
|
|
122
|
+
if task is None:
|
|
123
|
+
return
|
|
124
|
+
checkout_site, checkout_stack = _capture_checkout_stack()
|
|
125
|
+
tracked = _TrackedHolder(
|
|
126
|
+
config_name=config_name,
|
|
127
|
+
connection=connection,
|
|
128
|
+
task=task,
|
|
129
|
+
acquired_monotonic=time.monotonic(),
|
|
130
|
+
acquired_at=datetime.now(UTC),
|
|
131
|
+
checkout_site=checkout_site,
|
|
132
|
+
checkout_stack=checkout_stack,
|
|
133
|
+
)
|
|
134
|
+
_holders.setdefault(config_name, {})[id(connection)] = tracked
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def mark_connection_releasing(config_name: str, connection: Any) -> None:
|
|
138
|
+
"""Mark a checkout as resetting/returning rather than caller-held."""
|
|
139
|
+
tracked = _holders.get(config_name, {}).get(id(connection))
|
|
140
|
+
if tracked is not None:
|
|
141
|
+
tracked.state = "releasing"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def untrack_connection_holder(config_name: str, connection: Any) -> None:
|
|
145
|
+
"""Remove a connection after its pool release actually completes."""
|
|
146
|
+
config_holders = _holders.get(config_name)
|
|
147
|
+
if config_holders is None:
|
|
148
|
+
return
|
|
149
|
+
config_holders.pop(id(connection), None)
|
|
150
|
+
if not config_holders:
|
|
151
|
+
_holders.pop(config_name, None)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def connection_holder_census(config_name: str) -> tuple[PoolHolderSnapshot, ...]:
|
|
155
|
+
"""Return oldest-first snapshots of every tracked checkout for a pool."""
|
|
156
|
+
now = time.monotonic()
|
|
157
|
+
snapshots: list[PoolHolderSnapshot] = []
|
|
158
|
+
for tracked in list(_holders.get(config_name, {}).values()):
|
|
159
|
+
current_site, current_stack = _task_stack(tracked.task)
|
|
160
|
+
snapshots.append(
|
|
161
|
+
PoolHolderSnapshot(
|
|
162
|
+
task_name=tracked.task.get_name(),
|
|
163
|
+
task_id=id(tracked.task),
|
|
164
|
+
state=tracked.state,
|
|
165
|
+
age_seconds=round(max(0.0, now - tracked.acquired_monotonic), 3),
|
|
166
|
+
acquired_at=tracked.acquired_at,
|
|
167
|
+
checkout_site=tracked.checkout_site,
|
|
168
|
+
checkout_stack=tracked.checkout_stack,
|
|
169
|
+
current_site=current_site,
|
|
170
|
+
current_stack=current_stack,
|
|
171
|
+
task_done=tracked.task.done(),
|
|
172
|
+
task_cancelled=tracked.task.cancelled(),
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
snapshots.sort(key=lambda holder: holder.age_seconds, reverse=True)
|
|
176
|
+
return tuple(snapshots)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def register_pool_saturation_sink(sink: PoolSaturationSink) -> None:
|
|
180
|
+
"""Register a synchronous first-timeout observer (normally a task scheduler)."""
|
|
181
|
+
if sink not in _sinks:
|
|
182
|
+
_sinks.append(sink)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def reset_pool_watch_state() -> None:
|
|
186
|
+
"""Clear process state for isolated tests."""
|
|
187
|
+
_holders.clear()
|
|
188
|
+
_sinks.clear()
|
|
189
|
+
_open_saturation_incidents.clear()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def note_pool_recovered(config_name: str) -> None:
|
|
193
|
+
"""Re-arm the first-timeout alarm after the pool has an idle connection."""
|
|
194
|
+
_open_saturation_incidents.discard(config_name)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@asynccontextmanager
|
|
198
|
+
async def watched_pool_acquire(
|
|
199
|
+
pool: Any,
|
|
200
|
+
*,
|
|
201
|
+
config_name: str = "<external-pool>",
|
|
202
|
+
timeout_seconds: float = 10.0,
|
|
203
|
+
) -> AsyncIterator[Any]:
|
|
204
|
+
"""Track a caller-supplied pool checkout and alarm if its wait times out."""
|
|
205
|
+
acquired = False
|
|
206
|
+
connection: Any = None
|
|
207
|
+
try:
|
|
208
|
+
async with pool.acquire(timeout=timeout_seconds) as connection:
|
|
209
|
+
acquired = True
|
|
210
|
+
track_connection_holder(config_name, connection)
|
|
211
|
+
try:
|
|
212
|
+
yield connection
|
|
213
|
+
finally:
|
|
214
|
+
mark_connection_releasing(config_name, connection)
|
|
215
|
+
except TimeoutError:
|
|
216
|
+
# A TimeoutError from the caller's body is not an acquire timeout.
|
|
217
|
+
if acquired:
|
|
218
|
+
raise
|
|
219
|
+
|
|
220
|
+
def _stat(method: str) -> int:
|
|
221
|
+
try:
|
|
222
|
+
return int(getattr(pool, method)())
|
|
223
|
+
except Exception:
|
|
224
|
+
return -1
|
|
225
|
+
|
|
226
|
+
emit_first_pool_timeout(
|
|
227
|
+
config_name=config_name,
|
|
228
|
+
timeout_seconds=timeout_seconds,
|
|
229
|
+
acquire_attempt=1,
|
|
230
|
+
pool_size=_stat("get_size"),
|
|
231
|
+
pool_max=_stat("get_max_size"),
|
|
232
|
+
pool_idle=_stat("get_idle_size"),
|
|
233
|
+
pool_min=_stat("get_min_size"),
|
|
234
|
+
)
|
|
235
|
+
raise
|
|
236
|
+
finally:
|
|
237
|
+
if acquired:
|
|
238
|
+
untrack_connection_holder(config_name, connection)
|
|
239
|
+
try:
|
|
240
|
+
if pool.get_idle_size() > 0:
|
|
241
|
+
note_pool_recovered(config_name)
|
|
242
|
+
except Exception:
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def emit_first_pool_timeout(
|
|
247
|
+
*,
|
|
248
|
+
config_name: str,
|
|
249
|
+
timeout_seconds: float,
|
|
250
|
+
acquire_attempt: int,
|
|
251
|
+
pool_size: int,
|
|
252
|
+
pool_max: int,
|
|
253
|
+
pool_idle: int,
|
|
254
|
+
pool_min: int,
|
|
255
|
+
) -> PoolSaturationEvent:
|
|
256
|
+
"""Scream once per saturation incident and return its holder census."""
|
|
257
|
+
waiter = asyncio.current_task()
|
|
258
|
+
waiter_site, _ = _capture_checkout_stack()
|
|
259
|
+
event = PoolSaturationEvent(
|
|
260
|
+
config_name=config_name,
|
|
261
|
+
timeout_seconds=timeout_seconds,
|
|
262
|
+
acquire_attempt=acquire_attempt,
|
|
263
|
+
occurred_at=datetime.now(UTC),
|
|
264
|
+
pool_size=pool_size,
|
|
265
|
+
pool_max=pool_max,
|
|
266
|
+
pool_idle=pool_idle,
|
|
267
|
+
pool_min=pool_min,
|
|
268
|
+
waiter_task_name=waiter.get_name() if waiter is not None else "<no task>",
|
|
269
|
+
waiter_site=waiter_site,
|
|
270
|
+
holders=connection_holder_census(config_name),
|
|
271
|
+
)
|
|
272
|
+
if config_name in _open_saturation_incidents:
|
|
273
|
+
return event
|
|
274
|
+
_open_saturation_incidents.add(config_name)
|
|
275
|
+
|
|
276
|
+
holder_lines = [
|
|
277
|
+
(
|
|
278
|
+
f" - task={holder.task_name!r} state={holder.state} "
|
|
279
|
+
f"age={holder.age_seconds:.3f}s checkout={holder.checkout_site} "
|
|
280
|
+
f"current={holder.current_site or '<done>'}"
|
|
281
|
+
)
|
|
282
|
+
for holder in event.holders
|
|
283
|
+
]
|
|
284
|
+
if not holder_lines:
|
|
285
|
+
holder_lines.append(" - <no tracked holders; inspect raw pool bypasses or pool growth>")
|
|
286
|
+
message = (
|
|
287
|
+
"\n🚨 MATRX ORM POOL SATURATION — FIRST ACQUIRE TIMEOUT\n"
|
|
288
|
+
f"config={config_name} attempt={acquire_attempt} timeout={timeout_seconds:.3f}s "
|
|
289
|
+
f"pool={pool_idle} idle / {pool_size} open / {pool_max} max (min={pool_min})\n"
|
|
290
|
+
f"waiter={event.waiter_task_name!r} at {event.waiter_site}\n"
|
|
291
|
+
f"tracked_holders={len(event.holders)}\n" + "\n".join(holder_lines)
|
|
292
|
+
)
|
|
293
|
+
vcprint(message, color="red")
|
|
294
|
+
logger.critical(
|
|
295
|
+
"pool_saturation_first_timeout",
|
|
296
|
+
extra={"pool_saturation": event.model_dump(mode="json")},
|
|
297
|
+
)
|
|
298
|
+
for sink in list(_sinks):
|
|
299
|
+
try:
|
|
300
|
+
sink(event)
|
|
301
|
+
except Exception as exc: # noqa: BLE001 - observability never masks the timeout
|
|
302
|
+
logger.exception("pool saturation sink %r failed: %s", sink, exc)
|
|
303
|
+
return event
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
__all__ = [
|
|
307
|
+
"PoolHolderSnapshot",
|
|
308
|
+
"PoolSaturationEvent",
|
|
309
|
+
"connection_holder_census",
|
|
310
|
+
"emit_first_pool_timeout",
|
|
311
|
+
"mark_connection_releasing",
|
|
312
|
+
"note_pool_recovered",
|
|
313
|
+
"register_pool_saturation_sink",
|
|
314
|
+
"reset_pool_watch_state",
|
|
315
|
+
"track_connection_holder",
|
|
316
|
+
"untrack_connection_holder",
|
|
317
|
+
"watched_pool_acquire",
|
|
318
|
+
]
|