Skip to content

Commit 1ea86ea

Browse files
fix: handle panics (#76)
1 parent 55c45b6 commit 1ea86ea

File tree

8 files changed

+241
-101
lines changed

8 files changed

+241
-101
lines changed

openfeature-provider/js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"build": "tsdown",
3434
"dev": "tsdown --watch",
3535
"test": "vitest",
36-
"proto:gen": "rm -rf src/proto && mkdir -p src/proto && protoc --plugin=node_modules/.bin/protoc-gen-ts_proto --ts_proto_opt useOptionals=messages --ts_proto_opt esModuleInterop=true --ts_proto_out src/proto -Iproto api.proto messages.proto"
36+
"proto:gen": "rm -rf src/proto && mkdir -p src/proto && protoc --plugin=node_modules/.bin/protoc-gen-ts_proto --ts_proto_opt useOptionals=messages --ts_proto_opt esModuleInterop=true --ts_proto_out src/proto -Iproto api.proto messages.proto test-only.proto"
3737
},
3838
"dependencies": {
3939
"@bufbuild/protobuf": "^2.9.0"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
syntax = "proto3";
2+
3+
message WriteFlagLogsRequest {
4+
repeated bytes flag_assigned = 1;
5+
6+
bytes telemetry_data = 2;
7+
8+
repeated bytes client_resolve_info = 3;
9+
repeated bytes flag_resolve_info = 4;
10+
}

openfeature-provider/js/src/ConfidenceServerProviderLocal.e2e.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const {
1111

1212
const moduleBytes = readFileSync(__dirname + '/../../../wasm/confidence_resolver.wasm');
1313
const module = new WebAssembly.Module(moduleBytes);
14-
const resolver = await WasmResolver.load(module);
14+
const resolver = new WasmResolver(module);
1515
const confidenceProvider = new ConfidenceServerProviderLocal(resolver, {
1616
flagClientSecret: 'RxDVTrXvc6op1XxiQ4OaR31dKbJ39aYV',
1717
apiClientId: JS_E2E_CONFIDENCE_API_CLIENT_ID,

openfeature-provider/js/src/WasmResolver.test.ts

Lines changed: 126 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,149 @@
1-
import { beforeEach, describe, expect, it, test } from 'vitest';
2-
import { WasmResolver } from './WasmResolver';
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { UnsafeWasmResolver, WasmResolver } from './WasmResolver';
33
import { readFileSync } from 'node:fs';
4-
import { ResolveReason } from './proto/api';
5-
import { spawnSync } from 'node:child_process';
6-
import { error } from 'node:console';
7-
import { stderr } from 'node:process';
4+
import { ResolveWithStickyRequest, ResolveReason } from './proto/api';
5+
import { WriteFlagLogsRequest } from './proto/test-only';
86

97
const moduleBytes = readFileSync(__dirname + '/../../../wasm/confidence_resolver.wasm');
108
const stateBytes = readFileSync(__dirname + '/../../../wasm/resolver_state.pb');
119

10+
const module = new WebAssembly.Module(moduleBytes);
1211
const CLIENT_SECRET = 'mkjJruAATQWjeY7foFIWfVAcBWnci2YF';
1312

13+
const RESOLVE_REQUEST:ResolveWithStickyRequest = {
14+
resolveRequest: {
15+
flags: ['flags/tutorial-feature'],
16+
clientSecret: CLIENT_SECRET,
17+
apply: true,
18+
evaluationContext: {
19+
targeting_key: 'tutorial_visitor',
20+
visitor_id: 'tutorial_visitor',
21+
},
22+
},
23+
materializationsPerUnit: {},
24+
failFastOnSticky: false
25+
};
26+
27+
const SET_STATE_REQUEST = { state: stateBytes, accountId: 'confidence-test' };
28+
29+
1430
let wasmResolver: WasmResolver;
15-
beforeEach(async () => {
16-
wasmResolver = await WasmResolver.load(new WebAssembly.Module(moduleBytes));
17-
});
18-
19-
it('should fail to resolve without state', () => {
20-
expect(() => {
21-
wasmResolver.resolveWithSticky({
22-
resolveRequest: { flags: [], clientSecret: 'xyz', apply: false },
23-
materializationsPerUnit: {},
24-
failFastOnSticky: false
25-
});
26-
}).toThrowError('Resolver state not set');
27-
});
2831

29-
describe('with state', () => {
32+
describe('basic operation', () => {
33+
3034
beforeEach(() => {
31-
wasmResolver.setResolverState({ state: stateBytes, accountId: 'confidence-test' });
35+
wasmResolver = new WasmResolver(module);
36+
});
37+
38+
it('should fail to resolve without state', () => {
39+
expect(() => {
40+
wasmResolver.resolveWithSticky(RESOLVE_REQUEST);
41+
}).toThrowError('Resolver state not set');
3242
});
43+
44+
describe('with state', () => {
45+
beforeEach(() => {
46+
wasmResolver.setResolverState(SET_STATE_REQUEST);
47+
});
48+
49+
it('should resolve flags', () => {
50+
const resp = wasmResolver.resolveWithSticky(RESOLVE_REQUEST);
3351

34-
it('should resolve flags', () => {
35-
try {
36-
const resp = wasmResolver.resolveWithSticky({
37-
resolveRequest: {
38-
flags: ['flags/tutorial-feature'],
39-
clientSecret: CLIENT_SECRET,
40-
apply: true,
41-
evaluationContext: {
42-
targeting_key: 'tutorial_visitor',
43-
visitor_id: 'tutorial_visitor',
44-
},
45-
},
46-
materializationsPerUnit: {},
47-
failFastOnSticky: false
52+
expect(resp).toMatchObject({
53+
success: {
54+
response: {
55+
resolvedFlags: [
56+
{
57+
reason: ResolveReason.RESOLVE_REASON_MATCH,
58+
},
59+
],
60+
}
61+
}
4862
});
63+
});
64+
65+
describe('flushLogs', () => {
66+
67+
it('should be empty before any resolve', () => {
68+
const logs = wasmResolver.flushLogs();
69+
expect(logs.length).toBe(0);
70+
})
71+
72+
it('should contain logs after a resolve', () => {
73+
wasmResolver.resolveWithSticky(RESOLVE_REQUEST);
74+
75+
const decoded = WriteFlagLogsRequest.decode(wasmResolver.flushLogs());
76+
77+
expect(decoded.flagAssigned.length).toBe(1)
78+
expect(decoded.clientResolveInfo.length).toBe(1);
79+
expect(decoded.flagResolveInfo.length).toBe(1);
80+
})
81+
})
82+
});
83+
})
4984

50-
expect(resp.success).toBeDefined();
51-
expect(resp.success?.response).toMatchObject({
52-
resolvedFlags: [
53-
{
54-
reason: ResolveReason.RESOLVE_REASON_MATCH,
55-
},
56-
],
57-
});
58-
} catch (e) {
59-
console.log('yo', e);
60-
}
85+
describe('panic handling', () => {
86+
87+
const resolveWithStickySpy = vi.spyOn(UnsafeWasmResolver.prototype, 'resolveWithSticky');
88+
const setResolverStateSpy = vi.spyOn(UnsafeWasmResolver.prototype, 'setResolverState');
89+
90+
const throwUnreachable = () => {
91+
throw new WebAssembly.RuntimeError('unreachable');
92+
}
93+
94+
beforeEach(() => {
95+
vi.resetAllMocks();
96+
wasmResolver = new WasmResolver(module);
6197
});
6298

63-
describe('flushLogs', () => {
6499

65-
it('should be empty before any resolve', () => {
66-
const logs = wasmResolver.flushLogs();
67-
expect(logs.length).toBe(0);
68-
})
100+
it('throws and reloads the instance on panic', () => {
101+
wasmResolver.setResolverState(SET_STATE_REQUEST)
102+
resolveWithStickySpy.mockImplementationOnce(throwUnreachable);
69103

70-
it('should contain logs after a resolve', () => {
71-
wasmResolver.resolveWithSticky({
72-
resolveRequest: {
73-
flags: ['flags/tutorial-feature'],
74-
clientSecret: CLIENT_SECRET,
75-
apply: true,
76-
evaluationContext: {
77-
targeting_key: 'tutorial_visitor',
78-
visitor_id: 'tutorial_visitor',
79-
},
80-
},
81-
materializationsPerUnit: {},
82-
failFastOnSticky: false
83-
});
104+
105+
expect(() => {
106+
wasmResolver.resolveWithSticky(RESOLVE_REQUEST)
107+
}).to.throw('unreachable');
84108

85-
const decoded = decodeBuffer(wasmResolver.flushLogs());
109+
// now it should succeed since the instance is reloaded
110+
expect(() => {
111+
wasmResolver.resolveWithSticky(RESOLVE_REQUEST)
112+
}).to.not.throw();
86113

87-
expect(decoded).contains('flag_assigned');
88-
expect(decoded).contains('client_resolve_info');
89-
expect(decoded).contains('flag_resolve_info');
90-
})
91114
})
92-
});
93115

116+
it('can handle panic in setResolverState', () => {
117+
setResolverStateSpy.mockImplementation(throwUnreachable);
118+
119+
expect(() => {
120+
wasmResolver.setResolverState(SET_STATE_REQUEST)
121+
}).to.throw('unreachable');
122+
123+
expect(() => {
124+
wasmResolver.resolveWithSticky(RESOLVE_REQUEST)
125+
}).to.throw('state not set');
126+
127+
})
128+
129+
it('tries to extracts logs from panicked instance', () => {
130+
wasmResolver.setResolverState(SET_STATE_REQUEST)
131+
132+
// create some logs
133+
wasmResolver.resolveWithSticky(RESOLVE_REQUEST);
134+
135+
resolveWithStickySpy.mockImplementationOnce(throwUnreachable);
136+
137+
expect(() => {
138+
wasmResolver.resolveWithSticky(RESOLVE_REQUEST)
139+
}).to.throw('unreachable');
140+
141+
const logs = wasmResolver.flushLogs();
142+
143+
expect(logs.length).toBeGreaterThan(0);
144+
145+
});
146+
147+
148+
})
94149

95-
function decodeBuffer(input:Uint8Array):string {
96-
const res = spawnSync('protoc',[
97-
`-I${__dirname}/../../../confidence-resolver/protos`,
98-
`--decode=confidence.flags.resolver.v1.WriteFlagLogsRequest`,
99-
`confidence/flags/resolver/v1/internal_api.proto`
100-
], { input, encoding: 'utf8' });
101-
if(res.error) {
102-
throw res.error;
103-
}
104-
if(res.status !== 0) {
105-
throw new Error(res.stderr)
106-
}
107-
return res.stdout;
108-
}

0 commit comments

Comments
 (0)