Saga of E_RPC_DISCONNECT

    In the beginning there was a code


    And that code was written on dotnet (still version 1.1) many years ago. The code was simple and oak - somewhere in the wilds of the project lay a stack of Interop *. *. DLL for even more ancient TLBs. Obviously, an interface was implemented that implements three and a half methods, and a set of implementations was born in agony, at the time of excavation - there were sixteen (!) Of them. Factory and other singletones are included.

    The classic Application created that code, and for all 16 implementations in the place of interest to us, the code was copy-paste and identical - only the namespaces from the interops differed .

    Something like this:

    Type apptype = Type.GetTypeFromProgID("CoolAppID", false);
    var app = Activator.CreateInstance(apptype) as Cool.Application;
    var lib = app.Open(file, ... /* many flags */) as Cool.Library;
    foreach(var asset in lib.Assets) {
        /* some long operations */
    }
    

    Since then, the code has experienced a lot of things - moving to subnet 2.0, 3.5, 4.0, etc. It began to support those interops from two to the sixteen mentioned - but the code is the same and still does not change, it only propagates by budding sometimes. Not a single gap since 2007. Until one day they ran this code on Windows 8.1 .


    And the evil sorceress already wanted to eat Hans and Gretel


    And the notorious E_RPC_TIMEOUT visited this piece of dinosaur . And on different versions of that COM server, so the point is clearly in the code.

    Bullshit question! Well it works for a very long time, right? So the iterator just does not live to see the end of the loop! - thought those who fixed before me.

    Change:

    ...
    var lib = app.Open(file, ... /* many flags */) as Cool.Library;
    var list = new List();
    foreach(var asset in lib.Assets) {
       list.Add(asset);
    }
    foreach(var asset in list) {
        /* some long operations */
    }
    /* some other stuff */
    /* end of function */
    

    Um um. Better not.

    Not to say not to hit at all - but not to hit the ball


    It seemed to help - in the sense that E_RPC_TIMEOUT disappeared. But in its place came even more evil E_RPC_DISCONNECT . And at that moment this piano was rolled to me.

    Tools and materials:
    • Windows 8.1 - one thing
    • Strange project - 1 pc.
    • Adoubi Indiz Ah what external com server - 6 pcs, playable on all
    • Secret tambourine - 1 pc.

    Let's get started.

    Zebra attack


    First of all, we arm ourselves with methods from the distant and dark past, namely, we poke every second line the output to the log (yes, I know about the debugger). Like that:
    ...
    var lib = app.Open(file, ... /* many flags */) as Cool.Library;
    var list = new List();
    foreach(var asset in lib.Assets) {
       list.Add(asset);
    }
    try {
        log(">> foreach");
        foreach(var asset in list) {
            log(">> foreach got " + asset);
            /* some long operations */
            log(">> foreach asset " + asset + " is OK");
        }
        log("<< foreach");
    }
    catch (Exception e) {
        log(">> foreach failed due to " + e + "\n" + e.StackTrace);
    }
    /* some other stuff */
    /* end of function */
    

    We start, meditate , and what do we see?
    Some garbage
    >> foreach
    >> foreach got foo
    ...
    >> foreach asset foo is OK
    ...
    >> foreach got bar
    ...
    >> foreach asset bar is OK
    >> foreach failed due to COMException ... E_RPC_DISCONNECT ... 
    


    That is, looking at the code -
            log(">> foreach asset " + asset + " is OK");
        }
        log("<< foreach");
    

    Dropped on the closing brace.

    How can this be?
    We will write
    ...
    try {
        log(">> foreach");
        var itr = list.GetEnumerator();
        for(;;) {
            log(">> foreach new cycle...");
            if(!itr.MoveNext()) break;
            log(">> foreach new cycle and it have extra elements to iterate...");
            var asset = itr.Current;
            log(">> foreach got " + asset);
            /* some long operations */
            log(">> foreach asset " + asset + " is OK");
        }
        log("<< foreach");
    }
    catch (Exception e) {
        log(">> foreach failed due to " + e + "\n" + e.StackTrace);
    }
    /* some other stuff */
    /* end of function */
    

    Of course in the log we get this

    More detailed bullshit. Unhealthy
    >> foreach
    >> foreach new cycle...
    >> foreach new cycle and it have extra elements to iterate...
    >> foreach got foo
    ...
    >> foreach asset foo is OK
    ...
    >> foreach got bar
    ...
    >> foreach asset bar is OK
    >> foreach new cycle...
    >> foreach failed due to COMException ... E_RPC_DISCONNECT ... 
    

    That is, it falls - on MoveNext ().

    But wait a moment! This is pure .NET iterator from pure .NET List! How is that? In fact, we just ran for our own tail, not finding anything.

    Tail twirls a dog


    Taking another coffee and opening the manual, as well as loudly scolding the Indians of both companies, I throw away all the experiments in general, bring them back to their original form, and replace the cycle with the counter with a cycle counter:

    ...
        for(int i=0; i < lib.Assets.Count; ++i) {
            var asset = lib.Assets[i];
            /* some long operations */
        }
    ...
    

    Imagine my (and not only) surprise when the code worked without errors - happily avoiding E_RPC_TIMEOUT and E_RPC_DISCONNECT ! Moreover, on the very Windows 8.1, where the reproduction of the problem was one hundred percent.

    Workaround seems to be found, but it does not explain anything. Yes, and he was found only because in those days when I was a junior, there were no foreach constructions, and instead of a deliberate action, I just patted my vile old-school habits ...

    The evening is no longer languishing


    We return to the original foreach, the matter is still somewhere here. I turn to the hypothesis that after all, something is not right with our com object. I add a couple of cosmetic lines - for debugging convenience:

    ...
        var assetsCollection = lib.Assets;
        foreach(var asset in assetsCollection) {
            /* some long operations */
        }
        assetsCollection = null;
    ...
    


    It would be obvious that these two lines

    ...
        var assetsCollection = lib.Assets;
        ...
        assetsCollection = null;
    ...
    

    they don’t affect anything around the cycle, right? But it ’s very convenient to set breakpoints on them.

    I put, bryak-bryak, I launch, tynts-tynts, I read the habr those 20 minutes while it chews those assets. Did not fall. Uh?

    And, probably, the debugger interfered, the Vigilant Eye guessed. Without changing anything, I launch without debugging. Did not fall. Sorry what? Six more launches with each implementation and each interope - show that the ancient mammoth code works again as before - everywhere. Yes, I corrected it 16 times ;-)

    And now - hunchbacked!


    It would seem - and what is the difference?

    Let's recall this set of facts:
    • In addition to directly our sick code, there is also a garbage collector;
    • Around COM we have a proxy;
    • This proxy hides AddRef () / Release ();
    • In a classic implementation in Release (), there is usually if (count == 0) delete this; on the COM server side:
    • For our com proxy (which is something inherited from MarshalByRefObject), when a hunchback collector arrives, they will call Dispose (), and in it our Release () will be pulled .


    Now we can already assume what is wrong.

    Obviously, our lib.Assets is calculated once and is not used anywhere else. This means that a careful compiler notes this fact immediately after the first cycle, where we add up a collection of assets in a sheet.

    And further on in our method - the collector can pick up that link at any time, and the fact that the method works for a very long time - this probability increases to almost one hundred percent. But child items are obvious - value objects with lazy initialization, and we can only guess what they are and how they are stored inside. After all, it does not at all follow that each itemcalls AddRef () when horrible. I would even suggest - which is not guaranteed. For when writing a server com (which is called from anywhere), expect the master collections to say Release () and continue to use child elements, some of which will remain uninitialized ... A strange pattern.

    But adding two “insignificant” lines - I kind of made it clear to the compiler that this is a local variable that lives from the beginning of the declaration to the end of the function, and its assembly is guaranteed to begin no earlier than “after the last use”.

    And what does Windows 8.1 have to do with it? And with version 4.5. A slightly more aggressive default garbage collection - and here it is.

    I even tested this hypothesis - repeating the same effect with Windows 2012R2 / .NET 4.5 64bit, taking AWS t2.micro instance for verification. Moreover, E_RPC_DISCONNECT ran there much faster than on the prepared system, so there is something in it.

    However, these are nevertheless intellectualizations from the realm of afterlife , perhaps there are other factors.

    Also popular now: