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

com.spotify.confidence.Confidence Maven / Gradle / Ivy

The newest version!
package com.spotify.confidence;

import static com.spotify.confidence.ConfidenceTypeMapper.getTyped;
import static com.spotify.confidence.ConfidenceUtils.FlagPath.getPath;
import static com.spotify.confidence.ConfidenceUtils.getValueForPath;

import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.io.Closer;
import com.spotify.confidence.ConfidenceUtils.FlagPath;
import com.spotify.confidence.Exceptions.IllegalValuePath;
import com.spotify.confidence.Exceptions.IllegalValueType;
import com.spotify.confidence.Exceptions.IncompatibleValueType;
import com.spotify.confidence.Exceptions.ValueNotFound;
import com.spotify.confidence.shaded.flags.resolver.v1.ResolveFlagsResponse;
import com.spotify.confidence.shaded.flags.resolver.v1.ResolvedFlag;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;
import java.io.Closeable;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collector;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.slf4j.Logger;

@Beta
public abstract class Confidence implements EventSender, Closeable {

  protected Map context = Maps.newHashMap();
  private static final Logger log = org.slf4j.LoggerFactory.getLogger(Confidence.class);

  private Confidence() {}

  protected abstract ClientDelegate client();

  protected Stream> contextEntries() {
    return context.entrySet().stream().filter(e -> !e.getValue().isNull());
  }

  @Override
  public ConfidenceValue.Struct getContext() {
    return contextEntries()
        .collect(
            Collector.of(
                ImmutableMap.Builder::new,
                ImmutableMap.Builder::put,
                (b1, b2) -> b1.putAll(b2.build()),
                builder -> ConfidenceValue.Struct.of(builder.build())));
  }

  @Override
  public void setContext(ConfidenceValue.Struct context) {
    this.context = Maps.newHashMap(context.asMap());
  }

  @Override
  public void updateContextEntry(String key, ConfidenceValue value) {
    this.context.put(key, value);
  }

  @Override
  public void removeContextEntry(String key) {
    this.context.put(key, ConfidenceValue.NULL_VALUE);
  }

  @Override
  public void clearContext() {
    this.context.clear();
  }

  @Override
  public Confidence withContext(ConfidenceValue.Struct context) {
    final Confidence child = new ChildInstance(this);
    child.setContext(context);
    return child;
  }

  @Override
  public Confidence withContext(Map context) {
    return this.withContext(ConfidenceValue.of(context));
  }

  @Override
  public void track(String eventName) {
    try {
      client().emit(eventName, getContext(), Optional.empty());
    } catch (IllegalStateException e) {
      // swallow this exception
    }
  }

  @Override
  public void track(String eventName, ConfidenceValue.Struct data) {
    if (data.asMap().containsKey("context")) {
      throw new Exceptions.InvalidContextInMessaageError(
          "Field 'context' is not allowed in event's data");
    }
    try {
      client().emit(eventName, getContext(), Optional.of(data));
    } catch (IllegalStateException e) {
      // swallow this exception
    }
  }

  public  T getValue(String key, T defaultValue) {
    return getEvaluation(key, defaultValue).getValue();
  }

  public  FlagEvaluation getEvaluation(String key, T defaultValue) {
    try {
      final FlagPath flagPath = getPath(key);
      final String requestFlagName = "flags/" + flagPath.getFlag();
      final ResolveFlagsResponse response = resolveFlags(requestFlagName).get();
      if (response.getResolvedFlagsList().isEmpty()) {
        final String errorMessage =
            String.format("No active flag '%s' was found", flagPath.getFlag());
        log.warn(errorMessage);
        return new FlagEvaluation<>(
            defaultValue, "", "ERROR", ErrorType.FLAG_NOT_FOUND, errorMessage);
      }

      final ResolvedFlag resolvedFlag = response.getResolvedFlags(0);
      final String clientKey = client().clientSecret;
      final String flag = resolvedFlag.getFlag();
      final String context = URLEncoder.encode(getContext().toString(), StandardCharsets.UTF_8);
      final String logMessage =
          String.format(
              "See resolves for '%s' in Confidence: "
                  + "https://app.confidence.spotify.com/flags/resolver-test?client-key=%s&flag=flags/%s&context=%s",
              flag, clientKey, flag, context);
      log.debug(logMessage);
      if (!requestFlagName.equals(resolvedFlag.getFlag())) {
        final String errorMessage =
            String.format(
                "Unexpected flag '%s' from remote",
                resolvedFlag.getFlag().replaceFirst("^flags/", ""));
        log.warn(errorMessage);
        return new FlagEvaluation<>(
            defaultValue, "", "ERROR", ErrorType.INTERNAL_ERROR, errorMessage);
      }
      if (resolvedFlag.getVariant().isEmpty()) {
        final String errorMessage =
            String.format(
                "The server returned no assignment for the flag '%s'. Typically, this happens "
                    + "if no configured rules matches the given evaluation context.",
                flagPath.getFlag());
        log.debug(errorMessage);
        return new FlagEvaluation<>(defaultValue, "", resolvedFlag.getReason().toString());
      } else {
        final ConfidenceValue confidenceValue;
        confidenceValue =
            getValueForPath(
                flagPath.getPath(),
                ConfidenceTypeMapper.from(resolvedFlag.getValue(), resolvedFlag.getFlagSchema()));

        // regular resolve was successful
        return new FlagEvaluation<>(
            getTyped(confidenceValue, defaultValue),
            resolvedFlag.getVariant(),
            resolvedFlag.getReason().toString());
      }
    } catch (IllegalValuePath | ValueNotFound e) {
      log.warn(e.getMessage());
      return new FlagEvaluation<>(
          defaultValue, "", "ERROR", ErrorType.INVALID_VALUE_PATH, e.getMessage());
    } catch (IncompatibleValueType | IllegalValueType e) {
      log.warn(e.getMessage());
      return new FlagEvaluation<>(
          defaultValue, "", "ERROR", ErrorType.INVALID_VALUE_TYPE, e.getMessage());
    } catch (StatusRuntimeException e) {
      log.warn(e.getMessage());
      return new FlagEvaluation<>(
          defaultValue, "", "ERROR", ErrorType.NETWORK_ERROR, e.getMessage());
    } catch (Exception e) {
      // catch all for any runtime exception
      if (e.getCause() instanceof StatusRuntimeException) {
        log.warn(e.getMessage());
        return new FlagEvaluation<>(
            defaultValue, "", "ERROR", ErrorType.NETWORK_ERROR, e.getMessage());
      }
      log.warn(e.getMessage());
      return new FlagEvaluation<>(
          defaultValue, "", "ERROR", ErrorType.INTERNAL_ERROR, e.getMessage());
    }
  }

  CompletableFuture resolveFlags(String flagName) {
    return client().resolveFlags(flagName, getContext());
  }

  @VisibleForTesting
  static Confidence create(
      EventSenderEngine eventSenderEngine,
      FlagResolverClient flagResolverClient,
      String clientSecret) {
    final Closer closer = Closer.create();
    closer.register(eventSenderEngine);
    closer.register(flagResolverClient);
    return new RootInstance(
        new ClientDelegate(closer, flagResolverClient, eventSenderEngine, clientSecret));
  }

  public static Confidence.Builder builder(String clientSecret) {
    return new Confidence.Builder(clientSecret);
  }

  private static class ClientDelegate implements FlagResolverClient, EventSenderEngine {
    private final Closeable closeable;
    private final FlagResolverClient flagResolverClient;
    private final EventSenderEngine eventSenderEngine;
    private String clientSecret;

    private ClientDelegate(
        Closeable closeable,
        FlagResolverClient flagResolverClient,
        EventSenderEngine eventSenderEngine,
        String clientSecret) {
      this.closeable = closeable;
      this.flagResolverClient = flagResolverClient;
      this.eventSenderEngine = eventSenderEngine;
      this.clientSecret = clientSecret;
    }

    @Override
    public void emit(
        String name, ConfidenceValue.Struct context, Optional message) {
      this.eventSenderEngine.emit(name, context, message);
    }

    @Override
    public void flush() {
      this.eventSenderEngine.flush();
    }

    @Override
    public CompletableFuture resolveFlags(
        String flag, ConfidenceValue.Struct context) {
      return flagResolverClient.resolveFlags(flag, context);
    }

    @Override
    public void close() throws IOException {
      closeable.close();
    }
  }

  private static class ChildInstance extends Confidence {

    private final Confidence parent;
    private boolean closed = false;

    private ChildInstance(Confidence parent) {
      this.parent = parent;
    }

    @Override
    protected Stream> contextEntries() {
      final Set ownKeys = context.keySet();
      return Stream.concat(
          parent.contextEntries().filter(entry -> !ownKeys.contains(entry.getKey())),
          super.contextEntries());
    }

    @Override
    protected ClientDelegate client() {
      if (closed) throw new IllegalStateException("Resource closed");
      return parent.client();
    }

    @Override
    public void close() throws IOException {
      closed = true;
    }

    @Override
    public void flush() {
      parent.flush();
    }
  }

  private static class RootInstance extends Confidence {
    @Nullable private ClientDelegate client;

    private RootInstance(ClientDelegate client) {
      this.client = client;
    }

    @Override
    protected ClientDelegate client() {
      if (client == null) throw new IllegalStateException("Resource closed");
      return client;
    }

    @Override
    public void close() throws IOException {
      try {
        client().close();
      } finally {
        client = null;
      }
    }

    @Override
    public void flush() {
      client.flush();
    }
  }

  public static class Builder {
    private final String clientSecret;
    private final Closer closer = Closer.create();

    private final ManagedChannel DEFAULT_CHANNEL =
        ManagedChannelBuilder.forAddress("edge-grpc.spotify.com", 443)
            .keepAliveTime(Duration.ofMinutes(5).getSeconds(), TimeUnit.SECONDS)
            .build();
    private ManagedChannel flagResolverManagedChannel = DEFAULT_CHANNEL;
    private boolean disableTelemetry = false;
    private boolean isProvider = false;

    public Builder(@Nonnull String clientSecret) {
      this.clientSecret = clientSecret;
      registerChannelForShutdown(DEFAULT_CHANNEL);
    }

    public Builder flagResolverManagedChannel(String host, int port) {
      this.flagResolverManagedChannel =
          ManagedChannelBuilder.forAddress(host, port).usePlaintext().build();
      registerChannelForShutdown(this.flagResolverManagedChannel);
      return this;
    }

    public Builder flagResolverManagedChannel(ManagedChannel managedChannel) {
      this.flagResolverManagedChannel = managedChannel;
      return this;
    }

    public Builder disableTelemetry(boolean disableTelemetry) {
      this.disableTelemetry = disableTelemetry;
      return this;
    }

    public Confidence buildForProvider() {
      this.isProvider = true;
      return build();
    }

    public Confidence build() {
      final FlagResolverClient flagResolverClient;
      final Telemetry telemetry = disableTelemetry ? null : new Telemetry(isProvider);
      final TelemetryClientInterceptor telemetryInterceptor =
          new TelemetryClientInterceptor(telemetry);
      final GrpcFlagResolver flagResolver =
          new GrpcFlagResolver(clientSecret, flagResolverManagedChannel, telemetryInterceptor);

      flagResolverClient = new FlagResolverClientImpl(flagResolver, telemetry);

      final EventSenderEngine eventSenderEngine =
          new EventSenderEngineImpl(clientSecret, DEFAULT_CHANNEL, Instant::now);
      closer.register(flagResolverClient);
      closer.register(eventSenderEngine);
      return new RootInstance(
          new ClientDelegate(closer, flagResolverClient, eventSenderEngine, clientSecret));
    }

    private void registerChannelForShutdown(ManagedChannel channel) {
      this.closer.register(
          () -> {
            channel.shutdown();
            try {
              channel.awaitTermination(10, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
              Thread.currentThread().interrupt();
              channel.shutdownNow();
            }
          });
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy