Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added ODPManager implementation #322

Merged
merged 18 commits into from
Dec 7, 2022
Merged

Conversation

mikechu-optimizely
Copy link
Contributor

Summary

Added ODPManager Implementation which does the following.

  1. Initializes and provides access to ODPEventManager and ODPSegmentManager
  2. Provides updated ODPConfig settings to event manager and segment manager.
  3. Stops Event Manager thread when closed.

Test plan

  1. Manually tested thoroughly
  2. Added unit tests.

Issues

FSSDK-8453

@mikechu-optimizely mikechu-optimizely marked this pull request as ready for review November 29, 2022 20:04
@mikechu-optimizely mikechu-optimizely requested a review from a team as a code owner November 29, 2022 20:04
@mikechu-optimizely mikechu-optimizely removed the request for review from a team November 29, 2022 20:17
<Compile Include="Odp\IOdpSegmentManager.cs" />
<Compile Include="Odp\OdpConfig.cs" />
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was already declared on line 112/113 below. Added OdpManager.cs in its place.

Copy link
Contributor

@msohailhussain msohailhussain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please address.

/// <param name="apiHost">Host portion of the URL of ODP</param>
/// <param name="segmentsToCheck">Audience segments to consider</param>
/// <returns>True if settings were update otherwise False</returns>
bool UpdateSettings(string apiKey, string apiHost, List<string> segmentsToCheck);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't it be OdpConfig in args

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, I too like to pass around objects, but I'm looking towards the Swift and also the Python as references.

/// <param name="userId">FS User ID</param>
/// <param name="options">Options used during segment cache handling</param>
/// <returns>Qualified segments for the user from the cache or the ODP server</returns>
List<string> FetchQualifiedSegments(string userId, List<OdpSegmentOption> options);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This should be Array

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, yes. Sorry. Old habit from previous work.

/// <summary>
/// Sends signal to stop Event Manager and clean up ODP Manager use
/// </summary>
void Close();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be Dispose. My understanding is if it's closed, then can't use again.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're right here.

I didn't look carefully at the Java close() method as part of implementing AutoCloseable in that language. I judged that we wanted Close() after looking at Python.

C#'s IDisposable makes way more sense.

/// </summary>
/// <param name="toCompare">OdpConfig object to compare current instance against</param>
/// <returns>True if equal otherwise False</returns>
public bool Equals(OdpConfig toCompare)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the reason to add this method. This will raise exception if ApiKey is null. Secondly use equals method where needed in this class and implement IEqu... interface as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal of this is to control the equality determination of two ODP Configs.

Thanks for the callout on null exception. I'll change to use IEqualityComparer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh maybe IEquatable<T> might work instead of having a comparer. 🤔

public bool UpdateSettings(string apiKey, string apiHost, List<string> segmentsToCheck)
{
var newConfig = new OdpConfig(apiKey, apiHost, segmentsToCheck);
if (_odpConfig.Equals(newConfig))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't we use ==

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes Sir. I'll update once using the IEqualityComparer from OdpConfig

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried overriding the implicit operator, but it gets messy and unclear. I'd like to stay with the use of .Equals()


_odpConfig = newConfig;

EventManager.Flush();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add comments in a code, why flushing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// because @jaeopt did it in Swift 😁

/// <returns>Qualified segments for the user from the cache or the ODP server</returns>
public List<string> FetchQualifiedSegments(string userId, List<OdpSegmentOption> options)
{
if (!_enabled || SegmentManager == null || !_odpConfig.IsReady())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a minor comment,
SegmentManager == null || !_enabled || !_odpConfig.IsReady()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can re-arrange (I think that's what you're asking)

I thought that perhaps !_enabled would evaluate to true more often than the others, but happy to switch.

Dictionary<string, object> data
)
{
if (!_enabled || EventManager == null || !_odpConfig.IsReady())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This validations should be in a single method and return true or false. please refactor it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My copy-paste finger started hurting, and I thought of that too.

L89, would have me injecting the Manager that should be checked for null so I just left it.

Let me play with some options....

Copy link
Contributor Author

@mikechu-optimizely mikechu-optimizely left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comments @msohailhussain .

I'll hold for @jaeopt 's too before pushing to the repo

/// <param name="apiHost">Host portion of the URL of ODP</param>
/// <param name="segmentsToCheck">Audience segments to consider</param>
/// <returns>True if settings were update otherwise False</returns>
bool UpdateSettings(string apiKey, string apiHost, List<string> segmentsToCheck);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, I too like to pass around objects, but I'm looking towards the Swift and also the Python as references.

/// <summary>
/// Sends signal to stop Event Manager and clean up ODP Manager use
/// </summary>
void Close();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're right here.

I didn't look carefully at the Java close() method as part of implementing AutoCloseable in that language. I judged that we wanted Close() after looking at Python.

C#'s IDisposable makes way more sense.

/// </summary>
/// <param name="toCompare">OdpConfig object to compare current instance against</param>
/// <returns>True if equal otherwise False</returns>
public bool Equals(OdpConfig toCompare)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal of this is to control the equality determination of two ODP Configs.

Thanks for the callout on null exception. I'll change to use IEqualityComparer.

public bool UpdateSettings(string apiKey, string apiHost, List<string> segmentsToCheck)
{
var newConfig = new OdpConfig(apiKey, apiHost, segmentsToCheck);
if (_odpConfig.Equals(newConfig))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes Sir. I'll update once using the IEqualityComparer from OdpConfig

/// <returns>Qualified segments for the user from the cache or the ODP server</returns>
public List<string> FetchQualifiedSegments(string userId, List<OdpSegmentOption> options)
{
if (!_enabled || SegmentManager == null || !_odpConfig.IsReady())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can re-arrange (I think that's what you're asking)

I thought that perhaps !_enabled would evaluate to true more often than the others, but happy to switch.

Dictionary<string, object> data
)
{
if (!_enabled || EventManager == null || !_odpConfig.IsReady())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My copy-paste finger started hurting, and I thought of that too.

L89, would have me injecting the Manager that should be checked for null so I just left it.

Let me play with some options....

/// <param name="userId">FS User ID</param>
/// <param name="options">Options used during segment cache handling</param>
/// <returns>Qualified segments for the user from the cache or the ODP server</returns>
List<string> FetchQualifiedSegments(string userId, List<OdpSegmentOption> options);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, yes. Sorry. Old habit from previous work.

Copy link
Contributor

@jaeopt jaeopt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. A few changes suggested.

// NOTE: It should be rare but possible that odp public key is changed for the same datafile (sdkKey).
// - Try to send all old events with the previous public key.
// - If it fails to flush all the old events here (network error), remaining events will be discarded.
EventManager.Flush();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can we guarantee all pending events use the old ODPConfig with the non-blocking flush?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good consideration. I think this .Flush() should grab a copy of the _odpConfig in the OdpEventManager and inject it into the _flushSignal object. The secondary thread would then have the config to use for flush requests.

Does this seem like a suitable solution, or is this a corner case?
How does Swift (or Python) handle this situation?
What's your direction?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mikechu-optimizely We can consider -

  1. (swift) the current key and host are captures in the flush thread (so they can be used until flushing completed) before updating odpConfig or
  2. (python) delay config update until flush is completed with the current key and host.

return false;
}

_odpConfig = newConfig;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When ODPConfig is updated, the OdpConfig object itself is replaced in ODPManager, while copying fields in ODPEventManager and ODPSegmentManager. Can we make it consistent in either way?

Comment on lines 114 to 119
if (!EventManager.IsStarted)
{
_logger.Log(LogLevel.DEBUG,
"ODP identify event not dispatched (ODP not integrated).");
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can drop this. It should be rare and it'll be filtered out in EventManager anyway.

{
if (EventManagerOrConfigNotReady())
{
_logger.Log(LogLevel.DEBUG, "ODP event not dispatched (ODP disabled).");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_logger.Log(LogLevel.DEBUG, "ODP event not dispatched (ODP disabled).");
_logger.Log(LogLevel.ERROR, "ODP event not dispatched (ODP disabled).");

Implicit event sending (IdentifyUser) should be DEBUG level, but when clients try to send events when ODP is not ready, it should be ERROR.

Comment on lines 141 to 146
if (!EventManager.IsStarted)
{
_logger.Log(LogLevel.DEBUG,
"ODP event not dispatched (ODP not integrated).");
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can drop this too.

{
var manager = new OdpManager
{
_odpConfig = _odpConfig,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_odpConfig can be null if not set withOdpConfig. Is it safe?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put a check in the Build() for you 😄


_errorHandler = _errorHandler ?? new NoOpErrorHandler();

if (_eventManager == null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if _enabled == false, then we do not need instantiate EventManager and SegmentManager.

Copy link
Contributor

@msohailhussain msohailhussain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link
Contributor

@jaeopt jaeopt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more changes suggested.

@@ -81,27 +81,6 @@ public OdpConfig(string apiKey, string apiHost, List<string> segmentsToCheck)
SegmentsToCheck = segmentsToCheck ?? new List<string>(0);
}

/// <summary>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the entire odpconfig is replaced for update, then we do not need t"volitile" for internal fields.

@@ -349,7 +349,7 @@ public void IdentifyUser(string userId)
/// <param name="odpConfig">Configuration object containing new values</param>
public void UpdateSettings(OdpConfig odpConfig)
{
_odpConfig.Update(odpConfig.ApiKey, odpConfig.ApiHost, odpConfig.SegmentsToCheck);
_odpConfig = odpConfig;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you use to replace _odpConfig, don't we need to set it to "volitile"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

volatile can only be set on some simple/primitive types.

Replacing the config as a whole now (instead of individual field values on the OdpConfig) should not be good. I'm not seeing where a child thread updates on the config.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait, this is a Reference type. Duh.

I can mark it as volatile if you see where other threads may update the config on this instance of the EventManager.

@@ -149,7 +149,7 @@ private static string GetCacheKey(string userKey, string userValue)
/// <param name="odpConfig">New ODP Configuration to apply</param>
public void UpdateSettings(OdpConfig odpConfig)
{
_odpConfig.Update(odpConfig.ApiKey, odpConfig.ApiHost, odpConfig.SegmentsToCheck);
_odpConfig = odpConfig;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

volitile

_logger = _logger ?? new DefaultLogger();
_errorHandler = _errorHandler ?? new NoOpErrorHandler();

if (_odpConfig == null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about setting if (odpConfig == null) _odpConfig = new ODPConfig() default.
Initially that's the best they can do when initializing ODPManager.

// NOTE: It should be rare but possible that odp public key is changed for the same datafile (sdkKey).
// - Try to send all old events with the previous public key.
// - If it fails to flush all the old events here (network error), remaining events will be discarded.
EventManager.Flush();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mikechu-optimizely We can consider -

  1. (swift) the current key and host are captures in the flush thread (so they can be used until flushing completed) before updating odpConfig or
  2. (python) delay config update until flush is completed with the current key and host.


if (_eventManager == null)
{
var eventApiManager = new OdpEventApiManager(_logger, _errorHandler);

manager.EventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig).
manager.EventManager = new OdpEventManager.Builder().
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering why using builder for OdpEventManager while not for OdpSegmentManager. Looks weird with 2 different patterns for similar objects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Segment Manager is not too complex to instantiate. There's not a lot of logic that needed to be in the constructor, so I let a standard constructor do that work.

With Event Manager and ODP Manager, the combinations of instantiation parameters and logic in those combinations warranted (in my opinion) to use the Builder Pattern.

In practice, I don't have a hard limit of constructor length before breaking the logic out.

Copy link
Contributor

@jaeopt jaeopt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. One more fix for thread-safe.

@@ -35,7 +35,7 @@ public class OdpManager : IOdpManager, IDisposable
/// <summary>
/// Configuration used to communicate with ODP
/// </summary>
private volatile OdpConfig _odpConfig;
private OdpConfig _odpConfig;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you remove "volitile" here? Multiple thread read/write it here too.

Copy link
Contributor

@jaeopt jaeopt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@mikechu-optimizely mikechu-optimizely merged commit bdaef7f into master Dec 7, 2022
@mikechu-optimizely mikechu-optimizely deleted the mike/odp-manager branch December 7, 2022 16:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants