Skip to content

Conversation

@thomaszurkan-optimizely
Copy link
Contributor

Summary

  • This implements the new project config. It also integrates with the android datafile handler polling. By using a FileObserver available in Android, we update the project config when the cached file is modified.

What this means is that now when a new datafile comes in for android, the config is updated on the fly.
I haven't added any tests to this yet. But, I wanted to get some comments more sooner than later.

Test plan

@thomaszurkan-optimizely thomaszurkan-optimizely changed the title (chore): first attempt at using the project config manager with android (chore): Use the project config manager in java sdk with android. Also add listener. Jun 26, 2019
Copy link

@mikecdavis mikecdavis left a comment

Choose a reason for hiding this comment

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

I think we can remove all of the injectOptimizely logic since this is now happening internally within the ProjectConfigManager. Was this kept for backwards compatibility with custom datafile handlers?

try {
config = new DatafileProjectConfig.Builder().withDatafile(datafile).build();
}
catch (Exception e) {

Choose a reason for hiding this comment

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

Can we throw from the method signature?

datafileHandler.startBackgroundUpdates(context, datafileConfig, datafileDownloadInterval);
datafileHandler.startBackgroundUpdates(context, datafileConfig, datafileDownloadInterval, datafile1 -> {
if (getOptimizely() != null && getOptimizely().getNotificationCenter() != null)
getOptimizely().getNotificationCenter().send(new UpdateConfigNotification());

Choose a reason for hiding this comment

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

Can we use braces? "goto fail;"

datafileHandler.startBackgroundUpdates(context, datafileConfig, datafileDownloadInterval, datafile1 -> {
// fire
if (getOptimizely() != null && getOptimizely().getNotificationCenter() != null)
getOptimizely().getNotificationCenter().send(new UpdateConfigNotification());

Choose a reason for hiding this comment

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

braces please

* interaction point to the datafile-handler module.
*/
public class DefaultDatafileHandler implements DatafileHandler {
private Logger logger = LoggerFactory.getLogger("DefaultDatafileHandler");

Choose a reason for hiding this comment

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

private static final Logger logger = LoggerFactory.getLogger(DefaultDatafileHandler.class);

);

File filesFolder = context.getFilesDir();
fileObserver = new FileObserver(filesFolder.getPath()) {

Choose a reason for hiding this comment

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

should we check that fileObserver == null? Just curious what would happen if startBackgroundUpdates was called multiple times?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

there is a stop in the start. the stop clears the fileObserver properly.

}

public DefaultDatafileHandler(DatafileConfig datafileConfig) {
this.datafileConfig = datafileConfig;

Choose a reason for hiding this comment

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

Not used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

datafile handler can be instantiated by the use and passed into the constructor. this is meant for that. it is also used by the convenience constructor.

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 question here, is should deprecate the old methods that passed in a project config and use the internal dataconfig created?

@@ -0,0 +1,5 @@
package com.optimizely.ab.android.datafile_handler;

public interface DatafileChangeListener {

Choose a reason for hiding this comment

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

Do we need this listener? Can't we leverage the existing DatafileLoadedListener

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Didn't want to overload datafile load listener. Also, the one method per interface allows for lambdas.

Choose a reason for hiding this comment

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

Not sure it's overloading as semantically we are "loading" a datafile.
JavaDoc for DatafileLoadedListener:

Listens for new Optimizely datafiles

}

public void setDatafile(String datafile) {
if (currentProjectConfig == null && datafile != null && !datafile.isEmpty()) {

Choose a reason for hiding this comment

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

Q: We only want to set this once?

Style thing, but I prefer to fail fast and to group logical conditions. I think it makes the code easier to grok, log and requires less nesting which helps readability.

public void setDatafile(String datafile) {
    if (currentProjectConfig == null) {
        logger.info("Datafile already set, ignoring update.");
        return;
    } 
    
    if (datafile != null && !datafile.isEmpty()) {
        logger.info("datafile is empty, ignoring update");
        return;
    }
        
    try {
        currentProjectConfig = new DatafileProjectConfig.Builder().withDatafile(datafile).build();
        logger.info("Datafile successfully loaded with revision: {}", currentProjectConfig.getRevision());
    } catch (ConfigParseException ex) {
        logger.error("Unable to parse the datafile", ex);
        logger.info("Datafile is invalid: {}", datafile);
    }
}

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 bad. the currentProjectConfig is not checked.

Choose a reason for hiding this comment

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

thanks. consider, removing the nesting by exiting early. Think of the empty check as a precondition.

if (datafile != null && !datafile.isEmpty()) {
    logger.info("datafile is empty, ignoring update");
    return;
}

this also adds visibility, etc.

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'm not sure I follow you. Are you saying that this check should be pulled out of the method? In general I think that if you are taking a more functional approach (not easy in java), you want functions to be self contained and not be potentially fatal because of bad data. Am I following this correctly?

builder.withUserProfileService(userProfileService);
}

if (datafileHandler != null) {

Choose a reason for hiding this comment

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

This is never null.

if (datafileHandler != null) {
if (datafileHandler instanceof DefaultDatafileHandler) {
DefaultDatafileHandler handler = (DefaultDatafileHandler)datafileHandler;
handler.setDatafile(datafile);

Choose a reason for hiding this comment

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

Shouldn't these defaults be part of the OptimizelyManager.Builder and not defaulted here?

.travis.yml Outdated
- $HOME/.gradle/wrapper/
before_install:
- yes | sdkmanager "platforms;android-28"
- yes | sdkmanager "platforms;android-29"
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't it possible to use ANDROID_API variable here?

});
optimizelyManager.getOptimizely().getNotificationCenter().addNotificationHandler(UpdateConfigNotification.class, new NotificationHandler<UpdateConfigNotification>() {
@Override
public void handle(UpdateConfigNotification message) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit. message is not the right choice of variable name I feel. Perhaps updateNotification.

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 is the test-app and it was what the notification defaults to. the change you are requesting would be at the java level.

Choose a reason for hiding this comment

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

yeah NotificationManager<T> is a generic class with handle(T message) as the signature.

optimizelyManager.getOptimizely().getNotificationCenter().addNotificationHandler(UpdateConfigNotification.class, new NotificationHandler<UpdateConfigNotification>() {
@Override
public void handle(UpdateConfigNotification message) {
System.out.println("got datafile change ");
Copy link
Contributor

Choose a reason for hiding this comment

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

nit. Extraneous space at end.

attributes.put(BUCKETING_ATTRIBUTE, bucketingId);
Variation v = optimizelyClient.activate(FEATURE_ANDROID_EXPERIMENT_KEY, GENERIC_USER_ID, attributes);
verify(bucketer).bucket( experiment, bucketingId);
verify(bucketer).bucket( experiment, bucketingId, optimizely.getProjectConfig());
Copy link
Contributor

Choose a reason for hiding this comment

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

nit. Remove space between ( and experiment.

* The default implementation of {@link DatafileHandler} and the main
* interaction point to the datafile-handler module.
*/
public class DefaultDatafileHandler implements DatafileHandler {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we not remove reliance on DefaultDatafileHandler and move to an implementation of ProjectConfigManager

Copy link
Contributor Author

@thomaszurkan-optimizely thomaszurkan-optimizely Jun 27, 2019

Choose a reason for hiding this comment

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

This is more for backward compatibility. We also have issues where mobile developers have wanted more control (i.e. look to see if there is a cached datafile).

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 marked DatafileHandler interface as deprecated.

* overridden so that the sdk user can provide a override for the default DatafileHandler.
*/
public interface DatafileHandler {
public interface DatafileHandler extends ProjectConfigManager {

Choose a reason for hiding this comment

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

I think these changes break backwards compatibility with custom implementations.

Copy link

@mikecdavis mikecdavis left a comment

Choose a reason for hiding this comment

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

Some superficial recommendations, but I think we should clean up OptimizelyManager#buildOptimizely and leverage the OptimizelyManager#Builder to consolidate the default logic a bit more. As it stands now a customer could get themselves in a funky state since we're implicitly mutating the datafileHandler.

String datafile = optimizelyManager.getDatafile(InstrumentationRegistry.getTargetContext(), R.raw.datafile);
ProjectConfig config = null;

config = new DatafileProjectConfig.Builder().withDatafile(datafile).build();

Choose a reason for hiding this comment

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

Can you put the declaration and initialization on the same line?

ProjectConfig config = new DatafileProjectConfig.Builder().withDatafile(datafile).build();

Context appContext = mock(Context.class);
when(context.getApplicationContext()).thenReturn(appContext);
when(appContext.getPackageName()).thenReturn("com.optly");
when(defaultDatafileHandler.getConfig()).thenReturn(null);

Choose a reason for hiding this comment

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

Surprised this mock works since we already mocked with:

when(defaultDatafileHandler.getConfig()).thenReturn(config);

in setup(). Should we reset(defaultDatafileHandler) first?

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 when override works fine. these are unit tests. however, it might also be possible to just include the default datafile handler instead of a mock and achieve the same thing.

}

private void startDatafileHandler(Context context) {
if (datafileDownloadInterval > 0) {

Choose a reason for hiding this comment

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

nit, can we add logging? we have some silent ignores that might be confusing to debug. Stylistically as well I prefer we fail fast and return and minimize nesting whenever possible:

if (datafileDownloadInterval <= 0) {
    logger.debug("Invalid download interval, ignoring background updates.");
    return;
}

datafileHandler.startBackgroundUpdates(context, datafileConfig, datafileDownloadInterval, datafile1 -> {
    NotificationCenter notificationCenter = getOptimizely().getNotificationCenter();
    if (notificationCenter == null) {
        logger.debug("NotificationCenter null, not sending notification");
        return;
    }

    notificationCenter.send(new UpdateConfigNotification());
});

.withClientEngine(clientEngine)
Optimizely.Builder builder = Optimizely.builder();

if (datafileHandler instanceof DefaultDatafileHandler) {

Choose a reason for hiding this comment

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

I think this logic should be part of the OptimizelyManager.build() and not this method. This would make this much cleaner.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At build time the developer has not called an initialize/start. It was decided that there would be explicit ways to initialize sync and async. So, moving it would break backward compatibility. However, this does bring up the point on whether we should also be be deprecating the initialize methods.

// we're done here. meaning, we have notified you of either the cache coming in or of a new file.
// so, we are notifying you that the data file service has stopped.
datafileLoadedListener.onStop(datafileService.getApplicationContext());
//datafileLoadedListener.onStop(datafileService.getApplicationContext());

Choose a reason for hiding this comment

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

Can remove this line.

*/
public class DefaultDatafileHandler implements DatafileHandler {
public class DefaultDatafileHandler implements DatafileHandler, ProjectConfigManager {
private static final Logger logger = LoggerFactory.getLogger("DefaultDatafileHandler");

Choose a reason for hiding this comment

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

DefaultDatafileHandler.class as opposed to "DefaultDatafileHandler"

public void startBackgroundUpdates(Context context, DatafileConfig datafileConfig, Long updateInterval) {
public void startBackgroundUpdates(Context context, DatafileConfig datafileConfig, Long updateInterval, DatafileLoadedListener listener) {
// if already running, stop it
stopBackgroundUpdates(context, datafileConfig);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

stop background updates clears out the previous update if there was any. It also stops the file observer and sets it to null.

import com.optimizely.ab.OptimizelyRuntimeException;
import com.optimizely.ab.android.datafile_handler.DefaultDatafileHandler;
import com.optimizely.ab.android.event_handler.DefaultEventHandler;
import com.optimizely.ab.android.shared.DatafileConfig;
Copy link
Contributor

Choose a reason for hiding this comment

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

This import seems unnecessary.

public void initializeIntUseForcedVariation() {
optimizelyManager.initialize(InstrumentationRegistry.getTargetContext(), R.raw.datafile);


Copy link
Contributor

Choose a reason for hiding this comment

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

Seems extraneous. Remove.

@thomaszurkan-optimizely thomaszurkan-optimizely merged commit 3d0fe42 into master Jul 11, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants