Skip to content

feat: Added Offline storage support to Event processor for React Native Apps #517

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 29 commits into from
Jul 21, 2020

Conversation

zashraf1985
Copy link
Contributor

@zashraf1985 zashraf1985 commented Jul 3, 2020

Summary

Added Offline storage support to Event Processor for React Native Apps.

  1. Stores events when device is offline and then redispatches when the device is online again, new event is ready to be dispatched or SDK is re-initialized.
  2. Adds a new eventMaxQueueSize config option which represents number of offline events SDK will store at a time. Deault is 10k for now.
  3. Adds a connectivity listener to redispatch events when device is back online.

Follow-up changes expected in optimizely-sdk package.

  1. Add a new option to EventProcessor constructor to represent maxQueueSize in React Native entry point.
  2. Change callback parameter to http status in DefaultEventDispacher for browser entry point which is also used for React Native entry point.
  3. Add AsyncStorage module configuration for umd bundles in rollup config.

Test plan

  1. Wrote new unit tests
  2. Tested offline with FSC suite to make sure it does not break existing event processor functionality.
  3. Tested sequence of redispatching events using a local mock server and timestamps.

@coveralls
Copy link

coveralls commented Jul 3, 2020

Coverage Status

Coverage remained the same at 96.709% when pulling dd7c837 on zeeshan/rn-event-processor-proto into 8ca4242 on master.

@zashraf1985 zashraf1985 marked this pull request as ready for review July 6, 2020 07:19
@zashraf1985 zashraf1985 requested a review from a team as a code owner July 6, 2020 07:19
@zashraf1985 zashraf1985 removed their assignment Jul 6, 2020
@zashraf1985 zashraf1985 requested a review from jaeopt July 6, 2020 18:13
export { LogTierV1EventProcessor } from './v1/v1EventProcessor'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why change the exports like this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because i exported both RN and normal event processor from the single file. i will probably separate them out.

Comment on lines 18 to 22
export type EventDispatcherResponse = {
statusCode: number
} | {
status: number
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Node event dispatcher calls the ED callback with an object like this ({ statusCode: 200 }). Is that a problem? Why change this to accept a number?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think i changed it initially to try something quickly and then forgot to revert back. i will reverts it

// This results in race condition when two items are added to the map or array in parallel.
// for ex. Req 1 gets the map. Req 2 gets the map. Req 1 sets the map. Req 2 sets the map. The map now loses item from Req 1.
// This synchronizer makes sure the operations are atomic using promises.
class Synchronizer {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Synchronizer is similar to the RequestTracker. Can we use RequestTracker instead of adding this? We can consider renaming RequestTracker to something more generic.

// This stores individual events generated from the SDK till they are part of the pending buffer.
// The store is cleared right before the event is formatted to be dispatched.
// This is to make sure that individual events are not lost when app closes before the buffer was flushed.
export class ReactNativeEventBufferStore {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need two different classes ReactNativePendingEventStore and ReactNativeEventBufferStore? They seem very similar. Also I think they should both have a limit on how much they will store.

@@ -27,3 +28,13 @@ export class LogTierV1EventProcessor extends AbstractEventProcessor {
}
}
}

export class LogTierV1ReactNativeEventProcessor extends AbstractReactNativeEventProcessor {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can put this here. This causes the RN code to be included in the main bundle.

await this.pendingEventsStore.set(cacheKey, formattedEvent)

// Clear buffer because the buffer has become a formatted event and is already stored in pending cache.
this.eventBufferStore.clear()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to await the completion of this clear?

Comment on lines 39 to 40
private isProcessingPendingEvents: boolean = false
private pendingEventsPromise: Promise<void> | null = null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need isProcessingPendingEvents? Can we say if pendingEventsPromise is not null, then it is processing pending events?

this.pendingEventsPromise = new Promise(async (resolvePendingEventPromise) => {
this.isProcessingPendingEvents = true
const formattedEvents: {[key: string]: any} = await this.pendingEventsStore.getEventsMap()
const eventKeys = Object.keys(formattedEvents)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest: use utils.objectEntries for iteration here rather than for loop.

if (this.isProcessingPendingEvents && this.pendingEventsPromise) {
return this.pendingEventsPromise
}
this.pendingEventsPromise = new Promise(async (resolvePendingEventPromise) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's hard to follow what's going on with two nested Promise constructors, event dispatcher callbacks, the loop continue, etc. I would try refactoring this to use async/await as much as possible. To deal with the callback-based event dispatcher, create a helper async function that hides the complexity away from the main logic of processPendingEvents.

const eventKeys = Object.keys(formattedEvents)
for (let i = 0; i < eventKeys.length; i++) {
const eventKey = eventKeys[i]
if (this.eventsInProgress[eventKey]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this check? How does the same event get dispatched twice?

@zashraf1985 zashraf1985 removed their assignment Jul 13, 2020
Copy link
Contributor

@mjc1283 mjc1283 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I added a few suggestions which you can consider (non-blocking). Please have @jaeopt approve also.

/**
* React Native Events Processor with Caching support for events when app is offline.
*/
export abstract class LogTierV1EventProcessor implements EventProcessor {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we name it ReactNativeEventProcessor? Or something with React Native in the name to differentiate from the standard one.


process(event: ProcessableEvent): void {
// Adding events to buffer store. If app closes before dispatch, we can reprocess next time the app initializes
this.eventBufferStore.set(generateUUID(), event)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is awkward that we have to generate the UUID to use as a key here, but we don't retrieve it using this key, we just get all of them by calling getEventsList. Consider changing the interface so you can set/append without passing a key.

Copy link
Contributor

@jaeopt jaeopt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about the implications, but I see a few places of out-of-order event dispatch. Check it out.
A couple of other suggestions too.

await this.synchronizer.getLock()
const eventsMap: {[key: string]: T} = await this.cache.get(this.storeKey) || {}
this.synchronizer.releaseLock()
return objectValues(eventsMap)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this list guarantee FIFO ordering from map?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Looping through map always iterates in the order of insertion.


async drainQueue(buffer: ProcessableEvent[]): Promise<void> {
// Retry pending failed events while draining queue
await this.processPendingEvents()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about the implication of this out-of-order dispatching, but if "processPendingEvents" fail to flush all pending events, the new dispatch below (line 147) can be fired before previous buffered events. Check it out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed. skipping subsequent events on failure now.

Comment on lines 167 to 169
for (const [eventKey, event] of eventEntries) {
await this.dispatchEvent(eventKey, event)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another case of out-of-order delivery. If some events failed with errors, they stay in the buffer for retry later (out-of-order) since other events buffered later can be dispatched ok.

constructor({
dispatcher,
flushInterval = 30000,
batchSize = 3000,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check out this huge batch size default value. I understand we have max batched event size. 3000 events will be batched to be > 3MB. Not sure if this fits in the restrictions.
I think default batch size can be much smaller like 10-100.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I actually borrowed the value from the existing event processor. @mjc1283 ! Do you suggest to change it to something else.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually @jaeopt you are correct. this particular default never gets used by optimizely SDK, the sdk package already sets default of 10 if the user does not provide a value. This default will only take effect if someone uses event processor package directly. I will change it to 10 to make it consistent with SDK default.

@zashraf1985 zashraf1985 removed their assignment Jul 16, 2020
Copy link
Contributor

@jaeopt jaeopt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@mjc1283 mjc1283 merged commit d2208ff into master Jul 21, 2020
@mjc1283 mjc1283 deleted the zeeshan/rn-event-processor-proto branch July 21, 2020 16:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants