polaris-python 0.0.1.dev3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. polaris/__init__.py +74 -0
  2. polaris/_version.py +16 -0
  3. polaris/api/__init__.py +55 -0
  4. polaris/api/_log_file.py +232 -0
  5. polaris/api/config.py +145 -0
  6. polaris/api/consumer.py +240 -0
  7. polaris/api/log.py +414 -0
  8. polaris/api/lossless.py +73 -0
  9. polaris/api/provider.py +152 -0
  10. polaris/api/skill.py +162 -0
  11. polaris/pkg/__init__.py +15 -0
  12. polaris/pkg/common/__init__.py +15 -0
  13. polaris/pkg/common/plugin.py +33 -0
  14. polaris/pkg/config/__init__.py +15 -0
  15. polaris/pkg/config/api.py +1205 -0
  16. polaris/pkg/config/default.py +92 -0
  17. polaris/pkg/config/event.py +76 -0
  18. polaris/pkg/config/loader.py +392 -0
  19. polaris/pkg/config/stat_reporter.py +100 -0
  20. polaris/pkg/flow/__init__.py +15 -0
  21. polaris/pkg/flow/admin_server.py +104 -0
  22. polaris/pkg/flow/async_flow.py +55 -0
  23. polaris/pkg/flow/base_flow.py +111 -0
  24. polaris/pkg/flow/impl.py +1277 -0
  25. polaris/pkg/flow/lossless.py +605 -0
  26. polaris/pkg/flow/notify.py +46 -0
  27. polaris/pkg/flow/register.py +210 -0
  28. polaris/pkg/flow/skill.py +110 -0
  29. polaris/pkg/flow/sync_flow.py +20 -0
  30. polaris/pkg/model/__init__.py +15 -0
  31. polaris/pkg/model/context.py +53 -0
  32. polaris/pkg/model/contract.py +89 -0
  33. polaris/pkg/model/core.py +51 -0
  34. polaris/pkg/model/engine.py +83 -0
  35. polaris/pkg/model/error.py +94 -0
  36. polaris/pkg/model/event.py +159 -0
  37. polaris/pkg/model/lossless.py +102 -0
  38. polaris/pkg/model/rule.py +152 -0
  39. polaris/pkg/model/server.py +288 -0
  40. polaris/pkg/model/service.py +820 -0
  41. polaris/pkg/model/skill.py +179 -0
  42. polaris/pkg/model/stat.py +168 -0
  43. polaris/pkg/model/util.py +58 -0
  44. polaris/pkg/version/__init__.py +18 -0
  45. polaris/plugin/__init__.py +32 -0
  46. polaris/plugin/circuit_breaker/__init__.py +19 -0
  47. polaris/plugin/circuit_breaker/base.py +54 -0
  48. polaris/plugin/circuit_breaker/composite.py +265 -0
  49. polaris/plugin/circuit_breaker/config.py +36 -0
  50. polaris/plugin/event/__init__.py +21 -0
  51. polaris/plugin/event/pushgateway.py +202 -0
  52. polaris/plugin/load_balancer/__init__.py +24 -0
  53. polaris/plugin/load_balancer/base.py +30 -0
  54. polaris/plugin/load_balancer/random.py +39 -0
  55. polaris/plugin/load_balancer/weighted_random.py +50 -0
  56. polaris/plugin/local_registry/__init__.py +18 -0
  57. polaris/plugin/local_registry/base.py +47 -0
  58. polaris/plugin/local_registry/file_cache.py +335 -0
  59. polaris/plugin/local_registry/local_cache.py +433 -0
  60. polaris/plugin/server_connector/__init__.py +20 -0
  61. polaris/plugin/server_connector/base.py +104 -0
  62. polaris/plugin/server_connector/convert.py +149 -0
  63. polaris/plugin/server_connector/grpc_connector/__init__.py +21 -0
  64. polaris/plugin/server_connector/grpc_connector/connection.py +401 -0
  65. polaris/plugin/server_connector/grpc_connector/grpc_client.py +384 -0
  66. polaris/plugin/server_connector/grpc_connector/tls.py +45 -0
  67. polaris/plugin/service_router/__init__.py +28 -0
  68. polaris/plugin/service_router/base.py +105 -0
  69. polaris/plugin/service_router/isolated.py +35 -0
  70. polaris/plugin/service_router/metadata.py +84 -0
  71. polaris/plugin/service_router/recover.py +62 -0
  72. polaris/plugin/service_router/util.py +54 -0
  73. polaris/plugin/skill/__init__.py +16 -0
  74. polaris/plugin/skill/connector.py +127 -0
  75. polaris/plugin/skill/persistence.py +94 -0
  76. polaris/plugin/stat_reporter/__init__.py +16 -0
  77. polaris/plugin/stat_reporter/base.py +34 -0
  78. polaris/plugin/stat_reporter/prometheus.py +375 -0
  79. polaris/plugin_manager.py +146 -0
  80. polaris_python-0.0.1.dev3.dist-info/METADATA +213 -0
  81. polaris_python-0.0.1.dev3.dist-info/RECORD +84 -0
  82. polaris_python-0.0.1.dev3.dist-info/WHEEL +5 -0
  83. polaris_python-0.0.1.dev3.dist-info/licenses/LICENSE +18 -0
  84. polaris_python-0.0.1.dev3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,179 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python available.
