noirello / bonsai

Simple Python 3 module for LDAP, using libldap2 and winldap C libraries.
MIT License
117 stars 33 forks source link

Memmory leak #19

Closed velopokatun closed 6 years ago

velopokatun commented 6 years ago

Good afternoon. I use a wonderful bonsai package with an asynchronous rpc aiomas server based on asyncio. I encountered a memory leak problem. When rpc works and LDAPClient () connect.search () is continually invoked, the system memory is noticeably consumed, without releasing it. This problem is only observed when I use the bonsai package. In the same cyclic call, for example other methods such as aiohttp.ClientSession, such a problem is not observed. The problem with the leak is well observed when calling tracemalloc. In the example, I showed what the problem is. If you use infinite call the pps method test, there is no such problem. If you call user_exist memory, it is absorbed before your eyes.

Tracemalloc before cycling call user_exist

[109] - Top 10 lines
[115] - #1: python3.6/_weakrefset.py:37: 5.2 KiB
[118] -     self.data = set()
[115] - #2: logging/__init__.py:1059: 5.1 KiB
[118] -     return open(self.baseFilename, self.mode, encoding=self.encoding)
[115] - #3: python3.6/_weakrefset.py:48: 4.4 KiB
[118] -     self._iterating = set()
[115] - #4: python3.6/_weakrefset.py:38: 4.2 KiB
[118] -     def _remove(item, selfref=ref(self)):
[115] - #5: python3.6/sre_parse.py:416: 3.8 KiB
[118] -     not nested and not items))
[115] - #6: asyncio/events.py:145: 2.9 KiB
[118] -     self._callback(*self._args)
[115] - #7: python3.6/sre_parse.py:112: 2.9 KiB
[118] -     self.pattern = pattern
[115] - #8: python3.6/sre_parse.py:623: 2.8 KiB
[118] -     subpattern[-1] = (MAX_REPEAT, (min, max, item))
[115] - #9: python3.6/_weakrefset.py:84: 2.7 KiB
[118] -     self.data.add(ref(item, self._remove))
[115] - #10: rpc-server.py:211: 2.1 KiB
[118] -     class RPCServer(Test, UserGroup):
[123] - 288 other: 115.1 KiB
[125] - Total allocated size: 151.3 KiB

Tracemalloc reading after 20 min of cyclic call

[109] - Top 10 lines
[115] - #1: bonsai/ldapentry.py:18: 69086.4 KiB
[118] -     super().__init__(str(dn), conn)
[115] - #2: python3.6/socket.py:489: 55269.7 KiB
[118] -     a = socket(family, type, proto, a.detach())
[115] - #3: python3.6/socket.py:490: 55269.2 KiB
[118] -     b = socket(family, type, proto, b.detach())
[115] - #4: python3.6/selectors.py:32: 27604.6 KiB
[118] -     if isinstance(fileobj, int):
[115] - #5: python3.6/selectors.py:457: 6151.9 KiB
[118] -     ready.append((key, events & key.events))
[115] - #6: python3.6/selectors.py:445: 3167.6 KiB
[118] -     fd_event_list = self._epoll.poll(timeout, max_ev)
[115] - #7: bonsai/ldapconnection.py:101: 1283.8 KiB
[118] -     return self._evaluate(super().open(), timeout)
[115] - #8: python3.6/_weakrefset.py:84: 563.7 KiB
[118] -     self.data.add(ref(item, self._remove))
[115] - #9: python3.6/linecache.py:137: 555.1 KiB
[118] -     lines = fp.readlines()
[115] - #10: asyncio/locks.py:221: 218.1 KiB
[118] -     self._waiters = collections.deque()
[123] - 399 other: 2285.5 KiB
[125] - Total allocated size: 221455.4 KiB

Here are the system readings before launch

VIRT   RES    SHR 
155М   29388  10192

Here are the system readings after half an hour of work

VIRT   RES    SHR 
978М   586M   10696

