Skip to content

Commit 1c3cac4

Browse files
[FSSDK-12148] Event retry adjustment (#398)
1 parent 0db9aab commit 1c3cac4

File tree

13 files changed

+508
-88
lines changed

13 files changed

+508
-88
lines changed

OptimizelySDK.Net35/OptimizelySDK.Net35.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@
145145
<Compile Include="..\OptimizelySDK\Event\Dispatcher\WebRequestEventDispatcher35.cs">
146146
<Link>Event\WebRequestEventDispatcher35.cs</Link>
147147
</Compile>
148+
<Compile Include="..\OptimizelySDK\Event\EventRetryConfig.cs">
149+
<Link>Event\EventRetryConfig.cs</Link>
150+
</Compile>
148151
<Compile Include="..\OptimizelySDK\Event\LogEvent.cs">
149152
<Link>Event\LogEvent.cs</Link>
150153
</Compile>

OptimizelySDK.Net40/OptimizelySDK.Net40.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@
147147
<Compile Include="..\OptimizelySDK\Event\Dispatcher\WebRequestEventDispatcher35.cs">
148148
<Link>Event\WebRequestEventDispatcher35.cs</Link>
149149
</Compile>
150+
<Compile Include="..\OptimizelySDK\Event\EventRetryConfig.cs">
151+
<Link>Event\EventRetryConfig.cs</Link>
152+
</Compile>
150153
<Compile Include="..\OptimizelySDK\Event\LogEvent.cs">
151154
<Link>Event\LogEvent.cs</Link>
152155
</Compile>

OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
<Compile Include="..\OptimizelySDK\Event\Dispatcher\WebRequestEventDispatcher35.cs" />
5151
<Compile Include="..\OptimizelySDK\Event\Dispatcher\HttpClientEventDispatcher45.cs" />
5252
<Compile Include="..\OptimizelySDK\Event\Dispatcher\IEventDispatcher.cs" />
53+
<Compile Include="..\OptimizelySDK\Event\EventRetryConfig.cs" />
5354
<Compile Include="..\OptimizelySDK\Event\LogEvent.cs" />
5455
<Compile Include="..\OptimizelySDK\Event\ForwardingEventProcessor.cs" />
5556
<Compile Include="..\OptimizelySDK\Exceptions\OptimizelyException.cs" />

OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,9 @@
288288
<Compile Include="..\OptimizelySDK\Event\Dispatcher\WebRequestEventDispatcher35.cs">
289289
<Link>Event\Dispatcher\WebRequestEventDispatcher35.cs</Link>
290290
</Compile>
291+
<Compile Include="..\OptimizelySDK\Event\EventRetryConfig.cs">
292+
<Link>Event\EventRetryConfig.cs</Link>
293+
</Compile>
291294
<Compile Include="..\OptimizelySDK\Event\Entity\ConversionEvent.cs">
292295
<Link>Event\Entity\ConversionEvent.cs</Link>
293296
</Compile>
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/*
2+
* Copyright 2026, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#if !NET35 && !NET40
18+
using System;
19+
using System.Collections.Generic;
20+
using System.Net;
21+
using System.Net.Http;
22+
using System.Threading;
23+
using System.Threading.Tasks;
24+
using Moq;
25+
using NUnit.Framework;
26+
using OptimizelySDK.Event;
27+
using OptimizelySDK.Event.Dispatcher;
28+
using OptimizelySDK.Logger;
29+
30+
namespace OptimizelySDK.Tests.EventTests
31+
{
32+
[TestFixture]
33+
public class HttpClientEventDispatcher45Test
34+
{
35+
[SetUp]
36+
public void Setup()
37+
{
38+
_mockLogger = new Mock<ILogger>();
39+
_mockLogger.Setup(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()));
40+
_requestTimestamps = new List<DateTime>();
41+
}
42+
43+
private Mock<ILogger> _mockLogger;
44+
private List<DateTime> _requestTimestamps;
45+
46+
[Test]
47+
public void DispatchEvent_Success_SingleAttempt()
48+
{
49+
var handler = new MockHttpMessageHandler(HttpStatusCode.OK);
50+
var httpClient = new HttpClient(handler);
51+
var dispatcher = new HttpClientEventDispatcher45(httpClient)
52+
{
53+
Logger = _mockLogger.Object,
54+
};
55+
var logEvent = CreateLogEvent();
56+
57+
dispatcher.DispatchEvent(logEvent);
58+
Thread.Sleep(500); // Wait for async dispatch
59+
60+
Assert.AreEqual(1, handler.RequestCount);
61+
_mockLogger.Verify(l => l.Log(LogLevel.ERROR, It.IsAny<string>()), Times.Never);
62+
}
63+
64+
[Test]
65+
public void DispatchEvent_ServerError500_RetriesThreeTimes()
66+
{
67+
var handler = new MockHttpMessageHandler(HttpStatusCode.InternalServerError);
68+
var httpClient = new HttpClient(handler);
69+
var dispatcher = new HttpClientEventDispatcher45(httpClient)
70+
{
71+
Logger = _mockLogger.Object,
72+
};
73+
var logEvent = CreateLogEvent();
74+
75+
dispatcher.DispatchEvent(logEvent);
76+
Thread.Sleep(1500);
77+
78+
Assert.AreEqual(3, handler.RequestCount);
79+
_mockLogger.Verify(
80+
l => l.Log(LogLevel.ERROR, It.Is<string>(s => s.Contains("3 attempt(s)"))),
81+
Times.Once);
82+
}
83+
84+
[Test]
85+
public void DispatchEvent_ClientError400_NoRetry()
86+
{
87+
var handler = new MockHttpMessageHandler(HttpStatusCode.BadRequest);
88+
var httpClient = new HttpClient(handler);
89+
var dispatcher = new HttpClientEventDispatcher45(httpClient)
90+
{
91+
Logger = _mockLogger.Object,
92+
};
93+
var logEvent = CreateLogEvent();
94+
95+
dispatcher.DispatchEvent(logEvent);
96+
Thread.Sleep(500);
97+
98+
Assert.AreEqual(1, handler.RequestCount);
99+
_mockLogger.Verify(l => l.Log(LogLevel.ERROR, It.IsAny<string>()), Times.Once);
100+
}
101+
102+
[Test]
103+
public void DispatchEvent_SucceedsOnSecondAttempt_StopsRetrying()
104+
{
105+
var handler = new MockHttpMessageHandler(new[]
106+
{
107+
HttpStatusCode.InternalServerError,
108+
HttpStatusCode.OK,
109+
});
110+
var httpClient = new HttpClient(handler);
111+
var dispatcher = new HttpClientEventDispatcher45(httpClient)
112+
{
113+
Logger = _mockLogger.Object,
114+
};
115+
var logEvent = CreateLogEvent();
116+
117+
dispatcher.DispatchEvent(logEvent);
118+
Thread.Sleep(1000);
119+
120+
Assert.AreEqual(2, handler.RequestCount);
121+
_mockLogger.Verify(l => l.Log(LogLevel.ERROR, It.IsAny<string>()), Times.Never);
122+
}
123+
124+
[Test]
125+
public void DispatchEvent_ExponentialBackoff_VerifyTiming()
126+
{
127+
var handler =
128+
new MockHttpMessageHandler(HttpStatusCode.InternalServerError, _requestTimestamps);
129+
var httpClient = new HttpClient(handler);
130+
var dispatcher = new HttpClientEventDispatcher45(httpClient)
131+
{
132+
Logger = _mockLogger.Object,
133+
};
134+
var logEvent = CreateLogEvent();
135+
136+
dispatcher.DispatchEvent(logEvent);
137+
Thread.Sleep(1500); // Wait for all retries
138+
139+
Assert.AreEqual(3, _requestTimestamps.Count);
140+
141+
// First retry after ~200ms
142+
var firstDelay = (_requestTimestamps[1] - _requestTimestamps[0]).TotalMilliseconds;
143+
Assert.That(firstDelay, Is.GreaterThanOrEqualTo(180).And.LessThan(350),
144+
$"First retry delay was {firstDelay}ms, expected ~200ms");
145+
146+
// Second retry after ~400ms
147+
var secondDelay = (_requestTimestamps[2] - _requestTimestamps[1]).TotalMilliseconds;
148+
Assert.That(secondDelay, Is.GreaterThanOrEqualTo(380).And.LessThan(550),
149+
$"Second retry delay was {secondDelay}ms, expected ~400ms");
150+
}
151+
152+
private static LogEvent CreateLogEvent()
153+
{
154+
return new LogEvent(
155+
"https://logx.optimizely.com/v1/events",
156+
new Dictionary<string, object>
157+
{
158+
{ "accountId", "12345" },
159+
{ "visitors", new object[] { } },
160+
},
161+
"POST",
162+
new Dictionary<string, string>());
163+
}
164+
165+
/// <summary>
166+
/// Mock HTTP message handler for testing.
167+
/// </summary>
168+
private class MockHttpMessageHandler : HttpMessageHandler
169+
{
170+
private readonly HttpStatusCode[] _statusCodes;
171+
private readonly List<DateTime> _timestamps;
172+
private int _currentIndex;
173+
174+
public MockHttpMessageHandler(HttpStatusCode statusCode,
175+
List<DateTime> timestamps = null
176+
)
177+
: this(new[] { statusCode }, timestamps) { }
178+
179+
public MockHttpMessageHandler(HttpStatusCode[] statusCodes,
180+
List<DateTime> timestamps = null
181+
)
182+
{
183+
_statusCodes = statusCodes;
184+
_timestamps = timestamps;
185+
_currentIndex = 0;
186+
}
187+
188+
public int RequestCount { get; private set; }
189+
190+
protected override Task<HttpResponseMessage> SendAsync(
191+
HttpRequestMessage request,
192+
CancellationToken cancellationToken
193+
)
194+
{
195+
RequestCount++;
196+
_timestamps?.Add(DateTime.Now);
197+
198+
var statusCode = _currentIndex < _statusCodes.Length ?
199+
_statusCodes[_currentIndex] :
200+
_statusCodes[_statusCodes.Length - 1];
201+
202+
_currentIndex++;
203+
204+
var response = new HttpResponseMessage(statusCode)
205+
{
206+
Content = new StringContent("{}"),
207+
};
208+
209+
return Task.FromResult(response);
210+
}
211+
}
212+
}
213+
}
214+
#endif

OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,9 @@ public void ShouldDispatchEventsWithCorrectPayload()
547547
[Test]
548548
public void ShouldRetryFailedEvents()
549549
{
550+
// With exponential backoff: 4 events × 3 attempts each × (200ms + 400ms) delays = ~2400ms+
551+
// Need longer timeout to account for backoff delays
552+
const int RETRY_TEST_TIMEOUT_MS = 5000;
550553
var cde = new CountdownEvent(12);
551554
_mockApiManager.Setup(a =>
552555
a.SendEvents(It.IsAny<string>(), It.IsAny<string>(),
@@ -566,7 +569,7 @@ public void ShouldRetryFailedEvents()
566569
eventManager.SendEvent(MakeEvent(i));
567570
}
568571

569-
cde.Wait(MAX_COUNT_DOWN_EVENT_WAIT_MS);
572+
cde.Wait(RETRY_TEST_TIMEOUT_MS);
570573

571574
// retry 3x (default) 4 events (batches of 1) = 12 calls to attempt to process
572575
_mockApiManager.Verify(

OptimizelySDK.Tests/OptimizelySDK.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
<Compile Include="EventTests\DefaultEventDispatcherTest.cs"/>
102102
<Compile Include="EventTests\EventBuilderTest.cs"/>
103103
<Compile Include="EventTests\ForwardingEventProcessorTest.cs"/>
104+
<Compile Include="EventTests\HttpClientEventDispatcher45Test.cs"/>
104105
<Compile Include="EventTests\LogEventTest.cs"/>
105106
<Compile Include="EventTests\TestEventDispatcher.cs"/>
106107
<Compile Include="EventTests\TestForwardingEventDispatcher.cs"/>

0 commit comments

Comments
 (0)