diff --git a/src/Grpc.Net.Client/Internal/GrpcCall.cs b/src/Grpc.Net.Client/Internal/GrpcCall.cs index a50126fb1..80bb6a49d 100644 --- a/src/Grpc.Net.Client/Internal/GrpcCall.cs +++ b/src/Grpc.Net.Client/Internal/GrpcCall.cs @@ -225,7 +225,18 @@ private void Cleanup(Status status) Channel.FinishActiveCall(this); _ctsRegistration?.Dispose(); - _deadlineTimer?.Dispose(); + + if (_deadlineTimer != null) + { + lock (this) + { + // Timer callback can call Timer.Change so dispose deadline timer in a lock + // and set to null to indicate to the callback that it has been disposed. + _deadlineTimer?.Dispose(); + _deadlineTimer = null; + } + } + HttpResponse?.Dispose(); ClientStreamReader?.Dispose(); ClientStreamWriter?.Dispose(); @@ -650,10 +661,15 @@ internal bool ResolveException(string summary, Exception ex, [NotNull] out Statu // The server could exceed the deadline and return a CANCELLED status before the // client's deadline timer is triggered. When CANCELLED is received check the // deadline against the clock and change status to DEADLINE_EXCEEDED if required. - var deadlineExceeded = s.StatusCode == StatusCode.Cancelled && IsDeadlineExceeded(); - if (deadlineExceeded) + if (s.StatusCode == StatusCode.Cancelled) { - s = new Status(StatusCode.DeadlineExceeded, s.Detail, s.DebugException); + lock (this) + { + if (IsDeadlineExceededUnsynchronized()) + { + s = new Status(StatusCode.DeadlineExceeded, s.Detail, s.DebugException); + } + } } status = s; @@ -769,7 +785,7 @@ private bool FinishCall(HttpRequestMessage request, bool diagnosticSourceEnabled // the client has processed that it has exceeded or not. lock (this) { - if (IsDeadlineExceeded()) + if (IsDeadlineExceededUnsynchronized()) { GrpcCallLog.DeadlineExceeded(Logger); GrpcEventSource.Log.CallDeadlineExceeded(); @@ -812,8 +828,9 @@ private bool FinishCall(HttpRequestMessage request, bool diagnosticSourceEnabled return true; } - private bool IsDeadlineExceeded() + private bool IsDeadlineExceededUnsynchronized() { + Debug.Assert(Monitor.IsEntered(this), "Check deadline in a lock. Updating a DateTime isn't guaranteed to be atomic. Avoid struct tearing."); return _deadline <= Channel.Clock.UtcNow; } @@ -958,14 +975,17 @@ private void DeadlineExceededCallback(object? state) DeadlineExceeded(); return; } - } - // Deadline has not been reached because timer maximum due time was smaller than deadline. - // Reschedule DeadlineExceeded again until deadline has been exceeded. - GrpcCallLog.DeadlineTimerRescheduled(Logger, remaining); + if (_deadlineTimer != null) + { + // Deadline has not been reached because timer maximum due time was smaller than deadline. + // Reschedule DeadlineExceeded again until deadline has been exceeded. + GrpcCallLog.DeadlineTimerRescheduled(Logger, remaining); - var dueTime = CommonGrpcProtocolHelpers.GetTimerDueTime(remaining, Channel.MaxTimerDueTime); - _deadlineTimer!.Change(dueTime, Timeout.Infinite); + var dueTime = CommonGrpcProtocolHelpers.GetTimerDueTime(remaining, Channel.MaxTimerDueTime); + _deadlineTimer.Change(dueTime, Timeout.Infinite); + } + } } }