Нere is a simple server :

def display_top(snapshot, key_type='lineno', limit=10):
    snapshot = snapshot.filter_traces((
        tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
        tracemalloc.Filter(False, "<unknown>"),
    ))
    top_stats = snapshot.statistics(key_type)

    tracelog.info("------- Trace --------")
    tracelog.info("Top %s lines" % limit)
    for index, stat in enumerate(top_stats[:limit], 1):
        frame = stat.traceback[0]
        # replace "/path/to/module/file.py" with "module/file.py"
        filename = os.sep.join(frame.filename.split(os.sep)[-2:])
        tracelog.info("#%s: %s:%s: %.1f KiB"
                      % (index, filename, frame.lineno, stat.size / 1024))
        line = linecache.getline(frame.filename, frame.lineno).strip()
        if line:
            tracelog.info('    %s' % line)

    other = top_stats[limit:]
    if other:
        size = sum(stat.size for stat in other)
        tracelog.info("%s other: %.1f KiB" % (len(other), size / 1024))
    total = sum(stat.size for stat in top_stats)
    tracelog.info("Total allocated size: %.1f KiB" % (total / 1024))
    tracelog.info("-------- End ---------")

class RPCServer(Test, UserGroup):
    def __init__(self, lp, ldap):
        ldap_conf.update(ldap)

        ldap_url = "ldap://{}:{}".format(ldap_conf['host'],
                                         ldap_conf['port'])
        self.ldap_cli = LDAPClient(ldap_url)
        self.ldap_cli.set_credentials("SIMPLE",
                                 (ldap_conf['user'], ldap_conf['pswd']))
    router = aiomas.rpc.Service()

    @aiomas.expose
    async def test(self, args):
        async with ClientSession() as session:
            try:
                with timeout(9999):
                    async with session.get('http://python.org/') as r:
                        await r.text()
                        logging.info('test -->')
                        return dict(status=True, info='ok')

            except BaseException as e:
                return dict(status=False, info=str(e))
            finally:
                logging.info('test <--')

    @aiomas.expose
    async def trace(self):
        try:
            logging.info('trace -->')

            snapshot = tracemalloc.take_snapshot()
            display_top(snapshot)
            await asyncio.sleep(0.1)
        except BaseException as b:
            tracelog.error("Error: {}".format(b))
        finally:
            logging.info('trace <--')

            return dict(status=True, info='Done')

    @aiomas.expose
    async def user_exist(self, args):
        logging.info('--> user_exist')
        request = args.get('request')
        s_scope = args.get('s_scope', 2)
        base_dn = args.get('base_dn', 'dc=test,dc=wt')

        if not request:
            logging.info('<-- Required arguments: request')
            return dict(status=False, info='Required arguments: request')

        async with self.ldap_cli.connect(is_async=True,
                                         timeout=5) as conn:
            res = await conn.search(base_dn, s_scope, request)
            logging.info('<-- Res: {}'.format(res))
            if res:
                return dict(status=True, info=res)
            return dict(status=False, info='User does not exist')

if __name__ == '__main__':
    logging.info(' *** Begin *** ')
    loop = asyncio.get_event_loop()

    serv = RPCServer(lp=loop, ldap=conf['ldap'])
    try:
        server_rpc = aiomas.run(
            aiomas.rpc.start_server(
                ('127.0.0.1', 5000),
                serv,
            ),
        )

        aiomas.run(until=server_rpc.wait_closed())
    except BaseException as servexpt:
        logging.info('Core server brake: {}'.format(servexpt))

Client :


async def user_exist(user):
    rpc_con = await aiomas.rpc.open_connection(('127.0.0.1', 5000))
    res = await rpc_con.remote.user_exist(args=dict(base_dn='....',request='cn=...'))
    await rpc_con.close()
    return res

async def test():
    rpc_con = await aiomas.rpc.open_connection(('127.0.0.1', 5000))
    res = await rpc_con.remote.call(method='test', args=dict(verbosity=1))
    await rpc_con.close()
    return res

