remote-cmd-manager 2.5.0__tar.gz → 2.6.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.
- {remote_cmd_manager-2.5.0/remote_cmd_manager.egg-info → remote_cmd_manager-2.6.0}/PKG-INFO +17 -20
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/README.md +16 -19
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/__init__.py +8 -3
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/_version.py +1 -1
- remote_cmd_manager-2.6.0/remote_cmd/api/__init__.py +22 -0
- {remote_cmd_manager-2.5.0/remote_cmd/core → remote_cmd_manager-2.6.0/remote_cmd/api}/host_manager.py +8 -2
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/core/async_connection_pool.py +23 -4
- remote_cmd_manager-2.6.0/remote_cmd/core/budget.py +211 -0
- remote_cmd_manager-2.6.0/remote_cmd/core/host_manager.py +16 -0
- remote_cmd_manager-2.5.0/remote_cmd/service/_pool_policy.py → remote_cmd_manager-2.6.0/remote_cmd/core/pool_policy.py +5 -1
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/core/sync_connection_pool.py +22 -4
- remote_cmd_manager-2.6.0/remote_cmd/service/_pool_policy.py +16 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/service/async_batch_executor.py +38 -9
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/service/batch_executor.py +27 -7
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/service/retry_policy.py +1 -1
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/utils/exceptions.py +21 -3
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0/remote_cmd_manager.egg-info}/PKG-INFO +17 -20
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd_manager.egg-info/SOURCES.txt +5 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_async_batch_executor.py +4 -2
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_batch_executor.py +6 -3
- remote_cmd_manager-2.6.0/tests/test_connection_budget.py +410 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_host_manager.py +14 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_pool_policy.py +10 -1
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/LICENSE +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/MANIFEST.in +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/config.example.yaml +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/examples/basic_usage.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/examples/deploy_script.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/examples/nginx_batch_update.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/examples/system_health_check.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/pyproject.toml +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/cli/__init__.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/cli/main.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/core/__init__.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/core/async_ssh_client.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/core/host.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/core/ssh_client.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/repository/__init__.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/repository/host_repository.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/repository/json_host_repository.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/repository/sqlite_host_repository.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/service/__init__.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/service/_host_runner.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/service/_types.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/service/credential_provider.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/service/host_service.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/service/ssh_service.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/service/storage_factory.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/service/task_runner.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/utils/__init__.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/utils/config.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/utils/credential_guard.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/utils/crypto.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/utils/logging_utils.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd_manager.egg-info/dependency_links.txt +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd_manager.egg-info/entry_points.txt +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd_manager.egg-info/requires.txt +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd_manager.egg-info/top_level.txt +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/requirements.txt +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/setup.cfg +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/__init__.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/conftest.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/integration/conftest.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/integration/test_ssh_connection.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/performance/__init__.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/performance/conftest.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/performance/test_benchmarks.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_async_ssh_client.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_cli.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_config.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_credential_provider.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_crypto.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_exceptions.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_host.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_host_service.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_keyring_provider.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_logging_utils.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_output_policy.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_release_metadata.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_repository.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_retry_policy.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_sqlite_repository.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_ssh_client.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_ssh_service.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_storage_factory.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_sync_connection_pool.py +0 -0
- {remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/tests/test_task_runner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remote_cmd_manager
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Summary: Cross-platform SSH remote server management tool (Linux/macOS/Windows) — lightweight, zero-configuration CLI and Python API for managing servers over SSH. Supports host CRUD, batch commands, file transfer, credential encryption, async execution, and more.
|
|
5
5
|
Author-email: Vae-Scrooge <vaescrooge@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -89,36 +89,32 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
|
|
|
89
89
|
|
|
90
90
|
---
|
|
91
91
|
|
|
92
|
-
## v2.
|
|
92
|
+
## v2.6.0 Release Highlights
|
|
93
93
|
|
|
94
|
-
v2.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
[full migration notes](./CHANGELOG.md)
|
|
94
|
+
v2.6.0 is an architecture and runtime-scalability release: the compatibility facade
|
|
95
|
+
moved out of `core` (eliminating the `core → service` dependency) and a process-wide
|
|
96
|
+
live-connection budget was added for fleet-scale resource control. Public APIs remain
|
|
97
|
+
backward compatible. See the [full migration notes](./CHANGELOG.md).
|
|
98
98
|
|
|
99
|
-
###
|
|
99
|
+
### Architecture
|
|
100
100
|
|
|
101
|
-
-
|
|
102
|
-
-
|
|
101
|
+
- `HostManager` now lives in `remote_cmd.api.host_manager`; `remote_cmd.core.host_manager` remains a re-export shim, so existing imports keep working.
|
|
102
|
+
- Pool policy helpers moved to `remote_cmd.core.pool_policy` (shim kept at the old path); `core` no longer imports `service`.
|
|
103
|
+
- The async kernel's worker queue now converts unexpected per-host errors into failure results, so a single internal error can no longer stall an entire batch.
|
|
103
104
|
|
|
104
|
-
###
|
|
105
|
+
### Global Connection Budget
|
|
105
106
|
|
|
106
|
-
- `
|
|
107
|
-
-
|
|
108
|
-
|
|
109
|
-
### Credential Safety
|
|
110
|
-
|
|
111
|
-
- `JsonHostRepository` / `SqliteHostRepository` accept `allow_plaintext_credentials` (default `None`): plaintext persistence now emits `PlaintextCredentialWarning`; pass `True` to opt in silently or `False` to reject it with `CredentialError`. v3.0 plans to reject by default.
|
|
112
|
-
- JSON repository concurrency semantics are now explicit: single-process/single-writer; use SQLite for multi-process writers.
|
|
113
|
-
- The `dev` extra now includes `asyncssh`, so `pip install -e ".[dev]"` runs the full test suite out of the box.
|
|
107
|
+
- New `ConnectionBudget` (exported from `remote_cmd`): caps **live** SSH connections process-wide across pools and executors — idle pooled connections count, and slots release on close/cleanup/`close_all`.
|
|
108
|
+
- One instance can be shared by both kernels via `connection_budget=` on `BatchExecutor`, `AsyncBatchExecutor`, `SyncConnectionPool`, and `AsyncConnectionPool`.
|
|
109
|
+
- Optional `acquire_timeout` raises the transient `BudgetTimeoutError`; the default (`None`) keeps unlimited behavior.
|
|
114
110
|
|
|
115
111
|
### Compatibility
|
|
116
112
|
|
|
117
|
-
-
|
|
113
|
+
- All existing imports and constructor signatures keep working; new parameters are optional and default to previous behavior.
|
|
118
114
|
|
|
119
115
|
## Table of Contents
|
|
120
116
|
|
|
121
|
-
- [v2.
|
|
117
|
+
- [v2.6.0 Release Highlights](#v260-release-highlights)
|
|
122
118
|
- [Why Remote CMD?](#why-remote-cmd)
|
|
123
119
|
- [Quick Start](#quick-start)
|
|
124
120
|
- [Use Cases](#use-cases)
|
|
@@ -268,6 +264,7 @@ with SSHClient(config) as client:
|
|
|
268
264
|
| **Tag System** | Filter hosts by tag (e.g., `production`, `web`, `db`) |
|
|
269
265
|
| **Batch Ops** | Run commands across any host group, synchronously or asynchronously; optional per-host retained-output cap (`max_output_bytes`) |
|
|
270
266
|
| **Async Kernel** | `AsyncSSHClient` / `AsyncConnectionPool` / `AsyncBatchExecutor` via the `[async]` extra |
|
|
267
|
+
| **Global Connection Budget** | Optional `ConnectionBudget` caps live SSH connections process-wide across pools and executors |
|
|
271
268
|
| **Task Runner** | Track and schedule long-running remote tasks with statuses (`TaskRunner`) |
|
|
272
269
|
| **Connection Test** | Test all host SSH connections and report status |
|
|
273
270
|
| **Secure Logging** | Structured logging that filters sensitive data (`SensitiveDataFilter`) |
|
|
@@ -36,36 +36,32 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
|
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
39
|
-
## v2.
|
|
39
|
+
## v2.6.0 Release Highlights
|
|
40
40
|
|
|
41
|
-
v2.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
[full migration notes](./CHANGELOG.md)
|
|
41
|
+
v2.6.0 is an architecture and runtime-scalability release: the compatibility facade
|
|
42
|
+
moved out of `core` (eliminating the `core → service` dependency) and a process-wide
|
|
43
|
+
live-connection budget was added for fleet-scale resource control. Public APIs remain
|
|
44
|
+
backward compatible. See the [full migration notes](./CHANGELOG.md).
|
|
45
45
|
|
|
46
|
-
###
|
|
46
|
+
### Architecture
|
|
47
47
|
|
|
48
|
-
-
|
|
49
|
-
-
|
|
48
|
+
- `HostManager` now lives in `remote_cmd.api.host_manager`; `remote_cmd.core.host_manager` remains a re-export shim, so existing imports keep working.
|
|
49
|
+
- Pool policy helpers moved to `remote_cmd.core.pool_policy` (shim kept at the old path); `core` no longer imports `service`.
|
|
50
|
+
- The async kernel's worker queue now converts unexpected per-host errors into failure results, so a single internal error can no longer stall an entire batch.
|
|
50
51
|
|
|
51
|
-
###
|
|
52
|
+
### Global Connection Budget
|
|
52
53
|
|
|
53
|
-
- `
|
|
54
|
-
-
|
|
55
|
-
|
|
56
|
-
### Credential Safety
|
|
57
|
-
|
|
58
|
-
- `JsonHostRepository` / `SqliteHostRepository` accept `allow_plaintext_credentials` (default `None`): plaintext persistence now emits `PlaintextCredentialWarning`; pass `True` to opt in silently or `False` to reject it with `CredentialError`. v3.0 plans to reject by default.
|
|
59
|
-
- JSON repository concurrency semantics are now explicit: single-process/single-writer; use SQLite for multi-process writers.
|
|
60
|
-
- The `dev` extra now includes `asyncssh`, so `pip install -e ".[dev]"` runs the full test suite out of the box.
|
|
54
|
+
- New `ConnectionBudget` (exported from `remote_cmd`): caps **live** SSH connections process-wide across pools and executors — idle pooled connections count, and slots release on close/cleanup/`close_all`.
|
|
55
|
+
- One instance can be shared by both kernels via `connection_budget=` on `BatchExecutor`, `AsyncBatchExecutor`, `SyncConnectionPool`, and `AsyncConnectionPool`.
|
|
56
|
+
- Optional `acquire_timeout` raises the transient `BudgetTimeoutError`; the default (`None`) keeps unlimited behavior.
|
|
61
57
|
|
|
62
58
|
### Compatibility
|
|
63
59
|
|
|
64
|
-
-
|
|
60
|
+
- All existing imports and constructor signatures keep working; new parameters are optional and default to previous behavior.
|
|
65
61
|
|
|
66
62
|
## Table of Contents
|
|
67
63
|
|
|
68
|
-
- [v2.
|
|
64
|
+
- [v2.6.0 Release Highlights](#v260-release-highlights)
|
|
69
65
|
- [Why Remote CMD?](#why-remote-cmd)
|
|
70
66
|
- [Quick Start](#quick-start)
|
|
71
67
|
- [Use Cases](#use-cases)
|
|
@@ -215,6 +211,7 @@ with SSHClient(config) as client:
|
|
|
215
211
|
| **Tag System** | Filter hosts by tag (e.g., `production`, `web`, `db`) |
|
|
216
212
|
| **Batch Ops** | Run commands across any host group, synchronously or asynchronously; optional per-host retained-output cap (`max_output_bytes`) |
|
|
217
213
|
| **Async Kernel** | `AsyncSSHClient` / `AsyncConnectionPool` / `AsyncBatchExecutor` via the `[async]` extra |
|
|
214
|
+
| **Global Connection Budget** | Optional `ConnectionBudget` caps live SSH connections process-wide across pools and executors |
|
|
218
215
|
| **Task Runner** | Track and schedule long-running remote tasks with statuses (`TaskRunner`) |
|
|
219
216
|
| **Connection Test** | Test all host SSH connections and report status |
|
|
220
217
|
| **Secure Logging** | Structured logging that filters sensitive data (`SensitiveDataFilter`) |
|
|
@@ -48,7 +48,7 @@ Remote CMD - SSH 远程服务器管理工具
|
|
|
48
48
|
- 文档: 参见 docs/ 目录
|
|
49
49
|
|
|
50
50
|
Author: Vae-Scrooge
|
|
51
|
-
Version: 2.
|
|
51
|
+
Version: 2.6.0(单一真相源见 remote_cmd._version)
|
|
52
52
|
License: MIT
|
|
53
53
|
"""
|
|
54
54
|
|
|
@@ -74,8 +74,9 @@ try:
|
|
|
74
74
|
except ImportError: # pragma: no cover - 依赖 asyncssh,未安装时不导出异步符号
|
|
75
75
|
_HAS_ASYNC = False
|
|
76
76
|
|
|
77
|
+
from remote_cmd.api.host_manager import HostManager
|
|
78
|
+
from remote_cmd.core.budget import ConnectionBudget
|
|
77
79
|
from remote_cmd.core.host import Host
|
|
78
|
-
from remote_cmd.core.host_manager import HostManager
|
|
79
80
|
from remote_cmd.core.ssh_client import SSHClient
|
|
80
81
|
from remote_cmd.core.sync_connection_pool import SyncConnectionPool
|
|
81
82
|
|
|
@@ -105,7 +106,7 @@ from remote_cmd.service.batch_executor import (
|
|
|
105
106
|
from remote_cmd.service.credential_provider import KeyringCredentialProvider
|
|
106
107
|
from remote_cmd.service.task_runner import Task, TaskRunner, TaskStatus
|
|
107
108
|
from remote_cmd.utils.crypto import CredentialEncryption
|
|
108
|
-
from remote_cmd.utils.exceptions import PlaintextCredentialWarning
|
|
109
|
+
from remote_cmd.utils.exceptions import BudgetTimeoutError, PlaintextCredentialWarning
|
|
109
110
|
from remote_cmd.utils.logging_utils import (
|
|
110
111
|
SensitiveDataFilter,
|
|
111
112
|
get_logger,
|
|
@@ -142,6 +143,8 @@ if _HAS_ASYNC:
|
|
|
142
143
|
"BatchHostResult",
|
|
143
144
|
"OutputPolicy",
|
|
144
145
|
"PlaintextCredentialWarning",
|
|
146
|
+
"ConnectionBudget",
|
|
147
|
+
"BudgetTimeoutError",
|
|
145
148
|
"SyncConnectionPool",
|
|
146
149
|
"TaskRunner",
|
|
147
150
|
"Task",
|
|
@@ -177,6 +180,8 @@ else:
|
|
|
177
180
|
"BatchHostResult",
|
|
178
181
|
"OutputPolicy",
|
|
179
182
|
"PlaintextCredentialWarning",
|
|
183
|
+
"ConnectionBudget",
|
|
184
|
+
"BudgetTimeoutError",
|
|
180
185
|
"SyncConnectionPool",
|
|
181
186
|
"TaskRunner",
|
|
182
187
|
"Task",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API facade 包(向后兼容层)
|
|
3
|
+
|
|
4
|
+
存放从 ``core`` 迁出的公共兼容 facade,使核心层(``core``)保持对
|
|
5
|
+
``service`` 的零依赖。当前包含:
|
|
6
|
+
|
|
7
|
+
- :class:`remote_cmd.api.host_manager.HostManager`:v1.x 的 HostManager
|
|
8
|
+
API,内部委托给 HostService + JsonHostRepository。
|
|
9
|
+
|
|
10
|
+
旧导入路径 ``remote_cmd.core.host_manager`` 仍然可用(re-export shim),
|
|
11
|
+
但新代码应使用 ``remote_cmd.api.host_manager`` 或直接使用 HostService。
|
|
12
|
+
|
|
13
|
+
依赖方向(v2.6 契约)::
|
|
14
|
+
|
|
15
|
+
cli / api → service → core → utils
|
|
16
|
+
repository ← service / cli
|
|
17
|
+
core 绝不 import service / cli / api 的实现
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from remote_cmd.api.host_manager import HostManager
|
|
21
|
+
|
|
22
|
+
__all__ = ["HostManager"]
|
{remote_cmd_manager-2.5.0/remote_cmd/core → remote_cmd_manager-2.6.0/remote_cmd/api}/host_manager.py
RENAMED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
主机管理模块(向后兼容 facade,canonical 位置)
|
|
3
3
|
|
|
4
|
-
保持原有 API 兼容,内部委托给新的 Repository + Service 层。
|
|
4
|
+
保持原有 HostManager API 兼容,内部委托给新的 Repository + Service 层。
|
|
5
5
|
|
|
6
6
|
新的代码应直接使用:
|
|
7
7
|
- remote_cmd.core.host.Host (代替 Host)
|
|
8
8
|
- remote_cmd.service.host_service.HostService (代替 HostManager)
|
|
9
9
|
- remote_cmd.repository.json_host_repository.JsonHostRepository
|
|
10
|
+
- remote_cmd.api.host_manager.HostManager(本模块;仅在需要旧 API 时)
|
|
11
|
+
|
|
12
|
+
v2.6 架构说明:
|
|
13
|
+
本模块自 ``remote_cmd.core.host_manager`` 迁至 ``remote_cmd.api``,
|
|
14
|
+
使 ``core`` 层不再依赖 ``service`` 层。旧导入路径由
|
|
15
|
+
``remote_cmd.core.host_manager`` 的 re-export shim 保持可用。
|
|
10
16
|
"""
|
|
11
17
|
|
|
12
18
|
import logging
|
{remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/core/async_connection_pool.py
RENAMED
|
@@ -19,13 +19,14 @@ import uuid
|
|
|
19
19
|
from typing import Any, Optional
|
|
20
20
|
|
|
21
21
|
from remote_cmd.core.async_ssh_client import AsyncSSHClient
|
|
22
|
-
from remote_cmd.core.
|
|
23
|
-
from remote_cmd.
|
|
22
|
+
from remote_cmd.core.budget import ConnectionBudget
|
|
23
|
+
from remote_cmd.core.pool_policy import (
|
|
24
24
|
ConnectionMeta,
|
|
25
25
|
idle_expired,
|
|
26
26
|
lifetime_expired,
|
|
27
27
|
should_close,
|
|
28
28
|
)
|
|
29
|
+
from remote_cmd.core.ssh_client import ConnectionConfig
|
|
29
30
|
from remote_cmd.utils.exceptions import PoolClosedError
|
|
30
31
|
|
|
31
32
|
logger = logging.getLogger(__name__)
|
|
@@ -40,6 +41,9 @@ class AsyncConnectionPool:
|
|
|
40
41
|
max_lifetime: 连接最大生命周期(秒),超过自动关闭
|
|
41
42
|
idle_timeout: 空闲超时(秒),超过自动关闭
|
|
42
43
|
health_check_interval: 后台清理任务周期(秒)
|
|
44
|
+
connection_budget: 可选的全局连接预算(v2.6)。提供时本池创建的
|
|
45
|
+
每条存活连接都占用一个预算槽位(含空闲连接),连接关闭/清理/
|
|
46
|
+
close_all 时释放;跨多个池共享同一预算实例可获得全局连接上限
|
|
43
47
|
"""
|
|
44
48
|
|
|
45
49
|
def __init__(
|
|
@@ -50,6 +54,7 @@ class AsyncConnectionPool:
|
|
|
50
54
|
idle_timeout: int = 300,
|
|
51
55
|
health_check_interval: int = 60,
|
|
52
56
|
client_factory: Optional[Any] = None,
|
|
57
|
+
connection_budget: Optional[ConnectionBudget] = None,
|
|
53
58
|
) -> None:
|
|
54
59
|
"""
|
|
55
60
|
Args:
|
|
@@ -60,6 +65,7 @@ class AsyncConnectionPool:
|
|
|
60
65
|
health_check_interval: 后台清理任务周期(秒)
|
|
61
66
|
client_factory: 客户端工厂,默认为 AsyncSSHClient;测试可注入
|
|
62
67
|
mock(与 SyncConnectionPool 对齐)
|
|
68
|
+
connection_budget: 可选的全局连接预算(ConnectionBudget)
|
|
63
69
|
"""
|
|
64
70
|
self.config = config
|
|
65
71
|
self._max = max_connections
|
|
@@ -68,6 +74,7 @@ class AsyncConnectionPool:
|
|
|
68
74
|
self._health_check_interval = health_check_interval
|
|
69
75
|
# 客户端工厂:默认为 AsyncSSHClient;测试可注入 mock
|
|
70
76
|
self._client_factory = client_factory or AsyncSSHClient
|
|
77
|
+
self._connection_budget = connection_budget
|
|
71
78
|
|
|
72
79
|
# 容器
|
|
73
80
|
self._connections: list[AsyncSSHClient] = []
|
|
@@ -190,10 +197,16 @@ class AsyncConnectionPool:
|
|
|
190
197
|
# ------------------------------------------------------------------
|
|
191
198
|
async def _create_connection(self) -> AsyncSSHClient:
|
|
192
199
|
client = self._client_factory(self.config)
|
|
200
|
+
if self._connection_budget is not None:
|
|
201
|
+
await self._connection_budget.acquire_async()
|
|
193
202
|
try:
|
|
194
203
|
await client.connect()
|
|
195
204
|
except Exception: # noqa: BLE001
|
|
196
|
-
# 信号量由 acquire() 的 except
|
|
205
|
+
# 信号量由 acquire() 的 except 统一释放,此处不再释放。
|
|
206
|
+
# 连接预算仅在成功建连后由 _close_connection 释放;
|
|
207
|
+
# 建连失败立即归还预算,避免预算泄漏
|
|
208
|
+
if self._connection_budget is not None:
|
|
209
|
+
await self._connection_budget.release_async()
|
|
197
210
|
self._total_failed += 1
|
|
198
211
|
raise
|
|
199
212
|
self._connections.append(client)
|
|
@@ -237,8 +250,14 @@ class AsyncConnectionPool:
|
|
|
237
250
|
with contextlib.suppress(Exception):
|
|
238
251
|
await conn.disconnect()
|
|
239
252
|
self._meta.pop(id(conn), None)
|
|
240
|
-
|
|
253
|
+
tracked = conn in self._connections
|
|
254
|
+
if tracked:
|
|
241
255
|
self._connections.remove(conn)
|
|
256
|
+
# exactly-once:仅对仍被池追踪的连接释放预算。
|
|
257
|
+
# close_all 与 release 可能对同一连接重复调用 _close_connection,
|
|
258
|
+
# 用 tracked 守卫避免预算被超额释放(BoundedSemaphore 会抛 ValueError)
|
|
259
|
+
if tracked and self._connection_budget is not None:
|
|
260
|
+
await self._connection_budget.release_async()
|
|
242
261
|
|
|
243
262
|
# ------------------------------------------------------------------
|
|
244
263
|
# 后台监控
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""
|
|
2
|
+
全局连接预算(v2.6 P2.5;同步 / 异步内核共用)
|
|
3
|
+
|
|
4
|
+
用于在**多个连接池 / 多个执行器**之间封顶进程内同时存活的 SSH 连接数,
|
|
5
|
+
避免大规模 fleet 场景下 per-pool 上限叠加导致的 fd/socket 耗尽:
|
|
6
|
+
|
|
7
|
+
池 A(max=10) + 池 B(max=10) + … 无预算时可无限叠加
|
|
8
|
+
预算(max=50) 全局硬上限 50 条存活连接
|
|
9
|
+
|
|
10
|
+
预算语义(评审定稿:存活连接预算):
|
|
11
|
+
- 预算单位 = **存活连接**(含池中空闲连接)。
|
|
12
|
+
- 获取时机:连接创建(``connect``)前;释放时机:连接断开 / 关闭 /
|
|
13
|
+
被清理 / 池 ``close_all``。
|
|
14
|
+
- 池归还空闲连接**不**释放预算(连接仍存活);``acquire`` 复用空闲连接
|
|
15
|
+
**不**重复获取预算。
|
|
16
|
+
|
|
17
|
+
双接口:
|
|
18
|
+
- 同步::meth:`acquire` / :meth:`release` / :meth:`acquire_context`
|
|
19
|
+
- 异步::meth:`acquire_async` / :meth:`release_async` /
|
|
20
|
+
:meth:`acquire_context_async`
|
|
21
|
+
|
|
22
|
+
同一实例可被同步与异步执行器共享(底层是单个
|
|
23
|
+
``threading.BoundedSemaphore``;异步侧以非阻塞轮询等待,轮询间隔
|
|
24
|
+
10ms 起步、指数退避至 50ms——相对于 SSH 建连耗时(数十到数百 ms)
|
|
25
|
+
开销可忽略)。
|
|
26
|
+
|
|
27
|
+
超时:``acquire_timeout=None``(默认)无限等待;配置正整数/浮点秒数时
|
|
28
|
+
超时抛出 :class:`~remote_cmd.utils.exceptions.BudgetTimeoutError`。
|
|
29
|
+
|
|
30
|
+
用法:
|
|
31
|
+
>>> from remote_cmd.core.budget import ConnectionBudget
|
|
32
|
+
>>> budget = ConnectionBudget(max_connections=50)
|
|
33
|
+
>>> with budget.acquire_context():
|
|
34
|
+
... pass # 持有预算期间创建/保有连接
|
|
35
|
+
>>> budget.get_metrics()["in_use"]
|
|
36
|
+
0
|
|
37
|
+
>>>
|
|
38
|
+
>>> async def use():
|
|
39
|
+
... async with budget.acquire_context_async():
|
|
40
|
+
... pass
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
import asyncio
|
|
44
|
+
import contextlib
|
|
45
|
+
import threading
|
|
46
|
+
import time
|
|
47
|
+
from typing import Any, AsyncIterator, Iterator, Optional
|
|
48
|
+
|
|
49
|
+
from remote_cmd.utils.exceptions import BudgetTimeoutError, ValidationError
|
|
50
|
+
|
|
51
|
+
# 异步轮询等待参数(秒):起步 10ms,指数退避至 50ms 上限
|
|
52
|
+
_DEFAULT_POLL_INTERVAL = 0.01
|
|
53
|
+
_MAX_POLL_INTERVAL = 0.05
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ConnectionBudget:
|
|
57
|
+
"""全局连接预算(同步 / 异步共用)。
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
max_connections: 全局最大存活连接数(正整数;bool 被拒绝)
|
|
61
|
+
acquire_timeout: 获取预算的等待上限(秒)。``None``(默认)
|
|
62
|
+
表示无限等待;正数表示超时后抛 ``BudgetTimeoutError``
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValidationError: 参数非法(构造时校验)
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
max_connections: int,
|
|
71
|
+
acquire_timeout: Optional[float] = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
if (
|
|
74
|
+
isinstance(max_connections, bool)
|
|
75
|
+
or not isinstance(max_connections, int)
|
|
76
|
+
or max_connections <= 0
|
|
77
|
+
):
|
|
78
|
+
raise ValidationError(
|
|
79
|
+
f"max_connections must be a positive integer, got: {max_connections!r}"
|
|
80
|
+
)
|
|
81
|
+
if acquire_timeout is not None and (
|
|
82
|
+
isinstance(acquire_timeout, bool)
|
|
83
|
+
or not isinstance(acquire_timeout, (int, float))
|
|
84
|
+
or acquire_timeout <= 0
|
|
85
|
+
):
|
|
86
|
+
raise ValidationError(
|
|
87
|
+
f"acquire_timeout must be None or a positive number, got: {acquire_timeout!r}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
self._max = max_connections
|
|
91
|
+
self._acquire_timeout = acquire_timeout
|
|
92
|
+
# 单一底层信号量:同步/异步共享同一个全局上限
|
|
93
|
+
self._semaphore = threading.BoundedSemaphore(max_connections)
|
|
94
|
+
self._lock = threading.Lock()
|
|
95
|
+
|
|
96
|
+
# 指标
|
|
97
|
+
self._in_use = 0
|
|
98
|
+
self._total_acquired = 0
|
|
99
|
+
self._total_released = 0
|
|
100
|
+
self._total_timeouts = 0
|
|
101
|
+
|
|
102
|
+
# ------------------------------------------------------------------
|
|
103
|
+
# 属性 / 指标
|
|
104
|
+
# ------------------------------------------------------------------
|
|
105
|
+
@property
|
|
106
|
+
def max_connections(self) -> int:
|
|
107
|
+
"""全局最大存活连接数。"""
|
|
108
|
+
return self._max
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def acquire_timeout(self) -> Optional[float]:
|
|
112
|
+
"""获取预算的等待上限(秒);``None`` 表示无限等待。"""
|
|
113
|
+
return self._acquire_timeout
|
|
114
|
+
|
|
115
|
+
def get_metrics(self) -> dict[str, Any]:
|
|
116
|
+
"""预算指标快照。"""
|
|
117
|
+
with self._lock:
|
|
118
|
+
return {
|
|
119
|
+
"max_connections": self._max,
|
|
120
|
+
"in_use": self._in_use,
|
|
121
|
+
"available": self._max - self._in_use,
|
|
122
|
+
"total_acquired": self._total_acquired,
|
|
123
|
+
"total_released": self._total_released,
|
|
124
|
+
"total_timeouts": self._total_timeouts,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# ------------------------------------------------------------------
|
|
128
|
+
# 同步接口
|
|
129
|
+
# ------------------------------------------------------------------
|
|
130
|
+
def acquire(self) -> None:
|
|
131
|
+
"""获取一个连接预算槽位(阻塞,受 ``acquire_timeout`` 约束)。
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
BudgetTimeoutError: 等待超过 ``acquire_timeout``
|
|
135
|
+
"""
|
|
136
|
+
acquired = self._semaphore.acquire(timeout=self._acquire_timeout)
|
|
137
|
+
if not acquired:
|
|
138
|
+
with self._lock:
|
|
139
|
+
self._total_timeouts += 1
|
|
140
|
+
raise BudgetTimeoutError(
|
|
141
|
+
"connection budget exhausted: "
|
|
142
|
+
f"waited {self._acquire_timeout}s for a slot "
|
|
143
|
+
f"(max_connections={self._max})"
|
|
144
|
+
)
|
|
145
|
+
with self._lock:
|
|
146
|
+
self._in_use += 1
|
|
147
|
+
self._total_acquired += 1
|
|
148
|
+
|
|
149
|
+
def release(self) -> None:
|
|
150
|
+
"""释放一个连接预算槽位。
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
ValueError: 释放次数超过获取次数(BoundedSemaphore 保护)
|
|
154
|
+
"""
|
|
155
|
+
# 先释放信号量:超额释放时 BoundedSemaphore 抛出 ValueError,
|
|
156
|
+
# 指标不会进入不一致状态
|
|
157
|
+
self._semaphore.release()
|
|
158
|
+
with self._lock:
|
|
159
|
+
self._in_use -= 1
|
|
160
|
+
self._total_released += 1
|
|
161
|
+
|
|
162
|
+
@contextlib.contextmanager
|
|
163
|
+
def acquire_context(self) -> Iterator[None]:
|
|
164
|
+
"""同步上下文管理器:进入获取预算,退出释放。"""
|
|
165
|
+
self.acquire()
|
|
166
|
+
try:
|
|
167
|
+
yield
|
|
168
|
+
finally:
|
|
169
|
+
self.release()
|
|
170
|
+
|
|
171
|
+
# ------------------------------------------------------------------
|
|
172
|
+
# 异步接口
|
|
173
|
+
# ------------------------------------------------------------------
|
|
174
|
+
async def acquire_async(self) -> None:
|
|
175
|
+
"""获取一个连接预算槽位(asyncio 友好,受 ``acquire_timeout`` 约束)。
|
|
176
|
+
|
|
177
|
+
以非阻塞尝试 + 指数退避轮询等待,不阻塞事件循环。
|
|
178
|
+
"""
|
|
179
|
+
timeout = self._acquire_timeout
|
|
180
|
+
deadline = None if timeout is None else time.monotonic() + timeout
|
|
181
|
+
interval = _DEFAULT_POLL_INTERVAL
|
|
182
|
+
while not self._semaphore.acquire(blocking=False):
|
|
183
|
+
if deadline is not None and time.monotonic() >= deadline:
|
|
184
|
+
with self._lock:
|
|
185
|
+
self._total_timeouts += 1
|
|
186
|
+
raise BudgetTimeoutError(
|
|
187
|
+
"connection budget exhausted: "
|
|
188
|
+
f"waited {timeout}s for a slot "
|
|
189
|
+
f"(max_connections={self._max})"
|
|
190
|
+
)
|
|
191
|
+
await asyncio.sleep(interval)
|
|
192
|
+
interval = min(interval * 2, _MAX_POLL_INTERVAL)
|
|
193
|
+
with self._lock:
|
|
194
|
+
self._in_use += 1
|
|
195
|
+
self._total_acquired += 1
|
|
196
|
+
|
|
197
|
+
async def release_async(self) -> None:
|
|
198
|
+
"""释放一个连接预算槽位(异步接口;与 :meth:`release` 等价)。"""
|
|
199
|
+
self.release()
|
|
200
|
+
|
|
201
|
+
@contextlib.asynccontextmanager
|
|
202
|
+
async def acquire_context_async(self) -> AsyncIterator[None]:
|
|
203
|
+
"""异步上下文管理器:进入获取预算,退出释放。"""
|
|
204
|
+
await self.acquire_async()
|
|
205
|
+
try:
|
|
206
|
+
yield
|
|
207
|
+
finally:
|
|
208
|
+
await self.release_async()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
__all__ = ["ConnectionBudget"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
向后兼容 shim:HostManager 实现已迁移到 ``remote_cmd.api.host_manager``。
|
|
3
|
+
|
|
4
|
+
v2.6 起 ``HostManager`` 的 canonical 位置是 ``remote_cmd.api.host_manager``,
|
|
5
|
+
使 ``core`` 层不再依赖 ``service``。本模块仅为保留既有导入路径而存在:
|
|
6
|
+
|
|
7
|
+
>>> from remote_cmd.core.host_manager import HostManager # 仍可用(shim)
|
|
8
|
+
>>> from remote_cmd.api.host_manager import HostManager # canonical
|
|
9
|
+
|
|
10
|
+
``Host`` 也原样 re-export,兼容历史用法
|
|
11
|
+
``from remote_cmd.core.host_manager import Host, HostManager``。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from remote_cmd.api.host_manager import Host, HostManager
|
|
15
|
+
|
|
16
|
+
__all__ = ["Host", "HostManager"]
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
从 SyncConnectionPool 与 AsyncConnectionPool 中提取的纯时间判断逻辑:
|
|
5
5
|
不涉及任何 I/O 与并发原语,便于两个池共享并单独测试。
|
|
6
6
|
|
|
7
|
+
v2.6(P2.1)起 canonical 位置为 ``remote_cmd.core.pool_policy``:该模块
|
|
8
|
+
只被 core 连接池消费,放在 core 可保持 ``core`` 对 ``service`` 的零依赖。
|
|
9
|
+
旧导入路径 ``remote_cmd.service._pool_policy`` 仍可用(re-export shim)。
|
|
10
|
+
|
|
7
11
|
注意:探活(liveness probe,如 ``conn.execute("true")``)是真实 I/O,
|
|
8
12
|
不属于本模块职责,保留在各池的 ``_check_connection`` 内。
|
|
9
13
|
|
|
@@ -13,7 +17,7 @@
|
|
|
13
17
|
的弱类型写法。
|
|
14
18
|
|
|
15
19
|
用法:
|
|
16
|
-
>>> from remote_cmd.
|
|
20
|
+
>>> from remote_cmd.core.pool_policy import (
|
|
17
21
|
... ConnectionMeta, lifetime_expired, idle_expired, should_close,
|
|
18
22
|
... )
|
|
19
23
|
>>> meta = ConnectionMeta(created_at=1000.0, last_used=1000.0, conn_id="x")
|
{remote_cmd_manager-2.5.0 → remote_cmd_manager-2.6.0}/remote_cmd/core/sync_connection_pool.py
RENAMED
|
@@ -19,13 +19,14 @@ import time
|
|
|
19
19
|
import uuid
|
|
20
20
|
from typing import Any, Optional
|
|
21
21
|
|
|
22
|
-
from remote_cmd.core.
|
|
23
|
-
from remote_cmd.
|
|
22
|
+
from remote_cmd.core.budget import ConnectionBudget
|
|
23
|
+
from remote_cmd.core.pool_policy import (
|
|
24
24
|
ConnectionMeta,
|
|
25
25
|
idle_expired,
|
|
26
26
|
lifetime_expired,
|
|
27
27
|
should_close,
|
|
28
28
|
)
|
|
29
|
+
from remote_cmd.core.ssh_client import ConnectionConfig, SSHClient
|
|
29
30
|
from remote_cmd.utils.exceptions import PoolClosedError
|
|
30
31
|
|
|
31
32
|
logger = logging.getLogger(__name__)
|
|
@@ -40,6 +41,9 @@ class SyncConnectionPool:
|
|
|
40
41
|
max_lifetime: 连接最大生命周期(秒),超过自动关闭
|
|
41
42
|
idle_timeout: 空闲超时(秒),超过自动关闭
|
|
42
43
|
health_check_interval: 后台清理线程周期(秒)
|
|
44
|
+
connection_budget: 可选的全局连接预算(v2.6)。提供时本池创建的
|
|
45
|
+
每条存活连接都占用一个预算槽位(含空闲连接),连接关闭/清理/
|
|
46
|
+
close_all 时释放;跨多个池共享同一预算实例可获得全局连接上限
|
|
43
47
|
"""
|
|
44
48
|
|
|
45
49
|
def __init__(
|
|
@@ -50,6 +54,7 @@ class SyncConnectionPool:
|
|
|
50
54
|
idle_timeout: int = 300,
|
|
51
55
|
health_check_interval: int = 60,
|
|
52
56
|
client_factory: Optional[Any] = None,
|
|
57
|
+
connection_budget: Optional[ConnectionBudget] = None,
|
|
53
58
|
) -> None:
|
|
54
59
|
self.config = config
|
|
55
60
|
self._max = max_connections
|
|
@@ -58,6 +63,7 @@ class SyncConnectionPool:
|
|
|
58
63
|
self._health_check_interval = health_check_interval
|
|
59
64
|
# 客户端工厂:默认为 SSHClient;测试可注入 mock
|
|
60
65
|
self._client_factory = client_factory or SSHClient
|
|
66
|
+
self._connection_budget = connection_budget
|
|
61
67
|
|
|
62
68
|
# 容器
|
|
63
69
|
self._connections: list[SSHClient] = []
|
|
@@ -181,10 +187,16 @@ class SyncConnectionPool:
|
|
|
181
187
|
# ------------------------------------------------------------------
|
|
182
188
|
def _create_connection(self) -> SSHClient:
|
|
183
189
|
client = self._client_factory(self.config)
|
|
190
|
+
if self._connection_budget is not None:
|
|
191
|
+
self._connection_budget.acquire()
|
|
184
192
|
try:
|
|
185
193
|
client.connect()
|
|
186
194
|
except Exception: # noqa: BLE001
|
|
187
|
-
# 信号量由 acquire() 的 except
|
|
195
|
+
# 信号量由 acquire() 的 except 统一释放,此处不再释放。
|
|
196
|
+
# 连接预算仅在成功建连后由 _close_connection 释放;
|
|
197
|
+
# 建连失败立即归还预算,避免预算泄漏
|
|
198
|
+
if self._connection_budget is not None:
|
|
199
|
+
self._connection_budget.release()
|
|
188
200
|
self._total_failed += 1
|
|
189
201
|
raise
|
|
190
202
|
with self._lock:
|
|
@@ -229,8 +241,14 @@ class SyncConnectionPool:
|
|
|
229
241
|
conn.disconnect()
|
|
230
242
|
self._meta.pop(id(conn), None)
|
|
231
243
|
with self._lock:
|
|
232
|
-
|
|
244
|
+
tracked = conn in self._connections
|
|
245
|
+
if tracked:
|
|
233
246
|
self._connections.remove(conn)
|
|
247
|
+
# exactly-once:仅对仍被池追踪的连接释放预算。
|
|
248
|
+
# close_all 与 release 可能对同一连接重复调用 _close_connection,
|
|
249
|
+
# 用 tracked 守卫避免预算被超额释放(BoundedSemaphore 会抛 ValueError)
|
|
250
|
+
if tracked and self._connection_budget is not None:
|
|
251
|
+
self._connection_budget.release()
|
|
234
252
|
|
|
235
253
|
# ------------------------------------------------------------------
|
|
236
254
|
# 后台监控
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
向后兼容 shim:连接池策略已迁移到 ``remote_cmd.core.pool_policy``。
|
|
3
|
+
|
|
4
|
+
v2.6(P2.1)起该模块只被 core 连接池消费,canonical 位置为
|
|
5
|
+
``remote_cmd.core.pool_policy``,以保持 ``core`` 对 ``service`` 的零依赖。
|
|
6
|
+
本模块仅为保留既有导入路径(含测试)而存在。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from remote_cmd.core.pool_policy import ( # noqa: F401
|
|
10
|
+
ConnectionMeta,
|
|
11
|
+
idle_expired,
|
|
12
|
+
lifetime_expired,
|
|
13
|
+
should_close,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = ["ConnectionMeta", "idle_expired", "lifetime_expired", "should_close"]
|