SIENTIAPDE-1974: fix(read): code-review fixes for the slot v2 read path
- config: GatewayConfig.certs_from_env() reads only CERT_BUCKET/CERT_CACHE_DIR; IngestorManager uses it, so a malformed write/gRPC env var (GRPC_PORT, WRITE_MIN_PAUSE_S, IDLE_CONN_MAX_S) can no longer stop the reads. - certs: CertProvider raises CertificateNotConfiguredError with an actionable message when no platform_certificates document exists; downloads once under an asyncio.Lock, caches the paths, writes atomically (tmp + os.replace); invalidate() forces a fresh download. - pool: server_id normalized to str (1 and '1' share a session); warns when a get() asks for other url/server_uri/use_security than the pooled client was opened with (client reused, the slot config wins); invalidates the cert cache when a secured connect fails. - SubscriptionEngine: tracks nodes per subscription and forgets them on unsubscribe (no more cycle alarms / tag count for removed tags), keeping addresses still monitored by another slot. - ServerReader.shutdown(evict) / IngestorManager.remove_server(evict): a server that only left this pod's slots keeps the shared connection for NATS writes/gRPC; lost servers and config changes still evict. - IngestorManager warns once per server when a slot still sets server_cert_path (server certificate pinning is no longer applied). - slots: CONNECTION_FIELDS exported and reused by ServerReader. Accepted, not changed: read metrics label server_name with the server_id (same as the write path, which only knows the id); a malformed slot is notified each lease attempt (alerts are deduplicated by notification_id). Tests: 236 unit tests pass (+18); ruff clean; local e2e against an asyncua server incl. slot moved (connection kept and reused by the write path) and lost-server eviction/re-initialization. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -255,3 +255,46 @@ async def test_unsubscribe_tolerates_dead_connection():
|
||||
assert 'slot1' not in eng.subscriptions
|
||||
eng.pool.unpin.assert_called_once_with('1')
|
||||
eng.logger.warning.assert_called_once()
|
||||
|
||||
|
||||
async def _subscribed_engine(slots):
|
||||
"""Engine with one subscription per slot: {slot: [addresses]}."""
|
||||
eng = make_engine()
|
||||
eng.pool = MagicMock(
|
||||
get=AsyncMock(
|
||||
return_value=MagicMock(create_subscription=AsyncMock(return_value=AsyncMock()))
|
||||
)
|
||||
)
|
||||
for slot, addresses in slots.items():
|
||||
await eng.create_subscription('1', {'subscription_period_ms': 500}, slot)
|
||||
nodes = {
|
||||
a: {'frequency': '1000', 'names': [{'collection': 'c', 'tag_name': a}]}
|
||||
for a in addresses
|
||||
}
|
||||
await eng.subscribe(slot, nodes, 10)
|
||||
return eng
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unsubscribe_forgets_the_slot_nodes():
|
||||
eng = await _subscribed_engine({'s1': ['ns=2;i=1', 'ns=2;i=2'], 's2': ['ns=2;i=3']})
|
||||
|
||||
await eng.unsubscribe('s1')
|
||||
|
||||
# no more cycle checks / tag counting for addresses nobody reads anymore
|
||||
assert set(eng.nodes) == {'ns=2;i=3'}
|
||||
eng.emit_metric.assert_any_await(
|
||||
metric_object=metrics.OPC_TAGS_SUBSCRIBED,
|
||||
method='set',
|
||||
value=1,
|
||||
tags={'pod_id': eng.pod_id, 'server_name': '1'},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unsubscribe_keeps_node_shared_with_another_slot():
|
||||
eng = await _subscribed_engine({'s1': ['ns=2;i=1', 'ns=2;i=2'], 's2': ['ns=2;i=2']})
|
||||
|
||||
await eng.unsubscribe('s1')
|
||||
|
||||
assert set(eng.nodes) == {'ns=2;i=2'}
|
||||
|
||||
Reference in New Issue
Block a user