async def trace():
    rpc_con = await aiomas.rpc.open_connection(('127.0.0.1', 5000))
    res = await rpc_con.remote.trace()
    await rpc_con.close()
    return res

async def get_result(tasks):
    for i in asyncio.as_completed(tasks):
        res = await i
        print("Response: {}".format(res))

user = '52fce218-ba2d-46d9-bda2-0d166a6af80d'
users = ['','','','']

loop = asyncio.get_event_loop()

while True:
    coros = [user_exist(i) for i in users]
    loop.run_until_complete(get_result(coros))
noirello commented 6 years ago

Thank you for the very detailed description about the issue, I'll look into it as soon as possible.

kelewind commented 6 years ago

I allow myself to add to the description

As a result, when working under load, we get a similar picture:

root@rpc-server:/home/deploy# netstat -A inet -p | grep 10.14.194.33:ldap | wc -l
3527
root@rpc-server:/home/deploy# netstat -A inet -p | grep 10.14.194.33:ldap | wc -l
4162
tcp        0      0 rpc-server.privat:15103 10.14.194.33:ldap       TIME_WAIT   -
tcp        0      0 rpc-server.privat:17079 10.14.194.33:ldap       TIME_WAIT   -
tcp        0      0 rpc-server.privat:17259 10.14.194.33:ldap       TIME_WAIT   -
tcp        0      0 rpc-server.privat:16528 10.14.194.33:ldap       TIME_WAIT   -
tcp        0      0 rpc-server.privat:15572 10.14.194.33:ldap       TIME_WAIT   -
tcp        0      0 rpc-server.privat:14267 10.14.194.33:ldap       TIME_WAIT   -
tcp        0      0 rpc-server.privat:17721 10.14.194.33:ldap       TIME_WAIT   -
tcp        0      0 rpc-server.privat:17327 10.14.194.33:ldap       TIME_WAIT   -
tcp        0      0 rpc-server.privat:14883 10.14.194.33:ldap       TIME_WAIT   -
tcp        0      0 rpc-server.privat:16274 10.14.194.33:ldap       TIME_WAIT   -
tcp        0      0 rpc-server.privat:15095 10.14.194.33:ldap       TIME_WAIT   -
tcp        0      0 rpc-server.privat:16585 10.14.194.33:ldap       TIME_WAIT   -
tcp        0      0 rpc-server.privat:17967 10.14.194.33:ldap       TIME_WAIT   -
kelewind commented 6 years ago

Those in addition to the memory leak described above (maybe these things are related)

When accessing the LDAP database at level 500 ++ rps we see a similar picture. I tried to put a reverse proxy ahead - but the result is the same (TIME_WAIT to proxy). Memory leak from this did not pass - open connections are just another observation about the problems of the package.

For example

root@rpc-server:/home/deploy# netstat -A inet -p | grep radius
tcp        0    149 rpc-server.privat:46277 10.14.194.36:ldap       ESTABLISHED 1424/freeradius
tcp        0      0 rpc-server.privat:42011 10.14.194.36:ldap       ESTABLISHED 1424/freeradius
tcp        0      0 rpc-server.privat:33376 10.14.194.36:ldap       ESTABLISHED 1424/freeradius
tcp        0      0 rpc-server.privati:9850 10.14.194.36:ldap       ESTABLISHED 1424/freeradius
tcp        0      0 rpc-server.privat:45671 10.14.194.36:ldap       ESTABLISHED 1424/freeradius
tcp        0      0 rpc-server.privat:30817 10.14.194.36:ldap       ESTABLISHED 1424/freeradius
tcp        0      0 rpc-server.privat:44026 10.14.194.36:ldap       ESTABLISHED 1424/freeradius
tcp        0      0 rpc-server.privat:42659 10.14.194.36:ldap       ESTABLISHED 1424/freeradius
tcp        0      0 rpc-server.privat:28149 10.14.194.36:ldap       ESTABLISHED 1424/freeradius
tcp        0      0 rpc-server.privat:32286 10.14.194.36:ldap       ESTABLISHED 1424/freeradius
tcp        0      0 rpc-server.privat:35643 10.14.194.36:ldap       ESTABLISHED 1424/freeradius
tcp        0    262 rpc-server.privat:22515 10.14.194.36:ldap       ESTABLISHED 1424/freeradius

