refactor: extract SOCKS proxy resolution into a cache - #11619
schneiderstefan wants to merge 1 commit into
Conversation
`CanisterHttpPoolManagerImpl::get_socks_proxy_addrs` resolved the API boundary nodes that outcalls may be proxied through. That is registry policy rather than consensus logic, and an upcoming second caller (HTTP outcalls from composite queries) needs the same answer. Move it into a new `ic-https-outcalls-socks-proxy` crate as a `SocksProxyCache`. In addition to the refactor, this commit also introduces caching for repeated lookups under the same registry version. The pool manager builds its own provider from the `registry_client` and `subnet_type` it already receives, so its constructor is unchanged, and it resolves the same addresses as before. Only the logging thins out: the per-node warnings now appear once per registry version rather than on every `on_state_change`. The failure metrics still tick on every call, which is why the resolution hands its failures back to the caller instead of recording them itself.
|
✅ No security or compliance issues detected. Reviewed everything up to b2d74c2. Security OverviewDetected Code Changes
|
| no_op_logger(), | ||
| ); | ||
|
|
||
| let first = cache.addrs(); |
There was a problem hiding this comment.
let's assert the lookup succeeds, e.g., by comparing the number of nodes in the lookup
| data_provider | ||
| .add( | ||
| &make_api_boundary_node_record_key(node_id), | ||
| version, | ||
| Some(ApiBoundaryNodeRecord::default()), | ||
| ) | ||
| .unwrap(); | ||
| data_provider | ||
| .add( | ||
| &make_node_record_key(node_id), | ||
| version, | ||
| Some(NodeRecord { | ||
| http: Some(ConnectionEndpoint { | ||
| ip_addr: format!("2001:db8::{i}"), | ||
| port: 8080, | ||
| }), | ||
| ..Default::default() | ||
| }), | ||
| ) | ||
| .unwrap(); |
There was a problem hiding this comment.
this could be refactored into a test helper shared with registry_with_boundary_nodes
| } | ||
|
|
||
| #[test] | ||
| fn returns_no_proxies_when_the_registry_is_unreadable() { |
There was a problem hiding this comment.
we could also assert that the errors are observed (in metrics)
| let ip_addr = &nodes | ||
| .iter() | ||
| .find(|(id, _)| id == node_id) | ||
| .expect("unknown boundary node id") | ||
| .1; | ||
| expected_addr(ip_addr) |
There was a problem hiding this comment.
this could be also factored out into a shared helper shared with skips_boundary_nodes_without_http_endpoint
| /// other subnet type through the app ones. The two sets are disjoint, which | ||
| /// is what makes this test able to tell them apart. |
There was a problem hiding this comment.
I'm not sure this test relies on disjointess: it just asserts that socks_proxy_addrs_at looks up the proxies based on the subnet type in the registry, any two unequal sets would confirm that.
|
|
||
| /// Never fails: an unreadable registry yields no proxies, degrading an | ||
| /// outcall to a direct attempt rather than failing it. | ||
| pub fn addrs(&self) -> Vec<String> { |
There was a problem hiding this comment.
| pub fn addrs(&self) -> Vec<String> { | |
| pub fn addrs(&self) -> Arc<Vec<String>> { |
then we don't need to clone the content
| /// a lookup per boundary node. Failures are reported on every call, memoized or | ||
| /// not, so a persistent one keeps being visible. |
There was a problem hiding this comment.
Can there be no transient failures that we wouldn't want to memoize but rather retry?
| None | ||
| }) | ||
| }) | ||
| .map(|http_info| format!("socks5h://[{0}]:{SOCKS_PROXY_PORT}", http_info.ip_addr)) |
There was a problem hiding this comment.
format!("socks5h://[{0}]:{SOCKS_PROXY_PORT}", http_info.ip_addr) could be a helper reused by tests instead of redefined there
| .filter_map(|node_id| socks_proxy_addr_of(registry_client, registry_version, node_id, log)) | ||
| .collect(); | ||
| if addrs.len() != eligible { | ||
| errors.push(errors::SOCKS_PROXY_ADDRS_UNRESOLVED); |
| log.clone(), | ||
| ); | ||
|
|
||
| let mut actual = pool_manager.get_socks_proxy_addrs(); |
There was a problem hiding this comment.
The new test does not go through CanisterHttpPoolManagerImpl and thus its wiring (subnet type, metrics updates) is not covered by tests.
CanisterHttpPoolManagerImpl::get_socks_proxy_addrsresolved the API boundary nodes that outcalls may be proxied through. That is registry policy rather than consensus logic, and an upcoming second caller (HTTP outcalls from composite queries) needs the same answer.Move it into a new
ic-https-outcalls-socks-proxycrate as aSocksProxyCache. In addition to the refactor, this commit also introduces caching for repeated lookups under the same registry version.The pool manager builds its own provider from the
registry_clientandsubnet_typeit already receives, so its constructor is unchanged, and it resolves the same addresses as before. Only the logging thins out: the per-node warnings now appear once per registry version rather than on everyon_state_change. The failure metrics still tick on every call, which is why the resolution hands its failures back to the caller instead of recording them itself.