Skip to content

Handle readiness logic with large segments #326

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 16, 2024
Merged
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
89 changes: 74 additions & 15 deletions src/readiness/__tests__/readinessManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,26 @@ import { readinessManagerFactory } from '../readinessManager';
import { EventEmitter } from '../../utils/MinEvents';
import { IReadinessManager } from '../types';
import { SDK_READY, SDK_UPDATE, SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED, SDK_READY_FROM_CACHE, SDK_SPLITS_CACHE_LOADED, SDK_READY_TIMED_OUT } from '../constants';
import { ISettings } from '../../types';

const settings = {
startup: {
readyTimeout: 0,
waitForLargeSegments: false
},
sync: {
largeSegmentEnabled: false
}
} as unknown as ISettings;

const settingsWithTimeout = {
...settings,
startup: {
...settings.startup,
readyTimeout: 50
}
};

const timeoutMs = 100;
const statusFlagsCount = 5;

function assertInitialStatus(readinessManager: IReadinessManager) {
Expand All @@ -17,7 +35,7 @@ function assertInitialStatus(readinessManager: IReadinessManager) {
test('READINESS MANAGER / Share splits but segments (without timeout enabled)', (done) => {
expect.assertions(2 + statusFlagsCount * 2);

const readinessManager = readinessManagerFactory(EventEmitter);
const readinessManager = readinessManagerFactory(EventEmitter, settings);
const readinessManager2 = readinessManager.shared();

assertInitialStatus(readinessManager); // all status flags must be false
Expand Down Expand Up @@ -50,7 +68,7 @@ test('READINESS MANAGER / Share splits but segments (without timeout enabled)',
});

test('READINESS MANAGER / Ready event should be fired once', () => {
const readinessManager = readinessManagerFactory(EventEmitter);
const readinessManager = readinessManagerFactory(EventEmitter, settings);
let counter = 0;

readinessManager.gate.on(SDK_READY, () => {
Expand All @@ -69,7 +87,7 @@ test('READINESS MANAGER / Ready event should be fired once', () => {
});

test('READINESS MANAGER / Ready from cache event should be fired once', (done) => {
const readinessManager = readinessManagerFactory(EventEmitter);
const readinessManager = readinessManagerFactory(EventEmitter, settings);
let counter = 0;

readinessManager.gate.on(SDK_READY_FROM_CACHE, () => {
Expand All @@ -94,7 +112,7 @@ test('READINESS MANAGER / Ready from cache event should be fired once', (done) =
});

test('READINESS MANAGER / Update event should be fired after the Ready event', () => {
const readinessManager = readinessManagerFactory(EventEmitter);
const readinessManager = readinessManagerFactory(EventEmitter, settings);
let isReady = false;
let counter = 0;

Expand All @@ -121,7 +139,7 @@ test('READINESS MANAGER / Update event should be fired after the Ready event', (
test('READINESS MANAGER / Segment updates should not be propagated', (done) => {
let updateCounter = 0;

const readinessManager = readinessManagerFactory(EventEmitter);
const readinessManager = readinessManagerFactory(EventEmitter, settings);
const readinessManager2 = readinessManager.shared();

readinessManager2.gate.on(SDK_UPDATE, () => {
Expand Down Expand Up @@ -149,7 +167,7 @@ describe('READINESS MANAGER / Timeout ready event', () => {

beforeEach(() => {
// Schedule timeout to be fired before SDK_READY
readinessManager = readinessManagerFactory(EventEmitter, 10);
readinessManager = readinessManagerFactory(EventEmitter, settingsWithTimeout);
timeoutCounter = 0;

readinessManager.gate.on(SDK_READY_TIMED_OUT, () => {
Expand All @@ -160,7 +178,7 @@ describe('READINESS MANAGER / Timeout ready event', () => {
setTimeout(() => {
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
}, 20);
}, settingsWithTimeout.startup.readyTimeout + 20);
});

test('should be fired once', (done) => {
Expand Down Expand Up @@ -194,7 +212,7 @@ test('READINESS MANAGER / Cancel timeout if ready fired', (done) => {
let sdkReadyCalled = false;
let sdkReadyTimedoutCalled = false;

const readinessManager = readinessManagerFactory(EventEmitter, timeoutMs);
const readinessManager = readinessManagerFactory(EventEmitter, settingsWithTimeout);

readinessManager.gate.on(SDK_READY_TIMED_OUT, () => { sdkReadyTimedoutCalled = true; });
readinessManager.gate.once(SDK_READY, () => { sdkReadyCalled = true; });
Expand All @@ -204,16 +222,16 @@ test('READINESS MANAGER / Cancel timeout if ready fired', (done) => {
expect(sdkReadyTimedoutCalled).toBeFalsy();
expect(sdkReadyCalled).toBeTruthy();
done();
}, timeoutMs * 3);
}, settingsWithTimeout.startup.readyTimeout * 3);

setTimeout(() => {
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
}, timeoutMs * 0.8);
}, settingsWithTimeout.startup.readyTimeout * 0.8);
});

test('READINESS MANAGER / Destroy after it was ready but before timedout', () => {
const readinessManager = readinessManagerFactory(EventEmitter, timeoutMs);
const readinessManager = readinessManagerFactory(EventEmitter, settingsWithTimeout);

let counter = 0;

Expand All @@ -238,7 +256,7 @@ test('READINESS MANAGER / Destroy after it was ready but before timedout', () =>
});

test('READINESS MANAGER / Destroy before it was ready and timedout', (done) => {
const readinessManager = readinessManagerFactory(EventEmitter, timeoutMs);
const readinessManager = readinessManagerFactory(EventEmitter, settingsWithTimeout);

readinessManager.gate.on(SDK_READY, () => {
throw new Error('SDK_READY should have not been emitted');
Expand All @@ -250,14 +268,55 @@ test('READINESS MANAGER / Destroy before it was ready and timedout', (done) => {

setTimeout(() => {
readinessManager.destroy(); // Destroy the gate, removing all the listeners and clearing the ready timeout.
}, timeoutMs * 0.5);
}, settingsWithTimeout.startup.readyTimeout * 0.5);

setTimeout(() => {
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); // ready state if the readiness manager wasn't destroyed

expect('Calling destroy should have removed the readyTimeout and the test should end now.');
done();
}, timeoutMs * 1.5);
}, settingsWithTimeout.startup.readyTimeout * 1.5);

});

test('READINESS MANAGER / with large segments', () => {
const readinessManager = readinessManagerFactory(EventEmitter, {
startup: { readyTimeout: 0, waitForLargeSegments: false },
sync: { largeSegmentsEnabled: true }
} as unknown as ISettings);

expect(readinessManager.largeSegments).toBeUndefined();

const readinessManagerWithLargeSegments = readinessManagerFactory(EventEmitter, {
startup: { readyTimeout: 0, waitForLargeSegments: true },
sync: { largeSegmentsEnabled: true }
} as unknown as ISettings);

expect(readinessManagerWithLargeSegments.largeSegments).toBeDefined();

[readinessManager, readinessManagerWithLargeSegments].forEach(rm => {
let counter = 0;

rm.gate.on(SDK_READY, () => {
expect(rm.isReady()).toBe(true);
counter++;
});

rm.splits.emit(SDK_SPLITS_ARRIVED);
rm.segments.emit(SDK_SEGMENTS_ARRIVED);
if (rm.largeSegments) {
expect(counter).toBe(0); // should not be called yet
rm.largeSegments.emit(SDK_SEGMENTS_ARRIVED);
}
expect(counter).toBe(1); // should be called

rm.splits.emit(SDK_SPLITS_ARRIVED);
rm.segments.emit(SDK_SEGMENTS_ARRIVED);
rm.splits.emit(SDK_SPLITS_ARRIVED);
rm.segments.emit(SDK_SEGMENTS_ARRIVED);
if (rm.largeSegments) rm.largeSegments.emit(SDK_SEGMENTS_ARRIVED);

expect(counter).toBe(1); // should be called once
});
});
23 changes: 12 additions & 11 deletions src/readiness/__tests__/sdkReadinessManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SDK_READY, SDK_READY_FROM_CACHE, SDK_READY_TIMED_OUT, SDK_UPDATE } from
import { sdkReadinessManagerFactory } from '../sdkReadinessManager';
import { IReadinessManager } from '../types';
import { ERROR_CLIENT_LISTENER, CLIENT_READY_FROM_CACHE, CLIENT_READY, CLIENT_NO_LISTENER } from '../../logger/constants';
import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks';

const EventEmitterMock = jest.fn(() => ({
on: jest.fn(),
Expand Down Expand Up @@ -40,7 +41,7 @@ describe('SDK Readiness Manager - Event emitter', () => {
test('Providing the gate object to get the SDK status interface that manages events', () => {
expect(typeof sdkReadinessManagerFactory).toBe('function'); // The module exposes a function.

const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock);
const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);
expect(typeof sdkReadinessManager).toBe('object'); // The function result contains the readiness manager and a sdkStatus object.
const gateMock = sdkReadinessManager.readinessManager.gate;
const sdkStatus = sdkReadinessManager.sdkStatus;
Expand Down Expand Up @@ -76,7 +77,7 @@ describe('SDK Readiness Manager - Event emitter', () => {
});

test('The event callbacks should work as expected - SDK_READY_FROM_CACHE', () => {
const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock);
const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);
const gateMock = sdkReadinessManager.readinessManager.gate;

const readyFromCacheEventCB = gateMock.once.mock.calls[2][1];
Expand All @@ -86,7 +87,7 @@ describe('SDK Readiness Manager - Event emitter', () => {
});

test('The event callbacks should work as expected - SDK_READY emits with no callbacks', () => {
const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock);
const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);

// Get the callbacks
const addListenerCB = sdkReadinessManager.readinessManager.gate.on.mock.calls[1][1];
Expand All @@ -112,7 +113,7 @@ describe('SDK Readiness Manager - Event emitter', () => {
});

test('The event callbacks should work as expected - SDK_READY emits with callbacks', () => {
const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock);
const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);

// Get the callbacks
const addListenerCB = sdkReadinessManager.readinessManager.gate.on.mock.calls[1][1];
Expand All @@ -130,7 +131,7 @@ describe('SDK Readiness Manager - Event emitter', () => {
});

test('The event callbacks should work as expected - If we end up removing the listeners for SDK_READY, it behaves as if it had none', () => {
const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock);
const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);
const gateMock = sdkReadinessManager.readinessManager.gate;

// Get the callbacks
Expand All @@ -150,7 +151,7 @@ describe('SDK Readiness Manager - Event emitter', () => {
});

test('The event callbacks should work as expected - If we end up removing the listeners for SDK_READY, it behaves as if it had none', () => {
const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock);
const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);
const gateMock = sdkReadinessManager.readinessManager.gate;

// Get the callbacks
Expand All @@ -172,7 +173,7 @@ describe('SDK Readiness Manager - Event emitter', () => {

test('The event callbacks should work as expected - SDK_READY emits with expected internal callbacks', () => {
// the sdkReadinessManager expects more than one SDK_READY callback to not log the "No listeners" warning
const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock);
const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);
sdkReadinessManager.incInternalReadyCbCount();
const gateMock = sdkReadinessManager.readinessManager.gate;

Expand All @@ -197,7 +198,7 @@ describe('SDK Readiness Manager - Event emitter', () => {
describe('SDK Readiness Manager - Ready promise', () => {

test('.ready() promise behaviour for clients', async () => {
const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock);
const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);

const ready = sdkReadinessManager.sdkStatus.ready();
expect(ready instanceof Promise).toBe(true); // It should return a promise.
Expand Down Expand Up @@ -226,7 +227,7 @@ describe('SDK Readiness Manager - Ready promise', () => {
// control assertion. stubs already reset.
expect(testPassedCount).toBe(2);

const sdkReadinessManagerForTimedout = sdkReadinessManagerFactory(loggerMock, EventEmitterMock);
const sdkReadinessManagerForTimedout = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);

const readyForTimeout = sdkReadinessManagerForTimedout.sdkStatus.ready();

Expand Down Expand Up @@ -265,7 +266,7 @@ describe('SDK Readiness Manager - Ready promise', () => {
});

test('Full blown ready promise count as a callback and resolves on SDK_READY', (done) => {
const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock);
const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);
const readyPromise = sdkReadinessManager.sdkStatus.ready();

// Get the callback
Expand All @@ -287,7 +288,7 @@ describe('SDK Readiness Manager - Ready promise', () => {
});

test('.ready() rejected promises have a default onRejected handler that just logs the error', (done) => {
const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock);
const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);
let readyForTimeout = sdkReadinessManager.sdkStatus.ready();

emitTimeoutEvent(sdkReadinessManager.readinessManager); // make the SDK "timed out"
Expand Down
16 changes: 11 additions & 5 deletions src/readiness/readinessManager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { objectAssign } from '../utils/lang/objectAssign';
import { IEventEmitter } from '../types';
import { IEventEmitter, ISettings } from '../types';
import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED, SDK_SEGMENTS_ARRIVED, SDK_READY_TIMED_OUT, SDK_READY_FROM_CACHE, SDK_UPDATE, SDK_READY } from './constants';
import { IReadinessEventEmitter, IReadinessManager, ISegmentsEventEmitter, ISplitsEventEmitter } from './types';

Expand Down Expand Up @@ -33,10 +33,13 @@ function segmentsEventEmitterFactory(EventEmitter: new () => IEventEmitter): ISe
*/
export function readinessManagerFactory(
EventEmitter: new () => IEventEmitter,
readyTimeout = 0,
settings: ISettings,
splits: ISplitsEventEmitter = splitsEventEmitterFactory(EventEmitter)): IReadinessManager {

const { startup: { readyTimeout, waitForLargeSegments }, sync: { largeSegmentsEnabled } } = settings;

const segments: ISegmentsEventEmitter = segmentsEventEmitterFactory(EventEmitter);
const largeSegments = largeSegmentsEnabled && waitForLargeSegments ? segmentsEventEmitterFactory(EventEmitter) : undefined;
const gate: IReadinessEventEmitter = new EventEmitter();

// emit SDK_READY_FROM_CACHE
Expand All @@ -62,6 +65,7 @@ export function readinessManagerFactory(
let isReady = false;
splits.on(SDK_SPLITS_ARRIVED, checkIsReadyOrUpdate);
segments.on(SDK_SEGMENTS_ARRIVED, checkIsReadyOrUpdate);
if (largeSegments) largeSegments.on(SDK_SEGMENTS_ARRIVED, checkIsReadyOrUpdate);

let isDestroyed = false;

Expand All @@ -87,7 +91,7 @@ export function readinessManagerFactory(
setTimeout(() => { throw e; }, 0);
}
} else {
if (splits.splitsArrived && segments.segmentsArrived) {
if (splits.splitsArrived && segments.segmentsArrived && (!largeSegments || largeSegments.segmentsArrived)) {
clearTimeout(readyTimeoutId);
isReady = true;
try {
Expand All @@ -105,11 +109,12 @@ export function readinessManagerFactory(
return {
splits,
segments,
largeSegments,
gate,

shared(readyTimeout = 0) {
shared() {
refCount++;
return readinessManagerFactory(EventEmitter, readyTimeout, splits);
return readinessManagerFactory(EventEmitter, settings, splits);
},

// @TODO review/remove next methods when non-recoverable errors are reworked
Expand All @@ -123,6 +128,7 @@ export function readinessManagerFactory(
isDestroyed = true;

segments.removeAllListeners();
if (largeSegments) largeSegments.removeAllListeners();
gate.removeAllListeners();
clearTimeout(readyTimeoutId);

Expand Down
14 changes: 7 additions & 7 deletions src/readiness/sdkReadinessManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import { objectAssign } from '../utils/lang/objectAssign';
import { promiseWrapper } from '../utils/promise/wrapper';
import { readinessManagerFactory } from './readinessManager';
import { ISdkReadinessManager } from './types';
import { IEventEmitter } from '../types';
import { IEventEmitter, ISettings } from '../types';
import { SDK_READY, SDK_READY_TIMED_OUT, SDK_READY_FROM_CACHE, SDK_UPDATE } from './constants';
import { ILogger } from '../logger/types';
import { ERROR_CLIENT_LISTENER, CLIENT_READY_FROM_CACHE, CLIENT_READY, CLIENT_NO_LISTENER } from '../logger/constants';

const NEW_LISTENER_EVENT = 'newListener';
Expand All @@ -18,10 +17,11 @@ const REMOVE_LISTENER_EVENT = 'removeListener';
* @param readinessManager optional readinessManager to use. only used internally for `shared` method
*/
export function sdkReadinessManagerFactory(
log: ILogger,
EventEmitter: new () => IEventEmitter,
readyTimeout = 0,
readinessManager = readinessManagerFactory(EventEmitter, readyTimeout)): ISdkReadinessManager {
settings: ISettings,
readinessManager = readinessManagerFactory(EventEmitter, settings)): ISdkReadinessManager {

const log = settings.log;

/** Ready callback warning */
let internalReadyCbCount = 0;
Expand Down Expand Up @@ -72,8 +72,8 @@ export function sdkReadinessManagerFactory(
return {
readinessManager,

shared(readyTimeout = 0) {
return sdkReadinessManagerFactory(log, EventEmitter, readyTimeout, readinessManager.shared(readyTimeout));
shared() {
return sdkReadinessManagerFactory(EventEmitter, settings, readinessManager.shared());
},

incInternalReadyCbCount() {
Expand Down
Loading
Loading