How to add the support of the get_next_object() callback to the 5-c_stats example?

EDIT 2021 - an updated data provider performance demo is available here:

As this post explains for the get_object() callback:

In order to improve the performance of the data provider API callbacks, for both external configuration as well as operational data, when dealing with lists with many leaf elements in them, it is recommended to provide the implementation of the get_object() API. This will reduce the number of round trip calls from the ConfD daemon process for retrieving the leaf elements of each instance in your list as they will all be fetched with a single API call as opposed to via multiple get_elem() calls.

In this post we will further reduce the number of round trip calls needed from the ConfD daemon process for retrieving a whole list of instances or a chunk of instances can be further reduced to getting all instances in one swoop by implementing the get_next_object() API.

The changes needed to add the get_next_object() callback support to the 5-c_stats example will be described in this posting. All the changes will be made to the arpstat.c file of the example.

In the main() function, you will add the registration of your get_next_object() callback as follows:

data.get_next_object = get_next_object;

Then it is cheap and handy to have the number of ARP instances available in our arpdata struct:

 #define N_LEAFS 5

 struct arpdata {
     struct aentry *arp_entries;
     struct timeval lastparse;
    int n_ae;
 };

We now need to increase and reset the number of ARP instances, n_ae, in our free_arp() and add_aentry() functions:

static void free_arp(struct arpdata *dp)
{
    struct aentry *ae = dp->arp_entries;

    while (ae) {
        struct aentry *next = ae->next;
        if(ae->hwaddr) free(ae->hwaddr);
        if(ae->iface) free(ae->iface);
        free(ae);
        ae = next;
    }
    dp->arp_entries = NULL;
    dp->n_ae = 0;
}

/* add an entry, keeping the list ordered */
static void add_aentry(struct aentry **first, struct aentry *new)
{
    struct aentry **prev = first;

    while (*prev != NULL &&
           (memcmp(&new->ip4, &(*prev)->ip4, sizeof(struct in_addr)) > 0 ||
            (memcmp(&new->ip4, &(*prev)->ip4, sizeof(struct in_addr)) == 0 &&
             strcmp(new->iface, (*prev)->iface) > 0))) {
          prev = &(*prev)->next;
    }
    new->next = *prev;
    *prev = new;
}

In the existing run_arp() function we need to increase the “n_ae” count when we call “add_aentry()”

@@ -180,6 +184,7 @@
             }
 
             add_aentry(&dp->arp_entries, ae);
+            dp->n_ae++;
         } else {
             /* skip this entry */
             free(ae);

Finally, the implementation of the get_next_object() callback is as follows:

/* Keypath example */
/* /arpentries/arpe */
/*    2         1   */

static int get_next_object(struct confd_trans_ctx *tctx,
			     confd_hkeypath_t *keypath, long next)
{
  int pos, i;
  confd_value_t *v;
  struct confd_next_object *obj;
  struct arpdata *dp = tctx->t_opaque;
  int n_list_entries;
  struct aentry *ae;

  if (need_arp(dp)) {
    if (run_arp(dp) == CONFD_ERR)
      return CONFD_ERR;
  }

  n_list_entries = dp->n_ae;
  ae = dp->arp_entries;
  
  if (next == -1) {
    pos = 0; /* ConfD wants to get the whole table / list from the beginning */
  } else if (next < n_list_entries) {
    pos = next; /* ConfD wants the table / list from a specific index */
  } else { /* next == n_list_entries, return nothing */
    confd_data_reply_next_object_array(tctx, NULL, -1, -1);
    return CONFD_OK;
  }

  for(i = 0; i < pos; i++)
    ae = ae->next;
  
  obj = malloc(sizeof(struct confd_next_object) * (n_list_entries + 2));
  v = (confd_value_t *) malloc(sizeof(confd_value_t) * n_list_entries * N_LEAFS);

  for (i = 0; pos + i < n_list_entries; i++) { /* Collect all the rows in the table / list entries */
    obj[i].v = &v[i * N_LEAFS];

    CONFD_SET_IPV4(&(obj[i].v[0]), ae->ip4);
    CONFD_SET_STR(&(obj[i].v[1]), ae->iface);
    if (ae->hwaddr == NULL) {
      CONFD_SET_NOEXISTS(&(obj[i].v[2]));
    } else {
      CONFD_SET_STR(&(obj[i].v[2]), ae->hwaddr);
    }
    CONFD_SET_BOOL(&(obj[i].v[3]), ae->perm);
    CONFD_SET_BOOL(&(obj[i].v[4]), ae->pub);
    obj[i].n = N_LEAFS;
    obj[i].next = -1;
    ae=ae->next;
  }
  if (pos + i == n_list_entries)
    obj[i++].v = NULL;
  confd_data_reply_next_object_arrays(tctx, obj, i, 0); /* Reply with an array of object arrays. I.e. the whole table / list */
  free(v);
  free(obj);
  return CONFD_OK;
}

After the above changes are made to the arpstat.c file, you will then be able to rebuild your project and see the get_next_object() callback in action through the TRACE statements of the arpstat program.

Say we have an ARP table with eight entries:

$ arp
Address                 HWtype  HWaddress           Flags Mask            Iface
apps-net0-5.test.com    ether   dd:aa:aa:aa:aa:aa   CM                    eth1
apps-net0-2.test.com    ether   aa:aa:aa:aa:aa:aa   CM                    eth0
apps-net15-14.test.com  ether   00:50:56:f3:13:d2   C                     eth2
apps-net0-4.test.com    ether   cc:aa:aa:aa:aa:aa   CM                    eth1
apps-net0-6.test.com    ether   ee:aa:aa:aa:aa:aa   CM                    eth1
apps-net0-3.test.com    ether   ff:aa:aa:aa:aa:aa   CM                    eth1
apps-net0-3.test.com    ether   bb:aa:aa:aa:aa:aa   CM                    eth0
apps-net0-1.test.com    ether   00:50:56:f7:9d:e2   C                     eth2

We <GET> the entire list over NETCONF using the netconf-console tool that comes with ConfD:

$ netconf-console --get -x '/arpentries/arpe'<?xml version="1.0" encoding="UTF-8"?>
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="1">
  <data>
    <arpentries xmlns="http://tail-f.com/ns/example/arpe">
      <arpe>
        <ip>172.16.171.2</ip>
        <ifname>eth2</ifname>
        <hwaddr>00:50:56:f7:9d:e2</hwaddr>
        <permanent>false</permanent>
        <published>false</published>
      </arpe>
      <arpe>
        <ip>172.16.171.3</ip>
        <ifname>eth0</ifname>
        <hwaddr>aa:aa:aa:aa:aa:aa</hwaddr>
        <permanent>true</permanent>
        <published>false</published>
      </arpe>
      <arpe>
        <ip>172.16.171.4</ip>
        <ifname>eth0</ifname>
        <hwaddr>bb:aa:aa:aa:aa:aa</hwaddr>
        <permanent>true</permanent>
        <published>false</published>
      </arpe>
      <arpe>
        <ip>172.16.171.4</ip>
        <ifname>eth1</ifname>
        <hwaddr>ff:aa:aa:aa:aa:aa</hwaddr>
        <permanent>true</permanent>
        <published>false</published>
      </arpe>
      <arpe>
        <ip>172.16.171.5</ip>
        <ifname>eth1</ifname>
        <hwaddr>cc:aa:aa:aa:aa:aa</hwaddr>
        <permanent>true</permanent>
        <published>false</published>
      </arpe>
      <arpe>
        <ip>172.16.171.6</ip>
        <ifname>eth1</ifname>
        <hwaddr>dd:aa:aa:aa:aa:aa</hwaddr>
        <permanent>true</permanent>
        <published>false</published>
      </arpe>
      <arpe>
        <ip>172.16.171.7</ip>
        <ifname>eth1</ifname>
        <hwaddr>ee:aa:aa:aa:aa:aa</hwaddr>
        <permanent>true</permanent>
        <published>false</published>
      </arpe>
      <arpe>
        <ip>172.16.171.254</ip>
        <ifname>eth2</ifname>
        <hwaddr>00:50:56:f3:13:d2</hwaddr>
        <permanent>false</permanent>
        <published>false</published>
      </arpe>
    </arpentries>
  </data>
</rpc-reply>

The entire list was fetched in one single swoop by ConfD from the application using the get_next_object() callback as we can see from the TRACE statements of the arpstat program.:

TRACE New user session: 1 for user:admin ctx:netconf --> CONFD_OK
TRACE CALL trans init(thandle=1,mode="r",db=running) --> CONFD_OK
TRACE CALL data get_next_object(thandle=1, /arpentries/arpe, -1) --> CONFD_OK
TRACE CALL trans finish(thandle=1) --> CONFD_OK
TRACE Close user sess 1
 --> CONFD_OK

Here ( ap->n_ae++) is missed

Thanks, left that part out by mistake. Added to the original post.

No mention . Hi i want to print each leaf-list in separate line instead of printing in single line separated by spaces . Could you please let me know if there are any tailf extensions to meet this . My yang file looks like as below
list port
{
tailf:callpoint feafrontportstats;
tailf:cli-no-key-completion;
key “port”;
leaf port
{
type uint16;
description “Front panel port number”;
}
leaf-list stats
{
type string;
description “List of stats for each port”;

        }
    }

And i need to print output in CLI as below

Total Packets Received (Octets)…0
Packets Received 64 Octets…0
Packets Received 65‐127 Octets…0
Packets Received 128‐255 Octets…0
Packets Received 256‐511 Octets…0
Packets Received 512‐1023 Octets…0
Packets Received 1024‐1518 Octets…0
Packets Received > 1518 Octets…0
Total Packets Received Without Errors…0
Unicast Packets Received…0
Multicast Packets Received…0
Broadcast Packets Received…0

This new question that is separate from the original posting has now been moved here.

Hello,
Is get_next_object call a standalone call of do i still need to implement get_elem and get_next alongwith get_next_object?
I tried following this example of get_next_object, but i am getting error in confd.log as " no registration found for callpoint show_dev_address/get_next of type=external"

You need to implement at least a get_elem or a get_object in addition to get_next_object.

Hi,

In case a nested list exists in the arpe list, how it will be filled with get_next_object?
For the functions get_next and get_elem, I have implemented new functions(for example get_next_nested, get_elem_nested) using a new callpoint for the nested list data. Should I also implement a new get_next_object_nested function (I’ve tried this but it is not triggered) or there is a special way to fill obj object?

Thank you,
Olia

Hello,

please see description in the man confd_types manpage,
namely section “XML STRUCTURES”, subsection “Value Array
(or Tagged Value Array, depending on which callback/data you need to return):
specifically:

2. List nodes use a single array element, with type C_NOEXISTS (value ignored), regardless of the actual number of entries or their contents

(plus other specific rules for list entry child nodes, e.g. empty leaves etc.)

Returning the “C_NOEXISTS” as a value of the inner list will then cause ConfD to invoke another get_object() (or other) callbacks to iterate the children list entries of inner list with appropriate input keypath…

Assuming e.g. pseudo-model:

list outer {
    key "a";
    leaf a { type int32; }
    list inner {
        key "x";
        leaf x { type int32; }
        // other child-leaves
    }
    leaf b { type string; }
}

For a get_next_object() request with input keypath “/outer”, you can yield response like 11 C_NOEXISTS london.
(array of three confd_value_t the one representing inner list == C_NOEXISTS)

In case ConfD is interested internally in inner list values too, it will invoke further requests with input keypath “/outer{11}/inner”…

Thank you very much for your help it worked!

For future reference I used:
CONFD_SET_NOEXISTS(&(obj[i].v[5]));
(in case my list was the 5th element).

and the get_next_object_nested function was called, but another problem occurred.

My application fails because the find_next callback is not implemented:
DEBUG No find_next() callback installed --> CONFD_ERR DEBUG No find_next() callback installed Error on worker socket request: Bad protocol usage or unexpected retval (21): No find_next() callback installed

From the user guide I found that it is not mandatory to implement it and it can be replaced with get_next.

find_next()
This callback primarily optimizes cases where ConfD wants to start a list traversal at some other point than at the first entry of the list. It is mainly useful for lists with a large number of entries. If it is not registered, ConfD will use a sequence of get_next() calls to find the desired list entry.

So is find_next callback mandatory in this case or not, and if not why get_next is not called?

Thank you for your time

Yes, if you return next with value -1 in one of obj,

struct confd_tag_next_object {
    confd_tag_value_t *tv;
    int n;
    long next;
};

then find_next or find_next_object is mandatory

Thus a data provider can choose to give next values that uniquely identify list entries if that is convenient, or otherwise use -1 for all next elements - or a combination, e.g. -1 for all but the last entry. If any next value is given as -1, at least one of the find_next() and find_next_object() callbacks must be registered.

Ok it’s clear now!
Thank you very much for you feedback!

To clarify, a get_next_object() that doesn’t necessarily need find_next_object() and optimize the memory and CPU resources needed by chunking the confd_data_reply_next_object_arrays() could be implemented like this:

/* Keypath example */
/* /arpentries/arpe */
/*    2         1   */

static int max_nobjs = 100; // Chunk size hardcoded here to simplify

static int get_next_object(struct confd_trans_ctx *tctx,
			     confd_hkeypath_t *keypath, long next)
{
  int pos, i;
  confd_value_t *v;
  struct confd_next_object *obj;
  struct arpdata *dp = tctx->t_opaque;
  int n_list_entries;
  struct aentry *ae;

  if (need_arp(dp)) {
    if (run_arp(dp) == CONFD_ERR)
      return CONFD_ERR;
  }

  n_list_entries = dp->n_ae;
  ae = dp->arp_entries;
  
  if (next == -1) {
    pos = 0; /* ConfD wants to get the whole table / list from the beginning */
  } else if (next < n_list_entries) {
    pos = next; /* ConfD wants the table / list from a specific index */
  } else { /* next == n_list_entries, return nothing */
    confd_data_reply_next_object_array(tctx, NULL, -1, -1);
    return CONFD_OK;
  }

  /* If we have more than say 100 objects for smaller objects to return, we chunk the reply to ConfD in 100 objects at a time for best possible performance.       
  */
  if (n_list_entries - pos > max_nobjs) {
    nobj = max_nobjs + pos;
  } else {
    nobj = n_list_entries;
  }

  for(i = 0; i < pos; i++)
    ae = ae->next;
  
  obj = malloc(sizeof(struct confd_next_object) * (nobj + 2));
  v = (confd_value_t *) malloc(sizeof(confd_value_t) * nobj * N_LEAFS);

  for (i = 0; pos + i < nobj; i++) { /* Collect all the rows in the table / list entries */
    obj[i].v = &v[i * N_LEAFS];

    CONFD_SET_IPV4(&(obj[i].v[0]), ae->ip4);
    CONFD_SET_STR(&(obj[i].v[1]), ae->iface);
    if (ae->hwaddr == NULL) {
      CONFD_SET_NOEXISTS(&(obj[i].v[2]));
    } else {
      CONFD_SET_STR(&(obj[i].v[2]), ae->hwaddr);
    }
    CONFD_SET_BOOL(&(obj[i].v[3]), ae->perm);
    CONFD_SET_BOOL(&(obj[i].v[4]), ae->pub);
    obj[i].n = N_LEAFS;
    /* If we pass -1 here we will invoke find_next_object. If we instead pass next object ConfD will invoke get_next_object if there are more objects to be fetched
    */
    obj[i].next = pos+i+1; //-1;
    ae=ae->next;
  }
  if (pos + i == n_list_entries)
    obj[i++].v = NULL;
  confd_data_reply_next_object_arrays(tctx, obj, i, 0); /* Reply with an array of object arrays. I.e. the whole table / list */
  free(v);
  free(obj);
  return CONFD_OK;
}

Why we need to apply nobj+2 , i think it should be nobj+1, the tv of last instance is filled with null as end indication.

Why i get error: non-unique key values: {01:02:03:04:05:02} ?

Correct, it should say “1” there. Thanks!

Don’t know with that little info on the error you get. Check the run_arp() function of the example and verify that it parses the ARP table in your system correctly after calling “arp -an”

I have been wanting to provide a tag value array variant to this post for a while now as I belive it is more easy to follow than a plain value array. It does the same thing but use the tag too. The memory and performance overhead of using tag_value instead of value is negligible here, while the advantage is slightly improved readability:

// NOTE: See the original post for the other changes needed to the original (here ConfD-6.7.1) arpstat.c file 
...
static int navals;
static int max_nobjs = 100; // Chunk size hardcoded here to simplify

static int get_next_object_tag(struct confd_trans_ctx *tctx,
			     confd_hkeypath_t *keypath, long next)
{
  int i;
  confd_tag_value_t *tv;
  struct confd_tag_next_object *tobj;
  struct arpdata *dp = tctx->t_opaque;
  struct aentry *curr;

  if (next == -1) {  /* first call */
    if (need_arp(dp)) {
      if (run_arp(dp) == CONFD_ERR)
	return CONFD_ERR;
    }
    curr = dp->arp_entries;
  } else {
    curr = (struct aentry *)next;
  }
  if (curr == NULL) {
    confd_data_reply_next_key(tctx, NULL, -1, -1);
    return CONFD_OK;
  }

  tobj = malloc(sizeof(struct confd_tag_next_object) * (max_nobjs + 1));
  tv = (confd_tag_value_t *) malloc(sizeof(confd_tag_value_t) * max_nobjs * navals);
  
  for (i = 0; curr != NULL && i < max_nobjs; curr = curr->next, i++) { /* Collect max_nobjs or as many as there is rows in the table / list entries */
    tobj[i].tv = &tv[i * navals];

    CONFD_SET_TAG_IPV4(&(tobj[i].tv[0]), arpe_ip, curr->ip4);
    CONFD_SET_TAG_STR(&(tobj[i].tv[1]), arpe_ifname, curr->iface);
    if (curr->hwaddr == NULL) {
      CONFD_SET_TAG_NOEXISTS(&(tobj[i].tv[2]), arpe_hwaddr);
    } else {
      CONFD_SET_TAG_STR(&(tobj[i].tv[2]), arpe_hwaddr, curr->hwaddr);
    }
    CONFD_SET_TAG_BOOL(&(tobj[i].tv[3]), arpe_permanent, curr->perm);
    CONFD_SET_TAG_BOOL(&(tobj[i].tv[4]), arpe_published, curr->pub);
    tobj[i].n = navals;
    tobj[i].next = (long)curr->next;
  }
  
  if (curr == NULL) {
    tobj[i++].tv = NULL;
  }
  
  confd_data_reply_next_object_tag_value_arrays(tctx, tobj, i, 0); /* Reply with an array of object arrays. I.e. the whole table / list */
  free(tv);
  free(tobj);
  return CONFD_OK;
}

int main(int argc, char *argv[])
{
...
    data.get_next_object = get_next_object_tag;
...
    if (confd_load_schemas((struct sockaddr*)&addr,
                           sizeof (struct sockaddr_in)) != CONFD_OK)
        confd_fatal("Failed to load schemas from confd\n");

    object = confd_cs_node_cd(NULL, "/arpe:arpentries/arpe");
    navals = confd_max_object_size(object);
...
}

Thanks . i addressed the issue, i use the same memory for different key, i found before calling confd_reply_XX, it just point to the memory.

I’m wondering that what will happen if the size of list in response is larger than the request one.i mean CONFD reply more lists(for example: 3 instances) , however we just ask 2 instances. it seems that CONFD does not know how many lists now is requesting