|
4 | 4 |
|
5 | 5 | using System; |
6 | 6 | using System.Data.Common; |
| 7 | +using System.Threading; |
7 | 8 | using System.Threading.Tasks; |
8 | 9 | using System.Transactions; |
9 | 10 | using Microsoft.Data.ProviderBase; |
10 | 11 | using Microsoft.Data.SqlClient.ConnectionPool; |
| 12 | +using Microsoft.Data.SqlClient.Tests.Common; |
11 | 13 | using Microsoft.Extensions.Time.Testing; |
12 | 14 | using Xunit; |
13 | 15 |
|
@@ -65,73 +67,110 @@ private WaitHandleDbConnectionPool CreatePool( |
65 | 67 |
|
66 | 68 | /// <summary> |
67 | 69 | /// Verifies that the <see cref="TimeoutTimer"/> the pool hands to the |
68 | | - /// connection factory on the synchronous path reports a reduced |
69 | | - /// remaining-time budget when the timer's clock has advanced before the |
70 | | - /// pool was entered. Mirrors |
| 70 | + /// connection factory reports a reduced remaining-time budget when the |
| 71 | + /// timer's clock has advanced before the pool was entered. Both the |
| 72 | + /// synchronous (<c>taskCompletionSource == null</c>) and asynchronous |
| 73 | + /// paths must forward the caller's already-advanced timer rather than |
| 74 | + /// constructing a fresh one from <c>CreationTimeout</c>. Mirrors |
71 | 75 | /// <c>ChannelDbConnectionPoolTest.GetConnection_TimeoutTimerReflectsPoolWaitTime</c>. |
72 | 76 | /// </summary> |
73 | | - [Fact] |
74 | | - public void GetConnection_Sync_TimeoutTimerReflectsTimeAlreadyConsumed() |
| 77 | + [Theory] |
| 78 | + [InlineData(false)] // sync |
| 79 | + [InlineData(true)] // async |
| 80 | + public async Task GetConnection_TimeoutTimerReflectsTimeAlreadyConsumed(bool async) |
75 | 81 | { |
76 | | - // Arrange: a capturing factory and a fake-time-backed timer with a |
| 82 | + // Arrange: capturing factory and a fake-time-backed timer with a |
77 | 83 | // 30-second budget anchored at virtual time t = 0. |
78 | 84 | var factory = new MockSqlConnectionFactory(); |
79 | 85 | var pool = CreatePool(factory); |
80 | 86 | var owner = new SqlConnection("Timeout=30"); |
81 | 87 | var fakeTime = new FakeTimeProvider(); |
82 | 88 | TimeoutTimer timer = TimeoutTimer.StartNew(TimeSpan.FromSeconds(30), fakeTime); |
| 89 | + TaskCompletionSource<DbConnectionInternal>? tcs = |
| 90 | + async ? new TaskCompletionSource<DbConnectionInternal>() : null; |
83 | 91 |
|
84 | 92 | // Act: simulate 5s of budget consumed elsewhere (e.g., higher-level |
85 | | - // Open() work) before the pool is entered. |
| 93 | + // Open() work) before the pool is entered, then request a connection. |
86 | 94 | fakeTime.Advance(TimeSpan.FromSeconds(5)); |
87 | 95 | bool completed = pool.TryGetConnection( |
88 | 96 | owner, |
89 | | - taskCompletionSource: null, |
| 97 | + tcs, |
90 | 98 | timer, |
91 | 99 | out DbConnectionInternal? connection); |
92 | 100 |
|
| 101 | + if (async) |
| 102 | + { |
| 103 | + // Bound the await so a regression in the pool can't hang the suite. |
| 104 | + Task winner = await Task.WhenAny(tcs!.Task, Task.Delay(TimeSpan.FromSeconds(30))); |
| 105 | + Assert.Same(tcs.Task, winner); |
| 106 | + connection = await tcs.Task; |
| 107 | + } |
| 108 | + else |
| 109 | + { |
| 110 | + Assert.True(completed); |
| 111 | + } |
| 112 | + |
93 | 113 | // Assert: factory received the same timer, and it reports the reduced |
94 | 114 | // 25-second remaining budget rather than the original 30s or the |
95 | 115 | // pool's static 15s CreationTimeout. |
96 | | - Assert.True(completed); |
97 | 116 | Assert.NotNull(connection); |
98 | 117 | Assert.Same(timer, factory.CapturedTimeout); |
99 | 118 | Assert.Equal(25_000, factory.CapturedTimeout!.MillisecondsRemainingInt); |
100 | 119 | } |
101 | 120 |
|
102 | 121 | /// <summary> |
103 | | - /// Async counterpart of <see cref="GetConnection_Sync_TimeoutTimerReflectsTimeAlreadyConsumed"/>. |
104 | | - /// Verifies that the async pool path also forwards the caller's |
105 | | - /// already-advanced <see cref="TimeoutTimer"/> to the factory. |
| 122 | + /// Identifies which kind of caller-supplied <see cref="TimeoutTimer"/> a |
| 123 | + /// parameterized test should construct. Used because |
| 124 | + /// <see cref="InlineDataAttribute"/> cannot carry a live |
| 125 | + /// <see cref="TimeoutTimer"/> instance. |
| 126 | + /// </summary> |
| 127 | + public enum TimerKind |
| 128 | + { |
| 129 | + Expired, |
| 130 | + Infinite, |
| 131 | + } |
| 132 | + |
| 133 | + /// <summary> |
| 134 | + /// Verifies the resolution matrix for the synchronous |
| 135 | + /// <c>WaitHandle.WaitAny</c> timeout: |
| 136 | + /// <list type="bullet"> |
| 137 | + /// <item>switch ON → use the caller timer's remaining budget |
| 138 | + /// (expired → 0, infinite → <see cref="Timeout.Infinite"/>);</item> |
| 139 | + /// <item>switch OFF → ignore the caller timer and use |
| 140 | + /// <c>CreationTimeout</c>, treating <c>0</c> as |
| 141 | + /// <see cref="Timeout.Infinite"/> per legacy behavior.</item> |
| 142 | + /// </list> |
106 | 143 | /// </summary> |
107 | | - [Fact] |
108 | | - public async Task GetConnection_Async_TimeoutTimerReflectsTimeAlreadyConsumed() |
| 144 | + [Theory] |
| 145 | + [InlineData(true, TimerKind.Expired, 5_000, 0u)] |
| 146 | + [InlineData(true, TimerKind.Infinite, 5_000, unchecked((uint)Timeout.Infinite))] |
| 147 | + [InlineData(false, TimerKind.Expired, 1_500, 1_500u)] |
| 148 | + [InlineData(false, TimerKind.Expired, 0, unchecked((uint)Timeout.Infinite))] |
| 149 | + public void ResolvePoolWaitTimeoutMs_ReturnsExpected( |
| 150 | + bool switchEnabled, |
| 151 | + TimerKind timerKind, |
| 152 | + int creationTimeoutMs, |
| 153 | + uint expected) |
109 | 154 | { |
110 | 155 | // Arrange |
111 | | - var factory = new MockSqlConnectionFactory(); |
112 | | - var pool = CreatePool(factory); |
113 | | - var owner = new SqlConnection("Timeout=30"); |
114 | | - var fakeTime = new FakeTimeProvider(); |
115 | | - TimeoutTimer timer = TimeoutTimer.StartNew(TimeSpan.FromSeconds(30), fakeTime); |
116 | | - var tcs = new TaskCompletionSource<DbConnectionInternal>(); |
| 156 | + using LocalAppContextSwitchesHelper switches = new() |
| 157 | + { |
| 158 | + UseOverallConnectTimeoutForPoolWait = switchEnabled, |
| 159 | + }; |
| 160 | + TimeoutTimer timer = timerKind switch |
| 161 | + { |
| 162 | + TimerKind.Expired => TimeoutTimer.StartExpired(), |
| 163 | + TimerKind.Infinite => TimeoutTimer.StartNew(TimeSpan.Zero), |
| 164 | + _ => throw new ArgumentOutOfRangeException(nameof(timerKind)), |
| 165 | + }; |
117 | 166 |
|
118 | | - // Act: 5s consumed before entering the pool, then an async request. |
119 | | - fakeTime.Advance(TimeSpan.FromSeconds(5)); |
120 | | - pool.TryGetConnection( |
121 | | - owner, |
122 | | - taskCompletionSource: tcs, |
| 167 | + // Act |
| 168 | + uint result = WaitHandleDbConnectionPool.ResolvePoolWaitTimeoutMs( |
123 | 169 | timer, |
124 | | - out DbConnectionInternal? connection); |
125 | | - |
126 | | - // Bound the await so a regression in the pool can't hang the suite. |
127 | | - Task completed = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromSeconds(30))); |
128 | | - Assert.Same(tcs.Task, completed); |
129 | | - DbConnectionInternal result = await tcs.Task; |
| 170 | + creationTimeoutMs); |
130 | 171 |
|
131 | | - // Assert: factory got the caller's timer with the reduced budget. |
132 | | - Assert.NotNull(result); |
133 | | - Assert.Same(timer, factory.CapturedTimeout); |
134 | | - Assert.Equal(25_000, factory.CapturedTimeout!.MillisecondsRemainingInt); |
| 172 | + // Assert |
| 173 | + Assert.Equal(expected, result); |
135 | 174 | } |
136 | 175 |
|
137 | 176 | /// <summary> |
|
0 commit comments