2
+ #
3
+ # Copyright (C) 2021 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software distributed
12
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
13
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations under the License.
15
+
16
+ """Public request and result models for the read-only Skill API."""
17
+
18
+ from dataclasses import dataclass, field
19
+
20
+ from polaris.pkg.model.error import ErrorCode, SDKError
21
+
22
+
23
+ @dataclass
24
+ class SkillGetRequest:
25
+ """Fetch a published skill, using the active version when version is empty."""
26
+
27
+ namespace: str
28
+ name: str
29
+ version: str = ""
30
+ timeout: int = 0
31
+
32
+ def validate(self):
33
+ """Validate the resource identity and optional timeout in milliseconds."""
34
+ if (not isinstance(self.namespace, str) or not self.namespace.strip()
35
+ or not isinstance(self.name, str) or not self.name.strip() or not isinstance(self.version, str)
36
+ or type(self.timeout) is not int or self.timeout < 0):
37
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument, "skill namespace/name are required and timeout must be >=0")
38
+
39
+
40
+ @dataclass
41
+ class SkillListRequest:
42
+ """Search published skills with filters, sorting and pagination."""
43
+
44
+ namespace: str = ""
45
+ name: str = ""
46
+ tag: str = ""
47
+ owner: str = ""
48
+ scope: str = ""
49
+ keyword: str = ""
50
+ order_by: str = ""
51
+ order_type: str = ""
52
+ offset: int = 0
53
+ limit: int = 100
54
+ timeout: int = 0
55
+
56
+ def validate(self):
57
+ """Validate pagination and optional request timeout."""
58
+ if (type(self.offset) is not int or type(self.limit) is not int or type(self.timeout) is not int
59
+ or self.offset < 0 or self.limit < 0 or self.timeout < 0):
60
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument, "skill offset, limit and timeout must be nonnegative integers")
61
+
62
+
63
+ @dataclass
64
+ class SkillDownloadRequest(SkillGetRequest):
65
+ """Download a skill as markdown or a complete ZIP archive."""
66
+
67
+ tag: str = ""
68
+ format: str = "markdown"
69
+
70
+ def validate(self):
71
+ """Validate the identity and supported download format."""
72
+ super().validate()
73
+ if self.format not in ("markdown", "zip"):
74
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument, "skill format must be markdown or zip")
75
+
76
+
77
+ @dataclass
78
+ class SkillVersionInfo:
79
+ """Version pointers published by the registry."""
80
+
81
+ active_version: str = ""
82
+ latest_version: str = ""
83
+ total_versions: int = 0
84
+
85
+
86
+ @dataclass
87
+ class SkillStorageInfo:
88
+ """Storage metadata for a skill package."""
89
+
90
+ provider: str = ""
91
+ files: list[str] = field(default_factory=list)
92
+ scope: str = ""
93
+ content_digest: str = ""
94
+
95
+
96
+ @dataclass
97
+ class SkillResource:
98
+ """Registry metadata describing a published skill."""
99
+
100
+ id: int = 0
101
+ name: str = ""
102
+ namespace: str = ""
103
+ description: str = ""
104
+ status: str = ""
105
+ tags: list[str] = field(default_factory=list)
106
+ examples: list[str] = field(default_factory=list)
107
+ ext: dict[str, str] = field(default_factory=dict)
108
+ source: str = ""
109
+ version_info: SkillVersionInfo = field(default_factory=SkillVersionInfo)
110
+ meta_version: int = 0
111
+ scope: str = ""
112
+ owner: str = ""
113
+ download_count: int = 0
114
+ create_time: str = ""
115
+ modify_time: str = ""
116
+
117
+
118
+ @dataclass
119
+ class SkillResourceVersion:
120
+ """Metadata for the resolved published version."""
121
+
122
+ id: int = 0
123
+ name: str = ""
124
+ namespace: str = ""
125
+ version: str = ""
126
+ author: str = ""
127
+ description: str = ""
128
+ status: str = ""
129
+ storage: SkillStorageInfo = field(default_factory=SkillStorageInfo)
130
+ download_count: int = 0
131
+ create_time: str = ""
132
+ modify_time: str = ""
133
+
134
+
135
+ @dataclass
136
+ class SkillGetResponse:
137
+ """A published skill and its SKILL.md content."""
138
+
139
+ resource: SkillResource
140
+ version: SkillResourceVersion
141
+ content: str
142
+ loaded_from_file: bool = False
143
+ code: int = 200000
144
+ info: str = ""
145
+
146
+ @property
147
+ def resource_version(self) -> SkillResourceVersion:
148
+ """Java-compatible name for the resolved version metadata."""
149
+ return self.version
150
+
151
+
152
+ @dataclass
153
+ class SkillListResponse:
154
+ """One page of skill search results."""
155
+
156
+ total: int
157
+ resources: list[SkillResource]
158
+ code: int = 200000
159
+ info: str = ""
160
+
161
+
162
+ @dataclass
163
+ class SkillDownloadResponse:
164
+ """A complete markdown document or ZIP package."""
165
+
166
+ version: str
167
+ content_type: str
168
+ content: str = ""
169
+ data: bytes = b""
170
+ total_size: int = 0
171
+ filename: str = ""
172
+ loaded_from_file: bool = False
173
+ code: int = 200000
174
+ info: str = ""
175
+
176
+ @property
177
+ def zip_content(self) -> bytes:
178
+ """Java-compatible name for the assembled ZIP payload."""
179
+ return self.data
@@ -0,0 +1,168 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python available.
2
+ #
3
+ # Copyright (C) 2021 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software distributed
12
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
13
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations under the License.
15
+
16
+ import time
17
+ import threading
18
+ from typing import List, Tuple
19
+
20
+
21
+ class InstanceGauge:
22
+ def get_id(self) -> str:
23
+ pass
24
+
25
+ def get_namespace(self) -> str:
26
+ pass
27
+
28
+ def get_service(self) -> str:
29
+ pass
30
+
31
+ def get_api(self) -> str:
32
+ pass
33
+
34
+ def get_host(self) -> str:
35
+ pass
36
+
37
+ def get_port(self) -> int:
38
+ pass
39
+
40
+ def get_ret_status(self) -> int:
41
+ pass
42
+
43
+ def get_ret_code(self) -> int:
44
+ pass
45
+
46
+ def get_delay(self) -> int:
47
+ pass
48
+
49
+ def validate(self) -> bool:
50
+ pass
51
+
52
+
53
+ class BucketMetric:
54
+ def __init__(self):
55
+ self.success = 0
56
+ self.total_failed = 0
57
+ self.continuous_failed = 0
58
+
59
+
60
+ class Bucket:
61
+ def __init__(self, begin_time=0, end_time=0):
62
+ self.lock = threading.RLock()
63
+ self.begin_time = begin_time
64
+ self.end_time = end_time
65
+ self.metric = BucketMetric()
66
+
67
+ @property
68
+ def success(self):
69
+ return self.metric.success
70
+
71
+ @success.setter
72
+ def success(self, val):
73
+ self.metric.success = val
74
+
75
+ @property
76
+ def total_failed(self):
77
+ return self.metric.total_failed
78
+
79
+ @total_failed.setter
80
+ def total_failed(self, val):
81
+ self.metric.total_failed = val
82
+
83
+ @property
84
+ def continuous_failed(self):
85
+ return self.metric.continuous_failed
86
+
87
+ @continuous_failed.setter
88
+ def continuous_failed(self, val):
89
+ self.metric.continuous_failed = val
90
+
91
+
92
+ class SlidingWindow:
93
+ def __init__(self, bucket_count: int, bucket_interval: int):
94
+ """
95
+
96
+ :param bucket_count: 窗口内桶总数
97
+ :param bucket_interval: 桶的时长(ms)
98
+ """
99
+ self.win_chain: List[Bucket] = []
100
+ self.bucket_count = bucket_count
101
+ self.bucket_interval = bucket_interval
102
+ self.lock = threading.RLock()
103
+
104
+ @property
105
+ def window_length(self):
106
+ return self.bucket_count * self.bucket_interval
107
+
108
+ def _bucket_for(self, now: int) -> Bucket:
109
+ tail = self.win_chain[-1] if self.win_chain else None
110
+ if tail is not None and tail.begin_time <= now < tail.end_time:
111
+ return tail
112
+
113
+ # A long idle gap recreates the window; otherwise append the bucket
114
+ # that contains the current timestamp.
115
+ if tail is None or tail.end_time + self.window_length <= now:
116
+ self.win_chain = [Bucket(now, now + self.bucket_interval)]
117
+ return self.win_chain[0]
118
+
119
+ next_start = tail.end_time + (now - tail.end_time) // self.bucket_interval
120
+ bucket = Bucket(next_start, next_start + self.bucket_interval)
121
+ while (self.win_chain and
122
+ self.win_chain[0].begin_time + self.window_length <= next_start):
123
+ self.win_chain.pop(0)
124
+ self.win_chain.append(bucket)
125
+ return bucket
126
+
127
+ def add(self, value, cb, *, now=None):
128
+ if now is None:
129
+ now = int(time.time() * 1000)
130
+ with self.lock:
131
+ cur_bucket = self._bucket_for(now)
132
+ with cur_bucket.lock:
133
+ ret = cb(cur_bucket, value)
134
+ return ret
135
+
136
+ def lookup(self, cur_time: int) -> Bucket:
137
+ with self.lock:
138
+ return self._bucket_for(cur_time)
139
+
140
+ def calc_interval_values(self, begin_time: int, end_time: int) -> Tuple:
141
+ total_success = 0
142
+ total_failed = 0
143
+ continuous_failed = 0
144
+
145
+ with self.lock:
146
+ for b in self.win_chain:
147
+ with b.lock:
148
+ if begin_time >= b.end_time:
149
+ continue
150
+ if end_time <= b.begin_time:
151
+ continue
152
+
153
+ total_success += b.success
154
+ total_failed += b.total_failed
155
+ continuous_failed += b.continuous_failed
156
+
157
+ return total_success, total_failed, continuous_failed
158
+
159
+
160
+ ApiGetOneInstance = "Consumer::GetOneInstance"
161
+ ApiGetInstances = "Consumer::GetInstances"
162
+ ApiAsyncGetOneInstance = "Consumer::AsyncGetOneInstance"
163
+ ApiAsyncGetInstances = "Consumer::AsyncGetInstances"
164
+ ApiGetRouteRule = "Consumer::GetRouteRule"
165
+
166
+ ApiRegister = "Provider::Register"
167
+ ApiDeregister = "Provider::Deregister"
168
+ ApiHeartbeat = "Provider::Heartbeat"
@@ -0,0 +1,58 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python available.
2
+ #
3
+ # Copyright (C) 2021 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software distributed
12
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
13
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations under the License.
15
+
16
+ import asyncio
17
+ import ipaddress
18
+ import socket
19
+ from contextlib import suppress
20
+
21
+ import psutil
22
+
23
+
24
+ def get_ip(ifname: str) -> str:
25
+ """Return an IP from the named interface without falling back to another one."""
26
+ for address in psutil.net_if_addrs().get(ifname, ()):
27
+ if address.family in (socket.AF_INET, socket.AF_INET6):
28
+ return str(ipaddress.ip_address(address.address.split("%", 1)[0]))
29
+ raise OSError(f"no IP address found for interface {ifname}")
30
+
31
+
32
+ async def get_local_ip(address: str, timeout: int) -> str:
33
+ """Probe the TCP source IP for a configured server within a millisecond timeout."""
34
+ host, port = get_ip_from_int_with_port(address)
35
+ if not host or not 0 < port < 65536:
36
+ raise ValueError(f"invalid server address {address}")
37
+ async with asyncio.timeout(timeout / 1000):
38
+ _, writer = await asyncio.open_connection(host, port)
39
+ try:
40
+ local_address = writer.get_extra_info("sockname")
41
+ if not local_address or not local_address[0]:
42
+ raise OSError("TCP connection has no local IP address")
43
+ return str(ipaddress.ip_address(local_address[0]))
44
+ finally:
45
+ writer.close()
46
+ with suppress(OSError):
47
+ await writer.wait_closed()
48
+
49
+
50
+ def get_ip_from_int_with_port(ipstr):
51
+ host, port = ipstr.rsplit(":", 1)
52
+ if host.startswith("[") and host.endswith("]"):
53
+ host = host[1:-1]
54
+ try:
55
+ host = str(ipaddress.IPv4Address(int(host)))
56
+ except ValueError:
57
+ pass
58
+ return host, int(port)
@@ -0,0 +1,18 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python available.
2
+ #
3
+ # Copyright (C) 2021 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software distributed
12
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
13
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations under the License.
15
+
16
+ from polaris._version import __version__
17
+
18
+ version = __version__
@@ -0,0 +1,32 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python available.
2
+ #
3
+ # Copyright (C) 2021 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software distributed
12
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
13
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations under the License.
15
+
16
+ from . import circuit_breaker
17
+ from . import event
18
+ from . import load_balancer
19
+ from . import local_registry
20
+ from . import server_connector
21
+ from . import service_router
22
+ from . import stat_reporter
23
+ from .skill import connector
24
+
25
+ __all__ = [
26
+ "circuit_breaker",
27
+ "event",
28
+ "load_balancer",
29
+ "local_registry",
30
+ "server_connector",
31
+ "service_router",
32
+ ]
@@ -0,0 +1,19 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python available.
2
+ #
3
+ # Copyright (C) 2021 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software distributed
12
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
13
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations under the License.
15
+
16
+ from .base import BaseCircuitBreaker, CircuitBreakResult
17
+ from .composite import CircuitBreaker
18
+
19
+ __all__ = ["BaseCircuitBreaker", "CircuitBreakResult", "CircuitBreaker"]
@@ -0,0 +1,54 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python available.
2
+ #
3
+ # Copyright (C) 2021 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software distributed
12
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
13
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations under the License.
15
+
16
+ from abc import ABC, abstractmethod
17
+ from typing import Dict, List
18
+
19
+ from polaris.pkg.model.service import Instance
20
+
21
+ __all__ = ["BaseCircuitBreaker", "CircuitBreakResult"]
22
+
23
+
24
+ class CircuitBreakResult:
25
+ def __init__(self, ts=None):
26
+ self.now = ts
27
+ self.instance_2_open = []
28
+ self.instance_2_halfopen = []
29
+ self.instance_2_close = []
30
+
31
+
32
+ class BaseCircuitBreaker(ABC):
33
+ """
34
+ 熔断判断返回是否需要进行熔断
35
+ """
36
+
37
+ @abstractmethod
38
+ def realtime_circuit_break(self, instance: Instance, ret: int, cost: int) -> bool:
39
+ pass
40
+
41
+ def timing_circuit_break(self, instance_list: List[Instance]) -> CircuitBreakResult:
42
+ pass
43
+
44
+ def next_check_time(self, instance: Instance):
45
+ return None
46
+
47
+ def now(self):
48
+ raise NotImplementedError
49
+
50
+ def on_state_change(self, instance: Instance):
51
+ pass
52
+
53
+ def get_half_open_request_count(self):
54
+ return 3