📜 ⬆️ ⬇️

Saga of E_RPC_DISCONNECT

At the beginning was the 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 TLB. Obviously, an interface was introduced that implements three and a half methods, and a set of implementations was born in agony, by the time of excavation there were sixteen (!) Pieces. Factory and other singletons - included.

That code was created by the classic Application, and for all 16 implementations in the place of interest, the code was copied and identical - only the namespaces from interopes differed .

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 gone through a lot of things - moving to 2.0, 3.5, 4.0, and so on. It began supporting those interopes from two to the sixteen mentioned — and the code is still the same and still does not change, it only reproduces by budding sometimes. Not a single gap since 2007. Until one day this code was run on Windows 8.1 .


And the evil witch 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 matter is clearly in the code.

Garbage question! It works very long, right? So the iterator just does not live to the end of the cycle! - those who thought to me thought.

Change:

 ... var lib = app.Open(file, ... /* many flags */) as Cool.Library; var list = new List<CoolAsset>(); 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 his place came even more evil E_RPC_DISCONNECT . And at that moment this piano came to me.

Tools and materials:

Let's get started

Attack on zebras


First and foremost, we arm ourselves with methods from the distant and dark past, namely, we output to the second every line the log (yes, I know about the debugger). Like that:
 ... var lib = app.Open(file, ... /* many flags */) as Cool.Library; var list = new List<CoolAsset>(); 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, we meditate , and what do we see?
Some kind of 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?
Write out
 ... 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

Even more detailed garbage. 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! This is a pure .NET iterator from pure .NET List <T> ! What is it like? In fact, we just ran over our own tail, finding nothing.

The tail turns the dog


Taking more coffee and opening the manual, as well as loudly cursing the Indians of both companies, I throw away all the experiments in general, bring it to its original form, and replace it with a cycle counter with a cycle with a counter:

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

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

Workaround is found, as it were, but he explains nothing. Yes, and he was found only because in those times when I was a junior, there were no foreach constructions, and instead of a conscious action, I just indulged my vile old school habits ...

The evening ceases to be languid


Returning to the original foreach, it's still around here. I turn to the hypothesis that after all something is not right with our object. I add a couple of cosmetic lines - for easy debugging:

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


As it is obvious that these two lines

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

around the loop don't influence anything, right? But bryakpoint put on them is very convenient.

I put, bryak-bryak, I launch, tynts-tynts, I read to Habra those 20 minutes while it chews those assets. Did not fall. Eh

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

And now - hunchback!


It would seem - and what's the difference?

Let's recall this set of facts:


Now we can already suggest what is wrong.

Obviously, our lib.Assets is calculated once and is not used anywhere else. Hence, a caring compiler notes this fact immediately after the first cycle, where we add a collection of assets to the sheet.

And then already 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 - the value objects with lazy initialization, and what they are stored inside - we can only guess. After all, it does not follow that each item calls AddRef () when horrible. I would even suggest that it is guaranteed not. For when writing a server com (which is called from anywhere), expect that the master collections will 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 start no earlier than “for the last use”.

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

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

However, this is still thinking from the field of after-knowledge , perhaps there are other factors.

Source: https://habr.com/ru/post/252103/


All Articles