Documentation / Internal package reference
Audience: framework configuration-composition and integration maintainers.
Status: implemented bounded SDK-component profile; single-server plaintext
test-environment checks have run. Production TLS and multi-node acceptance are
not certified.
Package: github.com/frost-leo/fathomry/internal/configsource/nacos/v2.
The integration uses official nacos-sdk-go/v2 v2.3.5 generated Request and
BiRequestStream clients, native configuration/control request types and native
response decoding. It does not start clients/config_client, the SDK global RPC
engine, snapshot/failover files, cloud credential plugins or native notification
workers. No SDK fork, private-field mutation or process-global hook replacement
is used. The dependency-upgrade TODO in go.mod and
GH-21 states the conditions for
reassessing this compatibility path; upstream fixes must pass the preserved
counterexamples and all applicable contracts before removing safeguards.
Fathomry owns admission, HTTP password login, native connection sessions, bounded push handling and cleanup. This is not a transparent facade over the high-level SDK client. It provides no public application loader, precedence policy, automatic application reload, service discovery, migration or Temporal commands.
- Resolve
OptionsV1outside Nacos.Openvalidates and freezes it before constructing local transport ownership; return is not service readiness. Readacquires one permitted key.ReadAllacquires every default key in input order, returning nil on any required failure.- Give independently owned
Document.RawCopy()bytes their authorizedresource.LayerKindand use existingresource.Prepareonce. Local content can come from the existing Viper path. Neither SDK chooses layer priority. Watchcreates a locally owned subscription.Nextreturns invalidation metadata or an explicit observation gap. AResyncrequires a complete re-read; it does not apply configuration or prove application adoption.- Close subscriptions or their owning client. Retain the same owner after a
timed-out Close and retry with a fresh cleanup budget.
ShutdownCompletereports drained native work plus confirmed assembly quiescence/release. Historical socket-close errors can remain even when this evidence is true; error presence alone is not a reliable ownership-release predicate.
DynamicKeys explicitly permits per-call keys and namespace-local search; false
retains the fixed-key boundary. The namespace remains frozen, and this option
does not grant server ACL permissions. With DynamicKeys, an empty default Keys
list is valid, but ReadAll/ReadRawAll/Watch/ObserveRaw refuse that empty selection.
Use Read/ReadRaw, WatchKeys or ObserveRawKeys for explicit selections.
Each subscription freezes 1–16 distinct keys without changing client defaults.
Writable is a separate opt-in for Publish and Delete, still under key/ACL policy.
Three internal acquisition seams remain available for a future public integration
without requiring another native engine:
ValidateOptions admits deferred bootstrap; ReadRawAll retains present-empty
content and explicit Document.Missing slots; ObserveRaw receives complete raw
batches from the same registration/reconciliation/recovery loop. Read/ReadAll keep
their required-document behavior, and Watch retains its invalidation contract.
Raw reconciliation enforces the 4-MiB aggregate bound before retaining a batch.
ObserveRaw's synchronous callback is internal bounded handoff, not caller business
logic. Public source/framework state and errors are not imported by this package.
ReadRawAll and ObserveRaw separately supply the failed selected-key index, or -1
when unknown/success. This attribution is produced at the query boundary, never
recovered by searching arbitrary retained context causes. A validated native
ChangedConfigs response schedules a paced follow-up in the same raw observation
loop; no new poller/session is added. Metadata-only Watch retains its hash-only
acquisition and does not inherit raw-batch retention limits.
The single-key ReadRaw and explicitly selected ObserveRawKeys preserve the same
missing/empty/error and raw-callback boundaries.
Open's context owns the whole lifetime. Canceling a Next wait does not cancel the subscription. Clients and subscriptions support concurrent operations; do not copy their runtime structs. Returned documents/changes are immutable, and RawCopy returns a new sensitive byte slice. Bootstrap values are borrowed only during Open; concurrent caller mutation during Open is unsupported.
Finite reads intentionally create and close one Nacos session per read/batch, rather than reuse a possibly replaced TCP connection without Nacos registration. A Watch owns a persistent session and rebuilds it after loss. This has measured startup/connection costs and is not a claim of parity with a warm high-level SDK cache or persistent native client.
OptionsV1 is a process-local Go contract, independent of SDK major v2 and of
application document formats. It refuses JSON persistence/reconstruction.
| Input or budget | Meaning |
|---|---|
| Name | Required 1–64 lowercase ASCII letters/digits/dot/underscore/hyphen; composition owns uniqueness and must exclude secrets |
| Namespace | Native default form when empty; other IDs pass unchanged, up to 128 UTF-8 bytes; server aliases such as public are version-dependent |
| AppName | Default fathomry; bounded 128-byte native application label, not a Run or Item identity |
| Servers | 1–8 explicit members of one authorized cluster, not configuration layers |
| ServerV1.HTTPURL | At most 2048 bytes; HTTP(S) root or /nacos context path; no userinfo, query, fragment or redirects; HTTP requires AllowInsecure |
| ServerV1.GRPCAddress | Explicit host:port, at most 512 bytes; port 1–65535; gRPC does not inherit security merely from the HTTP URL |
| Keys | Up to 16 distinct default pairs; empty requires DynamicKeys; group/data ID at most 128 UTF-8 bytes without edge whitespace/control characters; omitted group becomes DEFAULT_GROUP |
| DynamicKeys / Writable | Both false by default; per-call namespace-local selection/search and publication/removal are independently opted in |
| Username / Password | Both present or absent; limits 256 / 4096 UTF-8 bytes; no environment or cloud credential discovery |
| RootCAPEM | Optional trust roots, at most 64 KiB; otherwise system roots; no trust-all mode |
| AllowInsecure | False by default; true explicitly selects plaintext gRPC and permits isolated-test HTTP |
| RequestTimeout | Default 10 s, allowed 1 ms–1 min; cooperative admission/request/setup/decode phase budget |
| RetryDelay | Default 100 ms, allowed 1 ms–1 min; minimum delay for registration retries and observation recovery |
| ReconcileInterval | Default 30 s, allowed 1 s–5 min; technical state comparison, not application reload |
| ConcurrentRequests | Default 4, allowed 2–16; finite reads/writes/searches and persistent subscriptions share this admission allowance |
| QueuedRequests | Default 0 refuses overload; allowed 0–64, FIFO wait and separate byte reservations |
| Subscriptions | Default 1, positive and less than ConcurrentRequests, leaving finite-read capacity |
| QueueCapacity | Default 16, allowed 1–64 invalidations per subscription |
| RPC input / output | Receive/send at most 8 MiB per gRPC message; header list at most 32 KiB |
| Protocol JSON | Valid UTF-8, one value, no duplicate keys, at most 64 levels and 32768 value nodes |
| Raw content | At most 1 MiB per document and 4 MiB per returned batch; one additional bounded document may be acquired before aggregate rejection |
| Login response | At most 64 KiB; token at most 16 KiB; TTL must be integer seconds in 1–604800 |
| Publication | Nonempty UTF-8 content up to 1 MiB; optional CAS is 32 hexadecimal MD5 characters; each native metadata string at most 512 UTF-8 bytes |
| Search | Explicit accurate/blur mode; page 1–1,000,000 (default 1), page size 1–100 (default 10); filters at most 128 UTF-8 bytes; HTTP body at most 8 MiB; returned content total at most 4 MiB |
| Native acquisitions | At most ConcurrentRequests simultaneous dial/TLS phases, separately from logical admission; at most 64 resolved addresses attempted serially |
Each admitted logical operation reserves 16 MiB for a unary response and its session's incoming stream message, retaining that allowance through decode. Queues retain bounded metadata and bounded-count errors, not document payloads. Reservations are not process RSS: TLS/protobuf/JSON buffers, copies and native errors have additional costs. Composition bounds client population and retained results/copies. Native error graphs remain intentionally inspectable rather than silently truncated into supposedly lossless errors.
A subscription uses its permit for its entire lifetime. There is one receiver and one serialized stream sender per session; setup is sent before the receiver starts, and that receiver owns control acknowledgements. ACK timeout cancels the entire session instead of abandoning a goroutine-per-send. Default/native gRPC transport reconnection is refused within an existing Nacos epoch; new setup and listen registration are required.
DNS lookup cannot be forcibly joined by canceling the standard resolver's wait. It remains in the bounded native-acquisition account until actual return. A timed-out Close therefore retains pending ownership. Sockets are registered before handoff, late handoff is refused, and shutdown cancels sessions/closes sockets before waiting. No forced termination of arbitrary caller callbacks or native DNS is promised; caller-context expiry is not proof of released resources.
The session performs ServerCheck, opens the native bidirectional stream and sends ConnectionSetup, then requires a successful HealthCheck response. Sending setup or sleeping is not registration proof. Code 301 can be retried up to 16 times within the operation budget for setup/reads/listening; exhaustion retains the last native error. Mutation dispatch never enters that retry loop.
Configuration-query code 300 is missing, distinct from a successful empty string. Empty/whitespace required documents are refused with ErrEmpty. Required content is never replaced with an earlier read, snapshot or failover file. Encrypted content is unsupported. Optional native MD5 is checked against original UTF-8 bytes when present; it is not authentication or an ordered revision. A zero LastModifiedMillis or empty MD5 means unavailable observation.
Query replies do not echo the key; Document.Key is the requested identity. ContentType is a native hint, not validation of the application's schema. An explicit successful query observes the server's response; it does not prove that an earlier publication has propagated to every server-side cache. The service fixture waits for its exact published content/deletion before asserting dependent behavior, rather than equating a write ACK with read-after-write visibility. No stronger server consistency is supplied by this integration. Required validation, exact numbers, dynamic key casing, list/null/empty semantics and frozen configuration belong to existing preparation. Stable inputs are assumed; no common-time snapshot across keys, servers or local input is implied.
Password login follows the selected HTTP context's native
/v1/auth/users/login route. Tokens are instance-local, refreshed with bounded
serialized login, and invalidated conditionally after the token actually used is
denied. A late rejection cannot discard a newer cached token. Nacos headers are
placed in the SDK payload metadata, not gRPC HTTP metadata. Both HTTP login and
gRPC independently verify certificates/hostnames in the secure profile.
Finite read failover stays within the configured member set and one total budget. Failures never authorize addresses suggested by a server reset. No exact physical wire-attempt count or atomic multi-member consistency is claimed.
Publish uses the SDK's ConfigPublish request with content type, CAS MD5 and explicit native metadata. Empty CAS means unconditional publication, not create-only. Delete uses ConfigRemove. Both acquire the same bounded ownership/admission as reads; session setup may fail over before dispatch, but a mutation is issued at most once by this package. Neither a generic server error nor transport timeout authorizes an automatic retry, even for code 301 after mutation dispatch.
Always inspect MutationResult alongside the error:
| State | Observed evidence |
|---|---|
| MutationNotIssued (0) | No mutation RPC was attempted; admission/setup may still have performed reads/login/registration |
| MutationUnknown (1) | RPC was attempted but a validated acknowledgement/rejection is unavailable; a lost reply or generic server 500 cannot prove no effect |
| MutationAcknowledged (2) | Validated success response; retained even if the caller budget expires afterward; not a read-propagation guarantee |
| MutationRejected (3) | Validated native 301/401/403/409 rejection response; native codes remain inspectable, not a general rollback or retry policy |
The isolated service's stale CAS returns generic native code 500 rather than 409. That outcome remains Unknown; the test separately observes unchanged content. Do not infer a stronger effect state by parsing the server's human-readable text.
Search first uses native /v1/cs/configs; only HTTP 404/405/501 permits fallback to
/v3/admin/cs/config/list. Authentication denial is never hidden by that fallback
or endpoint failover. Search bodies preserve native codes and reject malformed,
duplicate-key, over-limit or cross-namespace/exact-key results. ConfigTags maps to
config_tags on v1 and configTags on v3, not the ignored SDK tag spelling.
This mapping follows the server controller
and v3 API.
SearchItem is a sensitive immutable observation. ContentPresent distinguishes content-bearing results from metadata-only v3 listings; absent content yields nil RawCopy, not a fabricated empty configuration. ReadRaw acquires actual content. When content/MD5 are both present they must agree. ItemsCopy detaches container storage; pagination is not a transactional or durable snapshot across requests.
Watch initially reports a full-set resynchronization requirement after successful registration. ConfigChangeNotify and the batch-listen response's ChangedConfigs both produce bounded invalidations. Detection/change/reset ACKs preserve native request IDs; reset acknowledges then retires the epoch, never follows an unapproved pushed endpoint.
Reconciliation retains only the previous hash/presence of each selected key, compares later queries, and commits its baseline only after the whole listen response is validated. Thus an unpushed update or deletion is not silently absorbed into a new baseline. Stream loss/reset creates a gap and new registration with full resync. Queue overflow discards the oldest event and makes resync sticky on the next delivery. Duplicates and coalescing are possible; this is not a durable, ordered, exactly-once or resumable event log.
For a default/empty-namespace listener, native null/absent tenant in a push is accepted as that default form. It is never accepted for a named namespace. Unknown keys, wrong namespaces and malformed required fields retire the session rather than mutate an unrelated configuration.
Qualified errors use ProviderID configsource.nacos.v2 and private fault kinds.
Native Go/gRPC/HTTP/JSON/TLS/context errors remain in standard error chains where
available. RemoteError retains observed result/error codes; its Message method
deliberately exposes sensitive native text, while ordinary formatting does not.
No missing metadata is invented by parsing human-readable error text.
Options, management inputs/results, search pages/items, endpoints, keys, clients, documents, subscriptions and changes have restricted ordinary formatting and runtime JSON refusal. Typed nil formatting is safe; Go's ordinary nil-to-JSON-null behavior is not runtime reconstruction. Namespace/key/MD5 getters and RawCopy deliberately expose data; they are not safe metric labels. The package installs no SDK logger/parser/cache hooks and is not a sandbox against unrelated Go code replacing global library facilities.
The maintainer workflow distinguishes ordinary loopback tests, actual consuming build evidence and opt-in service work. Earlier single-server qualification used a declared Nacos 3.2.4 instance through explicit isolated plaintext endpoints and password authentication: raw read/write fixtures, pushes, connection replacement/re-registration, denial, malformed content, deletion and cleanup were exercised. Periodic reconciliation is longer than the bounded push-test window so it cannot substitute for push acceptance. The service refused empty publication (result 500/error 400); successful-empty query handling remains a local-fixture check. This is not production TLS, multi-node failover/discovery or service-artifact integrity certification.
The opt-in management gate additionally exercises native Publish/Delete, CAS, dynamic ReadRaw/WatchKeys, v3 metadata-only search and positive/negative tag filters using one uniquely named temporary key. It confirms absence during cleanup. Neither service gate modifies an existing configuration or deployment setting.
Source/test categories are client ownership, native transport, authentication, protocol, raw acquisition and observation, each with adjacent tests. Management and search boundaries, Integration and rejecting preparation control, actual consumer, opt-in service gate and equal-session benchmark provide the executable calling examples.