Skip to content

Commit

Permalink
create WASM instance per NodeJS client
Browse files Browse the repository at this point in the history
  • Loading branch information
ajwootto committed Jul 3, 2024
1 parent c71cca7 commit fef5788
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 146 deletions.
13 changes: 6 additions & 7 deletions sdk/nodejs/__tests__/client.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { getBucketingLib } from '../src/bucketing'
import { DevCycleClient } from '../src/client'
import { DevCycleUser } from '@devcycle/js-cloud-server-sdk'

Expand Down Expand Up @@ -33,10 +32,10 @@ jest.mock('../src/eventQueue')
describe('DevCycleClient', () => {
it('imports bucketing lib on initialize', async () => {
const client = new DevCycleClient('token')
expect(() => getBucketingLib()).toThrow()
expect((client as any).bucketing).toBeUndefined()

Check warning on line 35 in sdk/nodejs/__tests__/client.spec.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type
await client.onClientInitialized()
const platformData = (getBucketingLib().setPlatformData as any).mock
.calls[0][0]
const platformData = ((client as any).bucketing.setPlatformData as any)

Check warning on line 37 in sdk/nodejs/__tests__/client.spec.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 37 in sdk/nodejs/__tests__/client.spec.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type
.mock.calls[0][0]

expect(JSON.parse(platformData)).toEqual({
platform: 'NodeJS',
Expand Down Expand Up @@ -92,19 +91,19 @@ describe('variable', () => {

it('returns a valid variable object for a variable that is not in the config', () => {
// @ts-ignore
getBucketingLib().variableForUser_PB.mockReturnValueOnce(null)
client.bucketing.variableForUser_PB.mockReturnValueOnce(null)
const variable = client.variable(user, 'test-key2', false)
expect(variable.value).toEqual(false)
expect(variable.isDefaulted).toEqual(true)

// @ts-ignore
getBucketingLib().variableForUser_PB.mockReturnValueOnce(null)
client.bucketing.variableForUser_PB.mockReturnValueOnce(null)
expect(client.variableValue(user, 'test-key2', false)).toEqual(false)
})

it('returns a defaulted variable object for a variable that is in the config but the wrong type', () => {
// @ts-ignore
getBucketingLib().variableForUser.mockReturnValueOnce(null)
client.bucketing.variableForUser.mockReturnValueOnce(null)
const variable = client.variable(user, 'test-key', 'test')
expect(variable.value).toEqual('test')
expect(variable.isDefaulted).toEqual(true)
Expand Down
53 changes: 24 additions & 29 deletions sdk/nodejs/__tests__/eventQueue.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { Exports } from '@devcycle/bucketing-assembly-script'

jest.mock('../src/request')
import { EventQueue, EventQueueOptions } from '../src/eventQueue'
import { EventTypes } from '../src/eventQueue'
import { BucketedUserConfig, DVCReporter, PublicProject } from '@devcycle/types'
import { mocked } from 'jest-mock'
import {
cleanupBucketingLib,
getBucketingLib,
importBucketingLib,
} from '../src/bucketing'
import { importBucketingLib } from '../src/bucketing'
import { setPlatformDataJSON } from './utils/setPlatformData'
import { Response } from 'cross-fetch'
import { dvcDefaultLogger } from '@devcycle/js-cloud-server-sdk'
Expand All @@ -21,6 +19,7 @@ const publishEvents_mock = mocked(publishEvents)
const defaultLogger = dvcDefaultLogger()

describe('EventQueue Unit Tests', () => {
let bucketing: Exports
const bucketedUserConfig: BucketedUserConfig = {
environment: { _id: '', key: '' },
features: {},
Expand Down Expand Up @@ -70,27 +69,23 @@ describe('EventQueue Unit Tests', () => {
clientUUID: string,
optionsOverrides?: Partial<EventQueueOptions>,
): EventQueue => {
getBucketingLib().setConfigData(sdkKey, JSON.stringify(config))
bucketing.setConfigData(sdkKey, JSON.stringify(config))
currentEventKey = sdkKey
const options = {
logger: defaultLogger,
...optionsOverrides,
}
return new EventQueue(sdkKey, clientUUID, options)
return new EventQueue(sdkKey, clientUUID, bucketing, options)
}

beforeAll(async () => {
await importBucketingLib()
setPlatformDataJSON()
})

afterAll(() => {
cleanupBucketingLib()
;[bucketing] = await importBucketingLib()
setPlatformDataJSON(bucketing)
})

afterEach(() => {
publishEvents_mock.mockReset()
getBucketingLib().cleanupEventQueue(currentEventKey)
bucketing.cleanupEventQueue(currentEventKey)
})

it('should report metrics', async () => {
Expand Down Expand Up @@ -853,12 +848,12 @@ describe('EventQueue Unit Tests', () => {
for (let i = 0; i < 500; i++) {
eventQueue.queueEvent(user, { type: 'test_event' })
}
expect(getBucketingLib().eventQueueSize(sdkKey)).toEqual(1000)
expect(bucketing.eventQueueSize(sdkKey)).toEqual(1000)

for (let i = 0; i < 1000; i++) {
eventQueue.queueEvent(user, { type: 'test_event' })
}
expect(getBucketingLib().eventQueueSize(sdkKey)).toEqual(2000)
expect(bucketing.eventQueueSize(sdkKey)).toEqual(2000)

eventQueue.queueEvent(user, { type: 'test_event2' })

Expand All @@ -868,7 +863,7 @@ describe('EventQueue Unit Tests', () => {
'Max event queue size reached, dropping event',
),
)
expect(getBucketingLib().eventQueueSize(sdkKey)).toEqual(2000)
expect(bucketing.eventQueueSize(sdkKey)).toEqual(2000)
},
)

Expand Down Expand Up @@ -901,12 +896,12 @@ describe('EventQueue Unit Tests', () => {
},
})
}
expect(getBucketingLib().eventQueueSize(sdkKey)).toEqual(1000)
expect(bucketing.eventQueueSize(sdkKey)).toEqual(1000)

for (let i = 0; i < 1000; i++) {
eventQueue.queueEvent(user, { type: 'test_event' })
}
expect(getBucketingLib().eventQueueSize(sdkKey)).toEqual(2000)
expect(bucketing.eventQueueSize(sdkKey)).toEqual(2000)

eventQueue.queueAggregateEvent(
user,
Expand All @@ -923,7 +918,7 @@ describe('EventQueue Unit Tests', () => {
'Max event queue size reached, dropping aggregate event',
),
)
expect(getBucketingLib().eventQueueSize(sdkKey)).toEqual(2000)
expect(bucketing.eventQueueSize(sdkKey)).toEqual(2000)
},
)

Expand Down Expand Up @@ -956,12 +951,12 @@ describe('EventQueue Unit Tests', () => {
})
}
expect(flushEvents_mock).toBeCalledTimes(0)
expect(getBucketingLib().eventQueueSize(sdkKey)).toEqual(1000)
expect(bucketing.eventQueueSize(sdkKey)).toEqual(1000)

// since max event queue size has been reached, attempting to add a new user event will flush the queue
eventQueue.queueEvent(user, { type: 'test_event' })
expect(flushEvents_mock).toBeCalledTimes(1)
expect(getBucketingLib().eventQueueSize(sdkKey)).toEqual(1001)
expect(bucketing.eventQueueSize(sdkKey)).toEqual(1001)
},
)

Expand Down Expand Up @@ -995,7 +990,7 @@ describe('EventQueue Unit Tests', () => {
})
}
expect(flushEvents_mock).toBeCalledTimes(0)
expect(getBucketingLib().eventQueueSize(sdkKey)).toEqual(1000)
expect(bucketing.eventQueueSize(sdkKey)).toEqual(1000)

// since max event queue size has been reached, attempting to add a new agg event will flush the queue
eventQueue.queueAggregateEvent(
Expand All @@ -1012,7 +1007,7 @@ describe('EventQueue Unit Tests', () => {
},
)
expect(flushEvents_mock).toBeCalledTimes(1)
expect(getBucketingLib().eventQueueSize(sdkKey)).toEqual(1001)
expect(bucketing.eventQueueSize(sdkKey)).toEqual(1001)
},
)
})
Expand All @@ -1021,14 +1016,14 @@ describe('EventQueue Unit Tests', () => {
it('should validate flushEventsMS', () => {
expect(
() =>
new EventQueue('test', 'uuid', {
new EventQueue('test', 'uuid', bucketing, {
logger: defaultLogger,
eventFlushIntervalMS: 400,
}),
).toThrow('eventFlushIntervalMS: 400 must be larger than 500ms')
expect(
() =>
new EventQueue('test', 'uuid', {
new EventQueue('test', 'uuid', bucketing, {
logger: defaultLogger,
eventFlushIntervalMS: 10 * 60 * 1000,
}),
Expand All @@ -1042,7 +1037,7 @@ describe('EventQueue Unit Tests', () => {
it('should validate flushEventQueueSize and maxEventQueueSize', () => {
expect(
() =>
new EventQueue('test', 'uuid', {
new EventQueue('test', 'uuid', bucketing, {
logger: defaultLogger,
flushEventQueueSize: 2000,
maxEventQueueSize: 2000,
Expand All @@ -1053,7 +1048,7 @@ describe('EventQueue Unit Tests', () => {

expect(
() =>
new EventQueue('test', 'uuid', {
new EventQueue('test', 'uuid', bucketing, {
logger: defaultLogger,
flushEventQueueSize: 1000,
maxEventQueueSize: 2000,
Expand All @@ -1066,7 +1061,7 @@ describe('EventQueue Unit Tests', () => {

expect(
() =>
new EventQueue('test', 'uuid', {
new EventQueue('test', 'uuid', bucketing, {
logger: defaultLogger,
flushEventQueueSize: 25000,
maxEventQueueSize: 40000,
Expand Down
5 changes: 3 additions & 2 deletions sdk/nodejs/__tests__/utils/setPlatformData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getBucketingLib } from '../../src/bucketing'
import { Exports } from '@devcycle/bucketing-assembly-script'

const defaultPlatformData = {
platform: 'NodeJS',
Expand All @@ -10,7 +10,8 @@ const defaultPlatformData = {
}

export const setPlatformDataJSON = (
bucketing: Exports,
data: unknown = defaultPlatformData,
): void => {
getBucketingLib().setPlatformData(JSON.stringify(data))
bucketing.setPlatformData(JSON.stringify(data))
}
50 changes: 20 additions & 30 deletions sdk/nodejs/src/__mocks__/bucketing.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { ProtobufTypes } from '@devcycle/bucketing-assembly-script'

let Bucketing: unknown
import { Exports, ProtobufTypes } from '@devcycle/bucketing-assembly-script'

const testVariable = {
_id: 'test-id',
Expand All @@ -25,31 +23,23 @@ enum VariableType {
JSON,
}

export const importBucketingLib = async (): Promise<void> => {
Bucketing = await new Promise((resolve) =>
resolve({
setConfigData: jest.fn(),
setConfigDataUTF8: jest.fn(),
setPlatformData: jest.fn(),
generateBucketedConfigForUser: jest.fn().mockReturnValue(
JSON.stringify({
variables: { 'test-key': testVariable },
}),
),
variableForUser: jest
.fn()
.mockReturnValue(JSON.stringify(testVariable)),
variableForUser_PB: jest.fn().mockReturnValue(buffer),
VariableType,
initEventQueue: jest.fn(),
flushEventQueue: jest.fn(),
}),
)
}

export const getBucketingLib = (): unknown => {
if (!Bucketing) {
throw new Error('Bucketing library not loaded')
}
return Bucketing
export const importBucketingLib = async (): Promise<[Exports, undefined]> => {
const bucketing = await Promise.resolve({
setConfigData: jest.fn(),
setConfigDataUTF8: jest.fn(),
setPlatformData: jest.fn(),
generateBucketedConfigForUser: jest.fn().mockReturnValue(
JSON.stringify({
variables: { 'test-key': testVariable },
}),
),
variableForUser: jest
.fn()
.mockReturnValue(JSON.stringify(testVariable)),
variableForUser_PB: jest.fn().mockReturnValue(buffer),
VariableType,
initEventQueue: jest.fn(),
flushEventQueue: jest.fn(),
})
return [bucketing as unknown as Exports, undefined]
}
48 changes: 15 additions & 33 deletions sdk/nodejs/src/bucketing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,67 +5,49 @@ import {
DevCycleServerSDKOptions,
} from '@devcycle/types'

let Bucketing: Exports | null
let InstantiatePromise: Promise<Exports> | null

export const importBucketingLib = async ({
logger,
options,
}: {
logger?: DVCLogger
options?: DevCycleServerSDKOptions
} = {}): Promise<void> => {
if (InstantiatePromise) {
await InstantiatePromise
return
}
} = {}): Promise<[Exports, NodeJS.Timer | undefined]> => {
const debugWASM = process.env.DEVCYCLE_DEBUG_WASM === '1'
InstantiatePromise = instantiate(debugWASM).then((exports) => {
Bucketing = exports
return Bucketing
})
await InstantiatePromise
startTrackingMemoryUsage(logger, options?.reporter)
const result = await instantiate(debugWASM)
const interval = startTrackingMemoryUsage(result, logger, options?.reporter)
return [result, interval]
}

export const startTrackingMemoryUsage = (
bucketing: Exports,
logger?: DVCLogger,
reporter?: DVCReporter,
interval: number = 30 * 1000,
): void => {
): NodeJS.Timer | undefined => {
if (!reporter) return
trackMemoryUsage(reporter, logger)
setInterval(() => trackMemoryUsage(reporter, logger), interval)
trackMemoryUsage(bucketing, reporter, logger)
return setInterval(
() => trackMemoryUsage(bucketing, reporter, logger),
interval,
)
}

export const trackMemoryUsage = (
bucketing: Exports,
reporter: DVCReporter,
logger?: DVCLogger,
): void => {
if (!reporter) return
if (!Bucketing) {
throw new Error('Bucketing lib not initialized')
}
const memoryUsageMB = Bucketing.memory.buffer.byteLength / 1e6
const memoryUsageMB = bucketing.memory.buffer.byteLength / 1e6
logger?.debug(`WASM memory usage: ${memoryUsageMB} MB`)
reporter.reportMetric('wasmMemoryMB', memoryUsageMB, {})
}

export const getBucketingLib = (): Exports => {
if (!Bucketing) {
throw new Error('Bucketing library not loaded')
}
return Bucketing
}

export const cleanupBucketingLib = (): void => {
Bucketing = null
}

export const setConfigDataUTF8 = (
bucketing: Exports,
sdkKey: string,
projectConfigStr: string,
): void => {
const configBuffer = Buffer.from(projectConfigStr, 'utf8')
getBucketingLib().setConfigDataUTF8(sdkKey, configBuffer)
bucketing.setConfigDataUTF8(sdkKey, configBuffer)
}
Loading

0 comments on commit fef5788

Please sign in to comment.