All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.launchdarkly.sdk.android.StreamUpdateProcessor Maven / Gradle / Ivy

package com.launchdarkly.sdk.android;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.launchdarkly.eventsource.EventHandler;
import com.launchdarkly.eventsource.EventSource;
import com.launchdarkly.eventsource.MessageEvent;
import com.launchdarkly.eventsource.UnsuccessfulResponseException;
import com.launchdarkly.sdk.LDUser;

import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;

import okhttp3.RequestBody;

import static com.launchdarkly.sdk.android.LDConfig.GSON;
import static com.launchdarkly.sdk.android.LDConfig.JSON;

class StreamUpdateProcessor {
    private static final String METHOD_REPORT = "REPORT";

    private static final String PING = "ping";
    private static final String PUT = "put";
    private static final String PATCH = "patch";
    private static final String DELETE = "delete";

    private static final long MAX_RECONNECT_TIME_MS = 3_600_000; // 1 hour

    private EventSource es;
    private final LDConfig config;
    private final UserManager userManager;
    private volatile boolean running = false;
    private final Debounce queue;
    private boolean connection401Error = false;
    private final ExecutorService executor;
    private final String environmentName;
    private final LDUtil.ResultCallback notifier;
    private final DiagnosticStore diagnosticStore;
    private long eventSourceStarted;

    StreamUpdateProcessor(LDConfig config, UserManager userManager, String environmentName, DiagnosticStore diagnosticStore, LDUtil.ResultCallback notifier) {
        this.config = config;
        this.userManager = userManager;
        this.environmentName = environmentName;
        this.notifier = notifier;
        this.diagnosticStore = diagnosticStore;
        queue = new Debounce();
        executor = new BackgroundThreadExecutor().newFixedThreadPool(2);
    }

    synchronized void start() {
        if (!running && !connection401Error) {
            LDConfig.LOG.d("Starting.");

            EventHandler handler = new EventHandler() {
                @Override
                public void onOpen() {
                    LDConfig.LOG.i("Started LaunchDarkly EventStream");
                    if (diagnosticStore != null) {
                        diagnosticStore.addStreamInit(eventSourceStarted, (int) (System.currentTimeMillis() - eventSourceStarted), false);
                    }
                }

                @Override
                public void onClosed() {
                    LDConfig.LOG.i("Closed LaunchDarkly EventStream");
                }

                @Override
                public void onMessage(final String name, MessageEvent event) {
                    final String eventData = event.getData();
                    LDConfig.LOG.d("onMessage: %s: %s", name, eventData);
                    handle(name, eventData, notifier);
                }

                @Override
                public void onComment(String comment) {

                }

                @Override
                public void onError(Throwable t) {
                    LDConfig.LOG.e(t, "Encountered EventStream error connecting to URI: %s", getUri(userManager.getCurrentUser()));
                    if (t instanceof UnsuccessfulResponseException) {
                        if (diagnosticStore != null) {
                            diagnosticStore.addStreamInit(eventSourceStarted, (int) (System.currentTimeMillis() - eventSourceStarted), true);
                        }
                        int code = ((UnsuccessfulResponseException) t).getCode();
                        if (code >= 400 && code < 500) {
                            LDConfig.LOG.e("Encountered non-retriable error: %s. Aborting connection to stream. Verify correct Mobile Key and Stream URI", code);
                            running = false;
                            notifier.onError(new LDInvalidResponseCodeFailure("Unexpected Response Code From Stream Connection", t, code, false));
                            if (code == 401) {
                                connection401Error = true;
                                try {
                                    LDClient.getForMobileKey(environmentName).setOffline();
                                } catch (LaunchDarklyException e) {
                                    LDConfig.LOG.e(e, "Client unavailable to be set offline");
                                }
                            }
                            stop(null);
                        } else {
                            eventSourceStarted = System.currentTimeMillis();
                            notifier.onError(new LDInvalidResponseCodeFailure("Unexpected Response Code From Stream Connection", t, code, true));
                        }
                    } else {
                        notifier.onError(new LDFailure("Network error in stream connection", t, LDFailure.FailureType.NETWORK_FAILURE));
                    }
                }
            };

            EventSource.Builder builder = new EventSource.Builder(handler, getUri(userManager.getCurrentUser()));

            builder.requestTransformer(input -> {
                Map> esHeaders = input.headers().toMultimap();
                HashMap collapsed = new HashMap<>();
                for (Map.Entry> entry: esHeaders.entrySet()) {
                    for (String headerVal: entry.getValue()) {
                        collapsed.put(entry.getKey(), headerVal);
                        // We never provide multiple values for a header so we collapse to just
                        // the first entry in the multimap.
                        break;
                    }
                }
                return input.newBuilder().headers(config.headersForEnvironment(environmentName, collapsed)).build();
            });

            if (config.isUseReport()) {
                builder.method(METHOD_REPORT);
                builder.body(getRequestBody(userManager.getCurrentUser()));
            }

            builder.maxReconnectTimeMs(MAX_RECONNECT_TIME_MS);

            eventSourceStarted = System.currentTimeMillis();
            es = builder.build();
            es.start();

            running = true;
        }
    }

    @NonNull
    private RequestBody getRequestBody(@Nullable LDUser user) {
        LDConfig.LOG.d("Attempting to report user in stream");
        return RequestBody.create(GSON.toJson(user), JSON);
    }

    private URI getUri(@Nullable LDUser user) {
        String str = config.getStreamUri().toString() + "/meval";

        if (!config.isUseReport() && user != null) {
            str += "/" + DefaultUserManager.base64Url(user);
        }

        if (config.isEvaluationReasons()) {
            str += "?withReasons=true";
        }

        return URI.create(str);
    }

    private void handle(final String name, final String eventData,
                        @NonNull final LDUtil.ResultCallback onCompleteListener) {
        switch (name.toLowerCase()) {
            case PUT: userManager.putCurrentUserFlags(eventData, onCompleteListener); break;
            case PATCH: userManager.patchCurrentUserFlags(eventData, onCompleteListener); break;
            case DELETE: userManager.deleteCurrentUserFlag(eventData, onCompleteListener); break;
            case PING:
                // We debounce ping requests as they trigger a separate asynchronous request for the
                // flags, overriding all flag values.
                queue.call(() -> {
                    userManager.updateCurrentUser(onCompleteListener);
                    return null;
                });
                break;
            default:
                LDConfig.LOG.d("Found an unknown stream protocol: %s", name);
                onCompleteListener.onError(new LDFailure("Unknown Stream Element Type", null, LDFailure.FailureType.UNEXPECTED_STREAM_ELEMENT_TYPE));
        }
    }

    void stop(final LDUtil.ResultCallback onCompleteListener) {
        LDConfig.LOG.d("Stopping.");
        // We do this in a separate thread because closing the stream involves a network
        // operation and we don't want to do a network operation on the main thread.
        executor.execute(() -> {
            stopSync();
            if (onCompleteListener != null) {
                onCompleteListener.onSuccess(null);
            }
        });
    }

    private synchronized void stopSync() {
        if (es != null) {
            es.close();
        }
        running = false;
        es = null;
        LDConfig.LOG.d("Stopped.");
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy