socketflow 0.2.0__tar.gz → 0.2.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.
- {socketflow-0.2.0 → socketflow-0.2.2}/PKG-INFO +1 -1
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/__init__.py +1 -1
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/client_side/client.py +37 -25
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/blueprint.py +16 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/event_loop.py +14 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/transport.py +11 -2
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/server_side/server.py +21 -1
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow.egg-info/PKG-INFO +1 -1
- {socketflow-0.2.0 → socketflow-0.2.2}/README.md +0 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/setup.cfg +0 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/setup.py +0 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/client_side/__init__.py +0 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/__init__.py +0 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/compression.py +0 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/dispatcher.py +0 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/event.py +0 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/exceptions.py +0 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/logs.py +0 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/message_handler.py +0 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/message_manager.py +0 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/metrics.py +0 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/global_side/protocol.py +0 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow/server_side/__init__.py +0 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow.egg-info/SOURCES.txt +0 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow.egg-info/dependency_links.txt +0 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow.egg-info/not-zip-safe +0 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow.egg-info/requires.txt +0 -0
- {socketflow-0.2.0 → socketflow-0.2.2}/socketflow.egg-info/top_level.txt +0 -0
|
@@ -169,7 +169,10 @@ class TcpClientProtocol:
|
|
|
169
169
|
else "Authentication failed"
|
|
170
170
|
)
|
|
171
171
|
self.client._auth_ack_event.set()
|
|
172
|
-
if
|
|
172
|
+
if (
|
|
173
|
+
self.client._auth_error is not None
|
|
174
|
+
or not self.client.require_handshake
|
|
175
|
+
):
|
|
173
176
|
self.client._auth_event.set()
|
|
174
177
|
continue
|
|
175
178
|
elif msg_type == "__handshake_ok__":
|
|
@@ -182,9 +185,7 @@ class TcpClientProtocol:
|
|
|
182
185
|
server_addr = self.client._server_addr
|
|
183
186
|
if not server_addr:
|
|
184
187
|
server_addr = (
|
|
185
|
-
self.socket.getpeername()
|
|
186
|
-
if self.socket
|
|
187
|
-
else ("unknown", 0)
|
|
188
|
+
self.socket.getpeername() if self.socket else ("unknown", 0)
|
|
188
189
|
)
|
|
189
190
|
wait_response = headers.get("wait_response", False)
|
|
190
191
|
status_code = headers.get("status_code", 200)
|
|
@@ -200,9 +201,7 @@ class TcpClientProtocol:
|
|
|
200
201
|
)
|
|
201
202
|
|
|
202
203
|
# Resolve pending wait_response futures
|
|
203
|
-
future = (
|
|
204
|
-
self.client.pending_responses.pop(data_id) if data_id else None
|
|
205
|
-
)
|
|
204
|
+
future = self.client.pending_responses.pop(data_id) if data_id else None
|
|
206
205
|
if future is not None:
|
|
207
206
|
if not future.done():
|
|
208
207
|
future.set_result(
|
|
@@ -236,6 +235,12 @@ class TcpClientProtocol:
|
|
|
236
235
|
with self._control_lock:
|
|
237
236
|
self.socket.sendall(data)
|
|
238
237
|
|
|
238
|
+
def has_backpressure(self) -> bool:
|
|
239
|
+
"""True when the outbound queue to the server is full."""
|
|
240
|
+
if self._writer is None:
|
|
241
|
+
return False
|
|
242
|
+
return self._writer.has_backpressure()
|
|
243
|
+
|
|
239
244
|
def close(self):
|
|
240
245
|
self._writer.abort()
|
|
241
246
|
|
|
@@ -287,7 +292,10 @@ class TcpClientProtocol:
|
|
|
287
292
|
try:
|
|
288
293
|
self.send_data(message_handler.create_ping())
|
|
289
294
|
except Exception:
|
|
290
|
-
return
|
|
295
|
+
# A failed write is evidence the peer is gone. Do not return here,
|
|
296
|
+
# or the silence check below is skipped and a dead connection is
|
|
297
|
+
# never detected. Fall through and let that check decide.
|
|
298
|
+
pass
|
|
291
299
|
|
|
292
300
|
if now - self._last_received_at >= interval * max(
|
|
293
301
|
self.client.keepalive_max_missed, 1
|
|
@@ -362,25 +370,29 @@ class TcpClient:
|
|
|
362
370
|
if handshake_timeout is not None and handshake_timeout <= 0:
|
|
363
371
|
raise ValueError("handshake_timeout must be positive")
|
|
364
372
|
if (auth_username is None) != (auth_password is None):
|
|
365
|
-
raise ValueError(
|
|
373
|
+
raise ValueError(
|
|
374
|
+
"auth_username and auth_password must be provided together"
|
|
375
|
+
)
|
|
366
376
|
if auth_token is not None and auth_username is not None:
|
|
367
|
-
raise ValueError(
|
|
377
|
+
raise ValueError(
|
|
378
|
+
"Use either auth_token or username/password authentication"
|
|
379
|
+
)
|
|
368
380
|
if min_protocol_version is not None and max_protocol_version is not None:
|
|
369
381
|
if min_protocol_version > max_protocol_version:
|
|
370
382
|
raise ValueError(
|
|
371
383
|
"min_protocol_version cannot exceed max_protocol_version"
|
|
372
384
|
)
|
|
373
385
|
if (tls_certfile is None) != (tls_keyfile is None):
|
|
374
|
-
raise ValueError(
|
|
375
|
-
"tls_certfile and tls_keyfile must be provided together"
|
|
376
|
-
)
|
|
386
|
+
raise ValueError("tls_certfile and tls_keyfile must be provided together")
|
|
377
387
|
|
|
378
388
|
self.host = host
|
|
379
389
|
self.port = port
|
|
380
390
|
self.metrics = metrics_module.MetricsRegistry()
|
|
381
391
|
self._log = logs.get_logger("socketflow.client")
|
|
382
392
|
self.metrics.describe("messages_sent_total", "Messages sent to the server")
|
|
383
|
-
self.metrics.describe(
|
|
393
|
+
self.metrics.describe(
|
|
394
|
+
"messages_received_total", "Messages received from the server"
|
|
395
|
+
)
|
|
384
396
|
self.metrics.describe("bytes_sent_total", "Bytes written to the server")
|
|
385
397
|
self.metrics.describe("errors_total", "Errors by context")
|
|
386
398
|
self.metrics.describe("backpressure_total", "Rejections caused by full queues")
|
|
@@ -503,6 +515,7 @@ class TcpClient:
|
|
|
503
515
|
return
|
|
504
516
|
self._negotiated_version = agreed
|
|
505
517
|
self._server_ready_event.set()
|
|
518
|
+
|
|
506
519
|
def _on_protocol_error(self, body):
|
|
507
520
|
"""Record a server-side version refusal."""
|
|
508
521
|
reason = "Server refused the protocol version"
|
|
@@ -607,8 +620,6 @@ class TcpClient:
|
|
|
607
620
|
self._protocol = None
|
|
608
621
|
self._connected = False
|
|
609
622
|
|
|
610
|
-
|
|
611
|
-
|
|
612
623
|
def connect(self):
|
|
613
624
|
"""Connect to server"""
|
|
614
625
|
if self._connected:
|
|
@@ -887,13 +898,9 @@ class TcpClient:
|
|
|
887
898
|
if not self._connected:
|
|
888
899
|
raise ExceptionType.NotConnected("Client is not connected")
|
|
889
900
|
if self.require_handshake and not self._handshake_completed:
|
|
890
|
-
raise ExceptionType.HandshakeError(
|
|
891
|
-
"Connection handshake is not complete"
|
|
892
|
-
)
|
|
901
|
+
raise ExceptionType.HandshakeError("Connection handshake is not complete")
|
|
893
902
|
|
|
894
|
-
data_id, frame = self._encode_outbound(
|
|
895
|
-
data, data_id, path, True, status_code
|
|
896
|
-
)
|
|
903
|
+
data_id, frame = self._encode_outbound(data, data_id, path, True, status_code)
|
|
897
904
|
self._record_send(path, frame)
|
|
898
905
|
|
|
899
906
|
# Register before sending because the peer can reply before send returns.
|
|
@@ -965,9 +972,7 @@ class TcpClient:
|
|
|
965
972
|
f"No response received within {wait_response_timeout}"
|
|
966
973
|
)
|
|
967
974
|
except Exception as e:
|
|
968
|
-
raise ExceptionType.ClientError(
|
|
969
|
-
f"Error while waiting for response: {e}"
|
|
970
|
-
)
|
|
975
|
+
raise ExceptionType.ClientError(f"Error while waiting for response: {e}")
|
|
971
976
|
finally:
|
|
972
977
|
request.cancel()
|
|
973
978
|
|
|
@@ -987,6 +992,13 @@ class TcpClient:
|
|
|
987
992
|
def is_connected(self):
|
|
988
993
|
return self._connected
|
|
989
994
|
|
|
995
|
+
def has_backpressure(self) -> bool:
|
|
996
|
+
"""True when the outbound queue to the server is full."""
|
|
997
|
+
protocol = self._protocol
|
|
998
|
+
if protocol is None:
|
|
999
|
+
return False
|
|
1000
|
+
return protocol.has_backpressure()
|
|
1001
|
+
|
|
990
1002
|
@property
|
|
991
1003
|
def handshake_completed(self):
|
|
992
1004
|
return self._handshake_completed
|
|
@@ -78,6 +78,22 @@ class Blueprint:
|
|
|
78
78
|
"Blueprint not registered with client or server"
|
|
79
79
|
)
|
|
80
80
|
|
|
81
|
+
def has_backpressure(self, client_addr: tuple = None) -> bool:
|
|
82
|
+
"""True when the outbound queue is full.
|
|
83
|
+
|
|
84
|
+
On a server blueprint, client_addr selects the client to check.
|
|
85
|
+
"""
|
|
86
|
+
if hasattr(self, "_client") and self._client:
|
|
87
|
+
return self._client.has_backpressure()
|
|
88
|
+
elif hasattr(self, "_server") and self._server:
|
|
89
|
+
if not client_addr:
|
|
90
|
+
raise ExceptionType.BlueprintError("client_addr parameter is required")
|
|
91
|
+
return self._server.has_backpressure(client_addr)
|
|
92
|
+
else:
|
|
93
|
+
raise ExceptionType.BlueprintError(
|
|
94
|
+
"Blueprint not registered with client or server"
|
|
95
|
+
)
|
|
96
|
+
|
|
81
97
|
def send(
|
|
82
98
|
self,
|
|
83
99
|
data: Any,
|
|
@@ -176,6 +176,12 @@ class EventLoopWriter:
|
|
|
176
176
|
return True
|
|
177
177
|
return self.connection.flush(timeout)
|
|
178
178
|
|
|
179
|
+
def has_backpressure(self) -> bool:
|
|
180
|
+
"""True when the next send would raise Backpressure."""
|
|
181
|
+
if self.connection is None:
|
|
182
|
+
return False
|
|
183
|
+
return self.connection.has_backpressure()
|
|
184
|
+
|
|
179
185
|
def abort(self, timeout=1.0):
|
|
180
186
|
if self.connection is not None:
|
|
181
187
|
self.connection.close()
|
|
@@ -233,6 +239,14 @@ class _SelectorConnection:
|
|
|
233
239
|
with self._lock:
|
|
234
240
|
return self._pending_writes
|
|
235
241
|
|
|
242
|
+
def has_backpressure(self) -> bool:
|
|
243
|
+
"""True when the next send would raise Backpressure."""
|
|
244
|
+
with self._lock:
|
|
245
|
+
return (
|
|
246
|
+
self._pending_writes >= self._max_pending_writes
|
|
247
|
+
or self._queued_bytes >= self._max_queue_bytes
|
|
248
|
+
)
|
|
249
|
+
|
|
236
250
|
def flush(self, timeout: float = 5.0) -> bool:
|
|
237
251
|
"""Wait until the send buffer is empty.
|
|
238
252
|
|
|
@@ -50,7 +50,6 @@ class MemoryBudget:
|
|
|
50
50
|
self._used_bytes = max(0, self._used_bytes - amount)
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
|
|
54
53
|
class TimeoutScheduler:
|
|
55
54
|
"""Run request timeouts from a single shared thread.
|
|
56
55
|
|
|
@@ -217,7 +216,9 @@ class PendingResponseRegistry:
|
|
|
217
216
|
key = self._key(data_id, owner)
|
|
218
217
|
with self._lock:
|
|
219
218
|
if key in self._responses:
|
|
220
|
-
raise ValueError(
|
|
219
|
+
raise ValueError(
|
|
220
|
+
f"A request with data_id {data_id!r} is already pending"
|
|
221
|
+
)
|
|
221
222
|
self._responses[key] = future
|
|
222
223
|
|
|
223
224
|
def pop(self, data_id: str, owner: Any = None) -> Optional[Any]:
|
|
@@ -335,6 +336,14 @@ class SocketWriter:
|
|
|
335
336
|
return False
|
|
336
337
|
time.sleep(0.002)
|
|
337
338
|
|
|
339
|
+
def has_backpressure(self) -> bool:
|
|
340
|
+
"""True when the next send would raise Backpressure."""
|
|
341
|
+
with self._state_lock:
|
|
342
|
+
return (
|
|
343
|
+
self._pending_writes >= self._max_pending_writes
|
|
344
|
+
or self._queued_bytes >= self._max_queue_bytes
|
|
345
|
+
)
|
|
346
|
+
|
|
338
347
|
def _complete(self, payload: bytes):
|
|
339
348
|
with self._state_lock:
|
|
340
349
|
self._queued_bytes -= len(payload)
|
|
@@ -436,6 +436,12 @@ class TcpServerProtocol:
|
|
|
436
436
|
except Exception:
|
|
437
437
|
return False
|
|
438
438
|
|
|
439
|
+
def has_backpressure(self) -> bool:
|
|
440
|
+
"""True when the outbound queue to this client is full."""
|
|
441
|
+
if self._writer is None:
|
|
442
|
+
return False
|
|
443
|
+
return self._writer.has_backpressure()
|
|
444
|
+
|
|
439
445
|
def close(self):
|
|
440
446
|
if self._writer is not None:
|
|
441
447
|
self._writer.abort()
|
|
@@ -504,7 +510,10 @@ class TcpServerProtocol:
|
|
|
504
510
|
try:
|
|
505
511
|
self.send(message_handler.create_ping())
|
|
506
512
|
except Exception:
|
|
507
|
-
return
|
|
513
|
+
# A failed write is evidence the peer is gone. Do not return here,
|
|
514
|
+
# or the silence check below is skipped and a dead connection is
|
|
515
|
+
# never detected. Fall through and let that check decide.
|
|
516
|
+
pass
|
|
508
517
|
|
|
509
518
|
if now - self._last_received_at >= interval * max(
|
|
510
519
|
self.server.keepalive_max_missed, 1
|
|
@@ -1429,6 +1438,17 @@ class TcpServer:
|
|
|
1429
1438
|
with self._clients_lock:
|
|
1430
1439
|
return client_addr in self._clients
|
|
1431
1440
|
|
|
1441
|
+
def has_backpressure(self, client_addr) -> bool:
|
|
1442
|
+
"""True when the outbound queue to one client is full.
|
|
1443
|
+
|
|
1444
|
+
Returns False when the client is not connected.
|
|
1445
|
+
"""
|
|
1446
|
+
with self._clients_lock:
|
|
1447
|
+
protocol = self._clients.get(client_addr)
|
|
1448
|
+
if protocol is None:
|
|
1449
|
+
return False
|
|
1450
|
+
return protocol.has_backpressure()
|
|
1451
|
+
|
|
1432
1452
|
def get_client_identity(self, client_addr: tuple) -> Optional[str]:
|
|
1433
1453
|
"""Return the verified certificate common name for a client, or None."""
|
|
1434
1454
|
with self._clients_lock:
|
|
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
|