Memory Leak when using DirectorySearcher.FindAll()
Asked Answered
F

5

26

I have a long running process that needs to do a lot of queries on Active Directory quite often. For this purpose I have been using the System.DirectoryServices namespace, using the DirectorySearcher and DirectoryEntry classes. I have noticed a memory leak in the application.

It can be reproduced with this code:

while (true)
{
    using (var de = new DirectoryEntry("LDAP://hostname", "user", "pass"))
    {
        using (var mySearcher = new DirectorySearcher(de))
        {
            mySearcher.Filter = "(objectClass=domain)";
            using (SearchResultCollection src = mySearcher.FindAll())
            {
            }            
         }
    }
}

The documentation for these classes say that they will leak memory if Dispose() is not called. I have tried without dispose as well, it just leaks more memory in that case. I have tested this with both framework versions 2.0 and 4.0 Has anyone run into this before? Are there any workarounds?

Update: I tried running the code in another AppDomain, and it didn't seem to help either.

Fountainhead answered 12/4, 2011 at 7:46 Comment(14)
That is just to illustrate the problem, in the real application it is not like that, obviously.Fountainhead
How can you "noticed a memory leak in the application"?Exanimate
Unfortunately, using while(true)... at this place would prevent the DirectorySearcher and the DirectoryEntry being disposed correctly. You might want to put it at the first level of "using" instead, and check what happens.Gumshoe
actually I have tried that first, with same results. It gives the same result either way. Edited my code in the question though and moved the loop outside.Fountainhead
@Can Gencer - have you tried to inspect the memory usage with WinDebug and managed extensions? I have successfully troubleshooted memory leaks using it.Lifeboat
@Haplo, I've used WinDbg and also Ants profiler, and the problem seems to be in the unmanaged memory part, which keeps growing.Fountainhead
@Can Gencer - you say "in the real application it is not like that, obviously". What may not be so obvious is that we really can't offer any real clues unless we know what your real code is like.Karrikarrie
@Andrew, in my earlier code the while loop was around the findall(), which is not a very realistic scenario. The point of the code above is to illustrate the memory leak, which seems to happen everytime FindAll() is called. It is not meant to show a piece of the real application.Fountainhead
@Can Gencer - again... without seeing actual code, all we are doing is making wild guesses in the dark.Karrikarrie
@Andrew That is the actual code leaking the memory. You can run it, and it will leak.. If I can prevent the code above from leaking, then I can use it in my real code, which has a lot of extra code not relevant for this question, that is why I skipped it.Fountainhead
which of these objects is leaking? Did you find out by using SOS.dll f.e.?Pless
Ants profiler shows "unmanaged memory" as the part that is increasing. It is not directly something within the GC's reach..Fountainhead
I recall I hit this problem in the .net 2 days, you may be better of using the LDAP classes to talk to ActiveDirectory, rather then the old ADSI mess!Finkle
@Ian Ringrose, which LDAP classes are you talking about?Fountainhead
F
18

As strange as it may be, it seems that the memory leak only occurs if you don't do anything with the search results. Modifying the code in the question as follows does not leak any memory:

using (var src = mySearcher.FindAll())
{
   var enumerator = src.GetEnumerator();
   enumerator.MoveNext();
}

This seems to be caused by the internal searchObject field having lazy initialization , looking at SearchResultCollection with Reflector :

internal UnsafeNativeMethods.IDirectorySearch SearchObject
{
    get
    {
        if (this.searchObject == null)
        {
            this.searchObject = (UnsafeNativeMethods.IDirectorySearch) this.rootEntry.AdsObject;
        }
        return this.searchObject;
    }
}

The dispose will not close the unmanaged handle unless searchObject is initialized.

protected virtual void Dispose(bool disposing)
{
    if (!this.disposed)
    {
        if (((this.handle != IntPtr.Zero) && (this.searchObject != null)) && disposing)
        {
            this.searchObject.CloseSearchHandle(this.handle);
            this.handle = IntPtr.Zero;
        }
    ..
   }
}

Calling MoveNext on the ResultsEnumerator calls the SearchObject on the collection thus making sure it is disposed properly as well.

public bool MoveNext()
{
  ..
  int firstRow = this.results.SearchObject.GetFirstRow(this.results.Handle);
  ..
}

The leak in my application was due to some other unmanaged buffer not being released properly and the test I made was misleading. The issue is resolved now.

Fountainhead answered 23/5, 2011 at 14:22 Comment(3)
From your explanation it looks like a bug in the .NET library. You should consider fileing a Connect bug: connect.microsoft.com/VisualStudioSideboard
Isn't using the enumerator the same as doing a foreach over the SearchResultCollection?Tenstrike
@AbhijeetPatel yes there is no difference. It was just there to illustrate the bug.Fountainhead
S
8

The managed wrapper doesn't really leak anything. If you don't call Dispose unused resources will still be reclaimed during garbage collection.

However, the managed code is a wrapper on top of the COM-based ADSI API and when you create a DirectoryEntry the underlying code will call the ADsOpenObject function. The returned COM object is released when the DirectoryEntry is disposed or during finalization.

There is a documented memory leak when you use the ADsOpenObject API together with a set of credentials and a WinNT provider:

  • This memory leak occurs on all versions of Windows XP, of Windows Server 2003, of Windows Vista, of Windows Server 2008, of Windows 7, and of Windows Server 2008 R2.
  • This memory leak occurs only when you use the WinNT provider together with credentials. The LDAP provider does not leak memory in this manner.

However, the leak is only 8 bytes and and as far as I can see you are using the LDAP provider and not the WinNT provider.

Calling DirectorySearcher.FindAll will perform a search that requires considerable cleanup. This cleanup is done in DirectorySearcher.Dispose. In your code this cleanup is performed in each iteration of the loop and not during garbage collection.

Unless there really is an undocumented memory leak in the LDAP ADSI API the only explanation I can come up with is fragmentation of the unmanaged heap. The ADSI API is implemented by an in-process COM server and each search will probably allocate some memory on the unmanaged heap of your process. If this memory becomes fragmented the heap may have to grow when space is allocated for new searches.

If my hypothesis is true, one option would be to run the searches in a separate AppDomain that then can be reclaimed to unload ADSI and recycle the memory. However, even though memory fragmentation may increase the demand for unmanaged memory I would expect that there would be an upper limit to how much unmanaged memory is required. Unless of course you have a leak.

Also, you could try to play around with the DirectorySearcher.CacheResults property. Does setting it to false remove the leak?

Sideboard answered 9/5, 2011 at 11:45 Comment(8)
nice find! that's exactly it. It is very little each time but builds up over time and there was definately a leak somewhere and I did create a new DirectoryEntry each time (with credentials).Fountainhead
@Martin, actually reading the link again, I don't use the WinNNT provider, it's using the LDAP provider instead.. Also the leak seems to occur when you do a search, not use a new DirectoryEntry.. (if I change the loop as such). Seems I got excited a bit prematurely. But probably there is some other memory leak in the COM API..Fountainhead
CacheResults did not seem to have any effect. Each FindAll call seems to increase memory usage by around 500-600 bytes on average (over 1000 runs), (measured using calling Gc.Collect(), GC.WaitForPending() just before measuring the memory), and using Process.GetCurrentProcess().PrivateMemorySize64 to get the memory size.Fountainhead
A seperate AppDomain would not help, as memory is collected on a process wide bases, so a seperate process would be needed.Finkle
@Ian Ringrose: Unloading an AppDomain will also unload any COM DLL's that was loaded into that AppDomain. However, I'm not quite sure what will happen with any unmanaged heap memory allocated by that DLL. Chances are that it relies on a C++ library for memory allocation and this library and its associated resources may still be loaded into the process after the AppDomain is unloaded. Obviously the COM DLL will delete/free the memory when it unloads (unless it has a leak) but the segment may still be mapped into the process.Sideboard
@Martin From I post as a C++ programmer, I recall that most delete/free imps do not give any memory back to the OS and don't cope well with fagments. You also have to hope another COM dll that uses the same C/C++ runtime has not been loaded by a different app domain - too many "ifs" for my liking!Finkle
@Martin, the mystery is solved. It seems to leak memory only if you don't do anything with the search result. See my answer.Fountainhead
I was dispoding everything but my program was still leaking some memory reported by a profiler tool. Setting DirectorySearcher.CacheResults to false did the trick. ThanksThoughtless
D
3

Due to implementation restrictions, the SearchResultCollection class cannot release all of its unmanaged resources when it is garbage collected. To prevent a memory leak, you must call the Dispose method when the SearchResultCollection object is no longer needed.

http://msdn.microsoft.com/en-us/library/system.directoryservices.directorysearcher.findall.aspx

EDIT:

I've been able to repro the apparent leak using perfmon, and adding a counter for Private Bytes on the process name of the test app (Experiments.vshost for me )

the Private Bytes counter will steadily grow while the app is looping, it starts around 40,000,000, and then grows by about a million bytes every few seconds. The good news is the counter drops back to normal (35,237,888) when you terminate the app, so some sort of cleanup is finally occurring then.

I've attached a screen shot of what perfmon looks like when its leakingperfmon screenshot of memory leak

Update:

I've tried a few workarounds, like disabling caching on the DirectoryServer object, and it didn't help.

The FindOne() command doesn't leak memory, but i'm not sure what you would have to do to make that option work for you, probably edit the filter constantly, on my AD controller, there is just a single domain, so findall & findone give the same result.

I also tried queuing 10,000 threadpool workers to make the same DirectorySearcher.FindAll(). It finished alot faster, however it still leaked memory, and actually private bytes went up to about 80MB, instead of just 48MB for the "normal" leak.

So for this issue, if you can make FindOne() work for you, you have a workaround. Good Luck!

Domitian answered 9/5, 2011 at 11:6 Comment(4)
the "using" scope is equivalent to calling dispose, and also it doesn't have any effect to call dispose more than once.Fountainhead
I hope your right, the MSDN document seems to indicate an explicit dispose call is needed because the collection has unmanaged resources. I'll test it out this morning.Domitian
it seems that manual dispose or using makes no difference on the leak. I'll see if I can figure out exactly what kind of memory is leaking.Domitian
I also verified that if you comment out the call to FindAll(), the leak stops. Of course the code is useless at this point :)Domitian
G
2

Have you tried using and Dispose()? Info from here

Update

Try calling de.Close(); before the end of the using.

I don't actually have an Active Domain Service to test this on, sorry.

Gambado answered 12/4, 2011 at 8:10 Comment(6)
puttng the dispose in the using block is in effect calling Dispose twice.. and it doesn't seem to have any effect (as expected). I also went through the dispose code for the classes using .NET Reflector which both check if the object is disposed already and do nothing otherwise.Fountainhead
It would be interresting to check your source code what is in your least level of Using: the example in the link shows other AD objects which also needs a Dispose(), like Properties object.Gumshoe
The Properties object doesn't implement IDispoable, it is just DirectorySearcher, DirectoryEntry and SearchResultCollection.Fountainhead
The close doesn't have any effect. I've verified this using Reflector. Both Close() and Dispose() call the Unbind() function and are essentially the same.Fountainhead
Is there a test Active Domain Service we could connect to on the web somewhere to test the code ourselves? Or is it simple to setup locally?Gambado
Not that I'm aware of. What you can do is download the Server 2008 R2 180 day trial Virtual machine from Microsoft and add "domain controller" role to the server. microsoft.com/windowsserver2008/en/us/trial-software.aspxFountainhead
C
0

Found a quick and dirty way around this.

I had a similar issue in my program with memory growth but by changing .GetDirectoryEntry().Properties("cn").Value to

.GetDirectoryEntry().Properties("cn").Value.ToString with a if before hand to make sure .value was not null

i was able to tell GC.Collect to get rid of the temporary value in my foreach. It looks like the .value was actually keeping the object alive rather then allowing it to be collected.

Clawson answered 16/9, 2013 at 20:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.