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.
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.
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?
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 leaking
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!
Have you tried using
and Dispose()
?
Info from here
Try calling de.Close();
before the end of the using.
I don't actually have an Active Domain Service to test this on, sorry.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With