Skip to content

Commit ee46b63

Browse files
Added an option to specify max idle time for a pooled socket. (#185)
Add an option to specify max idle time for a pooled socket Co-authored-by: Roman Polunin <[email protected]> Co-authored-by: dudu <[email protected]>
1 parent ecb2bce commit ee46b63

11 files changed

+176
-62
lines changed

src/Enyim.Caching/Configuration/ISocketPoolConfiguration.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ TimeSpan DeadTimeout
6868
set;
6969
}
7070

71+
/// <summary>
72+
/// Gets or sets a value that specified the amount of time after which a pooled socket will be discarded. Discarding will only happen at the time when the socket is picked up from the socket pool, e.g. it depends on usage.
73+
/// </summary>
74+
/// <returns>The value of the connection idle timeout.</returns>
75+
TimeSpan ConnectionIdleTimeout
76+
{
77+
get;
78+
set;
79+
}
80+
7181
TimeSpan InitPoolTimeout { get; set; }
7282

7383
INodeFailurePolicyFactory FailurePolicyFactory { get; set; }

src/Enyim.Caching/Configuration/MemcachedClientConfiguration.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ public MemcachedClientConfiguration(
8181
SocketPool.QueueTimeout = options.SocketPool.QueueTimeout;
8282
_logger.LogInformation($"{nameof(SocketPool.QueueTimeout)}: {SocketPool.QueueTimeout}");
8383

84+
SocketPool.ConnectionIdleTimeout = options.SocketPool.ConnectionIdleTimeout;
85+
_logger.LogInformation($"{nameof(SocketPool.ConnectionIdleTimeout)}: {SocketPool.ConnectionIdleTimeout}");
86+
8487
SocketPool.InitPoolTimeout = options.SocketPool.InitPoolTimeout;
8588

8689
SocketPool.FailurePolicyFactory = options.SocketPool.FailurePolicyFactory;

src/Enyim.Caching/Configuration/MemcachedClientOptions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public class SocketPoolOptions
7272
public TimeSpan ReceiveTimeout { get; set; } = new TimeSpan(0, 0, 10);
7373
public TimeSpan DeadTimeout { get; set; } = new TimeSpan(0, 0, 10);
7474
public TimeSpan QueueTimeout { get; set; } = new TimeSpan(0, 0, 0, 0, 100);
75+
public TimeSpan ConnectionIdleTimeout { get; set; } = TimeSpan.Zero;
7576
public TimeSpan InitPoolTimeout { get; set; } = new TimeSpan(0, 1, 0);
7677
public INodeFailurePolicyFactory FailurePolicyFactory { get; set; } = new ThrottlingFailurePolicyFactory(5, TimeSpan.FromMilliseconds(2000));
7778

@@ -93,6 +94,7 @@ public void CheckTimeout()
9394
CheckTimeout(nameof(ReceiveTimeout), ReceiveTimeout);
9495
CheckTimeout(nameof(DeadTimeout), DeadTimeout);
9596
CheckTimeout(nameof(QueueTimeout), QueueTimeout);
97+
CheckTimeout(nameof(ConnectionIdleTimeout), ConnectionIdleTimeout);
9698
}
9799

98100
private void CheckTimeout(string paramName, TimeSpan value)

src/Enyim.Caching/Configuration/SocketPoolConfiguration.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public class SocketPoolConfiguration : ISocketPoolConfiguration
1515
private TimeSpan _receiveTimeout = new TimeSpan(0, 0, 10);
1616
private TimeSpan _deadTimeout = new TimeSpan(0, 0, 10);
1717
private TimeSpan _queueTimeout = new TimeSpan(0, 0, 0, 0, 100);
18+
private TimeSpan _connectionIdleTimeout = TimeSpan.Zero;
1819
private TimeSpan _initPoolTimeout = new TimeSpan(0, 1, 0);
1920
private INodeFailurePolicyFactory _failurePolicyFactory = new ThrottlingFailurePolicyFactory(5, TimeSpan.FromMilliseconds(2000));
2021

@@ -95,6 +96,18 @@ TimeSpan ISocketPoolConfiguration.DeadTimeout
9596
}
9697
}
9798

99+
TimeSpan ISocketPoolConfiguration.ConnectionIdleTimeout
100+
{
101+
get { return _connectionIdleTimeout; }
102+
set
103+
{
104+
if (value < TimeSpan.Zero)
105+
throw new ArgumentOutOfRangeException("value", "value must be positive");
106+
107+
_connectionIdleTimeout = value;
108+
}
109+
}
110+
98111
INodeFailurePolicyFactory ISocketPoolConfiguration.FailurePolicyFactory
99112
{
100113
get { return _failurePolicyFactory; }

src/Enyim.Caching/Memcached/MemcachedNode.cs

Lines changed: 62 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
using Enyim.Caching.Memcached.Results;
44
using Enyim.Caching.Memcached.Results.Extensions;
55
using Enyim.Collections;
6+
67
using Microsoft.Extensions.Logging;
8+
79
using System;
810
using System.Collections.Concurrent;
911
using System.Collections.Generic;
@@ -25,7 +27,7 @@ namespace Enyim.Caching.Memcached
2527
public class MemcachedNode : IMemcachedNode
2628
{
2729
private readonly ILogger _logger;
28-
private static readonly object SyncRoot = new Object();
30+
private static readonly object SyncRoot = new();
2931
private bool _isDisposed;
3032
private readonly EndPoint _endPoint;
3133
private readonly ISocketPoolConfiguration _config;
@@ -46,8 +48,8 @@ public MemcachedNode(
4648
EndPointString = endpoint?.ToString().Replace("Unspecified/", string.Empty);
4749
_config = socketPoolConfig;
4850

49-
if (socketPoolConfig.ConnectionTimeout.TotalMilliseconds >= Int32.MaxValue)
50-
throw new InvalidOperationException("ConnectionTimeout must be < Int32.MaxValue");
51+
if (socketPoolConfig.ConnectionTimeout.TotalMilliseconds >= int.MaxValue)
52+
throw new InvalidOperationException("ConnectionTimeout must be < int.MaxValue");
5153

5254
if (socketPoolConfig.InitPoolTimeout.TotalSeconds < 1)
5355
{
@@ -281,6 +283,7 @@ private class InternalPoolImpl : IDisposable
281283
private readonly EndPoint _endPoint;
282284
private readonly TimeSpan _queueTimeout;
283285
private readonly TimeSpan _receiveTimeout;
286+
private readonly TimeSpan _connectionIdleTimeout;
284287
private SemaphoreSlim _semaphore;
285288

286289
private readonly object initLock = new Object();
@@ -298,12 +301,15 @@ internal InternalPoolImpl(
298301
throw new InvalidOperationException("queueTimeout must be >= TimeSpan.Zero", null);
299302
if (config.ReceiveTimeout < TimeSpan.Zero)
300303
throw new InvalidOperationException("ReceiveTimeout must be >= TimeSpan.Zero", null);
304+
if (config.ConnectionIdleTimeout < TimeSpan.Zero)
305+
throw new InvalidOperationException("ConnectionIdleTimeout must be >= TimeSpan.Zero", null);
301306

302307
_ownerNode = ownerNode;
303308
_isAlive = true;
304309
_endPoint = ownerNode.EndPoint;
305310
_queueTimeout = config.QueueTimeout;
306311
_receiveTimeout = config.ReceiveTimeout;
312+
_connectionIdleTimeout = config.ConnectionIdleTimeout;
307313

308314
_minItems = config.MinPoolSize;
309315
_maxItems = config.MaxPoolSize;
@@ -344,7 +350,7 @@ internal void InitPool()
344350
}
345351
catch (Exception e)
346352
{
347-
_logger.LogError("Could not init pool.", new EventId(0), e);
353+
_logger.LogError(e, "Could not init pool.");
348354

349355
MarkAsDead();
350356
}
@@ -379,7 +385,7 @@ internal async Task InitPoolAsync()
379385
}
380386
catch (Exception e)
381387
{
382-
_logger.LogError("Could not init pool.", new EventId(0), e);
388+
_logger.LogError(e, "Could not init pool.");
383389

384390
MarkAsDead();
385391
}
@@ -418,10 +424,9 @@ public DateTime MarkedAsDeadUtc
418424
public IPooledSocketResult Acquire()
419425
{
420426
var result = new PooledSocketResult();
421-
var message = string.Empty;
422-
423427
if (_isDebugEnabled) _logger.LogDebug($"Acquiring stream from pool on node '{_endPoint}'");
424428

429+
string message;
425430
if (!_isAlive || _isDisposed)
426431
{
427432
message = "Pool is dead or disposed, returning null. " + _endPoint;
@@ -432,8 +437,6 @@ public IPooledSocketResult Acquire()
432437
return result;
433438
}
434439

435-
PooledSocket retval = null;
436-
437440
if (!_semaphore.Wait(_queueTimeout))
438441
{
439442
message = "Pool is full, timeouting. " + _endPoint;
@@ -456,20 +459,23 @@ public IPooledSocketResult Acquire()
456459
return result;
457460
}
458461

462+
463+
PooledSocket socket;
459464
// do we have free items?
460-
if (_freeItems.TryPop(out retval))
465+
if (TryPopPooledSocket(out socket))
461466
{
462467
#region [ get it from the pool ]
463468

464469
try
465470
{
466-
retval.Reset();
471+
socket.Reset();
467472

468-
message = "Socket was reset. " + retval.InstanceId;
473+
message = "Socket was reset. " + socket.InstanceId;
469474
if (_isDebugEnabled) _logger.LogDebug(message);
470475

471476
result.Pass(message);
472-
result.Value = retval;
477+
socket.UpdateLastUsed();
478+
result.Value = socket;
473479
return result;
474480
}
475481
catch (Exception e)
@@ -495,9 +501,9 @@ public IPooledSocketResult Acquire()
495501
{
496502
// okay, create the new item
497503
var startTime = DateTime.Now;
498-
retval = CreateSocket();
504+
socket = CreateSocket();
499505
_logger.LogInformation("MemcachedAcquire-CreateSocket: {0}ms", (DateTime.Now - startTime).TotalMilliseconds);
500-
result.Value = retval;
506+
result.Value = socket;
501507
result.Pass();
502508
}
503509
catch (Exception e)
@@ -542,7 +548,7 @@ public async Task<IPooledSocketResult> AcquireAsync()
542548
return result;
543549
}
544550

545-
PooledSocket retval = null;
551+
PooledSocket socket = null;
546552

547553
if (!await _semaphore.WaitAsync(_queueTimeout))
548554
{
@@ -566,13 +572,13 @@ public async Task<IPooledSocketResult> AcquireAsync()
566572
}
567573

568574
// do we have free items?
569-
if (_freeItems.TryPop(out retval))
575+
if (TryPopPooledSocket(out socket))
570576
{
571577
#region [ get it from the pool ]
572578

573579
try
574580
{
575-
var resetTask = retval.ResetAsync();
581+
var resetTask = socket.ResetAsync();
576582

577583
if (await Task.WhenAny(resetTask, Task.Delay(_receiveTimeout)) == resetTask)
578584
{
@@ -581,19 +587,20 @@ public async Task<IPooledSocketResult> AcquireAsync()
581587
else
582588
{
583589
_semaphore.Release();
584-
retval.IsAlive = false;
590+
socket.IsAlive = false;
585591

586-
message = "Timeout to reset an acquired socket. InstanceId " + retval.InstanceId;
592+
message = "Timeout to reset an acquired socket. InstanceId " + socket.InstanceId;
587593
_logger.LogError(message);
588594
result.Fail(message);
589595
return result;
590596
}
591597

592-
message = "Socket was reset. InstanceId " + retval.InstanceId;
598+
message = "Socket was reset. InstanceId " + socket.InstanceId;
593599
if (_isDebugEnabled) _logger.LogDebug(message);
594600

595601
result.Pass(message);
596-
result.Value = retval;
602+
socket.UpdateLastUsed();
603+
result.Value = socket;
597604
return result;
598605
}
599606
catch (Exception e)
@@ -619,9 +626,9 @@ public async Task<IPooledSocketResult> AcquireAsync()
619626
{
620627
// okay, create the new item
621628
var startTime = DateTime.Now;
622-
retval = await CreateSocketAsync();
629+
socket = await CreateSocketAsync();
623630
_logger.LogInformation("MemcachedAcquire-CreateSocket: {0}ms", (DateTime.Now - startTime).TotalMilliseconds);
624-
result.Value = retval;
631+
result.Value = socket;
625632
result.Pass();
626633
}
627634
catch (Exception e)
@@ -737,6 +744,34 @@ private void ReleaseSocket(PooledSocket socket)
737744
}
738745
}
739746

747+
private bool TryPopPooledSocket(out PooledSocket pooledSocket)
748+
{
749+
if (_freeItems.TryPop(out var socket))
750+
{
751+
if (_connectionIdleTimeout > TimeSpan.Zero &&
752+
socket.LastUsed < DateTime.UtcNow.Subtract(_connectionIdleTimeout))
753+
{
754+
try
755+
{
756+
_logger.LogInformation("Connection idle timeout {idleTimeout} reached.", _connectionIdleTimeout);
757+
socket.Destroy();
758+
}
759+
catch (Exception ex)
760+
{
761+
_logger.LogError(ex, $"Failed to destroy {nameof(PooledSocket)}");
762+
}
763+
764+
pooledSocket = null;
765+
return false;
766+
}
767+
768+
pooledSocket = socket;
769+
return true;
770+
}
771+
772+
pooledSocket = null;
773+
return false;
774+
}
740775

741776
~InternalPoolImpl()
742777
{
@@ -758,12 +793,10 @@ public void Dispose()
758793
_isAlive = false;
759794
_isDisposed = true;
760795

761-
PooledSocket ps;
762-
763-
while (_freeItems.TryPop(out ps))
796+
while (_freeItems.TryPop(out var socket))
764797
{
765-
try { ps.Destroy(); }
766-
catch { }
798+
try { socket.Destroy(); }
799+
catch (Exception ex) { _logger.LogError(ex, $"failed to destroy {nameof(PooledSocket)}"); }
767800
}
768801

769802
_ownerNode = null;

src/Enyim.Caching/Memcached/PooledSocket.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,13 @@ public int Available
176176
get { return _socket.Available; }
177177
}
178178

179+
public DateTime LastUsed { get; set; } = DateTime.UtcNow;
180+
181+
public void UpdateLastUsed()
182+
{
183+
LastUsed = DateTime.UtcNow;
184+
}
185+
179186
public void Reset()
180187
{
181188
// _inputStream.Flush();

test/Enyim.Caching.Tests/MemcachedClientCasTests.cs

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,31 @@
66

77
namespace Enyim.Caching.Tests
88
{
9-
109
public class MemcachedClientCasTests : MemcachedClientTestsBase
11-
{
12-
13-
[Fact]
14-
public void When_Storing_Item_With_Valid_Cas_Result_Is_Successful()
15-
{
16-
var key = GetUniqueKey("cas");
17-
var value = GetRandomString();
18-
var storeResult = Store(StoreMode.Add, key, value);
19-
StoreAssertPass(storeResult);
20-
21-
var casResult = _client.ExecuteCas(StoreMode.Set, key, value, storeResult.Cas);
22-
StoreAssertPass(casResult);
23-
}
24-
25-
[Fact]
26-
public void When_Storing_Item_With_Invalid_Cas_Result_Is_Not_Successful()
27-
{
28-
var key = GetUniqueKey("cas");
29-
var value = GetRandomString();
30-
var storeResult = Store(StoreMode.Add, key, value);
31-
StoreAssertPass(storeResult);
32-
33-
var casResult = _client.ExecuteCas(StoreMode.Set, key, value, storeResult.Cas - 1);
34-
StoreAssertFail(casResult);
35-
}
10+
{
11+
[Fact]
12+
public void When_Storing_Item_With_Valid_Cas_Result_Is_Successful()
13+
{
14+
var key = GetUniqueKey("cas");
15+
var value = GetRandomString();
16+
var storeResult = Store(StoreMode.Add, key, value);
17+
StoreAssertPass(storeResult);
18+
19+
var casResult = _client.ExecuteCas(StoreMode.Set, key, value, storeResult.Cas);
20+
StoreAssertPass(casResult);
21+
}
22+
23+
[Fact]
24+
public void When_Storing_Item_With_Invalid_Cas_Result_Is_Not_Successful()
25+
{
26+
var key = GetUniqueKey("cas");
27+
var value = GetRandomString();
28+
var storeResult = Store(StoreMode.Add, key, value);
29+
StoreAssertPass(storeResult);
30+
31+
var casResult = _client.ExecuteCas(StoreMode.Set, key, value, storeResult.Cas - 1);
32+
StoreAssertFail(casResult);
33+
}
3634

3735

3836
[Fact]

test/Enyim.Caching.Tests/MemcachedClientTestsBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public MemcachedClientTestsBase(Action<MemcachedClientOptions> onAddEnyimMemcach
2626
onAddEnyimMemcached?.Invoke(options);
2727
});
2828

29-
services.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Information).AddConsole());
29+
services.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Warning).AddConsole());
3030
IServiceProvider serviceProvider = services.BuildServiceProvider();
3131
_client = serviceProvider.GetService<IMemcachedClient>() as MemcachedClient;
3232
}

0 commit comments

Comments
 (0)