This looks like a side effect of the deadlock-avoiding fix for #15118: The code used to be the way you suggested it before but the overly strong lock for the handleResult callback led to a potential deadlock in Tomcat.
I guess we need to find a looser way of enforcing a single handleResult callback, e.g. using a volatile variable.
I've revised DeferredResult to consistently make result handling decisions (including resultHandler != null checks) within its synchronized blocks but actually invoke handleResult outside, in both setResultInternal and setResultHandler. This should avoid any deadlock potential while still making sure that handleResult is only invoked by either setResultInternal or setResultHandler but never both.
Looks much better this way. I'm wondering if it's worth moving the timeout callback after setting the "timeout result" in a finally block. It is only a callback that can't influence or alter timeout handling. Either way a try-finally might be good to ensure setResultInternal is reached.
Hmm, onTimeout's documentation says: "It may invoke setResult or setErrorResult to resume processing." With the current setResultInternal call after the timeout callback invocation, we'd let that sort of custom result happen, with a timeoutResult effectively being ignored when any other result was set before... whereas if we turn it around, a specified timeoutResult will always win over any result the callback might set. So it looks like we'll rather go with a try-finally for the existing sequence?
Oops, good point indeed that the callback can set the DeferredResult (even says so in onTimeout :)). So yes it needs to stay as it is with a try-finally for the existing sequence. One more point though that if setResulnternal(timeoutResult) does succeed the interceptor method should return false. There is no point to allow other interceptors down the chain.