Skip to content

Commit 2e7792b

Browse files
committed
resolve mode for remote and local resolve
1 parent 25bce7b commit 2e7792b

File tree

12 files changed

+417
-66
lines changed

12 files changed

+417
-66
lines changed

openfeature-provider/src/main/java/com/spotify/confidence/ConfidenceFeatureProvider.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,14 @@ public ConfidenceFeatureProvider(Confidence confidence) {
5151
*/
5252
@Deprecated()
5353
public ConfidenceFeatureProvider(String clientSecret, ManagedChannel managedChannel) {
54-
this(Confidence.builder(clientSecret).flagResolverManagedChannel(managedChannel).build());
54+
this(
55+
Confidence.builder(
56+
clientSecret,
57+
RemoteResolve.builder()
58+
.flagResolverManagedChannel(managedChannel)
59+
.setClientSecret(clientSecret)
60+
.build())
61+
.build());
5562
}
5663

5764
/**

sdk-java/pom.xml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@
2121
</exclusion>
2222
</exclusions>
2323
</dependency>
24+
<dependency>
25+
<groupId>com.dylibso.chicory</groupId>
26+
<artifactId>runtime</artifactId>
27+
<version>1.4.0</version>
28+
</dependency>
29+
<dependency>
30+
<groupId>com.dylibso.chicory</groupId>
31+
<artifactId>compiler</artifactId>
32+
<version>1.4.0</version>
33+
</dependency>
2434
<dependency>
2535
<groupId>com.google.protobuf</groupId>
2636
<artifactId>protobuf-java-util</artifactId>
@@ -84,6 +94,24 @@
8494
</configuration>
8595
</plugin>
8696
</plugins>
97+
<resources>
98+
<resource>
99+
<directory>src/main/resources</directory>
100+
<filtering>true</filtering>
101+
<excludes>
102+
<exclude>**/*.wasm</exclude>
103+
<exclude>**/*.pb</exclude>
104+
</excludes>
105+
</resource>
106+
<resource>
107+
<directory>src/main/resources</directory>
108+
<filtering>false</filtering>
109+
<includes>
110+
<include>**/*.wasm</include>
111+
<include>**/*.pb</include>
112+
</includes>
113+
</resource>
114+
</resources>
87115
</build>
88116

89117
</project>

sdk-java/src/main/java/com/spotify/confidence/Confidence.java

Lines changed: 15 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,14 @@ static Confidence create(
244244
new ClientDelegate(closer, flagResolverClient, eventSenderEngine, clientSecret));
245245
}
246246

247+
public static Confidence.Builder builder(String clientSecret, ResolveMode resolveMode) {
248+
return new Confidence.Builder(clientSecret, resolveMode);
249+
}
250+
247251
public static Confidence.Builder builder(String clientSecret) {
248-
return new Confidence.Builder(clientSecret);
252+
final var remoteResolve =
253+
RemoteResolve.builder().setClientSecret(clientSecret).setResolveDeadlineMs(10_000).build();
254+
return new Confidence.Builder(clientSecret, remoteResolve);
249255
}
250256

251257
static class ClientDelegate implements FlagResolverClient, EventSenderEngine {
@@ -352,86 +358,32 @@ public void flush() {
352358

353359
public static class Builder {
354360
private final String clientSecret;
361+
private final ResolveMode resolveMode;
355362
private final Closer closer = Closer.create();
356-
357-
private final ManagedChannel DEFAULT_CHANNEL =
358-
ManagedChannelBuilder.forAddress("edge-grpc.spotify.com", 443)
359-
.keepAliveTime(Duration.ofMinutes(5).getSeconds(), TimeUnit.SECONDS)
360-
.build();
361-
private ManagedChannel flagResolverManagedChannel = DEFAULT_CHANNEL;
362-
private boolean disableTelemetry = false;
363-
private boolean isProvider = false;
364-
private int resolveDeadlineMs = 10_000;
365363
private int eventSenderDeadlineMs = 5_000;
366364

367-
public Builder(@Nonnull String clientSecret) {
365+
public Builder(@Nonnull String clientSecret, ResolveMode resolveMode) {
368366
this.clientSecret = clientSecret;
369-
registerChannelForShutdown(DEFAULT_CHANNEL);
370-
}
371-
372-
public Builder resolveDeadlineMs(int resolveDeadlineMs) {
373-
this.resolveDeadlineMs = resolveDeadlineMs;
374-
return this;
367+
this.resolveMode = resolveMode;
375368
}
376369

377370
public Builder eventSenderDeadlineMs(int eventSenderDeadlineMs) {
378371
this.eventSenderDeadlineMs = eventSenderDeadlineMs;
379372
return this;
380373
}
381374

382-
public Builder flagResolverManagedChannel(String host, int port) {
383-
this.flagResolverManagedChannel =
384-
ManagedChannelBuilder.forAddress(host, port).usePlaintext().build();
385-
registerChannelForShutdown(this.flagResolverManagedChannel);
386-
return this;
387-
}
388-
389-
public Builder flagResolverManagedChannel(ManagedChannel managedChannel) {
390-
this.flagResolverManagedChannel = managedChannel;
391-
return this;
392-
}
393-
394-
public Builder disableTelemetry(boolean disableTelemetry) {
395-
this.disableTelemetry = disableTelemetry;
396-
return this;
397-
}
398-
399-
public Confidence buildForProvider() {
400-
this.isProvider = true;
401-
return build();
402-
}
403-
404375
public Confidence build() {
405-
final FlagResolverClient flagResolverClient;
406-
final Telemetry telemetry = disableTelemetry ? null : new Telemetry(isProvider);
407-
final TelemetryClientInterceptor telemetryInterceptor =
408-
new TelemetryClientInterceptor(telemetry);
409-
final GrpcFlagResolver flagResolver =
410-
new GrpcFlagResolver(
411-
clientSecret, flagResolverManagedChannel, telemetryInterceptor, resolveDeadlineMs);
412-
413-
flagResolverClient = new FlagResolverClientImpl(flagResolver, telemetry);
376+
final ManagedChannel DEFAULT_CHANNEL =
377+
ManagedChannelBuilder.forAddress("edge-grpc.spotify.com", 443)
378+
.keepAliveTime(Duration.ofMinutes(5).getSeconds(), TimeUnit.SECONDS)
379+
.build();
414380

415381
final EventSenderEngine eventSenderEngine =
416382
new EventSenderEngineImpl(
417383
clientSecret, DEFAULT_CHANNEL, Instant::now, eventSenderDeadlineMs);
418-
closer.register(flagResolverClient);
419384
closer.register(eventSenderEngine);
420385
return new RootInstance(
421-
new ClientDelegate(closer, flagResolverClient, eventSenderEngine, clientSecret));
422-
}
423-
424-
private void registerChannelForShutdown(ManagedChannel channel) {
425-
this.closer.register(
426-
() -> {
427-
channel.shutdown();
428-
try {
429-
channel.awaitTermination(10, TimeUnit.SECONDS);
430-
} catch (InterruptedException e) {
431-
Thread.currentThread().interrupt();
432-
channel.shutdownNow();
433-
}
434-
});
386+
new ClientDelegate(closer, resolveMode, eventSenderEngine, clientSecret));
435387
}
436388
}
437389
}

sdk-java/src/main/java/com/spotify/confidence/FlagResolverClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
import java.io.Closeable;
55
import java.util.concurrent.CompletableFuture;
66

7-
interface FlagResolverClient extends Closeable {
7+
public interface FlagResolverClient extends Closeable {
88
CompletableFuture<ResolveFlagsResponse> resolveFlags(String flag, ConfidenceValue.Struct context);
99
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.spotify.confidence;
2+
3+
import com.dylibso.chicory.wasm.Parser;
4+
import com.dylibso.chicory.wasm.WasmModule;
5+
import com.google.protobuf.Struct;
6+
import com.spotify.confidence.shaded.flags.resolver.v1.ResolveFlagsRequest;
7+
import com.spotify.confidence.shaded.flags.resolver.v1.ResolveFlagsResponse;
8+
import java.io.IOException;
9+
import java.io.InputStream;
10+
import java.util.concurrent.CompletableFuture;
11+
12+
public class LocalResolve implements ResolveMode {
13+
private final LocalResolverApi localResolverApi;
14+
private final String clientSecret;
15+
16+
@Override
17+
public void close() {}
18+
19+
public LocalResolve(String clientSecret) {
20+
this.clientSecret = clientSecret;
21+
22+
// Load WASM module from resources
23+
try (InputStream wasmStream =
24+
getClass().getClassLoader().getResourceAsStream("wasm/rust_guest.wasm")) {
25+
if (wasmStream == null) {
26+
throw new RuntimeException("Could not find rust_guest.wasm in resources");
27+
}
28+
WasmModule module = Parser.parse(wasmStream);
29+
this.localResolverApi = new LocalResolverApi(module);
30+
setResolverState();
31+
} catch (IOException e) {
32+
throw new RuntimeException("Failed to load WASM module", e);
33+
}
34+
}
35+
36+
private void setResolverState() {
37+
// Load resolver state from resources
38+
try (InputStream stateStream =
39+
getClass().getClassLoader().getResourceAsStream("state/resolver_state.pb")) {
40+
if (stateStream == null) {
41+
throw new RuntimeException("Could not find resolver_state.pb in resources");
42+
}
43+
byte[] resolveState = stateStream.readAllBytes();
44+
this.localResolverApi.setResolverState(resolveState);
45+
} catch (IOException e) {
46+
throw new RuntimeException("Failed to load resolver state", e);
47+
}
48+
}
49+
50+
@Override
51+
public CompletableFuture<ResolveFlagsResponse> resolveFlags(
52+
String flag, ConfidenceValue.Struct context) {
53+
final var response =
54+
localResolverApi.resolve(
55+
ResolveFlagsRequest.newBuilder()
56+
.setClientSecret(clientSecret)
57+
.setApply(false)
58+
.setEvaluationContext(Struct.newBuilder().putAllFields(context.asProtoMap()))
59+
.addFlags(flag)
60+
.build());
61+
return CompletableFuture.completedFuture(response);
62+
}
63+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package com.spotify.confidence;
2+
3+
import com.dylibso.chicory.compiler.MachineFactoryCompiler;
4+
import com.dylibso.chicory.runtime.*;
5+
import com.dylibso.chicory.wasm.WasmModule;
6+
import com.dylibso.chicory.wasm.types.FunctionType;
7+
import com.dylibso.chicory.wasm.types.ValType;
8+
import com.google.protobuf.ByteString;
9+
import com.google.protobuf.GeneratedMessageV3;
10+
import com.google.protobuf.InvalidProtocolBufferException;
11+
import com.google.protobuf.Timestamp;
12+
import com.spotify.confidence.shaded.flags.resolver.v1.ResolveFlagsRequest;
13+
import com.spotify.confidence.shaded.flags.resolver.v1.ResolveFlagsResponse;
14+
import com.spotify.confidence.wasm.Messages;
15+
import java.util.List;
16+
import java.util.function.Function;
17+
18+
public class LocalResolverApi {
19+
20+
private static final FunctionType HOST_FN_TYPE =
21+
FunctionType.of(List.of(ValType.I32), List.of(ValType.I32));
22+
private final Instance instance;
23+
24+
// interop
25+
private final ExportFunction wasmMsgAlloc;
26+
private final ExportFunction wasmMsgFree;
27+
28+
// api
29+
private final ExportFunction wasmMsgGuestSetResolverState;
30+
private final ExportFunction wasmMsgGuestResolve;
31+
32+
public LocalResolverApi(WasmModule module) {
33+
34+
instance =
35+
Instance.builder(module)
36+
.withImportValues(
37+
ImportValues.builder()
38+
.addFunction(
39+
createImportFunction(
40+
"current_time", Messages.Void::parseFrom, this::currentTime))
41+
.build())
42+
.withMachineFactory(MachineFactoryCompiler::compile)
43+
.build();
44+
wasmMsgAlloc = instance.export("wasm_msg_alloc");
45+
wasmMsgFree = instance.export("wasm_msg_free");
46+
wasmMsgGuestSetResolverState = instance.export("wasm_msg_guest_set_resolver_state");
47+
wasmMsgGuestResolve = instance.export("wasm_msg_guest_resolve");
48+
}
49+
50+
private Timestamp currentTime(Messages.Void unused) {
51+
return Timestamp.getDefaultInstance();
52+
}
53+
54+
public void setResolverState(byte[] state) {
55+
final byte[] request =
56+
Messages.Request.newBuilder().setData(ByteString.copyFrom(state)).build().toByteArray();
57+
int addr = transfer(request);
58+
int respPtr = (int) wasmMsgGuestSetResolverState.apply(addr)[0];
59+
consumeResponse(respPtr, Messages.Void::parseFrom);
60+
}
61+
62+
public ResolveFlagsResponse resolve(ResolveFlagsRequest request) {
63+
int reqPtr = transferRequest(request);
64+
int respPtr = (int) wasmMsgGuestResolve.apply(reqPtr)[0];
65+
return consumeResponse(respPtr, ResolveFlagsResponse::parseFrom);
66+
}
67+
68+
private <T extends GeneratedMessageV3> T consumeResponse(int addr, ParserFn<T> codec) {
69+
try {
70+
Messages.Response response = Messages.Response.parseFrom(consume(addr));
71+
if (response.hasError()) {
72+
throw new RuntimeException(response.getError());
73+
} else {
74+
return codec.apply(response.getData().toByteArray());
75+
}
76+
} catch (InvalidProtocolBufferException e) {
77+
throw new RuntimeException(e);
78+
}
79+
}
80+
81+
private <T extends GeneratedMessageV3> T consumeRequest(int addr, ParserFn<T> codec) {
82+
try {
83+
Messages.Request request = Messages.Request.parseFrom(consume(addr));
84+
return codec.apply(request.toByteArray());
85+
} catch (InvalidProtocolBufferException e) {
86+
throw new RuntimeException(e);
87+
}
88+
}
89+
90+
private int transferRequest(GeneratedMessageV3 message) {
91+
final byte[] request =
92+
Messages.Request.newBuilder().setData(message.toByteString()).build().toByteArray();
93+
return transfer(request);
94+
}
95+
96+
private int transferResponseSuccess(GeneratedMessageV3 response) {
97+
final byte[] wrapperBytes =
98+
Messages.Response.newBuilder().setData(response.toByteString()).build().toByteArray();
99+
return transfer(wrapperBytes);
100+
}
101+
102+
private int transferResponseError(String error) {
103+
final byte[] wrapperBytes =
104+
Messages.Response.newBuilder().setError(error).build().toByteArray();
105+
return transfer(wrapperBytes);
106+
}
107+
108+
private byte[] consume(int addr) {
109+
final Memory mem = instance.memory();
110+
final int len = (int) (mem.readU32(addr - 4) - 4L);
111+
final byte[] data = mem.readBytes(addr, len);
112+
wasmMsgFree.apply(addr);
113+
return data;
114+
}
115+
116+
private int transfer(byte[] data) {
117+
final Memory mem = instance.memory();
118+
int addr = (int) wasmMsgAlloc.apply(data.length)[0];
119+
mem.write(addr, data);
120+
return addr;
121+
}
122+
123+
private <T extends GeneratedMessageV3> ImportFunction createImportFunction(
124+
String name, ParserFn<T> reqCodec, Function<T, GeneratedMessageV3> impl) {
125+
return new ImportFunction(
126+
"wasm_msg",
127+
"wasm_msg_host_" + name,
128+
HOST_FN_TYPE,
129+
(instance1, args) -> {
130+
try {
131+
final T message = consumeRequest((int) args[0], reqCodec);
132+
final GeneratedMessageV3 response = impl.apply(message);
133+
return new long[] {transferResponseSuccess(response)};
134+
} catch (Exception e) {
135+
return new long[] {transferResponseError(e.getMessage())};
136+
}
137+
});
138+
}
139+
140+
private interface ParserFn<T> {
141+
142+
T apply(byte[] data) throws InvalidProtocolBufferException;
143+
}
144+
}

0 commit comments

Comments
 (0)