brbzull0 opened a new issue, #13710:
URL: https://github.com/apache/trafficserver/issues/13710

   ### Impact
   
   `traffic_ctl hostdb status <hostname>` returns partition objects with no 
`records` key
   for every partition that holds entries but no match for the filter. 
`proxy.config.hostdb.partitions`
   defaults to `64`, so on a populated HostDB a single-hostname query returns 
the one real
   match alongside up to 63 bare `{"id": N}` stubs.
   
   A consumer iterating `partitions[].records` raises `KeyError` (or reads 
`undefined`) on
   those stubs, and has to special-case a key that the unfiltered response 
always carries.
   The output is also internally inconsistent: a partition holding zero entries 
is dropped
   from the array entirely, while a partition whose entries are all filtered 
out appears as
   a contentless stub. Two ways of having no records, two different shapes.
   
   Default configuration, no plugin required — any operator who passes a 
hostname argument
   hits it. There is no workaround beyond defensive parsing on the consumer 
side. The
   unfiltered `traffic_ctl hostdb status` is unaffected.
   
   ```
   Version:  master @ 6cc6a2d150b5631d6b3d7aa82d52e687a2e250ff
   Since:    6b10f87f1a4e5b4d699eeefca239a721f6a3ff2e, "Add HOSTNAME option to
             traffic_ctl hostdb status (#12858)", 2026-02-06 — first released 
in 10.2.0
   Config:   proxy.config.hostdb.partitions (default 64)
   ```
   
   ### Proof
   
   `partition_node["records"]` is created only as a side effect of `push_back`, 
inside the
   record loop. A partition survives the `empty()` check at :96, gets its `id` 
at :101, and
   is pushed at :111 whether or not any record passed the hostname filter at 
:105.
   
   ```cpp
   // src/mgmt/rpc/handlers/hostdb/HostDB.cc:96-111
         if (partition_entries.empty()) {
           continue;
         }
   
         Node partition_node;
         partition_node["id"] = i;
   
         for (RefCountCacheHashEntry *entry : partition_entries) {
           HostDBRecord *record = static_cast<HostDBRecord 
*>(entry->item.get());
           if (!hostname.empty() && record->name_view().find(hostname) == 
std::string_view::npos) {
             continue;
           }
           partition_node["records"].push_back(*record);
         }
   
         partitions.push_back(partition_node);
   ```
   
   
https://github.com/apache/trafficserver/blob/6cc6a2d150b5631d6b3d7aa82d52e687a2e250ff/src/mgmt/rpc/handlers/hostdb/HostDB.cc#L96-L111
   
   Emitting both shapes against the vendored yaml-cpp, for three partitions 
whose entries
   are all filtered out:
   
   Current:
   ```json
   {"partitions": [{"id": "0"}, {"id": "1"}, {"id": "2"}]}
   ```
   
   Expected:
   ```json
   {"partitions": [{"id": "0", "records": []}, {"id": "1", "records": []}, 
{"id": "2", "records": []}]}
   ```
   
   The omission is schema-legal only by omission: 
`src/mgmt/rpc/schema/hostdb_status_schema.json`
   declares `records` as `"type": "array"` but carries no `required` array 
anywhere in the
   document, so a partition object with only `id` validates.
   
   This is the same invariant #13609 established one level up. That PR changed 
`partitions`
   itself to be constructed as a sequence — `Node 
partitions{YAML::NodeType::Sequence}` at
   HostDB.cc:85 — and its test asserts `partitions=[]`
   (`tests/gold_tests/traffic_ctl/traffic_ctl_json_null.test.py:58`), on the 
principle that a
   container node which may stay empty has to be constructed as one. `records` 
is that same
   principle one level deeper and was missed.
   
   To be precise about scope: this is not a regression from #13609. The key is 
absent, not
   emitted as `~`. The behaviour dates from #12858.
   
   Found by inspection of the merged tree and by emitting both shapes against 
the bundled
   yaml-cpp. Not reproduced against a running ATS with a populated HostDB, so 
the stub count
   of "up to 63" is derived from the default partition count rather than 
measured.
   
   ### Proposed change
   
   Construct `records` as an empty sequence before the loop, so a partition 
that contributes
   nothing still carries the key:
   
   ```diff
   --- a/src/mgmt/rpc/handlers/hostdb/HostDB.cc
   +++ b/src/mgmt/rpc/handlers/hostdb/HostDB.cc
   @@ -98,7 +98,10 @@
          }
    
          Node partition_node;
   -      partition_node["id"] = i;
   +      partition_node["id"]      = i;
   +      // Always a sequence, so a partition whose entries are all filtered 
out by `hostname`
   +      // emits `"records": []` rather than dropping the key.
   +      partition_node["records"] = Node{YAML::NodeType::Sequence};
    
          for (RefCountCacheHashEntry *entry : partition_entries) {
            HostDBRecord *record = static_cast<HostDBRecord 
*>(entry->item.get());
   ```
   
   The alternative is to drop fully-filtered partitions from the array 
entirely, matching the
   `empty()` case at :96. That produces terser output, but it makes the 
response shape depend
   on the filter in a second way and diverges from the invariant #13609 just 
established for
   `partitions`. Constructing the sequence is the smaller and more consistent 
change.
   
   No compatibility concern: consumers that already tolerate the missing key 
keep working, and
   the JSON schema needs no edit since `records` is already typed as an array.
   
   Still to do: a filtered-query assertion in `traffic_ctl_json_null.test.py` 
alongside the
   existing `partitions=[]` case. That needs a populated HostDB, which the 
current test
   deliberately avoids, so it may belong in a separate autest.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to