Repository navigation
feat: CLIENT-5530 add pluggable discovery and address translation - #1303
Open
juliannguyen4 wants to merge 94 commits into
Open
juliannguyen4 wants to merge 94 commits into
juliannguyen4 wants to merge 94 commits into
Conversation
Add test/discovery with: - L0 config tests that need no server, including U-11 topology-neutral naming - an L1 runner for the shared conformance fixtures and python-declared.yaml, which declares every fixture case unsupported until the C client adds pluggable discovery - L2 pass-through tests N-15 and N-16, with the N-16 scale-out and NFR-1 timing - the L3 K-10 shared endpoint test on kind and AKO Co-authored-by: Cursor <cursoragent@cursor.com>
Build one manylinux wheel and run discovery test layers L0 to L3 against it. L2 runs the client in a container on the topology's client network, so it has no route to the node addresses. A skipped-layers job reports every layer and profile that does not run yet, and why. Co-authored-by: Cursor <cursoragent@cursor.com>
The C client moves the node it seeds through the ingress port to the address the node advertises, so no node stays at the seed. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Remove the test that discovery options are accepted with no server. It is not a strategy row, and test_client_config_level_options.py already covers ip_map and use_services_alternate against a server. Co-authored-by: Cursor <cursoragent@cursor.com>
The runner still decides each result itself, so the gate keeps comparing the client against its declarations, but it now copies the reason from python-declared.yaml instead of keeping a second copy of every reason. Co-authored-by: Cursor <cursoragent@cursor.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## stage #1303 +/- ##
==========================================
- Coverage 86.03% 83.79% -2.24%
==========================================
Files 104 106 +2
Lines 15577 16016 +439
==========================================
+ Hits 13401 13420 +19
- Misses 2176 2596 +420 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…nt can run Co-authored-by: Cursor <cursoragent@cursor.com>
The driver follows contract version 1 of l5-aws/DRIVER.md in aerospike-client-conformance, so the harness's rows.py can run W-1, W-2, W-3 and W-5 against the Python client. The manifest builds the client from the checked-out commit, C client submodule included. Co-authored-by: Cursor <cursoragent@cursor.com>
…L2 jobs L5 runs by hand on AWS, so the L2 jobs that seed one hostname with a port per node, the shape L5 seeds through, run the driver and check that it routes every node, serves every partition, answers a watch and exits 0 on quit. Co-authored-by: Cursor <cursoragent@cursor.com>
juliannguyen4
force-pushed
the
jnguyen/CLIENT-5853-discovery-l5
branch
from
November 10, 2026 02:29
761e78f to
5ba7847
Compare
Co-authored-by: Cursor <cursoragent@cursor.com>
…on seed provider and discovery exceptions Co-authored-by: Cursor <cursoragent@cursor.com>
…ery L0, L2 and L5 port-map coverage Co-authored-by: Cursor <cursoragent@cursor.com>
…as_addr_map_destroy Co-authored-by: Cursor <cursoragent@cursor.com>
… client Co-authored-by: Cursor <cursoragent@cursor.com>
…ect convention Co-authored-by: Cursor <cursoragent@cursor.com>
…alizer Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…sion helpers Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…list_option Co-authored-by: Cursor <cursoragent@cursor.com>
…y_dict Co-authored-by: Cursor <cursoragent@cursor.com>
…tables Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…alue Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
juliannguyen4
force-pushed
the
jnguyen/CLIENT-5530-pluggable-discovery
branch
from
November 12, 2026 19:40
ce07c95 to
d14958c
Compare
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…proxyTlsNode Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The L3 K-2 budget started when the new pod turned Ready, but its Service NodePort routes 2 to 3 seconds later, so K-2 failed in most runs. In the unresolvable-hostnames profile, N-16's new node also sometimes answered through its listener 3 seconds after the peers-generation bump. Each timed join now starts its budget at the later of the bump and the new node accepting a connection (L3) or answering an info request (L2), and logs the time to reach that point. Co-authored-by: Cursor <cursoragent@cursor.com>
CodeQL's clear-text logging rule treats values named after secrets as sensitive, and K-6 logs the name of the Secret holding the server certificate and the certificate's SANs. Neither is sensitive, so name them after the certificate instead. Co-authored-by: Cursor <cursoragent@cursor.com>
…ePort The NodePort routes within 70 ms of the pod turning Ready, so timing from routing did not explain K-2's 3 to 4 second delays. Probe with an info request instead, which a server with security answers with an error rather than ignoring, as the L2 joins already do. Co-authored-by: Cursor <cursoragent@cursor.com>
The new node answers through its NodePort within 60 ms of Ready, yet the client has added it 3 or more seconds later. Line the client's peer warnings up with K-2's bump, answer and active times to find which tends missed it. Co-authored-by: Cursor <cursoragent@cursor.com>
The client's warnings show the new node rejecting the test's credentials with AEROSPIKE_INVALID_CREDENTIAL for about 3 seconds after it answers through its NodePort, so every tend in that window fails to add it. Start the budget when a login through the NodePort succeeds. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Parse the subjectAltName extension as typed values instead of the text openssl x509 -ext prints. Co-authored-by: Cursor <cursoragent@cursor.com>
Check each node's advertised peers as one set and the pods the client holds as one list, and move the setup checks and the expected TLS alternate addresses into helpers. Co-authored-by: Cursor <cursoragent@cursor.com>
K-2 uses di.wait_joined and probes a login at the address the existing node advertises for the new one, replacing the thread that polled Kubernetes for the new pod. K-9's reschedule and recreate steps move into helpers. l3_cluster.py gains a query helper for single-object kubectl reads and a glossary of the Kubernetes terms the tests use. Co-authored-by: Cursor <cursoragent@cursor.com>
K-2's probe is a full client connect, so timing from before it counted the probe's own connect against the client's budget. Co-authored-by: Cursor <cursoragent@cursor.com>
… returns" K-2's longer join times come from the client retrying the new peer once per tend, not from the probe's connect, so timing from before the probe was right. Co-authored-by: Cursor <cursoragent@cursor.com>
…pluggable-discovery
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # aerospike-client-c
…ery branch Picks up the Windows export of as_cluster_translate_address, a Windows strerror_s fix for ca_path errors, the shared peers collision check and the C client's L1 conformance runner. The Python client calls none of the changed APIs, so it needs no changes. Co-authored-by: Cursor <cursoragent@cursor.com>
…pluggable-discovery Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # test/new_tests/test_vector.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CLIENT-5530: pluggable discovery in the Python client, on the C client's
CLIENT-5735-pluggable-discoverybranch. This PR also includes its CI test layers (CLIENT-5534, previously #1300) and the L5 driver (CLIENT-5853, previously #1301), so they can be reviewed together.C client
The submodule points at the C client's
CLIENT-5735-pluggable-discoverybranch (cceff7db). It has Cstagemerged in except forb95eb8546, which only changes when simple reads setINFO1_GET_ALL. Move it to a Cstagecommit once the C branch merges.That branch builds without changes to the Python client. Since the previous submodule commit, it:
as_cluster_translate_addresson Windows and describesca_patherrors withstrerror_sthere.as_discovery_conformance_collisionentry point for the C client's L1 conformance runner. The message is unchanged, so the collision tests still pass. The Python client does not call the C conformance entry points.Client config
Two new client config dicts map onto
as_config(documented in a new "Discovery and address translation" section ofaerospike.rst;validate_keyscovers both):discovery:provider("static","dns", or aSeedCandidateProviderinstance),merge_policy,execution_mode,refresh_interval_ms,refresh_timeout_ms,tend_refresh_deadline_ms, anddns(names,record_type,port,tls_name,timeout_ms,servers).translation:provider("static-map","port-map","pass-through"),on_miss("pass_through"or"strict"),static_map,port_map.ip_mapkeeps working: it is the map of thestatic-maptranslator, as in the C client, and setting it together withtranslation.static_mapor another translator is aParamError.hostsmay be empty or omitted when a seed provider is set.use_shared_connectionis rejected with any provider other thanstatic/static-map, since shared clients are looked up by their configured seeds.Behaviour change from the C client: with
use_services_alternatetrue, a node without analternate-access-addressis no longer added. A seed node without one raisesInvalidNodeError(-8), "Node ... has no alternate-access-address and use_services_alternate is true. Configure alternate-access-address on the server or set use_services_alternate to false", and a peer without one is logged as an error. It is documented underuse_services_alternate, andtest_bool_settings[use_services_alternate]is replaced bytest_use_services_alternate, which asks the server forservice-clear-alt(orservice-tls-altwith TLS) and expects the client to connect if it is set and this error if it is not.Python seed provider
aerospike_helpers.discovery.SeedCandidateProvider(withSeedCandidateContext) is registered throughas_config_set_binding_seed_provider, so it is accepted only withexecution_mode"thread"(the C client raisesParamErrorotherwise).close()is called once when the client object is destroyed (not byClient.close(), so the client can connect again). If a refresh is still running then, the C client releases the provider on the discovery thread when the refresh returns, and that thread takes the GIL to callclose(); nothing is called during interpreter finalization.SeedDiscoveryFailedcarrying it.aerospike_close()in the destructor now releases the GIL, becauseclosemay wait up torefresh_timeout_msfor a refresh that needs it.There is no Python address translator (the C client has a binding seed provider but no binding translator), and no
aws-tagsprovider (P2; see C client). YAML config is unchanged.Exceptions
TranslationMiss(-25),TranslationCollision(-26) andSeedDiscoveryFailed(-27), subclasses ofClientError, with stubs and docs. Their messages are the C client's.Other changes
error_from_raised_exception()(pending Python exception toas_error) moved toexception.cand replaces the serializer's copy. Deserialization failures fromVectorandHyperLogLognow include the exception type, e.g.Unable to deserialize bytes as Vector: ValueError: Vector buffer too short: missing 6-byte header.CI:
Discovery testsworkflowRuns the pluggable discovery test layers from the client libraries Testing Strategy and Pluggable Discovery: Testing a Client Library in CI. Everything that does not run is reported as a visible hole rather than skipped silently.
L0 unit teststest_l0_naming.py(U-11) andtest_l0_discovery.py, no serverL2 discovery (ce, ...)defaultandreachable(port-map translation, strict miss, DNS seeding, re-seed, a blocked provider refresh),port-map-range(pre-filled map, out-of-range nodes),pass-through-names(N-15),pass-through-portson a released server and on a server that advertises access addresses verbatim (N-16 with its NFR-1 scale-out timing, N-18, and the L5 driver smoke test), and the L2c fault profilesidle-reset(F-1),low-mtu(F-2) andport-range(F-3), with F-4 ondefaultL2 discovery (ee, ...)default(N-6) andcn-only(N-17, a strict expected failure). NeedsFEATURES_CONTENTL3 discovery (k8s)as-endpoint.testwith its own Service NodePort), on kind and AKOL3 discovery (k8s, TLS)Skipped layersreport-skipped-layersrow for every layer and profile that does not run, with the reasonL1 does not run. Its shared fixtures test translation, seed providers, the merge policy and TLS name matching, which are all C client built-ins, so the C client runs them (TG-4). The Python client has no entry point of its own for any of them; running the fixtures against the C client from Python would only test C code.
How it works:
buildreusesbuild-and-optionally-test-wheels-on-one-platform.ymlfor onemanylinux_x86_64wheel on Python 3.12, which every layer installs.discovery-l2-topologyaction. The tests run in apython:3.12-slim-bookwormcontainer on the client network, which has no route to the node addresses, with the topology's env file and--add-hostentries. The workspace is mounted at the same path, so the tests reach the node server's control directory.TOXIPROXY_URL:reset_peerfor F-1 and atimeouttoxic that holds data with no reply for F-4. Thelow-mtuandport-rangeprofiles set the client network's MTU and the proxy's source port range.setup-kind-akofrom shared-workflowsv5.1.0and runs on the runner, reading the cluster throughkubectlwith the action's kubeconfig (test/discovery/l3_cluster.py). The L3 TLS job sets the action'senable-tlsinput, which generates a CA and a certificate whose only SAN is the tls-name, and starts the AerospikeCluster serving TLS from it. That input is in aerospike/shared-workflows#405, so the TLS job is pinned to that PR's commit until a release carries it. Both jobs needFEATURES_CONTENT; without it they are skipped and reported.test/discovery/conftest.pydeselects L2 tests written for another profile (L2_PROFILEcomes from the topology's env file), skips L2 and L3 tests with no topology, and withDISCOVERY_FAIL_ON_SKIP=1, which every job sets, turns a skipped test into a failure, so a misconfigured job cannot pass silently.The C client moves the node it seeds through the L3 ingress port to the address the node advertises, so K-10 asserts that every node, the seeded one included, is held at its advertised address. Java keeps the seeded node at the seed.
Not run, and reported by
Skipped layers:max_socket_idledefault): C client built-ins, so the C client suite owns them.TranslationMapProvideris not in this release.unresolved-access: it checks how the server starts and runs no client.FEATURES_CONTENTis absent.Tests
test_l0_discovery.py, no server), ordered by ID to match the testing strategy:refresh_timeout_msleaves the client on its configured seeds, shown by the client's warning for its configured seed; a periodic refresh keeps running after errors;needs_periodic_refresh()must return a bool. Also the provider context, invalid hosts,SeedDiscoveryFailedwith no seed host left, tend-mode rejection, andclose()running once (including after a refresh that outlives the client).dnsprovider's config, andrecord_typein either case.merge_policy,execution_mode,discovery.provider,translation.providerandon_missis accepted.ip_mapandtranslation.static_mapaccept and reject the same input.test_l2_translation.py,default,reachableandport-map-rangeprofiles), ordered by ID:Peers not reachablenaming every advertised endpoint.dnsprovider seeds from the endpoint's SRV records, served bysetup-dns-server.port-map-range): a node started into the pre-filled range joins within two tend intervals of the later of thepeers-generationbump and the node answering through its range listener; a node outside the range is decided byon_miss(strict logs the miss, pass-through tries the advertised address).use_services_alternateagainst a seed with no alternate-access-address fails init withInvalidNodeError.close()returns afterrefresh_timeout_mswhile a refresh is blocked.test_l2_pass_through.py): N-15 onpass-through-names, and onpass-through-portsN-16 withtranslation: {"provider": "pass-through", "on_miss": "strict"}plus its scale-out step (active within two tend intervals of the later of the bump and the new node answering through its listener), and N-18 for two nodes sharing a listener.test_l2_tls.py, EE): N-6 validates each node against its own tls-name through the proxy, with no legacy-name fallback, and rejects a seed tls-name of the endpoint hostname; N-17 expects a CN-only certificate to be rejected. The C client still accepts a CN-only certificate through its deprecated legacy name check (FR-45) until the next major release, so N-17 is marked as a strict expected failure. It is reported as xfailed only when the handshake is accepted with the deprecation warning logged; any other failure fails it, and once the C client rejects the certificate it passes unexpectedly and fails, so the mark gets removed.conftest.py's fail-on-skip hook does not count expected failures as skips.test_l2_faults.py), ordered by ID. The client is the sync client, so these are the sync variants of the rows:idle-reset): every listener resets its connections with RST and is cycled while the client's connections sit idle in the pool. A raw probe connection per listener shows the RST, each node's closed connection count shows the client dropped the reset sockets, and no command fails to the caller.low-mtu): with the client network's MTU at 1200, a 64 KiB and a 1 MiB value write and read through every node's listener within a bound fromsocket_timeoutandtotal_timeout, or fail with an error that is not a timeout.port-range): more concurrent commands than the proxy has source ports, withmax_conns_per_nodeabove that, fail with client errors within the timeout bound, never with a timeout. The client counts the errors, and commands succeed again once the load drops.default), two tests: a listener that holds data with no reply makes commands to its node time out within the timeout bound, without blocking a thread, and the node serves again once the toxic is removed. Withmax_error_rateanderror_rate_windowboth 4, the timeouts trip the node's circuit breaker within the window, and later commands fail fast withMaxErrorRateExceeded. Java's 2 and 4 would not work here: the C client resets both to the defaults when their ratio is below 1. The test also setstimeout_delayto 0, since by default the C client hands a timed-out connection to recovery, and a timeout counts towardmax_error_rateonly when the connection is closed.test_l3_scale.py,test_l3_shared_endpoint.pyandtest_l3_tls_name.py), ordered by ID:peers-generationbump and the new node accepting a login through its Service NodePort, and keeps the nodes it held. A new node rejects the credentials withAEROSPIKE_INVALID_CREDENTIALfor about 3 seconds after it turns Ready and answers, so the budget starts at the first successful login.L5 driver
The Python client's side of the pluggable discovery L5 rows (W-1, W-2, W-3, W-5), which run on AWS through the
l5-awsharness in aerospike-client-conformance.test/discovery/l5_driver.py: a driver to contract version 1 ofl5-aws/DRIVER.md, modelled on the JavaL5Driver. It builds a client from the harness's flags, runs the retrying load, writesready,snapshot,log,served,watch,resetanderrorlines, and answersserve,watch,resetandquit. The harness'srows.pymakes every assertion.--shape port-mapbuilds the port-map translator (withuse_services_alternateoff), and--on-missis honoured.test/discovery/l5-aws.manifest:L5_SETUPinstalls the toolchain on Amazon Linux 2023,L5_BUILDinitializes the C client submodule (the harness clones without it) and builds and installs a wheel from the checked-out commit withpython -m build, andL5_DRIVERruns the driver..gitignoregets an exception, since*.manifestis ignored for PyInstaller.test/discovery/test_l2_l5_driver.py: thepass-through-portsL2 jobs, which seed one hostname with a port per node as L5 does, run the driver and check that it routes every node, serves all 4096 partitions, answers awatchand exits 0. L5 runs by hand, so this keeps the driver from breaking unnoticed between runs.What the driver cannot report:
unowned,ownersandlongestUnownedMs, andwatchrepliesownershipChecked: false. The contract allows this; the harness then asserts onserveand command latency instead.active: the client does not expose a node's active flag. Every node it holds is reported active, since the C client deactivates a node in the same tend that removes it from the node list.CLIENT-5853's done-when is "each row runs in the weekly job and passes". No weekly job exists yet: the strategy page (§3, §10, TG-6) lists L5 as run by hand with a weekly schedule planned and unowned, and
run.shruns from an operator's machine with SSO credentials and creates billable resources in the shared account. The AWS run itself is pending a go-ahead.Testing
python3 -m buildfrom a clean C build: no warnings in the Python client sources.actionlint(withshellcheck) andflake8pass.closereturned in 1.0 s), then N-10 with one node advertising its published address, and N-16 with every node advertisinglocalhostand its own port.hostname:listener portand served exactly the keys it masters, with no proxied commands; the N-16 scale-out node was active 84 to 205 ms after thepeers-generationbump, under the 500 ms budget; in L3, both pods were held atas-endpoint.testwith their own Service NodePorts.DRIVER.md's local check against a single server:ready, heartbeats 98-103 ms apart (also duringserve),served4096/4096,watchusable in 6 ms, error replies for malformed commands and a secondwatch, exit 0; every line passesrows.py'scontract_problem. Exit codes: 2 for bad flags and for--userwithoutAEROSPIKE_ADMIN_PASSWORD, 3 for an unreachable seed, 0 at end of input.lastError, the node reported unreachable, a WARN log line forwarded, then recovery andserve4096/4096.run.shruns it, inamazonlinux:2023as a sudo user: setup 10 s, build 47 s, then the installed driver served 4096/4096.rows.pywith the stubl5ctlfrom itstest_rows.py, against a local three-node CE cluster that advertises one address with a port per node: W-1pass-throughpassesrouting,node-port,node-hostandserve; W-2, with container restarts standing in for the Spot interruption and the rollout, passes (every node routed 200 ms after the cluster was ready, quiet 1.5 s later); W-3, with a fourth node advertised before a forwarder serves, passes (the new node was never held while unreachable, and was usable 59 ms after its listener served, bound 500 ms). W-5 needs per-zone endpoint addresses and was not simulated.--shape port-mapagainst the three-node cluster:ready, every node at its listener, about 29k commands with no failure.