views:

310

answers:

3

The examples for Cache.Add uses DateTime.Now.Add to compute the expiration, i.e. it passes:

 DateTime.Now.AddSeconds(60)

as the value of the absoluteExpiration parameter.

I'd have thought that computing it relative to DateTime.UtcNow would be more correct [as there is no ambiguity if Daylight Savings Time starts in the intervening time between now and the expiration point].

Before the introduction of DateTimeKind, I'd have guessed that there's some ugly hacks in the cache management to make it do something appropriate if the time was not a UTC time.

In .NET 2.0 and later, I'm guessing that it should handle a DateTime calculated as DateTime.UtcNow.AddSeconds(60) correctly given that it has DateTime.Kind to use as an input in its inferences.

I've been confidently using DateTime.UtcNow as the base for years, but wasnt able to come up with a rationale that this is definitely the correct thing to do in the absence of anything pointing out the documentation has been highly misleading for 4+ years.

The questions?

  1. Despite much bingage and googling I wasnt able to find any authoritative discussion on this from MS - can anyone locate something regarding this?
  2. Is there any reason why using UtcNow wouldnt be more correct and/or safe?

(Yes, I could peruse the source and/or the Reflector'd source, but am looking for a full blow-by-blow lowdown!)

+2  A: 

Cache.Add converts the expiration date to UTC before storing it.

From Reflector (I've omitted most of the parameters to make it easier to read):

public object Add(... DateTime absoluteExpiration ...)
{
    DateTime utcAbsoluteExpiration = DateTimeUtil.ConvertToUniversalTime(absoluteExpiration);
    return this._cacheInternal.DoInsert(... utcAbsoluteExpiration ...);
}

In CacheExpires.FlushExpiredItems, utcAbsoluteExpiration is compared to DateTime.UtcNow. As Joe notes in his answer, this causes unexpected behavior when a cache item's addition and expiration span the end of daylight saving time.

Jeff Sternal
Nice work! I take it that's on 3.5SP1. Any ideas on 2.0 raw, 1.0? Does `DateTimeUtil.ConvertToUniversalTime` do anything more than do a simple call to `DateTime.ToUniversalTime` when `Kind==DateTimeKind.Local`? Yes, I should load Reflector. Loading...
Ruben Bartelink
Thanks. I actually have an abiding interest in the inner workings of the cahce: http://stackoverflow.com/questions/1434284/when-does-asp-net-remove-expired-cache-items. :) That was actually the 2.0 framework. I haven't looked at the others, but I await the results of your investigation!
Jeff Sternal
Ah, `DateTime.ToUniversalTime` and friends have a stack of hacks as long as your arm that silently fix things up (In 1.x without `DateTime.Kind`, I'm sure it was more interesting - IIRC it always adjusted and hence you had to be more careful in calling it?).
Ruben Bartelink
A: 

[Highly derivative of insights from Jeff Sternal's answer, which I've +1'd in incomplete payment :D]

It seems that in 1.1 (didnt look at 1.0 but I assume its similar), in the absence of a DateTime.Kind and having published examples with a DateTime.Now-relative time, they feel comfortable immediately calling ToUniversalTime() immediately.

Hence...

  1. in 1.x, you'll end up with a mess if you [the API user] use DateTime.UtcNow (and there's a sensitivity to DST starting during the call to Cache.Add)

  2. in 2.0 [and 3.x], you're safe to use either as long as the Kind on the time you supply is set [which it normally would be if you got the time from DateTime.Now or UtcNow]. [See Joe's comments and answer for full rationale] Definitely prefer UtcNow as ambiguity for 1 hour of DST switchover.

  3. The example stays as-is so people working against 1.x dont get misled [and people can go on pretending that DST is a wacky edge case :P]. [Ditto, as pointed out by Joe] But this is a very debatable stance as this can result in stuff staying in the cache for an extra hour.

I'm still very interested to hear more details, including any from any Ponies out there.

EDIT: I realise from Joe's comments that I didn't explicitly call out the fact that it is definitely more correct to use UtcNow if using 2.0 or later as one is exposed to risk of the item being cached for an extra hour during the DST 'groundhog hour'. I also think the MS doc should point this fact out (with the proviso that they need to mention that this does not to apply to 1.1 [regardless of whether the page is marked 2.0+ specific]. Thanks Joe.

EDIT: Noda Time will have a neat wrapper to make this foolproof :D

Ruben Bartelink
Interesting, I'll have to look more closely at 1.1 when I have some time this weekend.
Jeff Sternal
"n 2.0 ... you're safe to use either" - no. Local time is ambiguous for one hour a year at the end of DST - so if you specify a local time during this hour, you can be an hour out. See my answer.
Joe
@Joe: AIUI the only issue is when you place an absoluteExpiration entry with a Local time at 1:59:59.999 and Cache.Add [indirectly] does the convert after the switchover has occurred. In all other cases, it knows the DateTimeKind of the time you're specifying and uses this to convert (or not) the time to Utc for its internal usage. You looked at the code?
Ruben Bartelink
Rereading... @Joe: AIUI [via Reflector] the conversion that ToUniversalTime uses takes into account the current time differential, hence as long as the switchover doesnt happen between the point at which you call DateTime.Now (which will always be adjusted for DST) and when DateTime.ToUniversalTime is called, you're safe. i.e., there is confusion about what 2:05 means to a human after the fact, but not to the machine which is doing the conversion at a point in time. Or do you still feel I'm wrong?
Ruben Bartelink
"... ToUniversalTime ... takes into account the current time differential". No, it takes into account the differential relevant for *the DateTime you're converting*, not the current differential at the time you call ToUniversalTime.
Joe
"There is confusion about what 2:05 means to a human". There is ambiguity for a machine too. Keeping with the example of European time, 02:05 at the end of DST can be 00:05 UTC (GMT+2), or 01:05 UTC (GMT+1). When you convert 02:05 local to UTC, it must choose one of them - in fact it chooses the 01:05 (without DST).
Joe
Ah, I skimmed it too quickly. You're 100% correct (and I should have worked that out from your example, sorry!).
Ruben Bartelink
+3  A: 

I reported this bug on Microsoft Connect some time ago, but it's been closed as won't fix.

You still have a problem in .NET 2.0 if you specify your absolute expiration in local time.

During one hour at the end of daylight savings time, your local time is ambiguous, so you can get unexpected results, i.e. the absolute expiration can be one hour longer than expected.

In Europe, daylight savings time ended at 02:00 on 25 October 2009. The sample below illustrates that if you placed an item in the cache at 01:59 with an expiration of 2 minutes, it would remain in the cache for one hour and two minutes.

DateTime startTime = new DateTime(2009, 10, 25, 1, 59,0);
DateTime endTime = startTime.AddMinutes(2);
// end time is two minutes after start time

DateTime startUtcTime = startTime.ToUniversalTime();
DateTime endUtcTime = endTime.ToUniversalTime();
// end UTC time is one hour and two minutes after start UTC time

Console.WriteLine("Start UTC time = " + startUtcTime.ToString());
Console.WriteLine("End UTC time = " + endUtcTime.ToString());

The workaround for .NET 2.0 or later is to specify the absolute expiration time in UTC as pointed out by Ruben.

Microsoft should perhaps be recommending to use UTC in the examples for absolute expiration, but I guess there is the potential for confusion since this recommendation is only valid for .NET 2.0 and later.

EDIT

From the comments:

But the exposure only occurs if the conversion happens during the overlap. The single conversion actually taking place is when you lodge the item with Cache.Add

The problem will only happen if you insert an item in the cache with an AbsoluteExpiration time in local time during that one ambiguous hour at the end of daylight savings time.

So for example, if you're local time zone is Central European (GMT+1 in winter, GMT+2 in summer), and you execute the following code at 01:59:00 on 25 October 2009:

DateTime absoluteExpiration = DateTime.Now.AddMinutes(2);
Cache.Add(... absoluteExpiration ...)

then the item will remain in the cache for one hour and two minutes, rather than the two minutes you would normally expect. This can be a problem for some highly time-critical applications (e.g. stock ticker, airline departures board).

What's happening here is (assuming European time, but the principle is the same for any time zone):

  • DateTime.Now = 2009-10-25 01:59:00 local. local=GMT+2, so UTC = 2009-10-24 23:59:00

  • .AddMinutes(2) = 2009-10-25 02:01:00 local. local = GMT+1, so UTC = 2009-11-25 01:01:00

  • Cache.Add internally converts the expiration time to UTC (2009-11-25 01:01:00) so the expiration is one hour and two minutes ahead of the current UTC time (23:59:00).

If you use DateTime.UtcNow in place of DateTime.Now, the cache expiration will be two minutes (.NET 2.0 or later):

DateTime absoluteExpiration = DateTime.UtcNow.AddMinutes(2);
Cache.Add(... absoluteExpiration ...)

From the comments:

Or am I missing something?

No you're not. Your analysis is spot on and if your application is time-critical and runs during that period at the end of DST, you're right to be using DateTime.UtcNow.

The statement in Ruben's answer that:

you're safe to use either as long as the Kind on the time you supply is set

is incorrect.

Joe
@Joe: I agree that the case you have presented above is correct. But the exposure only occurs if the conversion happens during the overlap. The single conversion actually taking place is when you lodge the item with Cache.Add. I'm not sure what points you're attributing to Jeff are out about best policy being to specify a Utc based expiration or how the docs should cover it - isnt that the point I made in my answer [to myself]? Or am I missing something? (I do understand the root issue, which is why I brought up the question in the first place)
Ruben Bartelink
@Joe: +1 re the link to the Connect item: I agree that its a doc bug
Ruben Bartelink
@Joe - you mis-attributed a few things to my answer, so I've edited your response to fix them (apart from that, nice find).
Jeff Sternal