
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, ... ) as Cool.Library; foreach(var asset in lib.Assets) { }
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, ... ) as Cool.Library; var list = new List<CoolAsset>(); foreach(var asset in lib.Assets) { list.Add(asset); } foreach(var asset in list) { }
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:
- Windows 8.1 - one thing
- Strange project - 1 pc
Adoubi Indiz Oh which external com server - 6 pcs, reproduced at all- Secret tambourine - 1 pc
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, ... ) 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); log(">> foreach asset " + asset + " is OK"); } log("<< foreach"); } catch (Exception e) { log(">> foreach failed due to " + e + "\n" + e.StackTrace); }
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); log(">> foreach asset " + asset + " is OK"); } log("<< foreach"); } catch (Exception e) { log(">> foreach failed due to " + e + "\n" + e.StackTrace); }
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]; } ...
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) { } 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:
- In addition to our directly sick code, there is also a garbage collector;
- We have a proxy around COM;
- This proxy hides AddRef () / Release () for us;
- In the classic implementation, Release () usually contains if (count == 0) delete this; on the COM server side:
- For our com proxy (which is something inherited from MarshalByRefObject), when the humpback collector arrives, they will call Dispose (), and in it our Release () will jerk .
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.