Skip to content

Driver config reporting: gaps in the normative schema, and doc prose that contradicts it (revision v6) #996

Description

@nikagra

Feedback on the Driver Configuration Reporting — JSON Schema Design document, collected while implementing the report in the java 3.x driver (#974) and cross-checking it against the three sibling implementations that have since merged. Filed here rather than left as prose in a PR description so all four drivers can point at one place; the document itself lives outside this repo.

Two independent classes, and they want different kinds of answer:

  • Part A — gaps in the normative block. Things a driver accepts but the schema cannot express, or that the schema requires with nothing to be relative to. These need a schema decision.
  • Part B — prose that contradicts the normative block in the same document. These affect no code (every driver copies the block verbatim), but they mislead anyone reading the document to understand the shape. These just need editing.

Nothing here is a request to change any driver's behaviour.

Provenance

All claims are against revision v6 of the document, whose normative block (lines 53–1078 of the export) is byte-for-byte what three of the four drivers vendor:

copy md5 revision
design doc v6, lines 53–1078 3961317590ec04da1098fdc835fc1027 v6
java 3.x — #974 3961317590ec04da1098fdc835fc1027 v6
java 4.x — #968 (merged, a3d7be633e) 3961317590ec04da1098fdc835fc1027 v6
gocql — scylladb/gocql#987 (merged) 3961317590ec04da1098fdc835fc1027 v6
csharp — scylladb/csharp-driver#263 (merged) 9395a5abe7e2a5406f8720fe27a50914 v5

csharp is one revision behind (orphaned still in required). It always emits orphaned.max, so nothing it sends is invalid under either revision — the drift is in the vendored contract only, and is tracked separately.

Since all three siblings merged with real reports, version: 1 is now fixed by shipped code. Every item below is a gap in something four drivers already implement, not a design question still in play.


Part A — gaps in the normative block

A1. percentile should be minimum: 0, not exclusiveMinimum: 0

"percentile": { "type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 100 }

java's PercentileSpeculativeExecutionPolicy validates percentile >= 0.0 && < 100, so 0 is a configuration the driver accepts and the schema cannot represent. It is not a corner case with a harmless cost: failing that branch drops the object out of the discriminated union, so one unrepresentable field invalidates the whole speculative-execution object rather than just itself.

The two ways to close it locally are both worse than fixing the schema — editing the vendored copy forks the contract, and tightening the policy to reject 0 is an API break in a maintenance line for a configuration that works today. So 3.x reports it as-is and pins the fallout with a test.

This is the one item actively blocking a review thread (#974 review comment, raised P1). gocql's copy carries the same exclusiveMinimum.

A2. connection.requests.in-flight.max has no lower escape hatch

$defs/requests.properties.in-flight.properties.max is positiveInteger (minimum: 1), while java's PoolingOptions.setMaxRequestsPerConnection rejects only negatives — so a deliberate 0 is unreportable, and the key is required, so omission is not available either.

The precedent for the fix is already in the document: control-plane.schema.agreement.timeout-ms is nonNegativeInteger and its own description spells out what the sentinel means ("0 means do not wait for agreement"). The same treatment here would do.

A3. No size limit is specified, though all four drivers enforce 32KiB

This is a wire-safety property, not a driver preference, and it belongs in the spec for that reason. STARTUP option values are written with an unchecked 16-bit length prefix, so a value over 65535 encoded bytes truncates the prefix modulo 65536 while still appending the whole body — a corrupt frame and a failed handshake, not merely a useless report. Parts of the report are user-supplied and unbounded (datacenter and rack names, custom policy class names), so without a cap the "reporting must never prevent a connection" invariant is a property of the user's configuration rather than of the driver.

All four drivers independently landed on 32KiB. Writing it down would make that a contract instead of a coincidence — including whether the limit is on encoded bytes (which is what the prefix counts).

A4. The built-in load-balancing branch has no name

The built-in branch is additionalProperties: false over exactly type, load-distribution, fallback-to-non-preferred-nodes, adaptive-ordering — no room for a name — and type is const: "token-aware".

So a chain containing anything the branch cannot describe has only one honest encoding: give up the discriminator and report the whole chain as custom. That is what 3.x does, and it fixed a real blind spot — WhiteListPolicy(RoundRobinPolicy) used to report custom/WhiteListPolicy while WhiteListPolicy(TokenAwarePolicy(…)) reported plain token-aware, so whether an operator could see that the client is pinned to a host list depended on an unrelated nesting choice. But the cost is that a token-aware chain with any wrapper now loses the token-aware discriminator entirely.

A name on the built-in branch — or a wrapper list, which the custom branch already permits as an additional property — would let both be stated at once.

ErrorAwarePolicy has no home for the same reason: it excludes hosts over an error-rate threshold (distance() delegates unchanged), which is a restriction rather than a reordering, so adaptive-ordering would be the wrong place for it despite its inviting response-rate signal.

A5. fallback-to-non-preferred-nodes is required but can have nothing to be relative to

The key is in the built-in branch's required, and its own description defines it against query.load-balancing.node-preference — "whether requests may fail over to nodes outside of the preference configured by …". But query.load-balancing requires only policy, so that preference is optional.

TokenAwarePolicy(RoundRobinPolicy) reports token-aware with no preference at all, and the required flag must then carry a value with no referent. 3.x reports false — a bare round-robin policy has no tiering to fall back from, which is not the same claim as "something is restricted" — and pins the pairing in a test, but that is a local guess.

Either make the flag optional alongside the preference, or say which value means "no preference configured".

A6. Pool sizes have nowhere to go under connection.pool

$defs/connection-pool is additionalProperties: false over shard-aware alone. All four drivers are blocked by this identically — it is the one item on this list that no driver has been able to route around.

Two things need settling in the shape: whether 0 is representable, and that java 3.x's per-distance core/max pair has no single "connections count" to report.

A7. Two optional homes for the node preference, with no guidance

connection.node-preference and query.load-balancing.node-preference are both optional, and the document says nothing about a driver whose single knob feeds both — which is every driver here.

All four now fill both slots, and java 3.x and gocql both fill them from a location filter as well as from the policy. So the convention is settled by convergence rather than by the document, which is worth writing down before a fifth driver re-derives it.

One sub-question deserves an explicit answer: does a rack ever belong in the connection slot? 3.x says no, and fills that slot with the datacenter half alone — a rack-aware distance() returns REMOTE, never IGNORED, for a local-datacenter host in another rack, so those hosts are still pooled and reporting type: "rack" there would claim a restriction the driver does not apply. The datacenter genuinely does scope pooling. That reasoning is java-specific and may not hold elsewhere.

A8. The re-nesting changed the meaning of existing keys with version pinned at 1

Design principle 8 says: "Adding new keys is backward-compatible and does not bump the version. Only changing or removing the meaning of an existing key requires a new major version."

Successive revisions have moved keys between groups, re-nested them, and dropped connection-pool from the top level — all changes to the meaning of existing keys — while version stayed 1. Nothing had shipped, so this is a question of what the spec says about itself rather than a compatibility break. But it has now happened across several revisions, and the drivers have shipped, so the next such change is a real one.


Part B — prose that contradicts the normative block

These are in the same document as the block they contradict, so a reader has no way to tell which is current without diffing them.

B1. Design principles 4 and 5 give different shapes for the same thing

4. Every policy is { "policy": "<normalized-name>", ...params }; unsupported params are omitted.
5. Every policy is a discriminated object { "type": "<normalized-name>", ...params }; unsupported params are omitted.

Adjacent, numbered, and mutually exclusive. The block uses type throughout, so 4 is stale and looks like it survived the revision that added 5.

B2. # Top-level envelope still shows the pre-re-nesting flat shape

Lines 33–47 list socket, reconnection-policy, retry-policy, speculative-execution-policy, load-balancing-policy, node-location-preference, connection-pool, query-defaults and tls as top-level keys. The normative block has exactly version, connection, control-plane, queryadditionalProperties: false — and connection-pool was dropped from the top level entirely.

This is the first JSON a reader sees, and none of it is valid.

B3. # Per-driver source mapping describes the same superseded shape

The mapping tables (line 1081 onward) and the gocql example instance use the flat layout and pre-rename spellings: control_plane, system-queries.timeout, base_delay_ms, fixed_delay_ms — against the normative control-plane, queries.system.timeout, base-ms, delay-ms.

The section self-declares non-normative, so the block wins; the concern is only that this is where an implementer naturally looks first.

B4. The ### retry-policy section contradicts the block four ways

Lines 1198–1217, against $defs/retry-policy:

prose says the block says
max-retries … (simple only) the key is on five variants: standard-error-aware, simple, never, downgrading-consistency, custom
standard-error-aware has "no numeric retry cap" standard-error-aware carries max-retries
type enum is (standard-error-aware, simple, fallthrough, downgrading-consistency, custom), and the bullet list matches there is also a never variant, absent from both
downgrading-consistency — "with required backoff" that variant's required is ["type"] alone

The last row has a structural half as well: backoff is a sibling of policy under query.retry, not a property of any policy variant. So "with optional backoff" / "with required backoff" is the wrong shape for every variant, not just for downgrading-consistency.

B5. standard-error-aware has no behavioural definition

Its description is now just "Standard error-aware retry policy." The revision that produced it removed an incorrect Bootstrapping clause, which was right — but it left the one retry variant every java driver reports by default defined only by restating its own name. A consumer reading the report cannot tell what behaviour it names.


Note

The document's own "TODO in v2" section lists two items — connection.heartbeat and connection.write.coalescing — and none of the above. Worth folding these in so the backlog reflects what implementing the spec four times actually turned up.

Related: #992 (would make max-retries: 1 reportable in 3.x if the reverted retry cap is restored), #993.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions