Python confd API class Subscriber (TwoPhaseSubscriber)

I run into trouble using the Python Class cdb Subscriber.

internal error (18): confd_internal.c(2740): Unexpected data on socket! 33 -2090335124

Traceback (most recent call last):
  File "/home/elcon/confd/confd_bin/src/confd/pyapi/confd/cdb.py", line 253, in _read_sub_socket
    points = cdb.read_subscription_socket(self.sock)
_confd.error.Error: internal error (18): confd_internal.c(2740): Unexpected data on socket! 33 -2090335124

When comparing the Python Class with the example cdb_subscription → iter_python -->cdbl.py,
the main difference is the usage of a single socket for subscription and data in the Python class Subscriber.
This might be leading to the issue “Unexpected data on socket”.

Why is such a difference in the confd API and the examples?
Am I right that the PyAPI is official part of the confd software?

The ConfD Python API is an official ConfD API, just as the C and Java API is. Do not use the same socket for subscription and data. Use two separate sockets.

Ok.
But the python class delivered as API, uses only one socket for both. So when I’am using this class, which is the official API, I run into an error. The high level python API (confd) is broken. Only the low level python API (_confd) works as expected.

Here the complete code which is triggering the issue:

import confd
import _confd
import _confd.cdb as cdb
import dhcpd_ns

from confd.cdb import Subscriber

class MyIter(object):
    def iterate(kp, op, oldv, newv, state):
        return confd.ITER_CONTINUE

def run():
    _confd.set_debug(_confd.PROTO_TRACE, sys.stderr)
    # Setup subscription
    sub2 = Subscriber(name="Sub2", host='127.0.0.1',  port=_confd.CONFD_PORT)
    point = sub2.register(path='/', iter_obj=MyIter, priority=10)
    sub2.start()
    sub2.run()


if __name__ == "__main__":
    run()
TRACE Connected (cdb) to ConfD                                                                                                                                                                             
TRACE CDB_SUBSCRIBE                                                                                                                                                                                        
 14-Jun-2023::21:11:06.361 227670/7f2f28676000/4 SEND op=32 isrel=0 th=-1 {1,0,0,10,0,[]}                                                                                                                  
 14-Jun-2023::21:11:06.361 227670/7f2f28676000/4 GOT 7                                                                                                                                                     
 --> CONFD_OK                                                                                                                                                                                              
TRACE CDB_SUBSCRIBE_DONE  --> CONFD_OK                                                                                                                                                                     
 14-Jun-2023::21:11:14.240 227670/7f2f28676000/4 GOT {2,1,[7]}                                                                                                                                             
TRACE CDB_SUBSCRIPTION_EVENT --> 7
TRACE CDB_SUB_ITERATE 7
INTERNAL ERROR: confd_internal.c(2740): Unexpected data on socket! 33 -2090335124

internal error (18): confd_internal.c(2740): Unexpected data on socket! 33 -2090335124

Traceback (most recent call last):
  File "/home/elcon/confd/confd_bin/src/confd/pyapi/confd/cdb.py", line 253, in _read_sub_socket
    points = cdb.read_subscription_socket(self.sock)
_confd.error.Error: internal error (18): confd_internal.c(2740): Unexpected data on socket! 33 -2090335124

Skip the sub2.run()
A high level example:

import logging
import confd as tm
from confd.maapi import Maapi

class MyIter:
    def __init__(self, log):
        self.log = log

    def iterate(self, kp, op, oldv, newv, state):
        self.log.info(f'iterate kp={kp} op={op} oldv={oldv} newv={newv}')
        return tm.ITER_CONTINUE


def run():
    log = tm.log.Log(logging.getLogger(__name__))
    sub = tm.cdb.Subscriber(log=log)
    sub.register('/', MyIter(log), priority=10)
    sub.start()


def load_schemas():
    with Maapi():
        pass


if __name__ == '__main__':
    load_schemas()
    logging.basicConfig(level=logging.INFO, filename='mysub.log',
        format='%(asctime)s %(levelname)-8s %(message)s')
    run()

Ok, I see my mistake. I must not call Subscriber.run, it’s called by Threading.
Thank You.

Hi,

I have a new issue when iterating over huge amount of data.

class MyIter(object):
    count = 0
    def __init__(log):
        self.log = log
        MyIter.count = 0

    def iterate(kp, op, oldv, newv, state):
        log.info(f'iterate kp={kp} op={op} oldv={oldv} newv={newv}\t\t{MyIter.count}')
        log.info(f'{MyIter.count} {sys.getrefcount(None)}')
        MyIter.count += 1
        return _confd.ITER_RECURSE

def load_schemas():
    with Maapi():
        pass

def run():
    #_confd.set_debug(_confd.PROTO_TRACE, sys.stderr)
    # Setup subscription
    sub2 = Subscriber(name="Sub2", host='127.0.0.1',  port=_confd.CONFD_PORT)
    point = sub2.register(path='/', iter_obj=MyIter, priority=10)
    for i in range (1,1000000):
        l.append(None)
    sub2.start()


if __name__ == "__main__":
    run()

It looks like the pyapi lacks a correct reference counting. The py_none ref count is decreasing each iterator loop.
When the ref count is reching 0 an error will be riased:

Fatal Python error: none_dealloc: deallocating None
Python runtime state: initialized

Current thread 0x00007fe4805ff640 (most recent call first):
  File "pyapi/confd/cdb.py", line 271 in _read_sub_socket
  File "pyapi/confd/cdb.py", line 211 in run
  File "/usr/lib/python3.10/threading.py", line 1016 in _bootstrap_inner
  File "/usr/lib/python3.10/threading.py", line 973 in _bootstrap

Thread 0x00007fe4829c5000 (most recent call first):
  File "/usr/lib/python3.10/threading.py", line 1567 in _shutdown

Hi,
I do not follow what you are doing here and what goes wrong

I’am using the example with the class MyIter, to iterate over all changes. The Yang model doesn’t matter in this case. With netconf edit-config I write a lot of data into the confd database.
The commit is triggering the MyIter callback many times (in my case 300000 times).

After round about 5000 callbacks Python crashes with the error message.
My research showed, that the reference counter to the Python object “None” is gone to 0. This triggeres the deletion of the None object in Python and this isn’t allowed.
To avoid this issue the PyAPI does something like this:
Py_INCREF(Py_None); in pyapi/src/_cdb.c::static enum cdb_iter_ret iterator

I hope this explains it a bit.

There is indeed an issue there. The state object (opaque) that can optionally be passed to the iterate method will be None if pre_iterate() is not implemented. But in _cdb.c the reference counter is only increased if the object/opaque is NULL, not Py_None.

You can make the correction yourself if you wish:

$ diff -u _cdb.c _cdb.c.orig
--- _cdb.c	2023-06-28 12:33:27.000000000 +0200
+++ _cdb.c.orig	2023-06-28 12:33:19.000000000 +0200
@@ -1273,7 +1273,7 @@
         pynewv = (PyObject*)PyConfd_Value_New_DupTo(newv);
     }
 
-    if (state->opaque) {
+    if (state->opaque != Py_None && state->opaque) {
         pystate = state->opaque;
     } else {
         Py_INCREF(Py_None);

If you don’t want to rebuild the ConfD Python API with the above fix, you can just add pre_iterate() and return something other than None as the state object as a workaround. Example:

class MyIter(object):
    def pre_iterate(self):
        return []

    def iterate(self, kp, op, oldv, newv, state):
        ...

Thanks!

1 Like

Thank you.
The workaround work fine.
However recompiling doesn’t.
I tried to build the pyapi with make confd-py3.
But I’ll get:

Traceback (most recent call last):                                                                                                                                                                         
  File "intro/python/1-2-3-start-query-model/dhcpd_conf.py", line 23, in <module>                                                                       
    import confd                                                                                                                                                                                             File "pyapi/confd/__init__.py", line 26, in <module>                                                                                                               
    import _confd                                                                                                                                                                                          
  File "pyapi/_confd/__init__.py", line 12, in <module>                                                                                                              
    from ._confd_py3 import cdb                                                                                                                                                                            
ImportError: libconfd.so: cannot open shared object file: No such file or directory                                                                                                                       

What’s missing?

Are you rebuilding the API on macOS arm64?
In any case, if you hit that issue, make, for example, the change below and rebuild the ConfD Python API again:

$CONFD_DIR/src/confd/pyapi$ diff -u Makefile Makefile.orig
--- Makefile.orig
+++ Makefile
@@ -176,7 +176,7 @@
 endif
 endif

-CONFD_SO_RPATH_BASE=../../confd_dir/lib
+CONFD_SO_RPATH_BASE=../../../lib

Now I can build it. Thank you!

1 Like