1 // Licensed to the .NET Foundation under one or more agreements. 2 // The .NET Foundation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System.Collections.Generic; 6 using System.Diagnostics; 7 using System.Data.Common; 8 using System.Threading; 9 using System.Threading.Tasks; 10 11 namespace System.Data.ProviderBase 12 { 13 internal abstract class DbConnectionFactory 14 { 15 private Dictionary<DbConnectionPoolKey, DbConnectionPoolGroup> _connectionPoolGroups; 16 private readonly List<DbConnectionPool> _poolsToRelease; 17 private readonly List<DbConnectionPoolGroup> _poolGroupsToRelease; 18 private readonly Timer _pruningTimer; 19 20 private const int PruningDueTime = 4 * 60 * 1000; // 4 minutes 21 private const int PruningPeriod = 30 * 1000; // thirty seconds 22 23 24 // s_pendingOpenNonPooled is an array of tasks used to throttle creation of non-pooled connections to 25 // a maximum of Environment.ProcessorCount at a time. 26 private static uint s_pendingOpenNonPooledNext = 0; 27 private static Task<DbConnectionInternal>[] s_pendingOpenNonPooled = new Task<DbConnectionInternal>[Environment.ProcessorCount]; 28 private static Task<DbConnectionInternal> s_completedTask; 29 DbConnectionFactory()30 protected DbConnectionFactory() 31 { 32 _connectionPoolGroups = new Dictionary<DbConnectionPoolKey, DbConnectionPoolGroup>(); 33 _poolsToRelease = new List<DbConnectionPool>(); 34 _poolGroupsToRelease = new List<DbConnectionPoolGroup>(); 35 _pruningTimer = CreatePruningTimer(); 36 } 37 38 39 public abstract DbProviderFactory ProviderFactory 40 { 41 get; 42 } 43 44 ClearAllPools()45 public void ClearAllPools() 46 { 47 Dictionary<DbConnectionPoolKey, DbConnectionPoolGroup> connectionPoolGroups = _connectionPoolGroups; 48 foreach (KeyValuePair<DbConnectionPoolKey, DbConnectionPoolGroup> entry in connectionPoolGroups) 49 { 50 DbConnectionPoolGroup poolGroup = entry.Value; 51 if (null != poolGroup) 52 { 53 poolGroup.Clear(); 54 } 55 } 56 } 57 ClearPool(DbConnection connection)58 public void ClearPool(DbConnection connection) 59 { 60 ADP.CheckArgumentNull(connection, nameof(connection)); 61 62 DbConnectionPoolGroup poolGroup = GetConnectionPoolGroup(connection); 63 if (null != poolGroup) 64 { 65 poolGroup.Clear(); 66 } 67 } 68 ClearPool(DbConnectionPoolKey key)69 public void ClearPool(DbConnectionPoolKey key) 70 { 71 Debug.Assert(key != null, "key cannot be null"); 72 ADP.CheckArgumentNull(key.ConnectionString, nameof(key) + "." + nameof(key.ConnectionString)); 73 74 DbConnectionPoolGroup poolGroup; 75 Dictionary<DbConnectionPoolKey, DbConnectionPoolGroup> connectionPoolGroups = _connectionPoolGroups; 76 if (connectionPoolGroups.TryGetValue(key, out poolGroup)) 77 { 78 poolGroup.Clear(); 79 } 80 } 81 CreateConnectionPoolProviderInfo(DbConnectionOptions connectionOptions)82 internal virtual DbConnectionPoolProviderInfo CreateConnectionPoolProviderInfo(DbConnectionOptions connectionOptions) 83 { 84 return null; 85 } 86 87 CreateNonPooledConnection(DbConnection owningConnection, DbConnectionPoolGroup poolGroup, DbConnectionOptions userOptions)88 internal DbConnectionInternal CreateNonPooledConnection(DbConnection owningConnection, DbConnectionPoolGroup poolGroup, DbConnectionOptions userOptions) 89 { 90 Debug.Assert(null != owningConnection, "null owningConnection?"); 91 Debug.Assert(null != poolGroup, "null poolGroup?"); 92 93 DbConnectionOptions connectionOptions = poolGroup.ConnectionOptions; 94 DbConnectionPoolGroupProviderInfo poolGroupProviderInfo = poolGroup.ProviderInfo; 95 DbConnectionPoolKey poolKey = poolGroup.PoolKey; 96 97 DbConnectionInternal newConnection = CreateConnection(connectionOptions, poolKey, poolGroupProviderInfo, null, owningConnection, userOptions); 98 if (null != newConnection) 99 { 100 newConnection.MakeNonPooledObject(owningConnection); 101 } 102 return newConnection; 103 } 104 CreatePooledConnection(DbConnectionPool pool, DbConnection owningObject, DbConnectionOptions options, DbConnectionPoolKey poolKey, DbConnectionOptions userOptions)105 internal DbConnectionInternal CreatePooledConnection(DbConnectionPool pool, DbConnection owningObject, DbConnectionOptions options, DbConnectionPoolKey poolKey, DbConnectionOptions userOptions) 106 { 107 Debug.Assert(null != pool, "null pool?"); 108 DbConnectionPoolGroupProviderInfo poolGroupProviderInfo = pool.PoolGroup.ProviderInfo; 109 110 DbConnectionInternal newConnection = CreateConnection(options, poolKey, poolGroupProviderInfo, pool, owningObject, userOptions); 111 if (null != newConnection) 112 { 113 newConnection.MakePooledConnection(pool); 114 } 115 return newConnection; 116 } 117 CreateConnectionPoolGroupProviderInfo(DbConnectionOptions connectionOptions)118 internal virtual DbConnectionPoolGroupProviderInfo CreateConnectionPoolGroupProviderInfo(DbConnectionOptions connectionOptions) 119 { 120 return null; 121 } 122 CreatePruningTimer()123 private Timer CreatePruningTimer() 124 { 125 TimerCallback callback = new TimerCallback(PruneConnectionPoolGroups); 126 return new Timer(callback, null, PruningDueTime, PruningPeriod); 127 } 128 FindConnectionOptions(DbConnectionPoolKey key)129 protected DbConnectionOptions FindConnectionOptions(DbConnectionPoolKey key) 130 { 131 Debug.Assert(key != null, "key cannot be null"); 132 if (!string.IsNullOrEmpty(key.ConnectionString)) 133 { 134 DbConnectionPoolGroup connectionPoolGroup; 135 Dictionary<DbConnectionPoolKey, DbConnectionPoolGroup> connectionPoolGroups = _connectionPoolGroups; 136 if (connectionPoolGroups.TryGetValue(key, out connectionPoolGroup)) 137 { 138 return connectionPoolGroup.ConnectionOptions; 139 } 140 } 141 return null; 142 } 143 GetCompletedTask()144 private static Task<DbConnectionInternal> GetCompletedTask() 145 { 146 Debug.Assert(Monitor.IsEntered(s_pendingOpenNonPooled), $"Expected {nameof(s_pendingOpenNonPooled)} lock to be held."); 147 return s_completedTask ?? (s_completedTask = Task.FromResult<DbConnectionInternal>(null)); 148 } 149 TryGetConnection(DbConnection owningConnection, TaskCompletionSource<DbConnectionInternal> retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, out DbConnectionInternal connection)150 internal bool TryGetConnection(DbConnection owningConnection, TaskCompletionSource<DbConnectionInternal> retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, out DbConnectionInternal connection) 151 { 152 Debug.Assert(null != owningConnection, "null owningConnection?"); 153 154 DbConnectionPoolGroup poolGroup; 155 DbConnectionPool connectionPool; 156 connection = null; 157 158 // Work around race condition with clearing the pool between GetConnectionPool obtaining pool 159 // and GetConnection on the pool checking the pool state. Clearing the pool in this window 160 // will switch the pool into the ShuttingDown state, and GetConnection will return null. 161 // There is probably a better solution involving locking the pool/group, but that entails a major 162 // re-design of the connection pooling synchronization, so is postponed for now. 163 164 // Use retriesLeft to prevent CPU spikes with incremental sleep 165 // start with one msec, double the time every retry 166 // max time is: 1 + 2 + 4 + ... + 2^(retries-1) == 2^retries -1 == 1023ms (for 10 retries) 167 int retriesLeft = 10; 168 int timeBetweenRetriesMilliseconds = 1; 169 170 do 171 { 172 poolGroup = GetConnectionPoolGroup(owningConnection); 173 // Doing this on the callers thread is important because it looks up the WindowsIdentity from the thread. 174 connectionPool = GetConnectionPool(owningConnection, poolGroup); 175 if (null == connectionPool) 176 { 177 // If GetConnectionPool returns null, we can be certain that 178 // this connection should not be pooled via DbConnectionPool 179 // or have a disabled pool entry. 180 poolGroup = GetConnectionPoolGroup(owningConnection); // previous entry have been disabled 181 182 if (retry != null) 183 { 184 Task<DbConnectionInternal> newTask; 185 CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); 186 lock (s_pendingOpenNonPooled) 187 { 188 // look for an available task slot (completed or empty) 189 int idx; 190 for (idx = 0; idx < s_pendingOpenNonPooled.Length; idx++) 191 { 192 Task task = s_pendingOpenNonPooled[idx]; 193 if (task == null) 194 { 195 s_pendingOpenNonPooled[idx] = GetCompletedTask(); 196 break; 197 } 198 else if (task.IsCompleted) 199 { 200 break; 201 } 202 } 203 204 // if didn't find one, pick the next one in round-robin fashion 205 if (idx == s_pendingOpenNonPooled.Length) 206 { 207 idx = (int)(s_pendingOpenNonPooledNext % s_pendingOpenNonPooled.Length); 208 unchecked 209 { 210 s_pendingOpenNonPooledNext++; 211 } 212 } 213 214 // now that we have an antecedent task, schedule our work when it is completed. 215 // If it is a new slot or a completed task, this continuation will start right away. 216 newTask = s_pendingOpenNonPooled[idx].ContinueWith((_) => 217 { 218 var newConnection = CreateNonPooledConnection(owningConnection, poolGroup, userOptions); 219 if ((oldConnection != null) && (oldConnection.State == ConnectionState.Open)) 220 { 221 oldConnection.PrepareForReplaceConnection(); 222 oldConnection.Dispose(); 223 } 224 return newConnection; 225 }, cancellationTokenSource.Token, TaskContinuationOptions.LongRunning, TaskScheduler.Default); 226 227 // Place this new task in the slot so any future work will be queued behind it 228 s_pendingOpenNonPooled[idx] = newTask; 229 } 230 231 // Set up the timeout (if needed) 232 if (owningConnection.ConnectionTimeout > 0) 233 { 234 int connectionTimeoutMilliseconds = owningConnection.ConnectionTimeout * 1000; 235 cancellationTokenSource.CancelAfter(connectionTimeoutMilliseconds); 236 } 237 238 // once the task is done, propagate the final results to the original caller 239 newTask.ContinueWith((task) => 240 { 241 cancellationTokenSource.Dispose(); 242 if (task.IsCanceled) 243 { 244 retry.TrySetException(ADP.ExceptionWithStackTrace(ADP.NonPooledOpenTimeout())); 245 } 246 else if (task.IsFaulted) 247 { 248 retry.TrySetException(task.Exception.InnerException); 249 } 250 else 251 { 252 if (!retry.TrySetResult(task.Result)) 253 { 254 // The outer TaskCompletionSource was already completed 255 // Which means that we don't know if someone has messed with the outer connection in the middle of creation 256 // So the best thing to do now is to destroy the newly created connection 257 task.Result.DoomThisConnection(); 258 task.Result.Dispose(); 259 } 260 } 261 }, TaskScheduler.Default); 262 263 return false; 264 } 265 266 connection = CreateNonPooledConnection(owningConnection, poolGroup, userOptions); 267 } 268 else 269 { 270 // TODO: move this to src/Common and integrate with SqlClient 271 //if (((SqlClient.SqlConnection)owningConnection).ForceNewConnection) 272 //{ 273 // Debug.Assert(!(oldConnection is DbConnectionClosed), "Force new connection, but there is no old connection"); 274 // connection = connectionPool.ReplaceConnection(owningConnection, userOptions, oldConnection); 275 //} 276 //else 277 //{ 278 if (!connectionPool.TryGetConnection(owningConnection, retry, userOptions, out connection)) 279 { 280 return false; 281 } 282 //} 283 284 if (connection == null) 285 { 286 // connection creation failed on semaphore waiting or if max pool reached 287 if (connectionPool.IsRunning) 288 { 289 // If GetConnection failed while the pool is running, the pool timeout occurred. 290 throw ADP.PooledOpenTimeout(); 291 } 292 else 293 { 294 // We've hit the race condition, where the pool was shut down after we got it from the group. 295 // Yield time slice to allow shut down activities to complete and a new, running pool to be instantiated 296 // before retrying. 297 Threading.Thread.Sleep(timeBetweenRetriesMilliseconds); 298 timeBetweenRetriesMilliseconds *= 2; // double the wait time for next iteration 299 } 300 } 301 } 302 } while (connection == null && retriesLeft-- > 0); 303 304 if (connection == null) 305 { 306 // exhausted all retries or timed out - give up 307 throw ADP.PooledOpenTimeout(); 308 } 309 310 return true; 311 } 312 GetConnectionPool(DbConnection owningObject, DbConnectionPoolGroup connectionPoolGroup)313 private DbConnectionPool GetConnectionPool(DbConnection owningObject, DbConnectionPoolGroup connectionPoolGroup) 314 { 315 // if poolgroup is disabled, it will be replaced with a new entry 316 317 Debug.Assert(null != owningObject, "null owningObject?"); 318 Debug.Assert(null != connectionPoolGroup, "null connectionPoolGroup?"); 319 320 // It is possible that while the outer connection object has 321 // been sitting around in a closed and unused state in some long 322 // running app, the pruner may have come along and remove this 323 // the pool entry from the master list. If we were to use a 324 // pool entry in this state, we would create "unmanaged" pools, 325 // which would be bad. To avoid this problem, we automagically 326 // re-create the pool entry whenever it's disabled. 327 328 // however, don't rebuild connectionOptions if no pooling is involved - let new connections do that work 329 if (connectionPoolGroup.IsDisabled && (null != connectionPoolGroup.PoolGroupOptions)) 330 { 331 // reusing existing pool option in case user originally used SetConnectionPoolOptions 332 DbConnectionPoolGroupOptions poolOptions = connectionPoolGroup.PoolGroupOptions; 333 334 // get the string to hash on again 335 DbConnectionOptions connectionOptions = connectionPoolGroup.ConnectionOptions; 336 Debug.Assert(null != connectionOptions, "prevent expansion of connectionString"); 337 338 connectionPoolGroup = GetConnectionPoolGroup(connectionPoolGroup.PoolKey, poolOptions, ref connectionOptions); 339 Debug.Assert(null != connectionPoolGroup, "null connectionPoolGroup?"); 340 SetConnectionPoolGroup(owningObject, connectionPoolGroup); 341 } 342 DbConnectionPool connectionPool = connectionPoolGroup.GetConnectionPool(this); 343 return connectionPool; 344 } 345 GetConnectionPoolGroup(DbConnectionPoolKey key, DbConnectionPoolGroupOptions poolOptions, ref DbConnectionOptions userConnectionOptions)346 internal DbConnectionPoolGroup GetConnectionPoolGroup(DbConnectionPoolKey key, DbConnectionPoolGroupOptions poolOptions, ref DbConnectionOptions userConnectionOptions) 347 { 348 if (string.IsNullOrEmpty(key.ConnectionString)) 349 { 350 return (DbConnectionPoolGroup)null; 351 } 352 353 DbConnectionPoolGroup connectionPoolGroup; 354 Dictionary<DbConnectionPoolKey, DbConnectionPoolGroup> connectionPoolGroups = _connectionPoolGroups; 355 if (!connectionPoolGroups.TryGetValue(key, out connectionPoolGroup) || (connectionPoolGroup.IsDisabled && (null != connectionPoolGroup.PoolGroupOptions))) 356 { 357 // If we can't find an entry for the connection string in 358 // our collection of pool entries, then we need to create a 359 // new pool entry and add it to our collection. 360 361 DbConnectionOptions connectionOptions = CreateConnectionOptions(key.ConnectionString, userConnectionOptions); 362 if (null == connectionOptions) 363 { 364 throw ADP.InternalConnectionError(ADP.ConnectionError.ConnectionOptionsMissing); 365 } 366 367 if (null == userConnectionOptions) 368 { // we only allow one expansion on the connection string 369 userConnectionOptions = connectionOptions; 370 } 371 372 // We don't support connection pooling on Win9x 373 if (null == poolOptions) 374 { 375 if (null != connectionPoolGroup) 376 { 377 // reusing existing pool option in case user originally used SetConnectionPoolOptions 378 poolOptions = connectionPoolGroup.PoolGroupOptions; 379 } 380 else 381 { 382 // Note: may return null for non-pooled connections 383 poolOptions = CreateConnectionPoolGroupOptions(connectionOptions); 384 } 385 } 386 387 388 DbConnectionPoolGroup newConnectionPoolGroup = new DbConnectionPoolGroup(connectionOptions, key, poolOptions); 389 newConnectionPoolGroup.ProviderInfo = CreateConnectionPoolGroupProviderInfo(connectionOptions); 390 391 lock (this) 392 { 393 connectionPoolGroups = _connectionPoolGroups; 394 if (!connectionPoolGroups.TryGetValue(key, out connectionPoolGroup)) 395 { 396 // build new dictionary with space for new connection string 397 Dictionary<DbConnectionPoolKey, DbConnectionPoolGroup> newConnectionPoolGroups = new Dictionary<DbConnectionPoolKey, DbConnectionPoolGroup>(1 + connectionPoolGroups.Count); 398 foreach (KeyValuePair<DbConnectionPoolKey, DbConnectionPoolGroup> entry in connectionPoolGroups) 399 { 400 newConnectionPoolGroups.Add(entry.Key, entry.Value); 401 } 402 403 // lock prevents race condition with PruneConnectionPoolGroups 404 newConnectionPoolGroups.Add(key, newConnectionPoolGroup); 405 connectionPoolGroup = newConnectionPoolGroup; 406 _connectionPoolGroups = newConnectionPoolGroups; 407 } 408 else 409 { 410 Debug.Assert(!connectionPoolGroup.IsDisabled, "Disabled pool entry discovered"); 411 } 412 } 413 Debug.Assert(null != connectionPoolGroup, "how did we not create a pool entry?"); 414 Debug.Assert(null != userConnectionOptions, "how did we not have user connection options?"); 415 } 416 else if (null == userConnectionOptions) 417 { 418 userConnectionOptions = connectionPoolGroup.ConnectionOptions; 419 } 420 return connectionPoolGroup; 421 } 422 423 PruneConnectionPoolGroups(object state)424 private void PruneConnectionPoolGroups(object state) 425 { 426 // First, walk the pool release list and attempt to clear each 427 // pool, when the pool is finally empty, we dispose of it. If the 428 // pool isn't empty, it's because there are active connections or 429 // distributed transactions that need it. 430 lock (_poolsToRelease) 431 { 432 if (0 != _poolsToRelease.Count) 433 { 434 DbConnectionPool[] poolsToRelease = _poolsToRelease.ToArray(); 435 foreach (DbConnectionPool pool in poolsToRelease) 436 { 437 if (null != pool) 438 { 439 pool.Clear(); 440 441 if (0 == pool.Count) 442 { 443 _poolsToRelease.Remove(pool); 444 } 445 } 446 } 447 } 448 } 449 450 // Next, walk the pool entry release list and dispose of each 451 // pool entry when it is finally empty. If the pool entry isn't 452 // empty, it's because there are active pools that need it. 453 lock (_poolGroupsToRelease) 454 { 455 if (0 != _poolGroupsToRelease.Count) 456 { 457 DbConnectionPoolGroup[] poolGroupsToRelease = _poolGroupsToRelease.ToArray(); 458 foreach (DbConnectionPoolGroup poolGroup in poolGroupsToRelease) 459 { 460 if (null != poolGroup) 461 { 462 int poolsLeft = poolGroup.Clear(); // may add entries to _poolsToRelease 463 464 if (0 == poolsLeft) 465 { 466 _poolGroupsToRelease.Remove(poolGroup); 467 } 468 } 469 } 470 } 471 } 472 473 // Finally, we walk through the collection of connection pool entries 474 // and prune each one. This will cause any empty pools to be put 475 // into the release list. 476 lock (this) 477 { 478 Dictionary<DbConnectionPoolKey, DbConnectionPoolGroup> connectionPoolGroups = _connectionPoolGroups; 479 Dictionary<DbConnectionPoolKey, DbConnectionPoolGroup> newConnectionPoolGroups = new Dictionary<DbConnectionPoolKey, DbConnectionPoolGroup>(connectionPoolGroups.Count); 480 481 foreach (KeyValuePair<DbConnectionPoolKey, DbConnectionPoolGroup> entry in connectionPoolGroups) 482 { 483 if (null != entry.Value) 484 { 485 Debug.Assert(!entry.Value.IsDisabled, "Disabled pool entry discovered"); 486 487 // entries start active and go idle during prune if all pools are gone 488 // move idle entries from last prune pass to a queue for pending release 489 // otherwise process entry which may move it from active to idle 490 if (entry.Value.Prune()) 491 { // may add entries to _poolsToRelease 492 QueuePoolGroupForRelease(entry.Value); 493 } 494 else 495 { 496 newConnectionPoolGroups.Add(entry.Key, entry.Value); 497 } 498 } 499 } 500 _connectionPoolGroups = newConnectionPoolGroups; 501 } 502 } 503 QueuePoolForRelease(DbConnectionPool pool, bool clearing)504 internal void QueuePoolForRelease(DbConnectionPool pool, bool clearing) 505 { 506 // Queue the pool up for release -- we'll clear it out and dispose 507 // of it as the last part of the pruning timer callback so we don't 508 // do it with the pool entry or the pool collection locked. 509 Debug.Assert(null != pool, "null pool?"); 510 511 // set the pool to the shutdown state to force all active 512 // connections to be automatically disposed when they 513 // are returned to the pool 514 pool.Shutdown(); 515 516 lock (_poolsToRelease) 517 { 518 if (clearing) 519 { 520 pool.Clear(); 521 } 522 _poolsToRelease.Add(pool); 523 } 524 } 525 QueuePoolGroupForRelease(DbConnectionPoolGroup poolGroup)526 internal void QueuePoolGroupForRelease(DbConnectionPoolGroup poolGroup) 527 { 528 Debug.Assert(null != poolGroup, "null poolGroup?"); 529 530 lock (_poolGroupsToRelease) 531 { 532 _poolGroupsToRelease.Add(poolGroup); 533 } 534 } 535 CreateConnection(DbConnectionOptions options, DbConnectionPoolKey poolKey, object poolGroupProviderInfo, DbConnectionPool pool, DbConnection owningConnection, DbConnectionOptions userOptions)536 protected virtual DbConnectionInternal CreateConnection(DbConnectionOptions options, DbConnectionPoolKey poolKey, object poolGroupProviderInfo, DbConnectionPool pool, DbConnection owningConnection, DbConnectionOptions userOptions) 537 { 538 return CreateConnection(options, poolKey, poolGroupProviderInfo, pool, owningConnection); 539 } 540 CreateConnection(DbConnectionOptions options, DbConnectionPoolKey poolKey, object poolGroupProviderInfo, DbConnectionPool pool, DbConnection owningConnection)541 protected abstract DbConnectionInternal CreateConnection(DbConnectionOptions options, DbConnectionPoolKey poolKey, object poolGroupProviderInfo, DbConnectionPool pool, DbConnection owningConnection); 542 CreateConnectionOptions(string connectionString, DbConnectionOptions previous)543 protected abstract DbConnectionOptions CreateConnectionOptions(string connectionString, DbConnectionOptions previous); 544 CreateConnectionPoolGroupOptions(DbConnectionOptions options)545 protected abstract DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions(DbConnectionOptions options); 546 GetConnectionPoolGroup(DbConnection connection)547 internal abstract DbConnectionPoolGroup GetConnectionPoolGroup(DbConnection connection); 548 GetInnerConnection(DbConnection connection)549 internal abstract DbConnectionInternal GetInnerConnection(DbConnection connection); 550 551 PermissionDemand(DbConnection outerConnection)552 internal abstract void PermissionDemand(DbConnection outerConnection); 553 SetConnectionPoolGroup(DbConnection outerConnection, DbConnectionPoolGroup poolGroup)554 internal abstract void SetConnectionPoolGroup(DbConnection outerConnection, DbConnectionPoolGroup poolGroup); 555 SetInnerConnectionEvent(DbConnection owningObject, DbConnectionInternal to)556 internal abstract void SetInnerConnectionEvent(DbConnection owningObject, DbConnectionInternal to); 557 SetInnerConnectionFrom(DbConnection owningObject, DbConnectionInternal to, DbConnectionInternal from)558 internal abstract bool SetInnerConnectionFrom(DbConnection owningObject, DbConnectionInternal to, DbConnectionInternal from); 559 SetInnerConnectionTo(DbConnection owningObject, DbConnectionInternal to)560 internal abstract void SetInnerConnectionTo(DbConnection owningObject, DbConnectionInternal to); 561 } 562 } 563