Skip to content

Resource Trie Implementation - #133

Open
matt001k wants to merge 25 commits into
mainfrom
feature/resource_trie
Open

Resource Trie Implementation#133
matt001k wants to merge 25 commits into
mainfrom
feature/resource_trie

Conversation

@matt001k

@matt001k matt001k commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Resource Trie

A compressed trie (radix tree) for topic-based pub/sub routing. Topics like
/sensor/temperature/raw are split into segments and stored in a tree structure where
shared prefixes share elements, minimizing memory on constrained devices.
Stored topics are evaluated as concrete (no wildcard patterns) or wildcard (containing either * or ?).

Node Structure

Each element in the trie is a ResourceTrieElement:

ResourceTrieElement
+===-----------------------+
| resource_id    (21b)     |  Valid ID = subscribed resource, 0x1FFFFF = passthrough
| port_mask      (16b)     |  Which ports expressed interest
| wildcard_port_mask (16b) |  Which ports have expressed a wildcard interest that matches this resource
| segment_length  (8b)     |  Length of this element's segment string
| local_interest   (1b)    |  Does local node subscribe to this
| wildcard_interest (1b)   |  Is there a local wildcard topic expressing interest in this resource
| is_wildcard      (1b)    |  Is this a wildcard topic (*, ?)
+--------------------------+
| *children                |  First child (linked list head)
| *sibling                 |  Next sibling (linked list)
| *segment                 |  String key for this node ("sensor", "temp/raw", etc.)
+--------------------------+

Children are a singly-linked list: parent->children points to the first child,
each child's ->sibling points to the next.

Passthrough elements have resource_id == invalid_resource_id (0x1FFFFF). They
exist only as structural connectors in the trie — no resource has expressed interest at
that path. They are created by splits (when adding a node) and cleaned up by
compression (when removing nodes).

Operations

Add (resource_trie_add)

Descends the trie via exact_match, consuming topic segments. Three outcomes:

1. Topic already exists — update the existing node's resource fields.

2. Topic partially matches an existing segment — split the node, then continue.

3. No match at current level — allocate a new leaf and attach it.

When a wildcard is added to the trie, the is_wildcard struct member flag is set.
After a new element is added to the trie, the following is performed based on the type of
element added:

  • concrete: The trie is searched for all matching wildcard resources,
    for every match the following is updated:

    1. The concrete topic's wildcard_port_mask is OR'd with the wildcard resource
    2. The concrete topic's wildcard_interest is OR'd with the wildcard resource
  • wildcard: The trie is searched for all matching concrete resources,
    for every match the following is updated:

    1. The concrete topic's wildcard_port_mask is OR'd with the wildcard resource
    2. The concrete topic's wildcard_interest is OR'd with the wildcard resource

This will be used in resource based routing to correlate concrete topics with wildcard topics
to ensure messages are routed properly throughout the network.

Example: Building a trie

Add "/sensor/temp"    |    Add "/sensor/pressure"    |   Add "/sensor/temp/raw"     |       Add "/sensor/*"
                      |                              |                              |
port_mask = 0x1000    |    port_mask = 0x0A0A        |   port_mask = 0x1100         |       port_mask = 0x0010
local_interest = false|    local_interest = false    |   local_interest = false     |       local_interest = true
                      |                              |                              |
                      |                              |                              |
  (root)              |      (root)                  |     (root)                   |           (root)
    |                 |        |                     |        |                     |              |
 "sensor/temp" (1)    |     "sensor" (pt)            |     "sensor" (pt)            |           "sensor" (pt)
                      |        /       \             |        /       \             |        /        |         \
                      |   "temp" (1)  "pressure" (2) |   "temp" (1)  "pressure" (2) | "temp" (1) "pressure" (2) "*" (4)
                      |                              |      |                       |      |
                      |                              |   "raw" (3)                  |   "raw" (3)

The second add causes a split: "sensor/temp" shares prefix "sensor" with
"sensor/pressure", so the node splits into a passthrough (pt) "sensor" with two
children.

When the last element is added "/sensor/*", it matches every previously added resource.
Meaning, those resources' wildcard_port_mask and wildcard_interest will be updated with
"/sensor/*"'s port_mask, 0x0010, and local_interest, true).

Match (resource_trie_match)

Uses match_wildcard to perform a full trie traversal with an iterative
in-order walk (Depth First Search). Reconstructs each node's full topic path in root->match_str
and compares against the query topic. Supports both concrete and wildcard matching.

Results are pointers to individual elements stored in root->result.matches[] (up to 64).

In order to descend the trie, a stack data structure is used, root->stack.
The stack is large enough to descend into the depth of the trie if every topic
segment was a single character (max of 255 characters or 127 possible trie elements).

                    x1/x2/x3/x4/x5......./x127

An example of how this works in practice is as follows:

Match against the topic "/a/?".
Note that if the first character in the match topic is "/" it is ignored,
meaning the topic to match against is "a/?".

Starting trie:

       (root)
         |
        "a" (pt)
        / \
 "b" (4) "x" (3)
      / \
"c" (1)  "d" (2)

root->match_str = ""
root->stack = []
root->result.matches = []

------------------------------------------------------------------------------

Step 1: Update root->match_str with "a" and determine if the node has children,
        it does, so add element to stack. Next evaluate child.

       (root)
         |
        ["a" (pt)]
        / \
 "b" (4) "x" (3)
      / \
"c" (1)  "d" (2)

root->match_str = "a"
root->stack = [a]
root->result.matches = []

------------------------------------------------------------------------------

Step 2: Update root->match_str with "b" and determine if the node has children,
        it does, so add element to stack. Next evaluate child.

       (root)
         |
        "a" (pt)
        / \
["b" (4)] "x" (3)
      / \
"c" (1)  "d" (2)

root->match_str = "a/b"
root->stack = [a, b]
root->result.matches = []

------------------------------------------------------------------------------

Step 3: Update root->match_str with "c" and determine if the node has children,
        it does not. Attempt to match the topic "a/?" to root->match_str. It
        does not match. Evaluate if it has a sibling, it does! Next evaluate
        sibling.

       (root)
         |
        "a" (pt)
        / \
 "b" (4) "x" (3)
      / \
["c" (1)] "d" (2)

root->match_str = "a/b/c"
root->stack = [a, b]
root->result.matches = []

------------------------------------------------------------------------------

Step 4: Remove "c" from root->match_str and update root->match_str with "d",
        determine if the node has children, it does not. Attempt to match the
        topic "a/?" to root->match_str. It does not match. Evaluate if it has
        a sibling, it does not! Next pop from the stack.

       (root)
         |
        "a" (pt)
        / \
 "b" (4) "x" (3)
      / \
"c" (1) ["d" (2)]

root->match_str = "a/b/d"
root->stack = [a, b]
root->result.matches = []

------------------------------------------------------------------------------

Step 5: Remove "d" from root->match_str. Attempt to match the topic "a/?" to
        root->match_str. It does match, add it to root->matches!. Evaluate if
        the element has a sibling, it does! Next evaluate sibling.

       (root)
         |
        "a" (pt)
        / \
 ["b" (4)] "x" (3)
      / \
"c" (1) "d" (2)

root->match_str = "a/b"
root->stack = [a]
root->result.matches = [b]

------------------------------------------------------------------------------

Step 6: Remove "b" from root->match_str and add "x" to root->match_str.
        Attempt to match the topic "a/?" to root->match_str. It does match,
        add it to root->matches!. Evaluate if the element has a sibling, it
        does not! Next pop from the stack.

       (root)
         |
        "a" (pt)
        / \
 "b" (4) ["x" (3)]
      / \
"c" (1) "d" (2)

root->match_str = "a/x"
root->stack = [a]
root->result.matches = [b, x]

------------------------------------------------------------------------------

Step 7: Remove "x" from root->match_str. Attempt to match the topic "a/?" to
        root->match_str. It does not match. Evaluate if the element has a
        sibling, it does not! Next pop from the stack.

       (root)
         |
        ["a" (pt)]
        / \
 "b" (4) "x" (3)
      / \
"c" (1) "d" (2)

root->match_str = "a"
root->stack = []
root->result.matches = [b, x]

------------------------------------------------------------------------------

Step 8: Back to root, finished.

       [(root)]
         |
        "a" (pt)
        / \
 "b" (4) "x" (3)
      / \
"c" (1) "d" (2)

root->match_str = ""
root->stack = []
root->result.matches = [b, x]

------------------------------------------------------------------------------

After `resource_trie_match` is invoked, two matches are found, elements b and
x which match the wildcard string "/a/?"

Remove (resource_trie_remove)

Removal has three phases, with an additional fourth phase when the removed topic
is a wildcard:

Phase 1: Find and track ancestry

exact_match descends to the target node. The track_ancestry callback pushes
each ancestor onto root->stack during descent:

Starting trie:

             (root)
               |
              "a" (pt)
              / \
       "b" (pt)   "g" (4)
         / \
   "c" (pt) "f" (3)
      / \
"d" (1) "e" (2)
Remove "/a/b/c/d":

  (root) ---> a (pt) ---> b (pt) ---> c (pt) ---> d (1)

  Stack after descent: [a, b, c]
  current = d, parent = c

Phase 2: Remove the node (remove_child)

Three cases depending on the target node:

Case 1: Node has children   |   Case 2: Node is first child   |   Case 3: Node is a sibling
  (becomes passthrough)     |     (unlink + free)             |     (unlink + free)
                            |                                 |
After Removal:              |   Before Removal:               |   Before Removal:
  parent                    |     parent                      |     parent
    |                       |       |                         |       |
  current (invalidate)      |  [current] --> sibling          |  child --> [current] --> sibling
    |                       |                                 |     |
  children                  |                                 |  children
                            |                                 |
                            |   After Removal:                |   After Removal:
                            |     parent                      |      parent
                            |       |                         |        |
                            |     child (previously sibling)  |   child --> sibling
                            |                                 |      |
                            |                                 |   children

In Case 1, check_and_compress attempts to merge the invalidated node with its
child if it has exactly one and parent if the parent is a pass-through node.

Phase 3: Ancestor compression

Walk the ancestry stack bottom-up, calling check_and_compress at each level.
This cleans up passthrough chains created by the removal.
In the removal case of /a/b/c/d the new trie will look like:

             (root)
               |
              "a" (pt)
              / \
       "b" (pt)   "g" (4)
         / \
  "c/e" (2) "f" (3)

Phase 4: Wildcard cross-reference cleanup (wildcard topics only)

When the removed topic is a wildcard, concrete topics that were matched by it
still carry stale wildcard_port_mask and wildcard_interest bits from the
now-deleted wildcard. This phase is the inverse of the cross-reference update
performed during add.

For each concrete topic that matches the removed wildcard pattern:

  1. Strip the removed wildcard's bits:

    • wildcard_port_mask &= ~removed_port_mask
    • wildcard_interest &= ~removed_local_interest
  2. Re-aggregate from any remaining wildcards — other wildcards may share the
    same bits, so the concrete topic is re-matched against the trie to restore
    any bits that are still valid.

Example — remove "/sensor/*" (port_mask = 0x0010, local_interest = true):

Before removal, "/sensor/temperature/raw" has:
  wildcard_port_mask = 0x0012   (0x0010 from "/sensor/*", 0x0002 from "/sensor/*/raw")
  wildcard_interest  = true

Phase 4:
  Step 1 — strip: wildcard_port_mask &= ~0x0010 -> 0x0002
                   wildcard_interest  &= ~true   -> false

  Step 2 — re-aggregate against remaining wildcard "/sensor/*/raw":
           wildcard_port_mask |= 0x0002 -> 0x0002 (unchanged)
           wildcard_interest  |= true   -> true   (restored from other wildcard)

After removal, "/sensor/temperature/raw" has:
  wildcard_port_mask = 0x0002
  wildcard_interest  = true

Compression (check_and_compress)

Compression merges a passthrough parent with its single child, eliminating one
level of indirection:

Before:                    After:
  grandparent                grandparent
  |                          |
  parent "a" (pt)  --->      "a/b" (child's data)
  |                          |
  child "b" (res/pt)         child's children (if any)
  |
  grandchildren

The operation:

  1. Build combined segment: parent_segment + "/" + child_segment
  2. Copy child's entire state onto parent (*parent = *child)
  3. Restore parent's segment (combined) and sibling pointer
  4. Free old parent segment and child node

Compression guards (any true = skip compression operation):

Guard Prevents
!child Compressing a childless node (nothing to compress)
child->sibling Compressing when parent has multiple children
parent == &root->element Compressing the root element is not possible
parent->resource_id != invalid Destroying a valid resource by overwriting it

Compression Example

Starting trie:

       (root)
         |
        "a" (pt)
        / \
 "b" (pt)   "x" (3)
      / \
"c" (1)  "d" (2)

Remove "/a/b/c":

1. remove c:
   - c is a leaf -> free(c)
   - b now has single child d
   - compression:
     b is passthrough, single child d (leaf), compress!
     *b = *d, segment = "b/d", free(d)

   Result:
          (root)
            |
           "a" (pt)
           / \
   "b/d" (2)  "x" (3)

2. Ancestor loop: pop a. a has 2 children -> check_and_compress returns false. Stop.

Remove "/a/x":

1. remove x:
   - x is a leaf -> free(x)
   - a has single child "b/d".
   - compression:
     a is passthrough, single child "b/d" (leaf), compress!
     *a = *"b/d", segment = "a/b/d", free("b/d" node).

   Result:
     (root)
       |
     "a/b/d" (2)     <-- a is now a valid leaf!

2. Ancestor loop: pop root -> is_root -> stop.

Remove "/a/b/d":

1. remove: leaf -> free. root has no children.

   Result: empty trie, all memory reclaimed.

Split (split_element)

When a new topic shares a partial prefix with an existing compressed segment,
the segment is split at the divergence point.

Split Example

Before:

     (root)
        |
    "a/b/c" (1)

Add "/a/b/d":
  - Shared prefix = "a/b", divergence at "c" vs "d"
  - create node for *d, segment = "d"
  - create node for *a/b, segment = "a/b", child = old *a/b/c
  - old *a/b/c, segment = "c", free("a/b/c" segment), sibling = *d


After:

     (root)
        |
    "a/b" (pt)
       / \
 "c" (1) "d" (2)

The original node keeps its resource and becomes a child. The new passthrough
takes over the shared prefix. This is the only way passthrough nodes are created.

Key Invariants

  1. Passthrough nodes always have children. Passthroughs are created by
    split_element (which produces 2+ children). Compression fires whenever a
    passthrough reaches exactly 1 child, absorbing it. This prevents passthroughs
    from ever reaching 0 children and leaking memory.

  2. Compression never destroys valid resources. The parent_has_resource
    guard ensures only passthrough nodes are overwritten during compression. A
    node with a real subscription resource is never compressed away.

  3. Ancestor cleanup is bounded. The ancestry stack is populated during
    exact_match descent and has at most resource_trie_max_depth entries
    (BM_TOPIC_MAX_LEN / 2 = 127). The compression loop breaks on the first
    non-compressible ancestor.

API

// Add a topic subscription. Splits compressed segments as needed.
BmErr resource_trie_add(ResourceTrieRoot *root, const char *topic,
                        uint32_t resource_id, uint16_t port_mask,
                        bool local_interest);

// Find all matching resources for a topic (concrete or wildcard).
// Results in root->result.matches[0..count-1].
BmErr resource_trie_match(ResourceTrieRoot *root, const char *topic);

// Remove a topic subscription. Compresses ancestors and cleans up
// wildcard cross-references.
BmErr resource_trie_remove(ResourceTrieRoot *root, const char *topic);

Memory Model

All allocations go through bm_malloc/bm_free.
Each element requires two allocations: the ResourceTrieElement struct and its
segment string upon being created.
ResourceTrieRoot is stack-allocated by the caller and contains
the root sentinel element, the traversal stack, match results buffer, and match
string workspace.

Mote Testing

I created a test for the mote that looks at the latency and memory usage for the resource trie implementation.
The diff for this can be applied to bm_protocol here:
resource_trie_mote_app_diff.txt

git apply path/to/resource_trie_mote_app_diff.txt

And then this can be built with:

cmake ../.. -DCMAKE_TOOLCHAIN_FILE=../../cmake/arm-none-eabi-gcc.cmake -DCMAKE_BUILD_TYPE=Debug -DUSE_BOOTLOADER=1 -DCMAKE_APP_TYPE=BMDK -DBSP=bm_mote_v1.0 -DAPP=resource_trie_test -DCMAKE_VERBOSE_MAKEFILE=OFF
make -j

This test implements a resource trie with 128 unique topics, 96 concrete topics and 32 wildcard topics. The test performs the following actions:

  • Add all of 128 topics
    • Evaluate the total time for the action
    • Evaluate each add, max, avg time taken
    • Evaluate the heap usage
  • Attempt to re-add all of the topics
    • Evaluate the total time for the action
    • Evaluate each add, max, avg time taken
    • Make sure there is no heap usage from this operation
  • Match all of the topics
    • Evaluate the total time for the action
    • Evaluate each match, max, avg time taken
  • Match topics that do not exist in the resource trie
    • Evaluate the total time for the action
    • Evaluate each match, max, avg time taken
  • Remove all of the topics
    • Evaluate the total time for the action
    • Evaluate each removal, max, avg time taken
    • Make sure all heap is reclaimed from the add operations
  • Add and remove topics 5 times
    • Validate there is no heap fragmentation

The output for this test can be seen below:

--------------------------------------------------------------
  RESOURCE TRIE STRESS TEST
  CPU clock: 160 MHz
  Topics: 96 concrete + 32 wildcard = 128 total
  Match queries: 24 x 5 passes = 120 total
  ResourceTrieRoot size: 1560 bytes (static)
--------------------------------------------------------------
  [HEAP @ before add]
    Free:             159808 bytes
    Largest block:    159768 bytes
    Smallest block:   40 bytes
    Free blocks:      2
    Min ever free:    159808 bytes
    Alloc calls:      206
    Free calls:       9
    Outstanding:      197

  [ADD] 128 topics
    Total:   37751 us
    Avg:     294 us
    Max:     1003 us
    Max topic: "*/*/ocean/atlantic/*/*"

  [HEAP @ after add (peak trie usage)]
    Free:             145840 bytes
    Largest block:    145840 bytes
    Smallest block:   145840 bytes
    Free blocks:      1
    Min ever free:    145840 bytes
    Alloc calls:      723
    Free calls:       96
    Outstanding:      627
    Trie heap cost:   13968 bytes (109 per topic avg)

  [DUPLICATE RE-ADD] 128 topics
    Total:   2245 us
    Avg:     17 us
    Max:     24 us
    Max topic: "net/diagnostics/bandwidth/rx/port2"
    Heap delta: 0 bytes (OK - no leak on re-add)

  [MATCH] 120 queries (5 passes)
    Total:   67119 us
    Avg:     559 us
    Max:     1005 us
    Max topic: "*/temperature/ocean/*/*/*" (9 hits)

  [MATCH DETAILS] per-query (final pass):
    514 us   9 hits  "sensor/temperature/ocean/atlantic/zone1/node01"
    498 us   4 hits  "device/buoy/pacific/north/station01/primary"
    481 us   5 hits  "data/raw/ctd/temperature/celsius/highres"
    478 us   4 hits  "net/bristlemouth/v1/discovery/announce"
    491 us   4 hits  "sensor/pressure/ocean/pacific/zone1/node01"
    486 us   4 hits  "device/glider/atlantic/north/unit01/telemetry"
    470 us   2 hits  "data/processed/ctd/salinity/derived"
    464 us   1 hits  "net/diagnostics/bandwidth/rx/port2"
    503 us   4 hits  "sensor/temperature/lake/erie/zone1/node01"
    486 us   2 hits  "device/mooring/pacific/south/anchor01/status"
    464 us   1 hits  "data/archive/ctd/daily/summary"
    458 us   1 hits  "net/firmware/ota/status/current"
    498 us   3 hits  "sensor/salinity/ocean/atlantic/zone2/node01"
    483 us   3 hits  "device/auv/atlantic/mission01/nav/position"
    465 us   2 hits  "data/stream/realtime/ctd/latest"
    456 us   1 hits  "net/routing/table/update/node01"
    614 us  10 hits  "sensor/*/ocean/atlantic/zone1/*"
    531 us   8 hits  "device/buoy/*/north/*/*"
    494 us   6 hits  "data/raw/ctd/*/*/*"
    510 us   5 hits  "net/bristlemouth/*/discovery/*"
    1005 us   9 hits  "*/temperature/ocean/*/*/*"
    982 us  14 hits  "*/*/atlantic/*/*/*"
    593 us  13 hits  "sensor/*/ocean/*/zone1/*"
    987 us  17 hits  "*/*/*/*/zone1/*"

  [NO-MATCH] 25 queries (5 passes, 0 hits expected)
    Total:   11949 us
    Avg:     477 us
    Max:     607 us
    Max topic: "sensor/*/*/?/temperature"

  [REMOVE] 128 topics
    Total:   162857 us
    Avg:     1272 us
    Max:     12740 us
    Max topic: "*/*/atlantic/*/*/*"

  [HEAP @ after remove (trie empty)]
    Free:             159808 bytes
    Largest block:    159768 bytes
    Smallest block:   40 bytes
    Free blocks:      2
    Min ever free:    145840 bytes
    Alloc calls:      810
    Free calls:       613
    Outstanding:      197
    Heap fully reclaimed (delta: +0 bytes)

  [CHURN] 5 cycles of add-all / remove-all
    Free blocks range: 2 - 2 (stable = no fragmentation)
    Heap after churn:  fully reclaimed (delta: +0 bytes)

--------------------------------------------------------------
  SUMMARY
--------------------------------------------------------------
  Timing (microseconds):
  Operation         Total        Avg        Max
--------------------------------------------------------------
  Add               37751        294       1003
  Re-add             2245         17         24
  Match             67119        559       1005
  No-match          11949        477        607
  Remove           162857       1272      12740
--------------------------------------------------------------
  Heap:
    Peak trie usage:  13968 bytes
    Per-topic avg:    109 bytes
    Re-add leak:      none
    Churn frag range: 2 - 2 free blocks
    Min ever free:    145840 bytes
    Leaked:           0 bytes
--------------------------------------------------------------

  Stress test complete.

@matt001k
matt001k changed the base branch from main to feature/hash_table April 22, 2026 00:27
@matt001k
matt001k changed the base branch from feature/hash_table to main April 22, 2026 00:27
@matt001k
matt001k force-pushed the feature/resource_trie branch from 88503c6 to afcf60c Compare April 23, 2026 01:07
@matt001k
matt001k marked this pull request as ready for review April 24, 2026 23:23
@matt001k matt001k changed the title Feature/resource trie Resource Trie Implementation Apr 24, 2026
Comment thread test/stubs/bm_os_stub.c

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code added here to track the amount of heap consumed in bm_malloc calls. Used in unit tests to validate memory was cleaned up with each test.

(const uint8_t *)data, service_handler, 1000),
false);
free(data);
bm_free(data);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These calls had to be updated to support the new wrappers in bm_os_stub.c.

