Skip to content
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

feat(datastore): Dispatch outboxStatus, subscriptionsEstablished, syncQueriesStarted events #721

Merged
merged 24 commits into from
Sep 7, 2020

Conversation

ruiguoamz
Copy link
Contributor

@ruiguoamz ruiguoamz commented Aug 17, 2020

Issue #, if available:

The rest of 6 events will be implemented in other PRs.

  • Network Status
  • modelSynced
  • syncQueriesReady
  • ready
  • outboxMutationEnqueued
  • outboxMutationProcessed

Description of changes:

Implemented three of nine hub events

  • outboxStatus
  • syncQueriesStarted
  • subscriptionEstablished

outboxStatus

{"isEmpty": true/false}

public struct {
    let isEmpty: Bool
}

subscriptionsEstablished

N/A

When all subscriptions have been established, 3 per model, for every model, then emit a single subscriptionsEstablished event

syncQueriesStarted

{"models":["Task","Note"]}

struct SyncQueriesStartedEvent {
    let models: [String]
}

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@ruiguoamz ruiguoamz added the datastore Issues related to the DataStore category label Aug 17, 2020
@ruiguoamz ruiguoamz marked this pull request as ready for review August 25, 2020 18:37
Copy link
Contributor

@lawmicha lawmicha left a comment

Choose a reason for hiding this comment

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

review for syncQueriesReady

Copy link
Contributor

@lawmicha lawmicha left a comment

Choose a reason for hiding this comment

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

review subscriptionsEstablished

Copy link
Contributor

@lawmicha lawmicha left a comment

Choose a reason for hiding this comment

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

partial review for outboxStatus

Copy link
Contributor

@lawmicha lawmicha left a comment

Choose a reason for hiding this comment

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

continued review for outboxStatus

@lawmicha
Copy link
Contributor

lawmicha commented Sep 3, 2020

can update title to be more explicit on the hub events being added?
[DataStore] Triggering hub events -> feat(datastore): Dispatch outboxStatus, subscriptionsEstablished, syncQueriesStarted events

@ruiguoamz ruiguoamz changed the title [DataStore] Triggering hub events feat(datastore): Dispatch outboxStatus, subscriptionsEstablished, syncQueriesStarted events Sep 3, 2020
@lawmicha
Copy link
Contributor

lawmicha commented Sep 3, 2020

One approach i'm thinking is in OutgoingMutationQueue, there is a state that is starting and a responder method that is start(api:mutationEventPublisher:). you can split this into two responder methods

when in .starting state, respond to it with checkStatus() responder method. In checkStatus, you can have the logic

func checkStatus() {
   // use storageAdapter and query for MutationEvents
   // dispatchOutBoxStatusEvent(isEmpty: events.isEmpty)
   stateMachine.notify(action: .dispatchedStatus)
}

Then (.starting, .dispatchedStatus) can resolve to the state .subscribing and triggers subscribe(api:mutationEvent)

/// note here that the old `start()` was replaced with this new `subscribe()` responder method, but content the same
private func subscribe(api: APICategoryGraphQLBehavior,
                       mutationEventPublisher: MutationEventPublisher) {
        log.verbose(#function)
        self.api = api
        operationQueue.isSuspended = false

        // State machine notification to ".receivedSubscription" will be handled in `receive(subscription:)`
        mutationEventPublisher.publisher.subscribe(self)
    }

This approach goes straight to introducing a new state and responder method, however you can try it out by putting checkStatus() as a call inside the existing start() logic, and you may find that we don't gain that much from introducing a new state in the state machine. the important part here is to get the actual number of mutation events to be processed at DataStore start, is it from storage adapter? is there a way do some sort of peek operation on the mutationEventPublisher? Once you have the actual number of mutation events to be processed, then you can emit the outbox status event with an accurate isEmpty field

@lawmicha
Copy link
Contributor

lawmicha commented Sep 3, 2020

can you add integration test that covers recieving the new events you are emitting in this PR? for outboxStatus event, since it's emitted one or more times, you can just make sure the expectation is fulfilled without failing on overfulfillment. the other two (subscriptionsEstblished and synQueriesStarted) it should probably be just a single count each

@lawmicha
Copy link
Contributor

lawmicha commented Sep 3, 2020

Are there any unit tests that you can improve or add?

  • For example, InitialSyncOrchestratorTests.testInvokesCompletionCallback can probably be improved by adding a hub listener for the event that you are emitted and asserting that the model names match the expected syncable models

  • OutgoingMutationQueueTests

  • OutgoingMutationQueueMockStateTest

  • RemoteSyncEngineTests, RemoteSyncEngineTests.testRemoteSyncEngineHappyPath, etc

Copy link
Contributor

@lawmicha lawmicha left a comment

Choose a reason for hiding this comment

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

looks good to me, question about naming for outboxStatus. Should we stick with JS/Android or if we should stick to something more common in iOS like outgoingMutationStatus?

@palpatim
Copy link
Member

palpatim commented Sep 4, 2020

looks good to me, question about naming for outboxStatus. Should we stick with JS/Android or if we should stick to something more common in iOS like outgoingMutationStatus?

@lawmicha We need to keep the names the same for platform consistency. We can revisit naming, event dispatch rules, payloads, etc in the future.

@@ -23,4 +23,15 @@ public extension HubPayload.EventName.DataStore {
/// Dispatched when DataStore receives a sync response from the remote API via the API category. The Hub Payload
/// will be a `MutationEvent` instance that caused the conditional save failed.
static let conditionalSaveFailed = "DataStore.conditionalSaveFailed"

/// Dispatched on DataStore start and also every time a local mutation is enqueued and processed in the outbox
Copy link
Member

Choose a reason for hiding this comment

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

Clarify wording: "Dispatched when: 1) the DataStore starts; 2) each time a local mutation is enqueued into the outbox; 3) each time a local mutation is finished processing."

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated

/// HubPayload `OutboxStatusEvent` contains a boolean value `isEmpty` to notify if there are mutations in the outbox
static let outboxStatus = "DataStore.outboxStatus"

/// Dispatched when all of the subscriptions to syncable models have been established
Copy link
Member

Choose a reason for hiding this comment

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

"Dispatched when DataStore has finished establishing its subscriptions to all syncable models"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated

/// Dispatched when all of the subscriptions to syncable models have been established
static let subscriptionsEstablished = "DataStore.subscriptionEstablished"

/// Dispatched when DataStore is about to start sync queries
Copy link
Member

Choose a reason for hiding this comment

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

"Dispatched when DataStore starts establishing its subscriptions to all syncable models"

Copy link
Contributor Author

@ruiguoamz ruiguoamz Sep 4, 2020

Choose a reason for hiding this comment

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

For syncQueriesStarted, it is not Dispatched when DataStore starts establishing its subscriptions to all syncable models but is Dispatched when DataStore starts to sync Queries

// SPDX-License-Identifier: Apache-2.0
//

import Foundation
Copy link
Member

Choose a reason for hiding this comment

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

Unnecessary import

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed


/// Used as HubPayload for the `OutboxStatus`
public struct OutboxStatusEvent {
/// status of outbox: empty or not
Copy link
Member

Choose a reason for hiding this comment

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

"true if there are no events in the outbox at the time the event was dispatched

// SPDX-License-Identifier: Apache-2.0
//

import Foundation
Copy link
Member

Choose a reason for hiding this comment

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

Unnecessary import

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed


/// Used as HubPayload for the `SyncQueriesStarted`
public struct SyncQueriesStartedEvent {
/// list of model names
Copy link
Member

Choose a reason for hiding this comment

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

"A list of all model names for which DataStore has started establishing subscriptions"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated

case .success(let events):
self.dispatchOutboxStatusEvent(isEmpty: events.isEmpty)
case .failure(let error):
log.error("Error quering mutation events: \(error)")
Copy link
Member

Choose a reason for hiding this comment

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

s/quering/querying/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated

@@ -241,10 +245,36 @@ final class OutgoingMutationQueue: OutgoingMutationQueueBehavior {
self.log.verbose("mutationEvent deleted successfully")
}

self.dispatchOutboxStatusEvent(isEmpty: true)
Copy link
Member

Choose a reason for hiding this comment

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

How do you know the outbox is empty here? Shouldn't you be querying the mutation events from storage, or checking the operationCount of the outbox's operation queue?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, it makes sense.
Incorrectly, I thought because subscription.request(.max(1)) ensure the queue to have only one mutationEvent at a time so I can assert queue is empty when the event is finish processing. I miss the fact that events are stored inside the mutationEventDatabase.
Updated.

Copy link
Contributor

@lawmicha lawmicha Sep 4, 2020

Choose a reason for hiding this comment

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

As i understand it, we were emitting outboxStatus events to this logic (this is probably wrong then)

  • when the remote sync engine starts, check if there are mutation events from storage to determine isEmpty
  • when a mutationEvent is being enqueued, we get this from the mutationEventPublisher, one mutationEvent at a time. hardcode isEmpty to false since we know we are processing one
  • when the mutationEvent is done being processed, we emit a following outboxStatus event with isEmpty = true

example output
outBoxStatus - isEmpty = false - on start, check mutationEvent from storage, say there 5 mutation events
outboxStatus - isEmpty = false - queue mutationEvent 1
outboxStatus - isEmpty = true - finish processing mutationEvent 1
outboxStatus - isEmpty = false - queue mutationEvent 2
outboxStatus - isEmpty = true - finish processing mutationEvent 2
outboxStatus - isEmpty = false - queue mutationEvent 3
outboxStatus - isEmpty = true - finish processing mutationEvent 3
outboxStatus - isEmpty = false - queue mutationEvent 4
outboxStatus - isEmpty = true - finish processing mutationEvent 4
outboxStatus - isEmpty = false - queue mutationEvent 5
outboxStatus - isEmpty = true - finish processing mutationEvent 5

Copy link
Contributor

Choose a reason for hiding this comment

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

or is the expected behavior just a ping of the outbox status? for example
outBoxStatus - isEmpty = false - on start, check mutationEvent from storage, say there 5 mutation events
outboxStatus - isEmpty = false - queue mutationEvent 1
outboxStatus - isEmpty = false - finish processing mutationEvent 1
outboxStatus - isEmpty = false - queue mutationEvent 2
outboxStatus - isEmpty = false - finish processing mutationEvent 2
outboxStatus - isEmpty = false - queue mutationEvent 3
outboxStatus - isEmpty = false - finish processing mutationEvent 3
outboxStatus - isEmpty = false - queue mutationEvent 4
outboxStatus - isEmpty = false - finish processing mutationEvent 4
outboxStatus - isEmpty = false - queue mutationEvent 5
outboxStatus - isEmpty = true - finish processing mutationEvent 5

Choose a reason for hiding this comment

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

@lawmicha the second behavior is the expected one.

For reference, this is the JS implementation:

https://github.com/aws-amplify/amplify-js/blob/a7feacea4ed506340d250249d0b15286fe3ef5fa/packages/datastore/src/sync/index.ts#L358-L365

It checks the queue

@ruiguoamz ruiguoamz merged commit 64fcd53 into main Sep 7, 2020
@ruiguoamz ruiguoamz deleted the datastore/hub branch September 7, 2020 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
datastore Issues related to the DataStore category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants