Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LINQ, Union followed by Select on empty IEnumerable fails to invoke Dispose #30607

Closed
acolombi opened this issue Aug 17, 2019 · 5 comments
Closed
Labels
area-System.Linq help wanted [up-for-grabs] Good issue for external contributors
Milestone

Comments

@acolombi
Copy link

It is my understanding that LINQ should always call IEnumerator's Dispose method when finished with an IEnumerator. I've found a case where this does not happen: Take two IEnumerators that immediately return false in their MoveNext() implementations (i.e. they are empty), Union them and then perform a Select on that. Here is a minimal reproduction that shows Dispose() is not called for either IEnumerator. This reproduces in dotnet core 2.1 & 2.2. It does not reproduce in .NET Framework 4.7.2.

using System;
using System.Threading;
using System.Linq;
using System.Collections.Generic;
using System.Collections;


namespace union
{
    class MyEnumerable : IEnumerable<long>
    {
        public IEnumerator<long> GetEnumerator()
        {
            return new MyEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }

    class MyEnumerator : IEnumerator<long>
    {
        public long Current
        {
            get
            {
                return 0;
            }
        }

        object IEnumerator.Current
        {
            get
            {
                return Current;
            }
        }

        public bool MoveNext()
        {
            return false;
        }

        public void Reset()
        {
            return;
        }

        public void Dispose()
        {
            Console.WriteLine("I got disposed");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var enum1 = new MyEnumerable();
            var enum2 = new MyEnumerable();

            enum1.Union(enum2).Select(x => x + 1).ToList();
            Console.WriteLine("All done!");
        }
    }
}

If the Dispose() was getting called you would see "I got disposed" twice on the console. Instead you get no "I got disposed"s. The Union and the Select are required to reproduce the issue.

@gabeluci
Copy link

gabeluci commented Aug 17, 2019

This was first asked on Stack Overflow, so I did some digging.

  1. This has nothing to with Select(), just Union() (but not if you use Union().ToList() since that uses a different implementation).
  2. This only occurs for empty collections. More specifically, when MoveNext() never returns true.
  3. I believe the bug is on line 105 of Union.cs. SetEnumerator() is only ever called after enumerator.MoveNext() returns true. So for an empty collection, it would never get set. Then in Dispose(), there is no enumerator saved that it can call Dispose() on.

When you call Union().ToList(), it ends up using UnionWith(), which relies on foreach, so it's not a problem there.

The .NET Framework implementation also relies on foreach, so it's not a problem there either.

Here are reproductions of it:

@acolombi acolombi changed the title LINQ, Select followed by Union on empty IEnumerable fails to invoke Dispose LINQ, Union followed by Select on empty IEnumerable fails to invoke Dispose Aug 17, 2019
@scalablecory
Copy link
Contributor

Thanks for the report. Would you care to submit a PR?

bbartels referenced this issue in bbartels/corefx Aug 24, 2019
@gabeluci
Copy link

gabeluci commented Aug 29, 2019

Sorry about the radio silence on this. I thought I might have time to give it a try, but it just didn't happen. I'm glad someone picked it up.

@stephentoub
Copy link
Member

Fixed in dotnet/corefx#40384

@GSPP
Copy link

GSPP commented Sep 10, 2019

Is there enough test coverage to ensure that disposing happens reliably? Some sequences hold up resources such as database connections or file handles. If those are dropped this can be a nasty surprise (especially suddenly under load in production).

I understand that the LINQ implementations have become a lot more complex internally due to various optimizations. There's now more potential for forgetting to dispose. Most queries are in-memory so this would not be noticed.

Update: I now see the referenced pull request that appears to add comprehensive coverage. 🍾

@msftgits msftgits transferred this issue from dotnet/corefx Feb 1, 2020
@msftgits msftgits added this to the 5.0 milestone Feb 1, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 12, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Linq help wanted [up-for-grabs] Good issue for external contributors
Projects
None yet
Development

No branches or pull requests

6 participants