Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package dev.openfeature.contrib.providers.flagd;

import dev.openfeature.contrib.providers.flagd.resolver.rpc.cache.CacheType;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;

/** Helper class to hold configuration default values. */
Expand Down Expand Up @@ -37,6 +40,7 @@ public final class Config {
static final String FLAGD_RETRY_BACKOFF_MAX_MS_VAR_NAME = "FLAGD_RETRY_BACKOFF_MAX_MS";
static final String STREAM_DEADLINE_MS_ENV_VAR_NAME = "FLAGD_STREAM_DEADLINE_MS";
static final String SOURCE_SELECTOR_ENV_VAR_NAME = "FLAGD_SOURCE_SELECTOR";
static final String FATAL_STATUS_CODES_ENV_VAR_NAME = "FLAGD_FATAL_STATUS_CODES";
/**
* Environment variable to fetch Provider id.
*
Expand Down Expand Up @@ -93,6 +97,18 @@ static long fallBackToEnvOrDefault(String key, long defaultValue) {
}
}

static List<String> fallBackToEnvOrDefaultList(String key, List<String> defaultValue) {
try {
return System.getenv(key) != null
? Arrays.stream(System.getenv(key).split(","))
.map(String::trim)
.collect(Collectors.toList())
: defaultValue;
} catch (Exception e) {
return defaultValue;
Copy link
Contributor

Choose a reason for hiding this comment

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

We should print an info/warn that the env vars are invalid

Copy link
Member Author

Choose a reason for hiding this comment

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

Just for this method? Or the other ones too? I'd either leave it or add it in all cases to be consistent

Copy link
Contributor

Choose a reason for hiding this comment

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

Then we should add it everywhere, but in a different PR

Copy link
Member Author

Choose a reason for hiding this comment

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

Alright, sounds good. Should we create a new issue for this or is that overkill?

}
}

static Resolver fromValueProvider(Function<String, String> provider) {
final String resolverVar = provider.apply(RESOLVER_ENV_VAR);
if (resolverVar == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.openfeature.contrib.providers.flagd;

import static dev.openfeature.contrib.providers.flagd.Config.fallBackToEnvOrDefault;
import static dev.openfeature.contrib.providers.flagd.Config.fallBackToEnvOrDefaultList;
import static dev.openfeature.contrib.providers.flagd.Config.fromValueProvider;

import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueueSource;
Expand Down Expand Up @@ -122,6 +123,15 @@ public class FlagdOptions {
@Builder.Default
private int retryGracePeriod =
fallBackToEnvOrDefault(Config.STREAM_RETRY_GRACE_PERIOD, Config.DEFAULT_STREAM_RETRY_GRACE_PERIOD);

/**
* List of grpc response status codes for which the provider transitions into fatal state upon first connection.
* Defaults to empty list
*/
@Builder.Default
private List<String> fatalStatusCodes =
fallBackToEnvOrDefaultList(Config.FATAL_STATUS_CODES_ENV_VAR_NAME, List.of());

/**
* Selector to be used with flag sync gRPC contract.
**/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package dev.openfeature.contrib.providers.flagd;

import dev.openfeature.contrib.providers.flagd.resolver.Resolver;
import dev.openfeature.contrib.providers.flagd.resolver.common.FlagdProviderEvent;
import dev.openfeature.contrib.providers.flagd.resolver.process.InProcessResolver;
import dev.openfeature.contrib.providers.flagd.resolver.rpc.RpcResolver;
import dev.openfeature.contrib.providers.flagd.resolver.rpc.cache.Cache;
import dev.openfeature.sdk.ErrorCode;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.EventProvider;
import dev.openfeature.sdk.Hook;
Expand Down Expand Up @@ -192,8 +192,8 @@ EvaluationContext getEnrichedContext() {
}

@SuppressWarnings("checkstyle:fallthrough")
private void onProviderEvent(FlagdProviderEvent flagdProviderEvent) {
log.debug("FlagdProviderEvent event {} ", flagdProviderEvent.getEvent());
private void onProviderEvent(ProviderEvent providerEvent, ProviderEventDetails providerEventDetails, Structure syncMetadata) {
log.debug("FlagdProviderEvent event {} ", providerEvent);
synchronized (syncResources) {
/*
* We only use Error and Ready as previous states.
Expand All @@ -204,10 +204,10 @@ private void onProviderEvent(FlagdProviderEvent flagdProviderEvent) {
* forward a configuration changed to the ready, if we are not in the ready
* state.
*/
switch (flagdProviderEvent.getEvent()) {
switch (providerEvent) {
case PROVIDER_CONFIGURATION_CHANGED:
if (syncResources.getPreviousEvent() == ProviderEvent.PROVIDER_READY) {
onConfigurationChanged(flagdProviderEvent);
emit(providerEvent, providerEventDetails);
break;
}
// intentional fall through
Expand All @@ -216,33 +216,29 @@ private void onProviderEvent(FlagdProviderEvent flagdProviderEvent) {
* Sync metadata is used to enrich the context, and is immutable in flagd,
* so we only need it to be fetched once at READY.
*/
if (flagdProviderEvent.getSyncMetadata() != null) {
syncResources.setEnrichedContext(contextEnricher.apply(flagdProviderEvent.getSyncMetadata()));
if (syncMetadata != null) {
syncResources.setEnrichedContext(contextEnricher.apply(syncMetadata));
}
onReady();
syncResources.setPreviousEvent(ProviderEvent.PROVIDER_READY);
break;

case PROVIDER_ERROR:
if (providerEventDetails != null && providerEventDetails.getErrorCode() == ErrorCode.PROVIDER_FATAL) {
onFatal();
break;
}

if (syncResources.getPreviousEvent() != ProviderEvent.PROVIDER_ERROR) {
onError();
syncResources.setPreviousEvent(ProviderEvent.PROVIDER_ERROR);
}
break;

default:
log.warn("Unknown event {}", flagdProviderEvent.getEvent());
log.warn("Unknown event {}", providerEvent);
}
}
}

private void onConfigurationChanged(FlagdProviderEvent flagdProviderEvent) {
this.emitProviderConfigurationChanged(ProviderEventDetails.builder()
.flagsChanged(flagdProviderEvent.getFlagsChanged())
.message("configuration changed")
.build());
}

private void onReady() {
if (syncResources.initialize()) {
log.info("Initialized FlagdProvider");
Expand Down Expand Up @@ -284,4 +280,16 @@ private void onError() {
TimeUnit.SECONDS);
}
}

private void onFatal() {
if (errorTask != null && !errorTask.isCancelled()) {
errorTask.cancel(false);
}

this.emitProviderError(ProviderEventDetails.builder()
.errorCode(ErrorCode.PROVIDER_FATAL)
.build());

shutdown();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@
import dev.openfeature.sdk.ImmutableMetadata;
import dev.openfeature.sdk.ProviderEvaluation;
import dev.openfeature.sdk.ProviderEvent;
import dev.openfeature.sdk.ProviderEventDetails;
import dev.openfeature.sdk.Reason;
import dev.openfeature.sdk.Structure;
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.exceptions.GeneralError;
import dev.openfeature.sdk.exceptions.ParseError;
import dev.openfeature.sdk.exceptions.TypeMismatchError;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import dev.openfeature.sdk.internal.TriConsumer;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

Expand All @@ -38,7 +42,7 @@
@Slf4j
public class InProcessResolver implements Resolver {
private final Storage flagStore;
private final Consumer<FlagdProviderEvent> onConnectionEvent;
private final TriConsumer<ProviderEvent, ProviderEventDetails, Structure> onConnectionEvent;
private final Operator operator;
private final String scope;
private final QueueSource queueSource;
Expand All @@ -52,7 +56,7 @@ public class InProcessResolver implements Resolver {
* @param onConnectionEvent lambda which handles changes in the
* connection/stream
*/
public InProcessResolver(FlagdOptions options, Consumer<FlagdProviderEvent> onConnectionEvent) {
public InProcessResolver(FlagdOptions options, TriConsumer<ProviderEvent, ProviderEventDetails, Structure> onConnectionEvent) {
this.queueSource = getQueueSource(options);
this.flagStore = new FlagStore(queueSource);
this.onConnectionEvent = onConnectionEvent;
Expand All @@ -73,14 +77,23 @@ public void init() throws Exception {
switch (storageStateChange.getStorageState()) {
case OK:
log.debug("onConnectionEvent.accept ProviderEvent.PROVIDER_CONFIGURATION_CHANGED");
onConnectionEvent.accept(new FlagdProviderEvent(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
storageStateChange.getChangedFlagsKeys(),
storageStateChange.getSyncMetadata()));

var eventDetails = ProviderEventDetails.builder()
.flagsChanged(storageStateChange.getChangedFlagsKeys())
.message("configuration changed")
.build();

onConnectionEvent.accept(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, eventDetails, storageStateChange.getSyncMetadata());

log.debug("post onConnectionEvent.accept ProviderEvent.PROVIDER_CONFIGURATION_CHANGED");
break;
case ERROR:
onConnectionEvent.accept(new FlagdProviderEvent(ProviderEvent.PROVIDER_ERROR));
case TRANSIENT_ERROR:
onConnectionEvent.accept(ProviderEvent.PROVIDER_ERROR, null, null);
break;
case FATAL_ERROR:
onConnectionEvent.accept(ProviderEvent.PROVIDER_ERROR, ProviderEventDetails.builder()
.errorCode(ErrorCode.PROVIDER_FATAL)
.build(), null);
break;
default:
log.warn(String.format(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,20 @@ private void streamerListener(final QueueSource connector) throws InterruptedExc
} catch (Throwable e) {
// catch all exceptions and avoid stream listener interruptions
log.warn("Invalid flag sync payload from connector", e);
if (!stateBlockingQueue.offer(new StorageStateChange(StorageState.STALE))) {
log.warn("Failed to convey STALE status, queue is full");
if (!stateBlockingQueue.offer(new StorageStateChange(StorageState.TRANSIENT_ERROR))) {
log.warn("Failed to convey TRANSIENT_ERROR status, queue is full");
}
}
break;
case ERROR:
if (!stateBlockingQueue.offer(new StorageStateChange(StorageState.ERROR))) {
log.warn("Failed to convey ERROR status, queue is full");
if (!stateBlockingQueue.offer(new StorageStateChange(StorageState.TRANSIENT_ERROR))) {
log.warn("Failed to convey TRANSIENT_ERROR status, queue is full");
}
break;
case SHUTDOWN:
shutdown();
if (!stateBlockingQueue.offer(new StorageStateChange(StorageState.FATAL_ERROR))) {
log.warn("Failed to convey FATAL_ERROR status, queue is full");
}
break;
default:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package dev.openfeature.contrib.providers.flagd.resolver.process.storage;

/** Satus of the storage. */
/** Status of the storage. */
public enum StorageState {
/** Storage is upto date and working as expected. */
OK,
/** Storage has gone stale(most recent sync failed). May get to OK status with next sync. */
STALE,
/** Storage has gone stale (most recent sync failed). May get to OK status with next sync. */
TRANSIENT_ERROR,
/** Storage is in an unrecoverable error stage. */
ERROR,
FATAL_ERROR,
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@
@AllArgsConstructor
@Getter
public class QueuePayload {
public static final QueuePayload ERROR = new QueuePayload(QueuePayloadType.ERROR);
public static final QueuePayload SHUTDOWN = new QueuePayload(QueuePayloadType.SHUTDOWN);

private final QueuePayloadType type;
private final String flagData;
private final Struct syncContext;

public QueuePayload(QueuePayloadType type, String flagData) {
this(type, flagData, null);
}
public QueuePayload(QueuePayloadType type) {
this(type, null, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
/** Payload type emitted by {@link QueueSource}. */
public enum QueuePayloadType {
DATA,
ERROR
ERROR,
SHUTDOWN
}
Loading
Loading