Skip to content

Commit e201425

Browse files
committed
tests
1 parent 7df96b3 commit e201425

File tree

3 files changed

+219
-16
lines changed

3 files changed

+219
-16
lines changed

lib/event_processor/event_store.spec.ts

Lines changed: 203 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,16 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
import { vi, describe, it, expect, beforeEach } from 'vitest';
17-
import { getMockAsyncCache, getMockSyncCache } from '../tests/mock/mock_cache';
18-
import { SyncStore } from '../utils/cache/store';
16+
import { vi, describe, it, expect } from 'vitest';
17+
import { getMockAsyncCache } from '../tests/mock/mock_cache';
1918
import { EventWithId } from './batch_event_processor';
2019
import { EventStore, StoredEvent } from './event_store';
2120
import { createImpressionEvent } from '../tests/mock/create_event';
2221

2322
import { DEFAULT_MAX_EVENTS_IN_STORE } from './event_store';
2423
import { exhaustMicrotasks } from '../tests/testUtils';
24+
import { EVENT_STORE_FULL } from '../message/log_message';
25+
import { OptimizelyError } from '../error/optimizly_error';
2526

2627
type TestStoreConfig = {
2728
maxSize?: number;
@@ -68,7 +69,7 @@ describe('EventStore', () => {
6869
expect(saved2).toEqual(expect.objectContaining(event));
6970
});
7071

71-
it('should return all keys from getKeys()', async () => {
72+
it('should return all keys from getKeys', async () => {
7273
const { store } = getEventStore();
7374
const event: EventWithId = {
7475
id: '1',
@@ -88,7 +89,7 @@ describe('EventStore', () => {
8889
expect(savedKeys).toEqual(keys);
8990
});
9091

91-
it('should limit the number of saved keys', async () => {
92+
it('should limit the number of saved keys when set is called concurrently', async () => {
9293
const { store } = getEventStore();
9394

9495
const event: EventWithId = {
@@ -120,6 +121,24 @@ describe('EventStore', () => {
120121
expect(savedKeys).toEqual(keys.slice(0, DEFAULT_MAX_EVENTS_IN_STORE));
121122
});
122123

124+
it('should limit the number of saved keys when set is called serially', async () => {
125+
const { store } = getEventStore({ maxSize: 2 });
126+
127+
const event: EventWithId = {
128+
id: '1',
129+
event: createImpressionEvent('test'),
130+
}
131+
132+
await expect(store.set('event-1', event)).resolves.not.toThrow();
133+
await expect(store.set('event-2', event)).resolves.not.toThrow();
134+
await expect(store.set('event-3', event)).rejects.toThrow(new OptimizelyError(EVENT_STORE_FULL, event.id));
135+
136+
const savedKeys = await store.getKeys();
137+
savedKeys.sort();
138+
139+
expect(savedKeys).toEqual(['event-1', 'event-2']);
140+
});
141+
123142
it('should save keys again when the number of stored events drops below maxSize', async () => {
124143
const { store } = getEventStore();
125144

@@ -182,7 +201,7 @@ describe('EventStore', () => {
182201
expect(await store.getKeys()).toEqual([]);
183202
});
184203

185-
it('should resave events without expireAt on get', async () => {
204+
it('should resave events without expiresAt on get', async () => {
186205
const ttl = 120_000;
187206
const { mockStore, store } = getEventStore({ ttl });
188207

@@ -191,7 +210,6 @@ describe('EventStore', () => {
191210
event: createImpressionEvent('test'),
192211
}
193212

194-
195213
const originalSet = mockStore.set.bind(mockStore);
196214

197215
let call = 0;
@@ -200,12 +218,12 @@ describe('EventStore', () => {
200218
return originalSet(key, value);
201219
}
202220

203-
// Simulate old stored event without expireAt
204-
const eventWithoutExpireAt: StoredEvent = {
221+
// Simulate old stored event without expiresAt
222+
const eventWithoutExpiresAt: StoredEvent = {
205223
id: value.id,
206224
event: value.event,
207225
};
208-
return originalSet(key, eventWithoutExpireAt);
226+
return originalSet(key, eventWithoutExpiresAt);
209227
});
210228

211229
await store.set('test', event);
@@ -222,4 +240,179 @@ describe('EventStore', () => {
222240
expect(secondCall[1].expiresAt).toBeDefined();
223241
expect(secondCall[1].expiresAt!).toBeGreaterThanOrEqual(Date.now() + ttl - 10);
224242
});
243+
244+
it('should store event when key expires after store being full', async () => {
245+
const ttl = 100;
246+
const { store } = getEventStore({ ttl, maxSize: 2 });
247+
248+
const event: EventWithId = {
249+
id: '1',
250+
event: createImpressionEvent('test'),
251+
}
252+
253+
await store.set('event-1', event);
254+
await store.set('event-2', event);
255+
await expect(store.set('event-3', event)).rejects.toThrow();
256+
257+
expect(await store.getKeys().then(keys => keys.sort())).toEqual(['event-1', 'event-2']);
258+
259+
// wait for ttl to expire
260+
await new Promise(resolve => setTimeout(resolve, ttl + 50));
261+
262+
// both events should be expired now
263+
expect(await store.get('event-1')).toBeUndefined();
264+
expect(await store.get('event-2')).toBeUndefined();
265+
266+
// should be able to add new events now
267+
await expect(store.set('event-3', event)).resolves.not.toThrow();
268+
await expect(store.set('event-4', event)).resolves.not.toThrow();
269+
270+
const savedEvent3 = await store.get('event-3');
271+
expect(savedEvent3).toEqual(expect.objectContaining(event));
272+
const savedEvent4 = await store.get('event-4');
273+
expect(savedEvent4).toEqual(expect.objectContaining(event));
274+
});
275+
276+
it('should return all requested events correctly from getBatched', async () => {
277+
const { store } = getEventStore();
278+
const event1: EventWithId = { id: '1', event: createImpressionEvent('test-1') };
279+
const event2: EventWithId = { id: '2', event: createImpressionEvent('test-2') };
280+
const event3: EventWithId = { id: '3', event: createImpressionEvent('test-3') };
281+
282+
await store.set('key-1', event1);
283+
await store.set('key-2', event2);
284+
await store.set('key-3', event3);
285+
286+
const results = await store.getBatched(['key-3', 'key-1', 'key-2', 'key-4']);
287+
288+
expect(results).toHaveLength(4);
289+
expect(results[0]).toEqual(expect.objectContaining(event3));
290+
expect(results[1]).toEqual(expect.objectContaining(event1));
291+
expect(results[2]).toEqual(expect.objectContaining(event2));
292+
expect(results[3]).toBeUndefined();
293+
});
294+
295+
296+
it('should handle expired events in getBatched', async () => {
297+
const ttl = 100;
298+
const { store } = getEventStore({ ttl });
299+
const event1: EventWithId = { id: '1', event: createImpressionEvent('test-1') };
300+
const event2: EventWithId = { id: '2', event: createImpressionEvent('test-2') };
301+
302+
await store.set('key-1', event1);
303+
await new Promise(resolve => setTimeout(resolve, 70));
304+
await store.set('key-2', event2);
305+
306+
// wait for first key to expire but not the second
307+
await new Promise(resolve => setTimeout(resolve, 50));
308+
309+
const results = await store.getBatched(['key-1', 'key-2']);
310+
311+
expect(results).toHaveLength(2);
312+
expect(results[0]).toBeUndefined();
313+
expect(results[1]).toEqual(expect.objectContaining(event2));
314+
315+
await expect(store.getKeys()).resolves.toEqual(['key-2']);
316+
});
317+
318+
it('should resave events without expiresAt during getBatched', async () => {
319+
const ttl = 120_000;
320+
const { mockStore, store } = getEventStore({ ttl });
321+
const event: EventWithId = { id: '1', event: createImpressionEvent('test') };
322+
323+
const originalSet = mockStore.set.bind(mockStore);
324+
325+
let call = 0;
326+
const setSpy = vi.spyOn(mockStore, 'set').mockImplementation(async (key: string, value: StoredEvent) => {
327+
if (call++ > 0) {
328+
return originalSet(key, value);
329+
}
330+
331+
// Simulate old stored event without expiresAt
332+
const eventWithoutExpiresAt: StoredEvent = {
333+
id: value.id,
334+
event: value.event,
335+
};
336+
return originalSet(key, eventWithoutExpiresAt);
337+
});
338+
339+
await store.set('key-1', event);
340+
await store.set('key-2', event);
341+
342+
const results = await store.getBatched(['key-1', 'key-2']);
343+
344+
expect(results).toHaveLength(2);
345+
expect(results[0]).toEqual(expect.objectContaining(event));
346+
expect(results[1]).toEqual(expect.objectContaining(event));
347+
348+
await exhaustMicrotasks();
349+
expect(setSpy).toHaveBeenCalledTimes(3);
350+
351+
const secondCall = setSpy.mock.calls[1];
352+
353+
expect(secondCall[1].expiresAt).toBeDefined();
354+
expect(secondCall[1].expiresAt!).toBeGreaterThanOrEqual(Date.now() + ttl - 10);
355+
});
356+
357+
it('should store event when keys expire during getBatched after store being full', async () => {
358+
const ttl = 100;
359+
const { store } = getEventStore({ ttl, maxSize: 2 });
360+
361+
const event: EventWithId = {
362+
id: '1',
363+
event: createImpressionEvent('test'),
364+
}
365+
366+
await store.set('event-1', event);
367+
await new Promise(resolve => setTimeout(resolve, 70));
368+
await store.set('event-2', event);
369+
await expect(store.set('event-3', event)).rejects.toThrow();
370+
371+
expect(await store.getKeys().then(keys => keys.sort())).toEqual(['event-1', 'event-2']);
372+
373+
// wait for the first event to expire
374+
await new Promise(resolve => setTimeout(resolve, 50));
375+
376+
const results = await store.getBatched(['event-1', 'event-2']);
377+
378+
expect(results).toHaveLength(2);
379+
expect(results[0]).toBeUndefined();
380+
expect(results[1]).toEqual(expect.objectContaining(event));
381+
382+
await new Promise(resolve => setTimeout(resolve));
383+
384+
// should be able to add new event now
385+
await expect(store.set('event-3', event)).resolves.not.toThrow();
386+
const savedEvent = await store.get('event-3');
387+
expect(savedEvent).toEqual(expect.objectContaining(event));
388+
expect(await store.getKeys().then(keys => keys.sort())).toEqual(['event-2', 'event-3']);
389+
});
390+
391+
it('should restore in-memory key consistency after getKeys is called', async () => {
392+
const { mockStore, store } = getEventStore({ maxSize: 2 });
393+
const event: EventWithId = { id: '1', event: createImpressionEvent('test') };
394+
395+
const originalSet = mockStore.set.bind(mockStore);
396+
397+
let call = 0;
398+
vi.spyOn(mockStore, 'set').mockImplementation(async (key: string, value: StoredEvent) => {
399+
// only the seconde set call should fail
400+
if (call++ != 1) return originalSet(key, value);
401+
return Promise.reject(new Error('Simulated set failure'));
402+
});
403+
404+
await expect(store.set('key-1', event)).resolves.not.toThrow();
405+
// this should fail, but in memory key list will become full
406+
await expect(store.set('key-2', event)).rejects.toThrow('Simulated set failure');
407+
await expect(store.set('key-3', event)).rejects.toThrow(new OptimizelyError(EVENT_STORE_FULL, event.id));
408+
409+
410+
let keys = await store.getKeys();
411+
expect(keys.sort()).toEqual(['key-1']);
412+
413+
await expect(store.set('key-3', event)).resolves.not.toThrow();
414+
415+
keys = await store.getKeys();
416+
expect(keys.sort()).toEqual(['key-1', 'key-3']);
417+
});
225418
});

lib/event_processor/event_store.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { EventWithId } from "./batch_event_processor";
2-
import { AsyncPrefixStore, AsyncStore, AsyncStoreWithBatchedGet, OpStore, Store, SyncPrefixStore } from "../utils/cache/store";
3-
import { Maybe, OpType, OpValue } from "../utils/type";
2+
import { AsyncPrefixStore, AsyncStore, AsyncStoreWithBatchedGet, Store, StoreWithBatchedGet, SyncPrefixStore } from "../utils/cache/store";
3+
import { Maybe } from "../utils/type";
44
import { SerialRunner } from "../utils/executor/serial_runner";
55
import { LoggerFacade } from "../logging/logger";
66
import { EVENT_STORE_FULL } from "../message/log_message";
@@ -26,7 +26,7 @@ export type EventStoreConfig = {
2626
export class EventStore extends AsyncStoreWithBatchedGet<EventWithId> implements AsyncStore<EventWithId> {
2727
readonly operation = 'async';
2828

29-
private store: Store<StoredEvent>;
29+
private store: StoreWithBatchedGet<StoredEvent>;
3030
private serializer: SerialRunner = new SerialRunner();
3131
private logger?: LoggerFacade;
3232
private maxSize: number;
@@ -75,6 +75,8 @@ export class EventStore extends AsyncStoreWithBatchedGet<EventWithId> implements
7575
async set(key: string, event: EventWithId): Promise<unknown> {
7676
await this.readKeys();
7777

78+
// readKeys might have failed, in that case we cannot enforce max size
79+
// that means, the store might grow beyond max size in failure scenarios
7880
if (this.keys !== undefined && this.keys.size >= this.maxSize) {
7981
this.logger?.info(EVENT_STORE_FULL, event.event.uuid);
8082
return Promise.reject(new OptimizelyError(EVENT_STORE_FULL, event.event.uuid));
@@ -90,8 +92,7 @@ export class EventStore extends AsyncStoreWithBatchedGet<EventWithId> implements
9092
return this.store.set(key, { ...event, expiresAt: Date.now() + this.ttl });
9193
}
9294

93-
async get(key: string): Promise<EventWithId | undefined> {
94-
const value = await this.store.get(key);
95+
private processStoredEvent(key: string, value: StoredEvent | undefined): Maybe<EventWithId> {
9596
if (!value) return undefined;
9697

9798
// if there is events in the stored saved by old version of the sdk,
@@ -112,6 +113,12 @@ export class EventStore extends AsyncStoreWithBatchedGet<EventWithId> implements
112113
return value;
113114
}
114115

116+
async get(key: string): Promise<EventWithId | undefined> {
117+
const value = await this.store.get(key);
118+
119+
return this.processStoredEvent(key, value);
120+
}
121+
115122
async remove(key: string): Promise<unknown> {
116123
await this.store.remove(key);
117124
this.keys?.delete(key);
@@ -125,6 +132,7 @@ export class EventStore extends AsyncStoreWithBatchedGet<EventWithId> implements
125132
}
126133

127134
async getBatched(keys: string[]): Promise<Maybe<EventWithId>[]> {
128-
return [];
135+
const values = await this.store.getBatched(keys);
136+
return values.map((value, index) => this.processStoredEvent(keys[index], value));
129137
}
130138
}

lib/utils/cache/store.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export abstract class AsyncStoreWithBatchedGet<V> implements AsyncStore<V> {
4848
abstract getBatched(keys: string[]): Promise<Maybe<V>[]>;
4949
}
5050

51+
export type StoreWithBatchedGet<V> = SyncStoreWithBatchedGet<V> | AsyncStoreWithBatchedGet<V>;
52+
5153
export const getBatchedSync = <V>(store: SyncStore<V>, keys: string[]): Maybe<V>[] => {
5254
if (store instanceof SyncStoreWithBatchedGet) {
5355
return store.getBatched(keys);

0 commit comments

Comments
 (0)