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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ $RECYCLE.BIN/
x64/
x86/
bld/
build/
[Bb]in/
[Oo]bj/
[Ll]og/
Expand Down
3 changes: 3 additions & 0 deletions Analytics-CSharp/Analytics-CSharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,8 @@
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>DynamicProxyGenAssembly2</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>e2e-cli</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>
16 changes: 15 additions & 1 deletion Analytics-CSharp/Segment/Analytics/Plugins/SegmentDestination.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Segment.Analytics.Retry;
using Segment.Analytics.Utilities;
using Segment.Serialization;
using Segment.Sovran;
Expand Down Expand Up @@ -77,10 +78,23 @@ public override void Update(Settings settings, UpdateType type)
base.Update(settings, type);

JsonObject segmentInfo = settings.Integrations?.GetJsonObject(Key);

string apiHost = segmentInfo?.GetString(ApiHost);
if (apiHost != null && _pipeline != null)
{
_pipeline.ApiHost = apiHost;

JsonObject httpConfigJson = segmentInfo?.GetJsonObject("httpConfig");
if (httpConfigJson != null)
{
HttpConfig parsedConfig = HttpConfigParser.Parse(httpConfigJson);
if (parsedConfig != null)
{
EventPipeline concretePipeline = _pipeline as EventPipeline;
concretePipeline?.UpdateHttpConfig(parsedConfig);

SyncEventPipeline syncPipeline = _pipeline as SyncEventPipeline;
syncPipeline?.UpdateHttpConfig(parsedConfig);
}
}
}

Expand Down
128 changes: 128 additions & 0 deletions Analytics-CSharp/Segment/Analytics/Retry/HttpConfigParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System.Collections.Generic;
using System.Globalization;
using Segment.Serialization;

namespace Segment.Analytics.Retry
{
internal static class HttpConfigParser
{
public static HttpConfig Parse(JsonObject httpConfigJson)
{
if (httpConfigJson == null)
return null;

JsonObject rateLimitJson = httpConfigJson.GetJsonObject("rateLimitConfig");
JsonObject backoffJson = httpConfigJson.GetJsonObject("backoffConfig");

// CDN-sourced config defaults enabled to true (presence implies active).
// Only honor explicit enabled: false from CDN.
bool rateLimitEnabled = true;
if (rateLimitJson != null)
{
string enabledStr = rateLimitJson.GetString("enabled");
if (enabledStr != null && bool.TryParse(enabledStr, out bool parsed))
rateLimitEnabled = parsed;
}

bool backoffEnabled = true;
if (backoffJson != null)
{
string enabledStr = backoffJson.GetString("enabled");
if (enabledStr != null && bool.TryParse(enabledStr, out bool parsed))
backoffEnabled = parsed;
}

RateLimitConfig rateLimitConfig = ParseRateLimitConfig(rateLimitJson, rateLimitEnabled);
BackoffConfig backoffConfig = ParseBackoffConfig(backoffJson, backoffEnabled);

return new HttpConfig(
rateLimitConfig: rateLimitConfig.Validated(),
backoffConfig: backoffConfig.Validated()
);
}

private static RateLimitConfig ParseRateLimitConfig(JsonObject json, bool enabled)
{
if (json == null)
return new RateLimitConfig(enabled: enabled);

int maxRetryCount = 100;
string maxRetriesStr = json.GetString("maxRetryCount");
if (maxRetriesStr != null && int.TryParse(maxRetriesStr, out int parsedMaxRetries))
maxRetryCount = parsedMaxRetries;

int maxRetryInterval = 300;
string intervalStr = json.GetString("maxRetryInterval");
if (intervalStr != null && int.TryParse(intervalStr, out int parsedInterval))
maxRetryInterval = parsedInterval;

return new RateLimitConfig(
enabled: enabled,
maxRetryCount: maxRetryCount,
maxRetryInterval: maxRetryInterval
);
}

private static BackoffConfig ParseBackoffConfig(JsonObject json, bool enabled)
{
if (json == null)
return new BackoffConfig(enabled: enabled);

int maxRetryCount = 100;
string maxRetriesStr = json.GetString("maxRetryCount");
if (maxRetriesStr != null && int.TryParse(maxRetriesStr, out int parsedMaxRetries))
maxRetryCount = parsedMaxRetries;

double baseBackoffInterval = 0.5;
string baseStr = json.GetString("baseBackoffInterval");
if (baseStr != null && double.TryParse(baseStr, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsedBase))
baseBackoffInterval = parsedBase;

int maxBackoffInterval = 300;
string maxStr = json.GetString("maxBackoffInterval");
if (maxStr != null && int.TryParse(maxStr, out int parsedMax))
maxBackoffInterval = parsedMax;

long maxTotalBackoffDuration = 43200;
string durationStr = json.GetString("maxTotalBackoffDuration");
if (durationStr != null && long.TryParse(durationStr, out long parsedDuration))
maxTotalBackoffDuration = parsedDuration;

int jitterPercent = 10;
string jitterStr = json.GetString("jitterPercent");
if (jitterStr != null && int.TryParse(jitterStr, out int parsedJitter))
jitterPercent = parsedJitter;

Dictionary<int, RetryBehavior> statusCodeOverrides = null;
JsonObject overridesJson = json.GetJsonObject("statusCodeOverrides");
if (overridesJson != null)
statusCodeOverrides = ParseStatusCodeOverrides(overridesJson);

return new BackoffConfig(
enabled: enabled,
maxRetryCount: maxRetryCount,
baseBackoffInterval: baseBackoffInterval,
maxBackoffInterval: maxBackoffInterval,
maxTotalBackoffDuration: maxTotalBackoffDuration,
jitterPercent: jitterPercent,
statusCodeOverrides: statusCodeOverrides
);
}

private static Dictionary<int, RetryBehavior> ParseStatusCodeOverrides(JsonObject json)
{
var result = new Dictionary<int, RetryBehavior>();
foreach (string key in json.Keys)
{
if (!int.TryParse(key, out int code) || code < 100 || code > 599)
continue;
string val = json.GetString(key);
if (val == "retry")
result[code] = RetryBehavior.Retry;
else if (val == "drop")
result[code] = RetryBehavior.Drop;
}
return result;
}
}
}
123 changes: 123 additions & 0 deletions Analytics-CSharp/Segment/Analytics/Retry/RetryConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;

namespace Segment.Analytics.Retry
{
internal class RateLimitConfig
{
public bool Enabled { get; }
public int MaxRetryCount { get; }
public int MaxRetryInterval { get; }

public RateLimitConfig(bool enabled = false, int maxRetryCount = 100, int maxRetryInterval = 300)
{
Enabled = enabled;
MaxRetryCount = maxRetryCount;
MaxRetryInterval = maxRetryInterval;
}

public RateLimitConfig Validated() => new RateLimitConfig(
enabled: Enabled,
maxRetryCount: Math.Max(0, Math.Min(MaxRetryCount, 1000)),
maxRetryInterval: Math.Max(1, Math.Min(MaxRetryInterval, 3600))
);
}

internal class BackoffConfig
{
public bool Enabled { get; }
public int MaxRetryCount { get; }
public double BaseBackoffInterval { get; }
public int MaxBackoffInterval { get; }
public long MaxTotalBackoffDuration { get; }
public int JitterPercent { get; }
public RetryBehavior Default4xxBehavior { get; }
public RetryBehavior Default5xxBehavior { get; }
public RetryBehavior UnknownCodeBehavior { get; }
public Dictionary<int, RetryBehavior> StatusCodeOverrides { get; }

public BackoffConfig(
bool enabled = false,
int maxRetryCount = 100,
double baseBackoffInterval = 0.5,
int maxBackoffInterval = 300,
long maxTotalBackoffDuration = 43200,
int jitterPercent = 10,
RetryBehavior default4xxBehavior = RetryBehavior.Drop,
RetryBehavior default5xxBehavior = RetryBehavior.Retry,
RetryBehavior unknownCodeBehavior = RetryBehavior.Drop,
Dictionary<int, RetryBehavior> statusCodeOverrides = null)
{
Enabled = enabled;
MaxRetryCount = maxRetryCount;
BaseBackoffInterval = baseBackoffInterval;
MaxBackoffInterval = maxBackoffInterval;
MaxTotalBackoffDuration = maxTotalBackoffDuration;
JitterPercent = jitterPercent;
Default4xxBehavior = default4xxBehavior;
Default5xxBehavior = default5xxBehavior;
UnknownCodeBehavior = unknownCodeBehavior;
StatusCodeOverrides = statusCodeOverrides ?? DefaultStatusCodeOverrides;
}

public BackoffConfig Validated() => new BackoffConfig(
enabled: Enabled,
maxRetryCount: Math.Max(0, Math.Min(MaxRetryCount, 1000)),
baseBackoffInterval: Math.Max(0.1, Math.Min(BaseBackoffInterval, 60.0)),
maxBackoffInterval: Math.Max(1, Math.Min(MaxBackoffInterval, 3600)),
maxTotalBackoffDuration: Math.Max(0, Math.Min(MaxTotalBackoffDuration, 604800)),
jitterPercent: Math.Max(0, Math.Min(JitterPercent, 50)),
default4xxBehavior: Default4xxBehavior,
default5xxBehavior: Default5xxBehavior,
unknownCodeBehavior: UnknownCodeBehavior,
statusCodeOverrides: ValidateOverrides(StatusCodeOverrides)
);

private static Dictionary<int, RetryBehavior> ValidateOverrides(
Dictionary<int, RetryBehavior> overrides)
{
var result = new Dictionary<int, RetryBehavior>();
foreach (var kvp in overrides)
{
if (kvp.Key >= 100 && kvp.Key <= 599)
result[kvp.Key] = kvp.Value;
}
return result;
}

private static readonly Dictionary<int, RetryBehavior> DefaultStatusCodeOverrides =
new Dictionary<int, RetryBehavior>
{
{ 408, RetryBehavior.Retry },
{ 410, RetryBehavior.Retry },
{ 429, RetryBehavior.Retry },
{ 460, RetryBehavior.Retry },
{ 501, RetryBehavior.Drop },
{ 505, RetryBehavior.Drop }
};
}

internal class RetryConfig
{
public RateLimitConfig RateLimitConfig { get; }
public BackoffConfig BackoffConfig { get; }

public RetryConfig(RateLimitConfig rateLimitConfig = null, BackoffConfig backoffConfig = null)
{
RateLimitConfig = rateLimitConfig ?? new RateLimitConfig();
BackoffConfig = backoffConfig ?? new BackoffConfig();
}
}

internal class HttpConfig
{
public RateLimitConfig RateLimitConfig { get; }
public BackoffConfig BackoffConfig { get; }

public HttpConfig(RateLimitConfig rateLimitConfig = null, BackoffConfig backoffConfig = null)
{
RateLimitConfig = rateLimitConfig ?? new RateLimitConfig();
BackoffConfig = backoffConfig ?? new BackoffConfig();
}
}
}
93 changes: 93 additions & 0 deletions Analytics-CSharp/Segment/Analytics/Retry/RetryState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.Collections.Generic;
using System.Linq;

namespace Segment.Analytics.Retry
{
internal class BatchMetadata
{
public int FailureCount { get; }
public long? NextRetryTime { get; }
public long? FirstFailureTime { get; }

public BatchMetadata(int failureCount = 0, long? nextRetryTime = null, long? firstFailureTime = null)
{
FailureCount = failureCount;
NextRetryTime = nextRetryTime;
FirstFailureTime = firstFailureTime;
}

public bool ShouldRetry(long currentTime)
{
if (NextRetryTime == null) return true;
return currentTime >= NextRetryTime.Value;
}

public bool ExceedsMaxDuration(long currentTime, long maxDurationMs)
{
if (FirstFailureTime == null) return false;
return (currentTime - FirstFailureTime.Value) > maxDurationMs;
}
}

internal class RetryState
{
public PipelineState PipelineState { get; }
public long? WaitUntilTime { get; }
public int GlobalRetryCount { get; }
public Dictionary<string, BatchMetadata> BatchMetadata { get; }

private static readonly Dictionary<string, BatchMetadata> s_emptyMetadata =
new Dictionary<string, BatchMetadata>();

public RetryState(
PipelineState pipelineState = PipelineState.Ready,
long? waitUntilTime = null,
int globalRetryCount = 0,
Dictionary<string, BatchMetadata> batchMetadata = null)
{
PipelineState = pipelineState;
WaitUntilTime = waitUntilTime;
GlobalRetryCount = globalRetryCount;
BatchMetadata = batchMetadata ?? s_emptyMetadata;
}

public bool IsRateLimited(long currentTime)
{
return PipelineState == PipelineState.RateLimited
&& WaitUntilTime != null
&& currentTime < WaitUntilTime.Value;
}

public RetryState With(
PipelineState? pipelineState = null,
long? waitUntilTime = null,
bool clearWaitUntilTime = false,
int? globalRetryCount = null,
Dictionary<string, BatchMetadata> batchMetadata = null)
{
return new RetryState(
pipelineState: pipelineState ?? PipelineState,
waitUntilTime: clearWaitUntilTime ? null : (waitUntilTime ?? WaitUntilTime),
globalRetryCount: globalRetryCount ?? GlobalRetryCount,
batchMetadata: batchMetadata ?? BatchMetadata
);
}

public RetryState RemoveBatch(string batchFile)
{
if (!BatchMetadata.ContainsKey(batchFile))
return this;

var newMetadata = BatchMetadata.Where(kvp => kvp.Key != batchFile)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
return With(batchMetadata: newMetadata);
}

public RetryState SetBatchMetadata(string batchFile, BatchMetadata metadata)
{
var newMetadata = new Dictionary<string, BatchMetadata>(BatchMetadata);
newMetadata[batchFile] = metadata;
return With(batchMetadata: newMetadata);
}
}
}
Loading