@matt001k

matt001k commented Apr 28, 2026

Copy link
Copy Markdown
Contributor Author

Note: In the mote test (adding and performing actions on 128 topics), there are a lot of pass-through nodes created due many splits that are occurring. The average alloc size is around 109 bytes because:

  • The ResourceTrieElement struct is 20 bytes
  • FreeRTOS allocates 8 extra bytes for malloc header
    • There are 2 allocations per node added so 16 bytes
  • Segments can be fairly long depending on compression (lets say up to 20 bytes)

Let's say the average node is 50 bytes in total. This would mean that 2.18 trie elements are created for every resource due to the creation of pass-through elements!

Comment thread test/stubs/bm_os_stub.c
Comment on lines -17 to +23
ret = malloc(size);
ret = malloc(sizeof(MEM_ALLOC) + size);
size_t *alloc_size = (size_t *)ret;
*alloc_size = size;
MEM_ALLOC += size;
ret += sizeof(MEM_ALLOC);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nifty!

Comment on lines +75 to +78
// Test match before any add
err = resource_trie_match(&ROOT, topic);
EXPECT_EQ(err, BmOK);
EXPECT_EQ(ROOT.result.count, 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just getting going on reviewing the code, so not super familiar yet with how resource_trie_match is used yet, but my initial reaction to this is that resource_trie_match should not return BmOK when there is nothing to match against. I'll keep an eye out for how resource_trie_match is used and if this does make sense, but I thought I'd ask why is it that way?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It certainly does not have to be that way! My thought process was: "We have successfully been able to traverse the trie without any errors and therefore do not need to return and error". Buttt, I can see how this could create some confusion, for example resource_trie_remove returns BmENODATA if it cannot delete the desired element, so maybe the better solution here is:

BmErr resource_trie_match(ResourceTrieRoot *root, const char *topic) {

...
  if (!root->result.count) {
    return BmENODATA;
  }
  return BmOK;
}

Thoughts?

Comment thread middleware/resource_trie.c Outdated
// Strip concrete nodes of wildcard interests
bool is_wildcard = topic_has_wildcard(topic);
if (is_wildcard) {
resource_trie_match(root, topic);

@victorsowa12 victorsowa12 May 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh I see, in relation to my comment in the unit tests, it looks like we don't actually check for the return value here. Is that something we should do? If not, would that warrant removing the return value from the function then?

@victorsowa12 victorsowa12 May 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we definitely use the return value of resource_trie_match in the unit tests enough to not warrant removing the return value. so just wondering if we should also be checking here. If not maybe we could add a (void) to indicate we are purposefully ignoring

@matt001k matt001k self-assigned this Jul 21, 2026
@matt001k matt001k added the enhancement New feature or request label Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants