Skip to content

Commit 9f2f471

Browse files
feat: WIP better way to handle loading & saving for UPS
I need more time to fix tests
1 parent 54a2dc2 commit 9f2f471

File tree

6 files changed

+181
-97
lines changed

6 files changed

+181
-97
lines changed

OptimizelySDK.Tests/OptimizelyUserContextTest.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,7 @@ public void DecideAllWithUpsShouldOnlyLookupSaveOnce()
540540
{ "122238", new Decision("122240") },
541541
{ "122241", new Decision("122242") },
542542
{ "122235", new Decision("122236") },
543+
{ "188880", new Decision("188881") },
543544
});
544545

545546
_ = user.DecideAll();

OptimizelySDK/Bucketing/DecisionService.cs

Lines changed: 49 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,8 @@ public class DecisionService
4343
private IErrorHandler ErrorHandler;
4444
private UserProfileService UserProfileService;
4545
private ILogger Logger;
46-
private UserProfile _userProfile;
47-
48-
private bool _decisionBatchInProgress;
49-
50-
public bool DecisionBatchInProgress
51-
{
52-
set
53-
{
54-
// Only save if the value is changing from true to false
55-
if (_decisionBatchInProgress && !value)
56-
{
57-
SaveToUserProfileService();
58-
_userProfile = null;
59-
}
60-
_decisionBatchInProgress = value;
61-
}
62-
}
46+
private readonly DecisionUnitOfWork _decisionUnitOfWork;
47+
private readonly UserProfileCache _userProfileCache;
6348

6449
/// <summary>
6550
/// Associative array of user IDs to an associative array
@@ -89,6 +74,8 @@ public DecisionService(Bucketer bucketer, IErrorHandler errorHandler,
8974
ErrorHandler = errorHandler;
9075
UserProfileService = userProfileService;
9176
Logger = logger;
77+
_decisionUnitOfWork = new DecisionUnitOfWork();
78+
_userProfileCache = new UserProfileCache(userProfileService, logger);
9279
#if NET35
9380
ForcedVariationMap = new Dictionary<string, Dictionary<string, string>>();
9481
#else
@@ -157,41 +144,17 @@ OptimizelyDecideOption[] options
157144
var ignoreUPS = Array.Exists(options,
158145
option => option == OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE);
159146

160-
147+
UserProfile userProfile = null;
161148
if (!ignoreUPS && UserProfileService != null)
162149
{
163150
try
164151
{
165-
if (_userProfile == null)
166-
{
167-
var userProfileMap = UserProfileService.Lookup(user.GetUserId());
168-
if (userProfileMap != null &&
169-
UserProfileUtil.IsValidUserProfileMap(userProfileMap))
170-
{
171-
_userProfile = UserProfileUtil.ConvertMapToUserProfile(userProfileMap);
172-
}
173-
else if (userProfileMap == null)
174-
{
175-
Logger.Log(LogLevel.INFO,
176-
reasons.AddInfo(
177-
"We were unable to get a user profile map from the UserProfileService."));
178-
}
179-
else
180-
{
181-
Logger.Log(LogLevel.ERROR,
182-
reasons.AddInfo("The UserProfileService returned an invalid map."));
183-
}
184-
}
185-
186-
if (_userProfile != null)
152+
userProfile = _userProfileCache.GetUserProfile(userId);
153+
decisionVariationResult = GetStoredVariation(experiment, userProfile, config);
154+
reasons += decisionVariationResult.DecisionReasons;
155+
if (decisionVariationResult.ResultObject != null)
187156
{
188-
decisionVariationResult =
189-
GetStoredVariation(experiment, _userProfile, config);
190-
reasons += decisionVariationResult.DecisionReasons;
191-
if (decisionVariationResult.ResultObject != null)
192-
{
193-
return decisionVariationResult.SetReasons(reasons);
194-
}
157+
return decisionVariationResult.SetReasons(reasons);
195158
}
196159
}
197160
catch (Exception exception)
@@ -221,7 +184,7 @@ OptimizelyDecideOption[] options
221184
{
222185
if (UserProfileService != null && !ignoreUPS)
223186
{
224-
var bucketerUserProfile = _userProfile ??
187+
var bucketerUserProfile = userProfile ??
225188
new UserProfile(userId,
226189
new Dictionary<string, Decision>());
227190
SaveVariation(experiment, decisionVariationResult.ResultObject,
@@ -491,60 +454,55 @@ UserProfile userProfile
491454
return;
492455
}
493456

494-
if (_userProfile == null)
495-
{
496-
_userProfile = userProfile;
497-
}
457+
var decision = new Decision(variation.Id);
458+
_decisionUnitOfWork.AddDecision(userProfile.UserId, experiment.Id, decision);
498459

499-
Decision decision;
500-
if (_userProfile.ExperimentBucketMap.ContainsKey(experiment.Id))
501-
{
502-
decision = _userProfile.ExperimentBucketMap[experiment.Id];
503-
decision.VariationId = variation.Id;
504-
}
505-
else
506-
{
507-
decision = new Decision(variation.Id);
508-
}
509-
510-
_userProfile.ExperimentBucketMap[experiment.Id] = decision;
460+
var cachedProfile = _userProfileCache.GetUserProfile(userProfile.UserId);
461+
cachedProfile.ExperimentBucketMap[experiment.Id] = decision;
462+
}
463+
464+
public void AddDecisionToUnitOfWork(string userId, string experimentId, Decision decision)
465+
{
466+
_decisionUnitOfWork.AddDecision(userId, experimentId, decision);
467+
}
511468

512-
if (!_decisionBatchInProgress)
469+
/// <summary>
470+
/// Commits the decisions to the user profile service.
471+
/// </summary>
472+
public void CommitDecisionsToUserProfileService()
473+
{
474+
if (UserProfileService == null || !_decisionUnitOfWork.HasDecisions)
513475
{
514-
SaveToUserProfileService(experiment, variation);
476+
return;
515477
}
516-
}
517478

518-
private void SaveToUserProfileService(Experiment experiment = null,
519-
Variation variation = null
520-
)
521-
{
522-
var hasExperimentDetails = experiment != null && variation != null &&
523-
!string.IsNullOrEmpty(_userProfile.UserId);
524-
try
479+
var decisions = _decisionUnitOfWork.GetDecisions();
480+
foreach (var userDecisions in decisions)
525481
{
526-
if (_userProfile == null)
482+
var userId = userDecisions.Key;
483+
var experimentDecisions = userDecisions.Value;
484+
485+
var userProfile = _userProfileCache.GetUserProfile(userId);
486+
foreach (var experimentDecision in experimentDecisions)
527487
{
528-
return;
488+
userProfile.ExperimentBucketMap[experimentDecision.Key] =
489+
experimentDecision.Value;
529490
}
530491

531-
UserProfileService.Save(_userProfile.ToMap());
532-
533-
Logger.Log(LogLevel.INFO,
534-
hasExperimentDetails ?
535-
$"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{_userProfile.UserId}\"." :
536-
"Saved user profile after batch decision.");
492+
try
493+
{
494+
UserProfileService.Save(userProfile.ToMap());
495+
Logger.Log(LogLevel.INFO, $"Saved decisions for user \"{userId}\".");
496+
}
497+
catch (Exception exception)
498+
{
499+
Logger.Log(LogLevel.ERROR, $"Failed to save decisions for user \"{userId}\".");
500+
ErrorHandler.HandleError(
501+
new Exceptions.OptimizelyRuntimeException(exception.Message));
502+
}
537503
}
538-
catch (Exception exception)
539-
{
540-
Logger.Log(LogLevel.ERROR,
541-
hasExperimentDetails ?
542-
$"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{_userProfile.UserId}\"." :
543-
"Failed to save user profile after batch decision.");
544504

545-
ErrorHandler.HandleError(
546-
new Exceptions.OptimizelyRuntimeException(exception.Message));
547-
}
505+
_decisionUnitOfWork.ClearDecisions();
548506
}
549507

550508
/// <summary>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2024 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+
* https://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+
using System.Collections.Generic;
18+
19+
namespace OptimizelySDK.Bucketing
20+
{
21+
public class DecisionUnitOfWork
22+
{
23+
private readonly Dictionary<string, Dictionary<string, Decision>> _decisions =
24+
new Dictionary<string, Dictionary<string, Decision>>();
25+
26+
public void AddDecision(string userId, string experimentId, Decision decision)
27+
{
28+
if (!_decisions.ContainsKey(userId))
29+
{
30+
_decisions[userId] = new Dictionary<string, Decision>();
31+
}
32+
33+
_decisions[userId][experimentId] = decision;
34+
}
35+
36+
public Dictionary<string, Dictionary<string, Decision>> GetDecisions()
37+
{
38+
return _decisions;
39+
}
40+
41+
public bool HasDecisions => _decisions.Count > 0;
42+
43+
public void ClearDecisions()
44+
{
45+
_decisions.Clear();
46+
}
47+
}
48+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2024 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+
* https://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+
using System.Collections.Generic;
18+
using OptimizelySDK.Logger;
19+
20+
namespace OptimizelySDK.Bucketing
21+
{
22+
public class UserProfileCache
23+
{
24+
private readonly Dictionary<string, UserProfile> _cache =
25+
new Dictionary<string, UserProfile>();
26+
27+
private readonly UserProfileService _userProfileService;
28+
private readonly ILogger _logger;
29+
30+
public UserProfileCache(UserProfileService userProfileService, ILogger logger)
31+
{
32+
_userProfileService = userProfileService;
33+
_logger = logger;
34+
}
35+
36+
public UserProfile GetUserProfile(string userId)
37+
{
38+
if (_cache.TryGetValue(userId, out var userProfile))
39+
{
40+
return _cache[userId];
41+
}
42+
43+
var userProfileMap = _userProfileService.Lookup(userId);
44+
if (userProfileMap != null && UserProfileUtil.IsValidUserProfileMap(userProfileMap))
45+
{
46+
userProfile = UserProfileUtil.ConvertMapToUserProfile(userProfileMap);
47+
}
48+
else if (userProfileMap == null)
49+
{
50+
_logger.Log(LogLevel.INFO,
51+
"We were unable to get a user profile map from the UserProfileService.");
52+
}
53+
else
54+
{
55+
_logger.Log(LogLevel.ERROR, "The UserProfileService returned an invalid map.");
56+
}
57+
58+
_cache[userId] = userProfile ??
59+
new UserProfile(userId, new Dictionary<string, Decision>());
60+
61+
return _cache[userId];
62+
}
63+
}
64+
}

OptimizelySDK/Optimizely.cs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -861,12 +861,15 @@ private OptimizelyUserContext CreateUserContextCopy(string userId,
861861
/// <li>If the SDK finds an error, it’ll return a decision with <b>null</b> for <b>variationKey</b>. The decision will include an error message in <b>reasons</b>.
862862
/// </ul>
863863
/// </summary>
864+
/// <param name="user">User context.</param>
864865
/// <param name="key">A flag key for which a decision will be made.</param>
865866
/// <param name="options">A list of options for decision-making.</param>
867+
/// <param name="commitImmediately">Whether to commit decisions to the user profile service (if in use) immediately.</param>
866868
/// <returns>A decision result.</returns>
867869
internal OptimizelyDecision Decide(OptimizelyUserContext user,
868870
string key,
869-
OptimizelyDecideOption[] options
871+
OptimizelyDecideOption[] options,
872+
bool commitImmediately = true
870873
)
871874
{
872875
var config = ProjectConfigManager?.GetConfig();
@@ -929,6 +932,8 @@ OptimizelyDecideOption[] options
929932
if (decision?.Variation != null)
930933
{
931934
featureEnabled = decision.Variation.FeatureEnabled.GetValueOrDefault();
935+
DecisionService.AddDecisionToUnitOfWork(userId, decision.Experiment?.Id,
936+
new Decision(decision.Variation.Id));
932937
}
933938

934939
if (featureEnabled)
@@ -1004,14 +1009,21 @@ OptimizelyDecideOption[] options
10041009
DecisionNotificationTypes.FLAG, userId,
10051010
userAttributes ?? new UserAttributes(), decisionInfo);
10061011

1007-
return new OptimizelyDecision(
1012+
var result = new OptimizelyDecision(
10081013
variationKey,
10091014
featureEnabled,
10101015
optimizelyJSON,
10111016
ruleKey,
10121017
key,
10131018
user,
10141019
reasonsToReport);
1020+
1021+
if (commitImmediately)
1022+
{
1023+
DecisionService.CommitDecisionsToUserProfileService();
1024+
}
1025+
1026+
return result;
10151027
}
10161028

10171029
internal Dictionary<string, OptimizelyDecision> DecideAll(OptimizelyUserContext user,
@@ -1056,18 +1068,17 @@ OptimizelyDecideOption[] options
10561068

10571069
var allOptions = GetAllOptions(options);
10581070

1059-
DecisionService.DecisionBatchInProgress = true;
10601071
foreach (var key in keys)
10611072
{
1062-
var decision = Decide(user, key, options);
1073+
var decision = Decide(user, key, options, false);
10631074
if (!allOptions.Contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) ||
10641075
decision.Enabled)
10651076
{
10661077
decisionDictionary.Add(key, decision);
10671078
}
10681079
}
1069-
1070-
DecisionService.DecisionBatchInProgress = false;
1080+
1081+
DecisionService.CommitDecisionsToUserProfileService();
10711082

10721083
return decisionDictionary;
10731084
}

OptimizelySDK/OptimizelySDK.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@
7575
<Compile Include="AudienceConditions\OrCondition.cs"/>
7676
<Compile Include="Bucketing\Bucketer.cs"/>
7777
<Compile Include="Bucketing\Decision.cs"/>
78+
<Compile Include="Bucketing\DecisionUnitOfWork.cs" />
79+
<Compile Include="Bucketing\UserProfileCache.cs" />
7880
<Compile Include="ClientConfigHandler.cs"/>
7981
<Compile Include="Entity\Attribute.cs"/>
8082
<Compile Include="Entity\Audience.cs"/>

0 commit comments

Comments
 (0)