cgse-common 0.16.0__tar.gz → 0.16.2__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.
- {cgse_common-0.16.0 → cgse_common-0.16.2}/PKG-INFO +1 -1
- {cgse_common-0.16.0 → cgse_common-0.16.2}/pyproject.toml +1 -1
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/env.py +9 -1
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/system.py +24 -0
- cgse_common-0.16.0/src/egse/connect.py +0 -369
- {cgse_common-0.16.0 → cgse_common-0.16.2}/.gitignore +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/README.md +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/justfile +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/noxfile.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/service_registry.db +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/cgse_common/__init__.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/cgse_common/cgse.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/cgse_common/settings.yaml +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/bits.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/calibration.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/config.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/counter.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/decorators.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/device.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/dicts.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/exceptions.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/heartbeat.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/hk.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/log.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/metrics.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/observer.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/obsid.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/persistence.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/plugin.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/plugins/metrics/duckdb.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/plugins/metrics/influxdb.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/plugins/metrics/timescaledb.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/process.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/py.typed +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/randomwalk.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/ratelimit.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/reload.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/resource.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/response.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/scpi.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/settings.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/settings.yaml +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/setup.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/signal.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/socketdevice.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/state.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/task.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/version.py +0 -0
- {cgse_common-0.16.0 → cgse_common-0.16.2}/src/egse/zmq_ser.py +0 -0
|
@@ -384,7 +384,7 @@ def set_log_file_location(location: str | Path | None):
|
|
|
384
384
|
_env.set("LOG_FILE_LOCATION", location)
|
|
385
385
|
|
|
386
386
|
|
|
387
|
-
def get_log_file_location(site_id: str = None) -> str:
|
|
387
|
+
def get_log_file_location(site_id: str = None, check_exists: bool = False) -> str:
|
|
388
388
|
"""
|
|
389
389
|
Returns the full path of the location of the log files. The log file location is read from the environment
|
|
390
390
|
variable `${PROJECT}_LOG_FILE_LOCATION`. The location shall be independent of any setting that is subject to change.
|
|
@@ -392,8 +392,12 @@ def get_log_file_location(site_id: str = None) -> str:
|
|
|
392
392
|
If the environment variable is not set, a default log file location is created from the data storage location as
|
|
393
393
|
follows: `<PROJECT>_DATA_STORAGE_LOCATION/<SITE_ID>/log`.
|
|
394
394
|
|
|
395
|
+
There is no check for the existence of the returned location. The caller function shall check if the
|
|
396
|
+
returned value is a directory and if it exists.
|
|
397
|
+
|
|
395
398
|
Args:
|
|
396
399
|
site_id: the site identifier
|
|
400
|
+
check_exists: check if the location that will be returned is a directory and exists
|
|
397
401
|
|
|
398
402
|
Returns:
|
|
399
403
|
The full path of location of the log files as a string.
|
|
@@ -417,6 +421,10 @@ def get_log_file_location(site_id: str = None) -> str:
|
|
|
417
421
|
data_root = data_root.rstrip("/")
|
|
418
422
|
log_data_root = f"{data_root}/log"
|
|
419
423
|
|
|
424
|
+
if check_exists:
|
|
425
|
+
if not Path(log_data_root).is_dir():
|
|
426
|
+
raise ValueError(f"The location that was constructed doesn't exist: {log_data_root}")
|
|
427
|
+
|
|
420
428
|
return log_data_root
|
|
421
429
|
|
|
422
430
|
|
|
@@ -48,6 +48,7 @@ from typing import Callable
|
|
|
48
48
|
from typing import Iterable
|
|
49
49
|
from typing import List
|
|
50
50
|
from typing import Optional
|
|
51
|
+
from typing import TextIO
|
|
51
52
|
from typing import Tuple
|
|
52
53
|
from typing import Type
|
|
53
54
|
from typing import Union
|
|
@@ -60,6 +61,7 @@ from rich.tree import Tree
|
|
|
60
61
|
from typer.core import TyperCommand
|
|
61
62
|
|
|
62
63
|
import signal
|
|
64
|
+
from egse.env import get_log_file_location
|
|
63
65
|
from egse.log import logger
|
|
64
66
|
|
|
65
67
|
EPOCH_1958_1970 = 378691200
|
|
@@ -2307,6 +2309,28 @@ def caffeinate(pid: int = None):
|
|
|
2307
2309
|
subprocess.Popen([shutil.which("caffeinate"), "-i", "-w", str(pid)])
|
|
2308
2310
|
|
|
2309
2311
|
|
|
2312
|
+
def redirect_output_to_log(output_fn: str, append: bool = False) -> TextIO:
|
|
2313
|
+
"""
|
|
2314
|
+
Open file in the log folder where process output will be redirected.
|
|
2315
|
+
|
|
2316
|
+
When no location can be determined, the user's home directory will be used.
|
|
2317
|
+
|
|
2318
|
+
The file is opened in text mode at the given location and the stream (file descriptor) will be returned.
|
|
2319
|
+
"""
|
|
2320
|
+
|
|
2321
|
+
try:
|
|
2322
|
+
location = get_log_file_location()
|
|
2323
|
+
output_path = Path(location, output_fn).expanduser()
|
|
2324
|
+
except ValueError:
|
|
2325
|
+
output_path = Path.home() / output_fn
|
|
2326
|
+
|
|
2327
|
+
out = open(output_path, "a" if append else "w")
|
|
2328
|
+
|
|
2329
|
+
logger.info(f"Output will be redirected to {output_path!s}")
|
|
2330
|
+
|
|
2331
|
+
return out
|
|
2332
|
+
|
|
2333
|
+
|
|
2310
2334
|
ignore_m_warning("egse.system")
|
|
2311
2335
|
|
|
2312
2336
|
if __name__ == "__main__":
|
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
import random
|
|
2
|
-
import threading
|
|
3
|
-
import time
|
|
4
|
-
from enum import Enum
|
|
5
|
-
|
|
6
|
-
from egse.log import logging
|
|
7
|
-
|
|
8
|
-
logger = logging.getLogger("egse.connect")
|
|
9
|
-
|
|
10
|
-
# random.seed(time.monotonic()) # uncomment for testing only, main application should set a seed.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class ConnectionState(Enum):
|
|
14
|
-
DISCONNECTED = "disconnected"
|
|
15
|
-
CONNECTING = "connecting"
|
|
16
|
-
CONNECTED = "connected"
|
|
17
|
-
CIRCUIT_OPEN = "circuit_open" # Temporarily stopped trying
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class BackoffStrategy(Enum):
|
|
21
|
-
"""
|
|
22
|
-
Specifies the strategy for increasing the delay between retry attempts
|
|
23
|
-
in backoff algorithms to reduce load and avoid overwhelming services.
|
|
24
|
-
|
|
25
|
-
Strategies:
|
|
26
|
-
EXPONENTIAL:
|
|
27
|
-
The delay doubles with each retry attempt (e.g., 1s, 2s, 4s, 8s).
|
|
28
|
-
This is the most widely used approach because it quickly reduces load on struggling systems.
|
|
29
|
-
LINEAR:
|
|
30
|
-
The delay increases by a fixed amount each time (e.g., 1s, 2s, 3s, 4s).
|
|
31
|
-
This provides a more gradual reduction in request rate.
|
|
32
|
-
FIXED:
|
|
33
|
-
Uses the same delay between all retry attempts.
|
|
34
|
-
Simple but less adaptive to system conditions.
|
|
35
|
-
|
|
36
|
-
References:
|
|
37
|
-
- AWS Architecture Blog: Exponential Backoff And Jitter
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
EXPONENTIAL = "exponential"
|
|
41
|
-
"""The delay doubles with each retry attempt (e.g., 1s, 2s, 4s, 8s).
|
|
42
|
-
This is the most widely used approach because it quickly reduces load on struggling systems."""
|
|
43
|
-
LINEAR = "linear"
|
|
44
|
-
"""The delay increases by a fixed amount each time (e.g., 1s, 2s, 3s, 4s).
|
|
45
|
-
This provides a more gradual reduction in request rate."""
|
|
46
|
-
FIXED = "fixed"
|
|
47
|
-
"""Uses the same delay between all retry attempts. Simple but less adaptive to system conditions."""
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class JitterStrategy(Enum):
|
|
51
|
-
"""
|
|
52
|
-
Specifies the strategy for applying jitter (randomization) to retry intervals
|
|
53
|
-
in backoff algorithms to avoid synchronized retries and reduce load spikes.
|
|
54
|
-
|
|
55
|
-
Strategies:
|
|
56
|
-
NONE:
|
|
57
|
-
No jitter is applied. The retry interval is deterministic.
|
|
58
|
-
FULL:
|
|
59
|
-
Applies full jitter by selecting a random value uniformly between 0 and the calculated interval.
|
|
60
|
-
This maximizes randomness but can result in very short delays.
|
|
61
|
-
EQUAL:
|
|
62
|
-
Applies "equal jitter" as described in the AWS Architecture Blog.
|
|
63
|
-
The interval is randomized within [interval/2, interval], ensuring a minimum delay of half the interval.
|
|
64
|
-
Note: This is not the same as "a jitter of 50% around interval" (which would be [0.5 * interval, 1.5 * interval]).
|
|
65
|
-
PERCENT_10:
|
|
66
|
-
Applies a jitter of ±10% around the base interval, resulting in a random interval within [0.9 * interval, 1.1 * interval].
|
|
67
|
-
|
|
68
|
-
References:
|
|
69
|
-
- AWS Architecture Blog: Exponential Backoff And Jitter
|
|
70
|
-
"""
|
|
71
|
-
|
|
72
|
-
NONE = "none"
|
|
73
|
-
"""No jitter is applied to the backoff."""
|
|
74
|
-
FULL = "full"
|
|
75
|
-
"""Maximum distribution but can be too random with very short intervals."""
|
|
76
|
-
EQUAL = "equal"
|
|
77
|
-
"""Best balance, maintains backoff properties while preventing synchronization."""
|
|
78
|
-
PERCENT_10 = "10%"
|
|
79
|
-
"""Add a jitter of 10% around the base interval."""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def calculate_retry_interval(
|
|
83
|
-
attempt_number,
|
|
84
|
-
base_interval,
|
|
85
|
-
max_interval,
|
|
86
|
-
backoff_strategy: BackoffStrategy = BackoffStrategy.EXPONENTIAL,
|
|
87
|
-
jitter_strategy: JitterStrategy = JitterStrategy.EQUAL,
|
|
88
|
-
):
|
|
89
|
-
"""
|
|
90
|
-
Calculates the next retry interval based on the given backoff and jitter strategies.
|
|
91
|
-
|
|
92
|
-
Args:
|
|
93
|
-
attempt_number (int): The current retry attempt (starting from 0).
|
|
94
|
-
base_interval (float): The initial interval in seconds.
|
|
95
|
-
max_interval (float): The maximum allowed interval in seconds.
|
|
96
|
-
backoff_strategy (BackoffStrategy): Strategy for increasing the delay (exponential, linear, or fixed).
|
|
97
|
-
jitter_strategy (JitterStrategy): Strategy for randomizing the delay to avoid synchronization.
|
|
98
|
-
|
|
99
|
-
Returns:
|
|
100
|
-
float: The computed retry interval in seconds.
|
|
101
|
-
|
|
102
|
-
Notes:
|
|
103
|
-
- See the docstrings for BackoffStrategy and JitterStrategy for details on each strategy.
|
|
104
|
-
- Based on best practices from the AWS Architecture Blog: Exponential Backoff And Jitter.
|
|
105
|
-
"""
|
|
106
|
-
|
|
107
|
-
if backoff_strategy == BackoffStrategy.EXPONENTIAL:
|
|
108
|
-
interval = min(base_interval * (2**attempt_number), max_interval)
|
|
109
|
-
elif backoff_strategy == BackoffStrategy.LINEAR:
|
|
110
|
-
interval = min(base_interval + attempt_number, max_interval)
|
|
111
|
-
else:
|
|
112
|
-
interval = base_interval
|
|
113
|
-
|
|
114
|
-
if jitter_strategy == JitterStrategy.NONE:
|
|
115
|
-
return interval
|
|
116
|
-
elif jitter_strategy == JitterStrategy.FULL:
|
|
117
|
-
return random.uniform(0, interval)
|
|
118
|
-
elif jitter_strategy == JitterStrategy.EQUAL:
|
|
119
|
-
return interval / 2 + random.uniform(0, interval / 2)
|
|
120
|
-
elif jitter_strategy == JitterStrategy.PERCENT_10:
|
|
121
|
-
jitter_amount = interval * 0.1
|
|
122
|
-
return interval + random.uniform(-jitter_amount, jitter_amount)
|
|
123
|
-
|
|
124
|
-
return interval
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
class AsyncServiceConnector:
|
|
128
|
-
"""
|
|
129
|
-
Asynchronous base class for robust service connection management with retry, backoff, and circuit breaker logic.
|
|
130
|
-
|
|
131
|
-
This class is intended to be subclassed for managing persistent connections to external services
|
|
132
|
-
(such as devices, databases, or remote APIs) that may be unreliable or temporarily unavailable.
|
|
133
|
-
|
|
134
|
-
Features:
|
|
135
|
-
- Automatic retry with configurable backoff and jitter strategies.
|
|
136
|
-
- Circuit breaker to prevent repeated connection attempts after multiple failures.
|
|
137
|
-
- Connection state tracking (disconnected, connecting, connected, circuit open).
|
|
138
|
-
|
|
139
|
-
Usage:
|
|
140
|
-
1. Subclass `AsyncServiceConnector` and override the `connect_to_service()` coroutine with your
|
|
141
|
-
actual connection logic. Optionally, override `health_check()` for custom health verification.
|
|
142
|
-
2. Store the actual connection object (e.g., socket, transport) as an instance attribute in your subclass.
|
|
143
|
-
3. Use `attempt_connection()` to initiate connection attempts; it will handle retries and backoff automatically.
|
|
144
|
-
4. Use `is_connected()` to check connection status.
|
|
145
|
-
|
|
146
|
-
Example:
|
|
147
|
-
class MyConnector(AsyncServiceConnector):
|
|
148
|
-
async def connect_to_service(self):
|
|
149
|
-
self.connection = await create_socket()
|
|
150
|
-
return self.connection is not None
|
|
151
|
-
|
|
152
|
-
def get_connection(self):
|
|
153
|
-
return self.connection
|
|
154
|
-
|
|
155
|
-
Note:
|
|
156
|
-
The base class does not manage or expose the underlying connection object.
|
|
157
|
-
Your subclass should provide a method or property to access it as needed.
|
|
158
|
-
"""
|
|
159
|
-
|
|
160
|
-
def __init__(self, service_name: str):
|
|
161
|
-
self.state = ConnectionState.DISCONNECTED
|
|
162
|
-
self.last_attempt = 0
|
|
163
|
-
self.base_interval = 1
|
|
164
|
-
self.retry_interval = 1 # Start with 1 second
|
|
165
|
-
self.max_retry_interval = 300 # Max 5 minutes
|
|
166
|
-
self.failure_count = 0
|
|
167
|
-
self.max_failures_before_circuit_break = 5
|
|
168
|
-
self.circuit_break_duration = 60 # 1 minute
|
|
169
|
-
self.circuit_opened_at = None
|
|
170
|
-
|
|
171
|
-
self.service_name = service_name
|
|
172
|
-
|
|
173
|
-
async def connect_to_service(self) -> bool:
|
|
174
|
-
logger.warning(
|
|
175
|
-
f"The connect_to_service() method is not implemented for {self.service_name}, connection will always fail."
|
|
176
|
-
)
|
|
177
|
-
return False
|
|
178
|
-
|
|
179
|
-
async def health_check(self) -> bool:
|
|
180
|
-
logger.warning(
|
|
181
|
-
f"The health_check() method is not implemented for {self.service_name}, check will always return false."
|
|
182
|
-
)
|
|
183
|
-
return False
|
|
184
|
-
|
|
185
|
-
def should_attempt_connection(self) -> bool:
|
|
186
|
-
"""Return True if we should attempt a new connection."""
|
|
187
|
-
now = time.monotonic()
|
|
188
|
-
|
|
189
|
-
# If circuit is open, check if we should close it
|
|
190
|
-
if self.state == ConnectionState.CIRCUIT_OPEN:
|
|
191
|
-
if now - self.circuit_opened_at > self.circuit_break_duration:
|
|
192
|
-
self.state = ConnectionState.DISCONNECTED
|
|
193
|
-
self.failure_count = 0
|
|
194
|
-
self.retry_interval = 1
|
|
195
|
-
return True
|
|
196
|
-
return False
|
|
197
|
-
|
|
198
|
-
# Regular backoff logic
|
|
199
|
-
return now - self.last_attempt >= self.retry_interval
|
|
200
|
-
|
|
201
|
-
async def attempt_connection(self):
|
|
202
|
-
"""Try to connect to the service.
|
|
203
|
-
|
|
204
|
-
This will execute the callable argument `connect_to_service` that was passed
|
|
205
|
-
into the constructor. That function shall return True when the connection
|
|
206
|
-
succeeded, False otherwise.
|
|
207
|
-
"""
|
|
208
|
-
if not self.should_attempt_connection():
|
|
209
|
-
return
|
|
210
|
-
|
|
211
|
-
self.state = ConnectionState.CONNECTING
|
|
212
|
-
self.last_attempt = time.monotonic()
|
|
213
|
-
|
|
214
|
-
try:
|
|
215
|
-
success = await self.connect_to_service()
|
|
216
|
-
|
|
217
|
-
if success:
|
|
218
|
-
self.state = ConnectionState.CONNECTED
|
|
219
|
-
self.failure_count = 0
|
|
220
|
-
self.retry_interval = 1 # Reset backoff
|
|
221
|
-
logger.info(f"Successfully connected to service {self.service_name}")
|
|
222
|
-
else:
|
|
223
|
-
# warning should have been logged by the connect_to_service() callable.
|
|
224
|
-
self.handle_connection_failure()
|
|
225
|
-
|
|
226
|
-
except Exception as exc:
|
|
227
|
-
logger.warning(f"Failed to connect to service {self.service_name}: {exc}")
|
|
228
|
-
self.handle_connection_failure()
|
|
229
|
-
|
|
230
|
-
def handle_connection_failure(self):
|
|
231
|
-
self.failure_count += 1
|
|
232
|
-
|
|
233
|
-
# Open circuit breaker if too many failures
|
|
234
|
-
if self.failure_count >= self.max_failures_before_circuit_break:
|
|
235
|
-
self.state = ConnectionState.CIRCUIT_OPEN
|
|
236
|
-
self.circuit_opened_at = time.time()
|
|
237
|
-
logger.warning(
|
|
238
|
-
f"Circuit breaker opened for service {self.service_name} after {self.failure_count} failures"
|
|
239
|
-
)
|
|
240
|
-
else:
|
|
241
|
-
self.state = ConnectionState.DISCONNECTED
|
|
242
|
-
self.retry_interval = calculate_retry_interval(
|
|
243
|
-
self.failure_count,
|
|
244
|
-
self.base_interval,
|
|
245
|
-
self.max_retry_interval,
|
|
246
|
-
BackoffStrategy.EXPONENTIAL,
|
|
247
|
-
JitterStrategy.EQUAL,
|
|
248
|
-
)
|
|
249
|
-
logger.debug(f"retry_interval={self.retry_interval}")
|
|
250
|
-
|
|
251
|
-
def is_connected(self) -> bool:
|
|
252
|
-
return self.state == ConnectionState.CONNECTED
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
class ServiceConnector:
|
|
256
|
-
"""
|
|
257
|
-
Synchronous base class for robust service connection management with retry, backoff, and circuit breaker logic.
|
|
258
|
-
|
|
259
|
-
This class is intended to be subclassed for managing persistent connections to external services
|
|
260
|
-
(such as devices, databases, or remote APIs) that may be unreliable or temporarily unavailable.
|
|
261
|
-
|
|
262
|
-
Features:
|
|
263
|
-
- Automatic retry with configurable backoff and jitter strategies.
|
|
264
|
-
- Circuit breaker to prevent repeated connection attempts after multiple failures.
|
|
265
|
-
- Connection state tracking (disconnected, connecting, connected, circuit open).
|
|
266
|
-
- Thread-safe operation using a lock for all state changes.
|
|
267
|
-
|
|
268
|
-
Usage:
|
|
269
|
-
1. Subclass `ServiceConnector` and override the `connect_to_service()` method with your
|
|
270
|
-
actual connection logic. Optionally, override `health_check()` for custom health verification.
|
|
271
|
-
2. Store the actual connection object (e.g., socket, transport) as an instance attribute in your subclass.
|
|
272
|
-
3. Use `attempt_connection()` to initiate connection attempts; it will handle retries and backoff automatically.
|
|
273
|
-
4. Use `is_connected()` to check connection status.
|
|
274
|
-
|
|
275
|
-
Example:
|
|
276
|
-
class MyConnector(ServiceConnector):
|
|
277
|
-
def connect_to_service(self):
|
|
278
|
-
self.connection = create_socket()
|
|
279
|
-
return self.connection is not None
|
|
280
|
-
|
|
281
|
-
def get_connection(self):
|
|
282
|
-
return self.connection
|
|
283
|
-
|
|
284
|
-
Note:
|
|
285
|
-
The base class does not manage or expose the underlying connection object.
|
|
286
|
-
Your subclass should provide a method or property to access it as needed.
|
|
287
|
-
"""
|
|
288
|
-
|
|
289
|
-
def __init__(self, service_name: str):
|
|
290
|
-
self.state = ConnectionState.DISCONNECTED
|
|
291
|
-
self.last_attempt = 0
|
|
292
|
-
self.base_interval = 1
|
|
293
|
-
self.retry_interval = 1
|
|
294
|
-
self.max_retry_interval = 300
|
|
295
|
-
self.failure_count = 0
|
|
296
|
-
self.max_failures_before_circuit_break = 5
|
|
297
|
-
self.circuit_break_duration = 60
|
|
298
|
-
self.circuit_opened_at = None
|
|
299
|
-
self.service_name = service_name
|
|
300
|
-
self._lock = threading.RLock()
|
|
301
|
-
|
|
302
|
-
def connect_to_service(self) -> bool:
|
|
303
|
-
logger.warning(
|
|
304
|
-
f"The connect_to_service() method is not implemented for {self.service_name}, connection will always fail."
|
|
305
|
-
)
|
|
306
|
-
return False
|
|
307
|
-
|
|
308
|
-
def health_check(self) -> bool:
|
|
309
|
-
logger.warning(
|
|
310
|
-
f"The health_check() method is not implemented for {self.service_name}, check will always return false."
|
|
311
|
-
)
|
|
312
|
-
return False
|
|
313
|
-
|
|
314
|
-
def should_attempt_connection(self) -> bool:
|
|
315
|
-
now = time.monotonic()
|
|
316
|
-
with self._lock:
|
|
317
|
-
if self.state == ConnectionState.CIRCUIT_OPEN:
|
|
318
|
-
if now - self.circuit_opened_at > self.circuit_break_duration:
|
|
319
|
-
self.state = ConnectionState.DISCONNECTED
|
|
320
|
-
self.failure_count = 0
|
|
321
|
-
self.retry_interval = 1
|
|
322
|
-
return True
|
|
323
|
-
return False
|
|
324
|
-
return now - self.last_attempt >= self.retry_interval
|
|
325
|
-
|
|
326
|
-
def attempt_connection(self):
|
|
327
|
-
with self._lock:
|
|
328
|
-
if not self.should_attempt_connection():
|
|
329
|
-
return
|
|
330
|
-
self.state = ConnectionState.CONNECTING
|
|
331
|
-
self.last_attempt = time.monotonic()
|
|
332
|
-
|
|
333
|
-
try:
|
|
334
|
-
success = self.connect_to_service()
|
|
335
|
-
with self._lock:
|
|
336
|
-
if success:
|
|
337
|
-
self.state = ConnectionState.CONNECTED
|
|
338
|
-
self.failure_count = 0
|
|
339
|
-
self.retry_interval = 1
|
|
340
|
-
logger.info(f"Successfully connected to service {self.service_name}")
|
|
341
|
-
else:
|
|
342
|
-
self.handle_connection_failure()
|
|
343
|
-
except Exception as exc:
|
|
344
|
-
logger.error(f"Failed to connect to service {self.service_name}: {exc}")
|
|
345
|
-
with self._lock:
|
|
346
|
-
self.handle_connection_failure()
|
|
347
|
-
|
|
348
|
-
def handle_connection_failure(self):
|
|
349
|
-
self.failure_count += 1
|
|
350
|
-
if self.failure_count >= self.max_failures_before_circuit_break:
|
|
351
|
-
self.state = ConnectionState.CIRCUIT_OPEN
|
|
352
|
-
self.circuit_opened_at = time.monotonic()
|
|
353
|
-
logger.warning(
|
|
354
|
-
f"Circuit breaker opened for service {self.service_name} after {self.failure_count} failures"
|
|
355
|
-
)
|
|
356
|
-
else:
|
|
357
|
-
self.state = ConnectionState.DISCONNECTED
|
|
358
|
-
self.retry_interval = calculate_retry_interval(
|
|
359
|
-
self.failure_count,
|
|
360
|
-
self.base_interval,
|
|
361
|
-
self.max_retry_interval,
|
|
362
|
-
BackoffStrategy.EXPONENTIAL,
|
|
363
|
-
JitterStrategy.EQUAL,
|
|
364
|
-
)
|
|
365
|
-
logger.debug(f"retry_interval={self.retry_interval}")
|
|
366
|
-
|
|
367
|
-
def is_connected(self) -> bool:
|
|
368
|
-
with self._lock:
|
|
369
|
-
return self.state == ConnectionState.CONNECTED
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|