Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Analytics-CSharp/Segment/Analytics/Configuration.cs
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Segment.Analytics.Policies;
using Segment.Analytics.Retry;
using Segment.Analytics.Utilities;
using Segment.Concurrent;
using Segment.Serialization;
Expand DownExpand Up@@ -47,6 +48,21 @@ private set

public IEventPipelineProvider EventPipelineProvider { get; }

/// <summary>
/// HTTP retry configuration for rate limiting and exponential backoff. Defaults to
/// <c>null</c>. Set it before constructing <c>Analytics</c>, e.g.
/// <c>new Configuration("writeKey") { HttpConfig = new HttpConfig(...) }</c>.
/// Mirrors analytics-kotlin's mutable <c>Configuration.httpConfig</c>.
/// <para>
/// This sets the pipeline's starting configuration only. CDN settings take precedence:
/// any settings payload carrying an <c>httpConfig</c> key replaces this value, and a CDN
/// payload is treated as enabling a subsystem unless it says <c>"enabled": "false"</c>.
/// A payload with no <c>httpConfig</c> key leaves this value in effect. This matches the
/// behaviour of analytics-kotlin and analytics-swift.
/// </para>
/// </summary>
public HttpConfig HttpConfig { get; set; }

/// <summary>
/// Configuration that analytics can use
/// </summary>
Expand Down
11 changes: 7 additions & 4 deletions Analytics-CSharp/Segment/Analytics/Retry/RetryConfig.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@

namespace Segment.Analytics.Retry
{
internal class RateLimitConfig
public class RateLimitConfig
{
public bool Enabled { get; }
public int MaxRetryCount { get; }
Expand All@@ -23,7 +23,7 @@ public RateLimitConfig(bool enabled = false, int maxRetryCount = 100, int maxRet
);
}

internal class BackoffConfig
public class BackoffConfig
{
public bool Enabled { get; }
public int MaxRetryCount { get; }
Expand DownExpand Up@@ -57,7 +57,10 @@ public BackoffConfig(
Default4xxBehavior = default4xxBehavior;
Default5xxBehavior = default5xxBehavior;
UnknownCodeBehavior = unknownCodeBehavior;
StatusCodeOverrides = statusCodeOverrides ?? DefaultStatusCodeOverrides;
// Copy: the property is public, and sharing the static default would let one
// caller's mutation corrupt every BackoffConfig built afterwards in the process.
StatusCodeOverrides = new Dictionary<int, RetryBehavior>(
statusCodeOverrides ?? DefaultStatusCodeOverrides);
}

public BackoffConfig Validated() => new BackoffConfig(
Expand DownExpand Up@@ -109,7 +112,7 @@ public RetryConfig(RateLimitConfig rateLimitConfig = null, BackoffConfig backoff
}
}

internal class HttpConfig
public class HttpConfig
{
public RateLimitConfig RateLimitConfig { get; }
public BackoffConfig BackoffConfig { get; }
Expand Down
2 changes: 1 addition & 1 deletion Analytics-CSharp/Segment/Analytics/Retry/RetryTypes.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ internal enum PipelineState
RateLimited
}

internal enum RetryBehavior
public enum RetryBehavior
{
Retry,
Drop
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,7 +48,7 @@ public EventPipeline(
string apiHost = HTTPClient.DefaultAPIHost)
: this(analytics, logTag, apiKey, flushPolicies, apiHost, (HttpConfig)null) { }

internal EventPipeline(
public EventPipeline(
Analytics analytics,
string logTag,
string apiKey,
Expand All@@ -69,7 +69,9 @@ internal EventPipeline(
Running = false;

var retryConfig = httpConfig != null
? new RetryConfig(httpConfig.RateLimitConfig, httpConfig.BackoffConfig)
// Validated(): user-supplied config reaches us unclamped, unlike the
// CDN path which HttpConfigParser already validates.
? new RetryConfig(httpConfig.RateLimitConfig.Validated(), httpConfig.BackoffConfig.Validated())
: new RetryConfig();
_retryStateMachine = new RetryStateMachine(retryConfig);
_retryState = RetryStateStorage.LoadRetryState(_storage);
Expand All@@ -78,7 +80,7 @@ internal EventPipeline(
internal void UpdateHttpConfig(HttpConfig config)
{
var retryConfig = config != null
? new RetryConfig(config.RateLimitConfig, config.BackoffConfig)
? new RetryConfig(config.RateLimitConfig.Validated(), config.BackoffConfig.Validated())
: new RetryConfig();
_retryStateMachine = new RetryStateMachine(retryConfig);
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,8 @@ public IEventPipeline Create(Analytics analytics, string key)
return new EventPipeline(analytics, key,
analytics.Configuration.WriteKey,
analytics.Configuration.FlushPolicies,
analytics.Configuration.ApiHost);
analytics.Configuration.ApiHost,
analytics.Configuration.HttpConfig);
}
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,7 +61,7 @@ public SyncEventPipeline(
CancellationToken? flushCancellationToken = null)
: this(analytics, logTag, apiKey, flushPolicies, apiHost, flushTimeout, flushCancellationToken, null) { }

internal SyncEventPipeline(
public SyncEventPipeline(
Analytics analytics,
string logTag,
string apiKey,
Expand All@@ -86,7 +86,9 @@ internal SyncEventPipeline(
_flushCancellationToken = flushCancellationToken ?? CancellationToken.None;

var retryConfig = httpConfig != null
? new RetryConfig(httpConfig.RateLimitConfig, httpConfig.BackoffConfig)
// Validated(): user-supplied config reaches us unclamped, unlike the
// CDN path which HttpConfigParser already validates.
? new RetryConfig(httpConfig.RateLimitConfig.Validated(), httpConfig.BackoffConfig.Validated())
: new RetryConfig();
_retryStateMachine = new RetryStateMachine(retryConfig);
_retryState = RetryStateStorage.LoadRetryState(_storage);
Expand All@@ -95,7 +97,7 @@ internal SyncEventPipeline(
internal void UpdateHttpConfig(HttpConfig config)
{
var retryConfig = config != null
? new RetryConfig(config.RateLimitConfig, config.BackoffConfig)
? new RetryConfig(config.RateLimitConfig.Validated(), config.BackoffConfig.Validated())
: new RetryConfig();
_retryStateMachine = new RetryStateMachine(retryConfig);
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,8 @@ public IEventPipeline Create(Analytics analytics, string key)
analytics.Configuration.FlushPolicies,
analytics.Configuration.ApiHost,
_flushTimeout,
_flushCancellationToken);
_flushCancellationToken,
analytics.Configuration.HttpConfig);
}
}
}
130 changes: 130 additions & 0 deletions Tests/Retry/ConfigurationHttpConfigTest.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
using Moq;
using Segment.Analytics;
using Segment.Analytics.Retry;
using Segment.Analytics.Utilities;
using Segment.Serialization;
using Tests.Utils;
using Xunit;

namespace Tests.Retry
{
/// <summary>
/// Configuration.HttpConfig is the user-facing entry point for retry settings,
/// mirroring Kotlin's Configuration.httpConfig and Swift's .httpConfig(_:).
/// These cover that a config supplied there actually reaches the pipeline's
/// retry state machine; CDN settings still override it later via UpdateHttpConfig.
/// </summary>
public class ConfigurationHttpConfigTest
{
private static Analytics CreateAnalytics(HttpConfig httpConfig)
{
Settings? settings = JsonUtility.FromJson<Settings?>(
"{\"integrations\":{\"Segment.io\":{\"apiKey\":\"k\"}},\"plan\":{},\"edgeFunction\":{}}");

var mockHttpClient = new Mock<HTTPClient>(null, null, null);
mockHttpClient.Setup(c => c.Settings()).ReturnsAsync(settings);

var config = new Configuration(
writeKey: "123",
autoAddSegmentDestination: false,
useSynchronizeDispatcher: true,
flushInterval: 0,
flushAt: 2,
httpClientProvider: new MockHttpClientProvider(mockHttpClient),
storageProvider: new MockStorageProvider(new Mock<IStorage>())
)
{
HttpConfig = httpConfig
};
return new Analytics(config);
}

[Fact]
public void Configuration_ExposesHttpConfig()
{
var httpConfig = new HttpConfig(backoffConfig: new BackoffConfig(enabled: true, maxRetryCount: 7));
Analytics analytics = CreateAnalytics(httpConfig);

Assert.Same(httpConfig, analytics.Configuration.HttpConfig);
}

[Fact]
public void Configuration_HttpConfigDefaultsToNull()
{
Analytics analytics = CreateAnalytics(null);

Assert.Null(analytics.Configuration.HttpConfig);
}

[Fact]
public void EventPipeline_WithoutHttpConfig_IsLegacyMode()
{
Analytics analytics = CreateAnalytics(null);

var pipeline = (EventPipeline)new EventPipelineProvider().Create(analytics, "key");

Assert.True(pipeline._retryStateMachine.IsLegacyMode);
}

[Fact]
public void EventPipeline_WithHttpConfig_LeavesLegacyMode()
{
Analytics analytics = CreateAnalytics(
new HttpConfig(backoffConfig: new BackoffConfig(enabled: true)));

var pipeline = (EventPipeline)new EventPipelineProvider().Create(analytics, "key");

Assert.False(pipeline._retryStateMachine.IsLegacyMode);
}

[Fact]
public void SyncEventPipeline_WithoutHttpConfig_IsLegacyMode()
{
Analytics analytics = CreateAnalytics(null);

var pipeline = (SyncEventPipeline)new SyncEventPipelineProvider().Create(analytics, "key");

Assert.True(pipeline._retryStateMachine.IsLegacyMode);
}

[Fact]
public void BackoffConfig_DoesNotShareTheDefaultOverrideMap()
{
var first = new BackoffConfig(enabled: true);
first.StatusCodeOverrides[500] = RetryBehavior.Drop;

var second = new BackoffConfig(enabled: true);

Assert.False(second.StatusCodeOverrides.ContainsKey(500));
Assert.NotSame(first.StatusCodeOverrides, second.StatusCodeOverrides);
}

[Fact]
public void UserSuppliedHttpConfig_IsValidatedOnTheWayIn()
{
// maxRetryInterval: 0 is out of range and must clamp to 1 second, exactly as the
// CDN path does via HttpConfigParser. Unvalidated it would schedule the retry at
// currentTime, i.e. no wait at all.
Analytics analytics = CreateAnalytics(
new HttpConfig(rateLimitConfig: new RateLimitConfig(enabled: true, maxRetryInterval: 0)));

var pipeline = (EventPipeline)new EventPipelineProvider().Create(analytics, "key");
RetryState state = pipeline._retryStateMachine.HandleResponse(
new RetryState(),
new ResponseInfo(429, retryAfterSeconds: null, batchFile: "b.json", currentTime: 1000));

Assert.Equal(2000, state.WaitUntilTime);
}

[Fact]
public void SyncEventPipeline_WithHttpConfig_LeavesLegacyMode()
{
Analytics analytics = CreateAnalytics(
new HttpConfig(rateLimitConfig: new RateLimitConfig(enabled: true)));

var pipeline = (SyncEventPipeline)new SyncEventPipelineProvider().Create(analytics, "key");

Assert.False(pipeline._retryStateMachine.IsLegacyMode);
}
}
}