Skip to content

Commit 50e4846

Browse files
authored
feat: Add resolve sticky assignments strategy api to the local resolve provider (#277)
* add resolve sticky assignments strategy api to the local resolve provider * simplify * fixup! simplify * update wasm * new wasm * add account id for resolver state * fix broken wasm tests * add default confidence resolver * fix sticky loop, add tests * fixup! fix sticky loop, add tests * writeflaglogs, checkpoint for logs, handle sticky * add grpc http client * fix flush logs * remove auth for remote
1 parent f1170b2 commit 50e4846

26 files changed

Lines changed: 1122 additions & 374 deletions

confidence-proto/src/main/proto/confidence/flags/admin/v1/flags_api.proto

Lines changed: 64 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -65,77 +65,78 @@ message WriteResolveInfoRequest {
6565
// Information about how flags were resolved
6666
repeated FlagResolveInfo flag_resolve_info = 2;
6767

68-
// Information about how a single flag has been resolved
69-
message FlagResolveInfo {
70-
// The flag the info is about
71-
string flag = 1 [
72-
(google.api.resource_reference).type = "flags.confidence.dev/Flag",
73-
(google.api.field_behavior) = REQUIRED
68+
// Information about how a client resolved
69+
}
70+
71+
// Information about how a single flag has been resolved
72+
message FlagResolveInfo {
73+
// The flag the info is about
74+
string flag = 1 [
75+
(google.api.resource_reference).type = "flags.confidence.dev/Flag",
76+
(google.api.field_behavior) = REQUIRED
77+
];
78+
// Information about how variants were resolved.
79+
repeated VariantResolveInfo variant_resolve_info = 2;
80+
// Information about how rules were resolved.
81+
repeated RuleResolveInfo rule_resolve_info = 3;
82+
83+
// Information about how a variant was resolved.
84+
message VariantResolveInfo {
85+
// If there was a variant assigned, otherwise not set
86+
string variant = 1 [
87+
(google.api.resource_reference).type = "flags.confidence.dev/Variant",
88+
(google.api.field_behavior) = OPTIONAL
7489
];
75-
// Information about how variants were resolved.
76-
repeated VariantResolveInfo variant_resolve_info = 2;
77-
// Information about how rules were resolved.
78-
repeated RuleResolveInfo rule_resolve_info = 3;
79-
80-
// Information about how a variant was resolved.
81-
message VariantResolveInfo {
82-
// If there was a variant assigned, otherwise not set
83-
string variant = 1 [
84-
(google.api.resource_reference).type = "flags.confidence.dev/Variant",
85-
(google.api.field_behavior) = OPTIONAL
86-
];
87-
// Number of times the variant was resolved in this period
88-
int64 count = 3 [(google.api.field_behavior) = REQUIRED];
89-
}
90-
91-
// Information about how a rule was resolved.
92-
message RuleResolveInfo {
93-
// The rule that was resolved
94-
string rule = 1 [
95-
(google.api.resource_reference).type = "flags.confidence.dev/Rule",
96-
(google.api.field_behavior) = REQUIRED
97-
];
98-
// Number of times the rule was resolved in this period
99-
int64 count = 2 [(google.api.field_behavior) = REQUIRED];
100-
101-
// Resolve counts on each assignment
102-
repeated AssignmentResolveInfo assignment_resolve_info = 3 [(google.api.field_behavior) = OPTIONAL];
103-
}
104-
105-
// Information about the assignment that was resolved.
106-
message AssignmentResolveInfo {
107-
// The assignment id of the resolved value, otherwise not set.
108-
string assignment_id = 1 [(google.api.field_behavior) = OPTIONAL];
109-
110-
// Number of times the assignment id was resolved in this period.
111-
int64 count = 2 [(google.api.field_behavior) = REQUIRED];
112-
}
90+
// Number of times the variant was resolved in this period
91+
int64 count = 3 [(google.api.field_behavior) = REQUIRED];
11392
}
11493

115-
// Information about how a client resolved
116-
message ClientResolveInfo {
117-
// Resource reference to a client.
118-
string client = 1 [
119-
(google.api.resource_reference).type = "iam.confidence.dev/Client",
94+
// Information about how a rule was resolved.
95+
message RuleResolveInfo {
96+
// The rule that was resolved
97+
string rule = 1 [
98+
(google.api.resource_reference).type = "flags.confidence.dev/Rule",
12099
(google.api.field_behavior) = REQUIRED
121100
];
101+
// Number of times the rule was resolved in this period
102+
int64 count = 2 [(google.api.field_behavior) = REQUIRED];
122103

123-
// Resource reference to a credential.
124-
string client_credential = 2 [
125-
(google.api.resource_reference).type = "iam.confidence.dev/ClientCredential",
126-
(google.api.field_behavior) = REQUIRED
127-
];
104+
// Resolve counts on each assignment
105+
repeated AssignmentResolveInfo assignment_resolve_info = 3 [(google.api.field_behavior) = OPTIONAL];
106+
}
107+
108+
// Information about the assignment that was resolved.
109+
message AssignmentResolveInfo {
110+
// The assignment id of the resolved value, otherwise not set.
111+
string assignment_id = 1 [(google.api.field_behavior) = OPTIONAL];
128112

129-
// The different evaluation context schema of the client that have been seen recently.
130-
repeated EvaluationContextSchemaInstance schema = 3;
113+
// Number of times the assignment id was resolved in this period.
114+
int64 count = 2 [(google.api.field_behavior) = REQUIRED];
115+
}
116+
}
131117

132-
// An instance of a schema that was seen
133-
message EvaluationContextSchemaInstance {
134-
// Schema of each field in the evaluation context.
135-
map<string, EvaluationContextSchemaField.Kind> schema = 1;
136-
// Optional semantic type per field.
137-
map<string, ContextFieldSemanticType> semantic_types = 2;
138-
}
118+
message ClientResolveInfo {
119+
// Resource reference to a client.
120+
string client = 1 [
121+
(google.api.resource_reference).type = "iam.confidence.dev/Client",
122+
(google.api.field_behavior) = REQUIRED
123+
];
124+
125+
// Resource reference to a credential.
126+
string client_credential = 2 [
127+
(google.api.resource_reference).type = "iam.confidence.dev/ClientCredential",
128+
(google.api.field_behavior) = REQUIRED
129+
];
130+
131+
// The different evaluation context schema of the client that have been seen recently.
132+
repeated EvaluationContextSchemaInstance schema = 3;
133+
134+
// An instance of a schema that was seen
135+
message EvaluationContextSchemaInstance {
136+
// Schema of each field in the evaluation context.
137+
map<string, EvaluationContextSchemaField.Kind> schema = 1;
138+
// Optional semantic type per field.
139+
map<string, ContextFieldSemanticType> semantic_types = 2;
139140
}
140141
}
141142

confidence-proto/src/main/proto/confidence/flags/admin/v1/internal_api.proto

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import "google/protobuf/struct.proto";
77
import "google/api/annotations.proto";
88
import "google/api/field_behavior.proto";
99

10+
import "confidence/flags/admin/v1/flags_api.proto";
1011
import "confidence/flags/resolver/v1/types.proto";
1112
import "confidence/flags/resolver/v1/events/events.proto";
1213

@@ -18,21 +19,33 @@ option java_outer_classname = "InternalApiProto";
1819
// operations, useful when the resolve engine runs on the customer's premises
1920
// (e.g. side-car)
2021
service InternalFlagLoggerService {
22+
rpc WriteFlagLogs(WriteFlagLogsRequest) returns (WriteFlagLogsResponse){}
23+
2124
// Writes flag assignment events. Mostly called from the sidecar resolver.
2225
// (-- api-linter: core::0136::http-uri-suffix=disabled
2326
// aip.dev/not-precedent: Disabled because the additional binding. --)
24-
rpc WriteFlagAssigned(WriteFlagAssignedRequest) returns (WriteFlagAssignedResponse){
25-
option (google.api.http) = {
26-
post: "/v1/flagAssigned:write"
27-
body: "*"
28-
additional_bindings {
29-
post: "/v1/flagAssigned:writeArray"
30-
body: "flag_assigned"
31-
}
32-
};
33-
}
27+
rpc WriteFlagAssigned(WriteFlagAssignedRequest) returns (WriteFlagAssignedResponse){}
3428
}
3529

30+
message WriteFlagLogsRequest {
31+
repeated confidence.flags.resolver.v1.events.FlagAssigned flag_assigned = 1 [
32+
(google.api.field_behavior) = OPTIONAL
33+
];
34+
35+
TelemetryData telemetry_data = 2 [
36+
(google.api.field_behavior) = OPTIONAL
37+
];
38+
39+
repeated confidence.flags.admin.v1.ClientResolveInfo client_resolve_info = 3 [
40+
(google.api.field_behavior) = OPTIONAL
41+
];
42+
repeated confidence.flags.admin.v1.FlagResolveInfo flag_resolve_info = 4 [
43+
(google.api.field_behavior) = OPTIONAL
44+
];
45+
}
46+
47+
message WriteFlagLogsResponse {}
48+
3649
// A request to write flag assignments
3750
message WriteFlagAssignedRequest {
3851
// List of flag assigned events to write

confidence-proto/src/main/proto/confidence/flags/admin/v1/resolver.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ message ResolverStateUriResponse {
4242
string signed_uri = 1;
4343
// At what time the state uri expires
4444
google.protobuf.Timestamp expire_time = 2;
45+
string account = 3;
46+
4547
}
4648

4749
// Request to get the resolver state for the whole account

confidence-proto/src/main/proto/confidence/wasm/messages.proto

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ option java_package = "com.spotify.confidence.wasm";
66

77
message Void {}
88

9+
message SetResolverStateRequest {
10+
bytes state = 1;
11+
string account_id = 2;
12+
}
913

1014
message Request {
1115
bytes data = 1;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
syntax = "proto3";
2+
3+
package confidence.flags.resolver.v1;
4+
5+
import "google/protobuf/struct.proto";
6+
import "google/protobuf/timestamp.proto";
7+
import "confidence/flags/types/v1/types.proto";
8+
import "confidence/flags/resolver/v1/types.proto";
9+
import "confidence/flags/resolver/v1/api.proto";
10+
11+
option java_package = "com.spotify.confidence.flags.resolver.v1";
12+
option java_multiple_files = true;
13+
option java_outer_classname = "WasmApiProto";
14+
15+
message LogMessage {
16+
string message = 1;
17+
}
18+
19+
message ResolveWithStickyRequest {
20+
ResolveFlagsRequest resolve_request = 1;
21+
22+
// Context about the materialization required for the resolve
23+
MaterializationContext materialization_context = 7;
24+
25+
// if a materialization info is missing, we want to return to the caller immediately
26+
bool fail_fast_on_sticky = 8;
27+
}
28+
29+
message MaterializationContext {
30+
map<string, MaterializationInfo> unit_materialization_info = 1;
31+
}
32+
33+
message MaterializationInfo {
34+
bool unit_in_info = 1;
35+
map<string, string> rule_to_variant = 2;
36+
}
37+
38+
message ResolveWithStickyResponse {
39+
oneof resolve_result {
40+
Success success = 1;
41+
MissingMaterializations missing_materializations = 2;
42+
}
43+
44+
message Success {
45+
ResolveFlagsResponse response = 1;
46+
repeated MaterializationUpdate updates = 2;
47+
}
48+
49+
message MissingMaterializations {
50+
repeated MissingMaterializationItem items = 1;
51+
}
52+
53+
message MissingMaterializationItem {
54+
string unit = 1;
55+
string rule = 2;
56+
string read_materialization = 3;
57+
}
58+
59+
message MaterializationUpdate {
60+
string unit = 1;
61+
string write_materialization = 2;
62+
string rule = 3;
63+
string variant = 4;
64+
}
65+
}
66+

openfeature-provider-local/pom.xml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<maven.compiler.source>17</maven.compiler.source>
2020
<maven.compiler.target>17</maven.compiler.target>
2121
<!-- WASM file version. When updated, make sure to mvn clean to force a new download -->
22-
<confidence.resolver.wasm.version>v0.2.0</confidence.resolver.wasm.version>
22+
<confidence.resolver.wasm.version>v0.3.1</confidence.resolver.wasm.version>
2323
</properties>
2424

2525

@@ -64,6 +64,22 @@
6464
<groupId>com.google.protobuf</groupId>
6565
<artifactId>protobuf-java</artifactId>
6666
</dependency>
67+
<!-- gRPC dependencies for HTTP client -->
68+
<dependency>
69+
<groupId>io.grpc</groupId>
70+
<artifactId>grpc-netty-shaded</artifactId>
71+
<exclusions>
72+
<exclusion>
73+
<groupId>com.google.guava</groupId>
74+
<artifactId>guava</artifactId>
75+
</exclusion>
76+
</exclusions>
77+
</dependency>
78+
79+
<dependency>
80+
<groupId>io.grpc</groupId>
81+
<artifactId>grpc-stub</artifactId>
82+
</dependency>
6783
<dependency>
6884
<groupId>com.google.protobuf</groupId>
6985
<artifactId>protobuf-java-util</artifactId>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.spotify.confidence;
2+
3+
import com.google.protobuf.Struct;
4+
import com.spotify.confidence.shaded.flags.resolver.v1.FlagResolverServiceGrpc;
5+
import com.spotify.confidence.shaded.flags.resolver.v1.ResolveFlagsRequest;
6+
import com.spotify.confidence.shaded.flags.resolver.v1.ResolveFlagsResponse;
7+
import com.spotify.confidence.shaded.flags.resolver.v1.Sdk;
8+
import com.spotify.confidence.shaded.flags.resolver.v1.Sdk.Builder;
9+
import com.spotify.confidence.shaded.flags.resolver.v1.SdkId;
10+
import io.grpc.ManagedChannel;
11+
import io.grpc.ManagedChannelBuilder;
12+
import java.time.Duration;
13+
import java.util.List;
14+
import java.util.Optional;
15+
import java.util.concurrent.CompletableFuture;
16+
import java.util.concurrent.TimeUnit;
17+
18+
/**
19+
* A simplified gRPC-based flag resolver for fallback scenarios in the local provider. This is a
20+
* copy of the core functionality from GrpcFlagResolver adapted for the local provider's needs.
21+
*/
22+
public class ConfidenceGrpcFlagResolver {
23+
private final ManagedChannel channel;
24+
private final Builder sdkBuilder =
25+
Sdk.newBuilder().setVersion("0.2.8"); // Using static version for local provider
26+
27+
private final FlagResolverServiceGrpc.FlagResolverServiceFutureStub stub;
28+
29+
public ConfidenceGrpcFlagResolver() {
30+
final String confidenceDomain =
31+
Optional.ofNullable(System.getenv("CONFIDENCE_DOMAIN")).orElse("edge-grpc.spotify.com");
32+
final boolean useGrpcPlaintext =
33+
Optional.ofNullable(System.getenv("CONFIDENCE_GRPC_PLAINTEXT"))
34+
.map(Boolean::parseBoolean)
35+
.orElse(false);
36+
37+
ManagedChannelBuilder<?> builder = ManagedChannelBuilder.forTarget(confidenceDomain);
38+
if (useGrpcPlaintext) {
39+
builder = builder.usePlaintext();
40+
}
41+
42+
final ManagedChannel channel =
43+
builder.intercept(new DefaultDeadlineClientInterceptor(Duration.ofMinutes(1))).build();
44+
45+
this.channel = channel;
46+
this.stub = FlagResolverServiceGrpc.newFutureStub(channel);
47+
}
48+
49+
public CompletableFuture<ResolveFlagsResponse> resolve(
50+
List<String> flags, String clientSecret, Struct context) {
51+
return GrpcUtil.toCompletableFuture(
52+
stub.withDeadlineAfter(10_000, TimeUnit.MILLISECONDS)
53+
.resolveFlags(
54+
ResolveFlagsRequest.newBuilder()
55+
.setClientSecret(clientSecret)
56+
.addAllFlags(flags)
57+
.setEvaluationContext(context)
58+
.setSdk(sdkBuilder.setId(SdkId.SDK_ID_JAVA_PROVIDER).build())
59+
.setApply(true)
60+
.build()));
61+
}
62+
63+
public void close() {
64+
channel.shutdownNow();
65+
}
66+
}

0 commit comments

Comments
 (0)