Reputation: 14585
We are using an API that basically does this
var t = TimeSpan.MaxValue;
int x = (int)t.TotalMilliseconds;
where x will end up being passed to System.Threading.WaitHandle.WaitOne(int).
The problem is when this code runs in our dev and staging environments, it doesn't throw any errors but when it runs in production it does throw:
Exception: System.ArgumentOutOfRangeException
Message: Number must be either non-negative and less than or equal to Int32.MaxValue or -1.
Parameter name: millisecondsTimeout
When I test this with a simple console app (both x86 and x64) the result of x = -2147483648 (int.MinValue) while when I run the code in the immediate window I get x = 1566804069.
What is going on?
Note: staging and production are all cloned from a single VM so there are no differences between them
THIS IS CODE THAT WE CANNOT CHANGE! Otherwise I wouldn't be asking this question.
Upvotes: 3
Views: 1097
Reputation: 5632
The only way this can happen with identical VM's is if the CPU is different on the production machine from the CPU on the staging machine - as Gabe asked about in the comments to the question and zdan suggested in his answer.
So specifically as to what is going on. For machines that support SSE2, the cvttsd2si
instruction is used by .NET to convert the double into an int, where overflow is mapped to 0x80000000 (Int.MinValue). On machines w/o SSE2 support, I could only look at the Rotor sources, and in jithelpers.cpp, it simply casts the double to an int32 - which w/o SSE2 on VC10 C++, ends up returning the value in the lower 32 bits so the value passed to wait should be 1566804069 (0x5D638865) as you saw in the immediate window.
The CPUs are different and your "fix" w/o modifying the code is to change machines to something that doesn't supports SSE2. See the SSE2 wikipedia entry to check the production server's CPU vs the staging server. If you're lucky, maybe it can be disabled in your server's bios (or the VMs config/bios).
If you're daring, you can try patching the IL to fix the problem - what the code really wanted was -1 as the timeout, which is "wait forever". By using ilasm and ildasm you might be able to fix it w/o source (I'm assuming this is the reason you can't change it). I did this succesfully myself on a test program - ildasm test.exe /out=test.il
to turn the an assembly into IL, edited the IL and finally ilasm test.il /exe
to create a new assembly. Below is what my IL looked like and how I fixed it.
// bad code
// var t = TimeSpan.MaxValue;
IL_0008: call instance float64System.TimeSpan::get_TotalMilliseconds()
// int x = (int)t.TotalMilliseconds;
IL_000D: conv.i4 // This is the line that becomes cvttsd2si when jitted
IL_000E: stloc.2
// wh.WaitOne(x);
IL_000F: ldloc.0
IL_0010: ldloc.2
IL_0011: callvirt instance bool System.Threading.WaitHandle::WaitOne(int32)
The fix is to reload x (location 2 here) with -1 before calling Wait one
// fixed code
// var t = TimeSpan.MaxValue;
IL_0008: call instance float64System.TimeSpan::get_TotalMilliseconds()
// int x = (int)t.TotalMilliseconds;
IL_000D: conv.i4 // This is the line that becomes cvttsd2si when jitted
IL_000E: stloc.2
// x = -1; // Fix by forcing x to -1 (infinite timeout)
ldc.i4.m1 // push a -1
stloc.2 // pop and store it in 'x'
// wh.WaitOne(x);
IL_000F: ldloc.0
IL_0010: ldloc.2
IL_0011: callvirt instance bool System.Threading.WaitHandle::WaitOne(int32)
Note that in this case, 'x' is local #2 - the IL at the top of the method will give you the correct # so the 2 in stloc.2 needs to be changed to whatever # x is has been assigned, which should match the # in the ldloc instruction just before the call the WaitOne at label IL_0010 in my example.
Upvotes: 4
Reputation: 29450
TimeSpan.MaxValue.TotalMilliseconds is a double that is equal to 922337203685477, which is larger than Int32.MaxValue (2147483647). What the cast will do in this case is implementation specific (technically it's undefined see @phoog's comment below) and will likely depend on the CPU, which might explain the differences you are seeing.
In one case, the cast leads to a value that is acceptable to System.Threading.WaitHandle.WaitOne(int)
and in the other case it isn't.
This seems to be a bug in the library you are using. There is a WaitOne overload that takes a TimeSpan as an argument, so I don't know why they didn't use that. If you can't change the library, you are out of luck.
Upvotes: 4
Reputation: 22235
TimeSpan.MaxValue is equivelant to Int64.MaxValue which is too large a value to pass to WaitOne(). If you want to pass a large value just use Int32.MaxValue.
Upvotes: 0
Reputation: 7449
Your Conversion overflows that why you are getting different results for different systems. Use long instead
var t = TimeSpan.MaxValue;
long x = (long)t.TotalMilliseconds;
Upvotes: 0