this is how freeradius works with LDAP

noirello commented 6 years ago

The memory leaking issue has been reproduced, the leak of LDAPEntry seems to be solved with commit 39d1175.

I'm still investigating the leak that's caused by the socketpairs, this could be related to the open connections.

socketpair commented 6 years ago

caused by the socketpairs

Who called me ?! :))

velopokatun commented 6 years ago

It is very good. But there is a problem. I uploaded the bonsai source code, made sure that the src / ldapentry.c file with the new changes , removed the old package installed with pip, and compiled a new one from the source code according to the instructions. Everything was compiled without errors. After the tests I saw how the memory continued to flow. So it did not solve the problem (

noirello commented 6 years ago

@velopokatun: I've added a new commit that solves the leaking socketpair (8fecb3c7c67). The two patches should solve the issue, but that'd be great if you could verify it.

@kelewind: About the TIME_WAIT connections, it seems like that it's not related to the leaks. I looked around a little and it might be normal. But if you still thinks it's worth to investigate further, it would be better to open a separate ticket for it.

velopokatun commented 6 years ago

Thanks. Checked. But still the problem remained. Here is the detailed trace. It is visible that the leak in ldapentry disappeared, but why would there be a problem in aioconnection.py?

[114] - Top 10 lines                                                                                                                                                               
[120] - #1: asyncio/aioconnection.py:39: 70512.2 KiB                                                                                                                               
[123] -     res = super().get_result(msg_id)                                                                                                                                       
[120] - #2: bonsai/ldapconnection.py:47: 1287.3 KiB                                                                                                                                
[123] -     return self._evaluate(super().open(), timeout)                                                                                                                         
[120] - #3: python3.6/linecache.py:137: 277.4 KiB                                                                                                                                  
[123] -     lines = fp.readlines()                                                                                                                                                 
[120] - #4: python3.6/_weakrefset.py:84: 148.1 KiB                                                                                                                                 
[123] -     self.data.add(ref(item, self._remove))                                                                                                                                 
[120] - #5: json/decoder.py:355: 110.1 KiB                                                                                                                                         
[123] -     obj, end = self.scan_once(s, idx)                                                                                                                                      
[120] - #6: aiomas/rpc.py:121: 96.9 KiB                                                                                                                                            
[123] -     loop.create_task(_handle_request(request, loop, router))                                                                                                               
[120] - #7: asyncio/locks.py:221: 74.8 KiB                                                                                                                                         
[123] -     self._waiters = collections.deque()                                                                                                                                    
[120] - #8: asyncio/base_events.py:284: 72.8 KiB                                                                                                                                   
[123] -     task = tasks.Task(coro, loop=self)                                                                                                                                     
[120] - #9: asyncio/queues.py:59: 64.1 KiB                                                                                                                                         
[123] -     self._queue = collections.deque()                                                                                                                                      
[120] - #10: asyncio/queues.py:50: 64.1 KiB                                                                                                                                        
[123] -     self._putters = collections.deque()                                                                                                                                    
[128] - 349 other: 984.6 KiB                                                                                                                                                       
[130] - Total allocated size: 73692.4 KiB                                                                                                                                          
[131] - -------- End ---------  
noirello commented 6 years ago

That's weird indeed. We should have seen this leak from the previous traces.

I couldn't reproduce this output. My Top 10 wasn't contain the asyncio/aioconnection.py:39 line, it started from bonsai/ldapconnection.py:47, memory consumption stayed almost constant (around 2500 KiB) after a 2.5h run.

Which OS do you use for this trace?

kelewind commented 6 years ago

Debian 8

noirello commented 6 years ago

Ok, I'm trying to reduce the necessary code for reproducing the leak. So far I've come up with this:

import asyncio
import logging
import sys
import os
import tracemalloc
import linecache
import time

import bonsai

tracemalloc.start(25)

logging.basicConfig(stream=sys.stdout, level=logging.INFO)

LDAP_URL = "ldap://172.17.0.2"
BASE_DN = "ou=nerdherd,dc=bonsai,dc=test"

def display_top(snapshot, key_type='lineno', limit=10):
    snapshot = snapshot.filter_traces((
        tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
        tracemalloc.Filter(False, "<unknown>"),
    ))
    top_stats = snapshot.statistics(key_type)

    logging.info("------- Trace --------")
    logging.info("Top %s lines" % limit)
    for index, stat in enumerate(top_stats[:limit], 1):
        frame = stat.traceback[0]
        # replace "/path/to/module/file.py" with "module/file.py"
        filename = os.sep.join(frame.filename.split(os.sep)[-2:])
        logging.info("#%s: %s:%s: %.1f KiB"
                      % (index, filename, frame.lineno, stat.size / 1024))
        line = linecache.getline(frame.filename, frame.lineno).strip()
        if line:
            logging.info('    %s' % line)

    other = top_stats[limit:]
    if other:
        size = sum(stat.size for stat in other)
        logging.info("%s other: %.1f KiB" % (len(other), size / 1024))
    total = sum(stat.size for stat in top_stats)
    logging.info("Total allocated size: %.1f KiB" % (total / 1024))
    logging.info("-------- End ---------")

async def query_ldap_server(ldap_cli):
    async with ldap_cli.connect(is_async=True, timeout=5) as conn:
        results = await conn.search(BASE_DN, bonsai.LDAPSearchScope.ONE)
        for res in results:
            logging.info('Res DN: {}'.format(res.dn))

loop = asyncio.get_event_loop()
ldap_cli = bonsai.LDAPClient(LDAP_URL)

start_time = time.time()
while time.time() <= start_time + 900:
    loop.run_until_complete(query_ldap_server(ldap_cli))

snapshot = tracemalloc.take_snapshot()
display_top(snapshot)

With this code, I got the following result running against the current head of the master branch (0.9.1 release):

INFO:root:------- Trace --------
INFO:root:Top 10 lines
INFO:root:#1: bonsai/ldapentry.py:18: 10754.8 KiB
INFO:root:#2: python3.6/socket.py:489: 1516.6 KiB
INFO:root:    a = socket(family, type, proto, a.detach())
INFO:root:#3: python3.6/socket.py:490: 1516.1 KiB
INFO:root:    b = socket(family, type, proto, b.detach())
INFO:root:#4: python3.6/selectors.py:32: 980.2 KiB
INFO:root:    if isinstance(fileobj, int):
INFO:root:#5: python3.6/re.py:311: 4.5 KiB
INFO:root:    _cache[type(pattern), pattern, flags] = p, loc
INFO:root:#6: asyncio/aioconnection.py:39: 4.2 KiB
INFO:root:#7: bonsai/ldapconnection.py:101: 3.8 KiB
INFO:root:#8: python3.6/sre_parse.py:416: 3.8 KiB
INFO:root:    not nested and not items))
INFO:root:#9: bonsai/ldapvaluelist.py:22: 2.4 KiB
INFO:root:#10: bonsai/ldapvaluelist.py:21: 2.2 KiB
INFO:root:167 other: 72.3 KiB
INFO:root:Total allocated size: 14860.9 KiB
INFO:root:-------- End ---------

And after applying the two commits on the master branch:

INFO:root:------- Trace --------
INFO:root:Top 10 lines
INFO:root:#1: python3.6/re.py:311: 4.5 KiB
INFO:root:    _cache[type(pattern), pattern, flags] = p, loc
INFO:root:#2: asyncio/aioconnection.py:39: 4.2 KiB
INFO:root:#3: bonsai/ldapconnection.py:101: 3.8 KiB
INFO:root:#4: python3.6/sre_parse.py:416: 3.8 KiB
INFO:root:    not nested and not items))
INFO:root:#5: bonsai/ldapvaluelist.py:22: 2.4 KiB
INFO:root:#6: bonsai/ldapvaluelist.py:21: 2.2 KiB
INFO:root:#7: python3.6/sre_compile.py:579: 2.0 KiB
INFO:root:    groupindex, indexgroup
INFO:root:#8: asyncio/events.py:145: 1.6 KiB
INFO:root:    self._callback(*self._args)
INFO:root:#9: ../test_leak.py:50: 1.5 KiB
INFO:root:    logging.info('Res DN: {}'.format(res.dn))
INFO:root:#10: asyncio/unix_events.py:57: 1.4 KiB
INFO:root:    self._signal_handlers = {}
INFO:root:161 other: 66.6 KiB
INFO:root:Total allocated size: 94.1 KiB
INFO:root:-------- End ---------

I used the testing LDAP server (that can be found in the repository) running in docker container on a Fedora 28. I'm planning to check out on Debian 8 as well (probably using a container).

That would be really helpful If you could check this code with your system, maybe point out some issues that my code reduction has caused.

kelewind commented 6 years ago

I'm sorry, maybe we are not the corrert version of the package installed after the change?

Can you give an exact reference to the branch from which to compile the package? in the master very old commits

And after applying the two commits on the master branch:

https://github.com/noirello/bonsai/commits/master

kelewind commented 6 years ago

I mean what branch we should download with all the changes?

noirello commented 6 years ago

Of course, the refactor branch has the mentioned two commits with some other ongoing refactorizations.

kelewind commented 6 years ago

Then it is unclear 1) We make a package from this branch 2) Performed tests on KVM virtualization (not containers)

Perhaps this is related to the installation method (compiler version for example)? You can publish the version of the package, so that it can be installed via pip install that we could install it in this way and exclude the above-described problems?

noirello commented 6 years ago

I don't think that there's much of a leeway with the installation method in this case:

Even if I publish a new version, you'll still have to compile the package on your computer when using Linux (no precompiled wheel is available).

But I might be able to test it with the distribution that you're using, or even send the compiled files, but I have to know which Linux distribution version and which Python version do you have exactly.

You mentioned earlier that you are using Debian 8, but the deb repository only have Python 3.4.2 by default. Do you use a testing repository or you compiled the Python interpreter manually?

kelewind commented 6 years ago

About python version

3.6.5 - self compiled from source

root@python-pip:/home/deploy# lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description:    Debian GNU/Linux 8.9 (jessie)
Release:        8.9
Codename:       jessie
noirello commented 6 years ago

I built the package from the current state of the refactor branch in a Debian 8.9 based Docker container: bonsai-1.0.0a1.linux-x86_64.tar.gz

I also ran the above described test code from the container, and I've got a same, constant memory allocation after a 15 minutes and a 30 minutes long test run.

Did you get the chance to run the test code on your system? It would be nice if I could compare your results with mine.

velopokatun commented 6 years ago

Checked on your example. But again there were questions. Actually in your example everything works fine if I do not do anything after results = await conn.search (BASE_DN, bonsai.LDAPSearchScope.ONE). But if I continue to work on the answer, I get a leak, which is clear from the traces.

INFO:root:------- Trace --------
INFO:root:Top 10 lines
INFO:root:#1: asyncio/aioconnection.py:39: 14097.0 KiB
INFO:root:    res = super().get_result(msg_id)
INFO:root:#2: bonsai/ldapconnection.py:47: 4.0 KiB
INFO:root:    return self._evaluate(super().open(), timeout)
INFO:root:#3: python3.6/sre_parse.py:416: 3.8 KiB
INFO:root:    not nested and not items))
INFO:root:#4: python3.6/sre_parse.py:112: 2.7 KiB
INFO:root:    self.pattern = pattern
INFO:root:#5: python3.6/sre_compile.py:579: 2.0 KiB
INFO:root:    groupindex, indexgroup
INFO:root:#6: bonsai_test.py:201: 2.0 KiB
INFO:root:    print(loop.run_until_complete(t_class.query_ldap_server(random.choice(users))))
INFO:root:#7: bonsai_test.py:68: 1.8 KiB
INFO:root:    class Test(Bon):
INFO:root:#8: asyncio/events.py:145: 1.6 KiB
INFO:root:    self._callback(*self._args)
INFO:root:#9: bonsai_test.py:54: 1.4 KiB
INFO:root:    class Bon:
INFO:root:#10: asyncio/unix_events.py:57: 1.4 KiB
INFO:root:    self._signal_handlers = {}
INFO:root:149 other: 58.4 KiB
INFO:root:Total allocated size: 14176.1 KiB
INFO:root:-------- End ---------

def display_top(snapshot, key_type='lineno', limit=10):
    snapshot = snapshot.filter_traces((
        tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
        tracemalloc.Filter(False, "<unknown>"),
    ))
    top_stats = snapshot.statistics(key_type)

    logging.info("------- Trace --------")
    logging.info("Top %s lines" % limit)
    for index, stat in enumerate(top_stats[:limit], 1):
        frame = stat.traceback[0]
        # replace "/path/to/module/file.py" with "module/file.py"
        filename = os.sep.join(frame.filename.split(os.sep)[-2:])
        logging.info("#%s: %s:%s: %.1f KiB"
                     % (index, filename, frame.lineno, stat.size / 1024))
        line = linecache.getline(frame.filename, frame.lineno).strip()
        if line:
            logging.info('    %s' % line)

    other = top_stats[limit:]
    if other:
        size = sum(stat.size for stat in other)
        logging.info("%s other: %.1f KiB" % (len(other), size / 1024))
    total = sum(stat.size for stat in top_stats)
    logging.info("Total allocated size: %.1f KiB" % (total / 1024))
    logging.info("-------- End ---------")

class Bon:
    def __init__(self):
        self.ldap_cli = bonsai.LDAPClient("ldap://{}".format(host))
        self.ldap_cli.set_credentials("SIMPLE", (user, pswd))

    async def query_ldap_server(self, user):
        async with self.ldap_cli.connect(is_async=True, timeout=timeout) as conn:
            res = await conn.search(base_dn, 2, 'cn={}'.format(user))
            if res:
                return res[0]['radiusGroupType'][0]
            return False

t_class = Bon()

users = [
    '52fce218-ba2d-46d9-bda2-0d166a6af80d',
    '739e0d98-3e96-451b-b2de-07d21a679b5f',
    'f7725165-26ce-4f0a-a652-531a09e97b56',
]

loop = asyncio.get_event_loop()

start_time = time.time()
while time.time() <= start_time + 900:
    print(loop.run_until_complete(t_class.query_ldap_server(random.choice(users))))

snapshot = tracemalloc.take_snapshot()
display_top(snapshot)
noirello commented 6 years ago

Thank you, guys, I really appreciate the time and effort that you have invested in this.

I could reproduce it. I worked on it in the past few days, it seems like I've fixed several reference counting problems related to connection and simple search (and I'll continue to check other parts of the code as well), and now the total memory size after taking a snapshot reduced from 2.4k-3.2k KiB to 80 KiB.

At least in my dev environment, but we've been here before, so it's not taken for granted until you can confirm. So If you could be so kind and check the current state on the refactor branch, that'd be great.

kelewind commented 6 years ago

You can attach a build package to the ticket like last time? So we will check it much faster

noirello commented 6 years ago

Sure, here you go: bonsai-1.0.0a1.linux-x86_64.tar.gz