diff --git a/xxmutils/restoreContacts.go b/xxmutils/restoreContacts.go index bf164b90465dffa6e3849e46ee53265d8b23ec29..c35bd141bf8782a89f199ba618b3bc59df1fbf03 100644 --- a/xxmutils/restoreContacts.go +++ b/xxmutils/restoreContacts.go @@ -12,6 +12,7 @@ import ( "errors" "fmt" "math" + "strings" "sync" "time" @@ -36,14 +37,50 @@ type RestoreContactsUpdater interface { RestoreContactsCallback(numFound, numRestored, total int, err string) } +// RestoreContactsReport is a gomobile friendly report structure +// for determining which IDs restored, which failed, and why. +type RestoreContactsReport struct { + restored []*id.ID + failed []*id.ID + errs []error +} + +// LenRestored returns the length of ID's restored. +func (r *RestoreContactsReport) LenRestored() int { + return len(r.restored) +} + +// LenFailed returns the length of the ID's failed. +func (r *RestoreContactsReport) LenFailed() int { + return len(r.failed) +} + +// GetRestoredAt returns the restored ID at index +func (r *RestoreContactsReport) GetRestoredAt(index int) []byte { + return r.restored[index].Bytes() +} + +// GetFailedAt returns the failed ID at index +func (r *RestoreContactsReport) GetFailedAt(index int) []byte { + return r.failed[index].Bytes() +} + +// GetErrorAt returns the error string at index +func (r *RestoreContactsReport) GetErrorAt(index int) string { + return r.errs[index].Error() +} + // RestoreContactsFromBackup takes as input the jason output of the // `NewClientFromBackup` function, unmarshals it into IDs, looks up // each ID in user discovery, and initiates a session reset request. // This function will not return until every id in the list has been sent a // request. It should be called again and again until it completes. +// xxDK users should not use this function. This function is used by +// the mobile phone apps and are not intended to be part of the xxDK. It +// should be treated as internal functions specific to the phone apps. func RestoreContactsFromBackup(backupPartnerIDs []byte, client *bindings.Client, udManager *bindings.UserDiscovery, - updatesCb RestoreContactsUpdater) error { + updatesCb RestoreContactsUpdater) (*RestoreContactsReport, error) { // Constants/control settings numRoutines := 8 @@ -59,15 +96,20 @@ func RestoreContactsFromBackup(backupPartnerIDs []byte, client *bindings.Client, // Unmarshal IDs and then check restore state var idList []*id.ID if err := json.Unmarshal(backupPartnerIDs, &idList); err != nil { - return err + return nil, err } - lookupIDs, resetContacts := checkRestoreState(idList, store) + lookupIDs, resetContacts, restored := checkRestoreState(idList, store) // State variables, how many we have looked up successfully // and how many we have already reset. totalCnt := len(idList) lookupCnt := len(resetContacts) resetCnt := totalCnt - len(resetContacts) - len(lookupIDs) + report := &RestoreContactsReport{ + restored: restored, + failed: make([]*id.ID, 0), + errs: make([]error, 0), + } // Before we start, report initial state updatesCb.RestoreContactsCallback(lookupCnt, resetCnt, totalCnt, "") @@ -81,6 +123,7 @@ func RestoreContactsFromBackup(backupPartnerIDs []byte, client *bindings.Client, foundCh := make(chan *contact.Contact, chanSize) resetContactCh := make(chan *contact.Contact, chanSize) restoredCh := make(chan *contact.Contact, chanSize) + failCh := make(chan failure, chanSize) // Start routines for processing lcWg := sync.WaitGroup{} @@ -88,8 +131,8 @@ func RestoreContactsFromBackup(backupPartnerIDs []byte, client *bindings.Client, rsWg := sync.WaitGroup{} rsWg.Add(numRoutines) for i := 0; i < numRoutines; i++ { - go LookupContacts(lookupCh, foundCh, udManager, lcWg) - go ResetSessions(resetContactCh, restoredCh, api, rsWg) + go LookupContacts(lookupCh, foundCh, failCh, udManager, lcWg) + go ResetSessions(resetContactCh, restoredCh, failCh, api, rsWg) } // Load channels based on previous state @@ -105,6 +148,18 @@ func RestoreContactsFromBackup(backupPartnerIDs []byte, client *bindings.Client, } }() + // Failure processing, done separately (in a single thread) + // because failures should not reset the timer + failWg := sync.WaitGroup{} + failWg.Add(1) + go func() { + defer failWg.Done() + for fail := range failCh { + report.failed = append(report.failed, fail.ID) + report.errs = append(report.errs, fail.Err) + } + }() + // Event Processing done := false var err error @@ -122,6 +177,7 @@ func RestoreContactsFromBackup(backupPartnerIDs []byte, client *bindings.Client, go func() { resetContactCh <- c }() case c := <-restoredCh: store.set(c, contactRestored) + report.restored = append(report.restored, c.ID) resetCnt += 1 } if resetCnt == totalCnt { @@ -134,52 +190,72 @@ func RestoreContactsFromBackup(backupPartnerIDs []byte, client *bindings.Client, // Cleanup close(lookupCh) close(resetContactCh) + close(failCh) // Now wait for subroutines to close before closing their output chans lcWg.Wait() close(foundCh) rsWg.Wait() close(restoredCh) + failWg.Wait() - return err + return report, err } // LookupContacts routine looks up contacts +// xxDK users should not use this function. This function is used by +// the mobile phone apps and are not intended to be part of the xxDK. It +// should be treated as internal functions specific to the phone apps. func LookupContacts(in chan *id.ID, out chan *contact.Contact, - udManager *bindings.UserDiscovery, wg sync.WaitGroup) { + failCh chan failure, udManager *bindings.UserDiscovery, + wg sync.WaitGroup) { defer wg.Done() // Start looking up contacts with user discovery and feed this // contacts channel. for lookupID := range in { c, err := LookupContact(lookupID, udManager) - if err != nil { - jww.WARN.Printf("could not lookup %s: %v", lookupID, - err) - // Retry later - in <- lookupID - } else { + if err == nil { out <- c + continue + } + // If an error, figure out if I should report or retry + errStr := err.Error() + if strings.Contains(errStr, "failed to lookup ID") { + failCh <- failure{ID: lookupID, Err: err} + continue } + jww.WARN.Printf("could not lookup %s: %v", lookupID, err) + // Retry later + in <- lookupID } } -func ResetSessions(in, out chan *contact.Contact, api api.Client, - wg sync.WaitGroup) { +// ResetSessions routine reads the in channel, sends a reset session +// request, then marks it done by sending to the out channel. +// xxDK users should not use this function. This function is used by +// the mobile phone apps and are not intended to be part of the xxDK. It +// should be treated as internal functions specific to the phone apps. +func ResetSessions(in, out chan *contact.Contact, failCh chan failure, + api api.Client, wg sync.WaitGroup) { defer wg.Done() me := api.GetUser().GetContact() msg := "Account reset from backup" for c := range in { _, err := api.ResetSession(*c, me, msg) - if err != nil { - jww.WARN.Printf("could not reset %s: %v", - c.ID, err) - in <- c - } else { + if err == nil { out <- c + continue } + // If an error, figure out if I should report or retry + // Note: Always retry here for now. + jww.WARN.Printf("could not reset %s: %v", c.ID, err) + in <- c } } // LookupContact lookups up a contact using the user discovery manager +// xxDK users should not use this function. This function is used by +// the mobile phone apps and are not intended to be part of the xxDK. It +// should be treated as internal functions specific to the phone apps. func LookupContact(userID *id.ID, udManager *bindings.UserDiscovery) ( *contact.Contact, error) { // This is a little wonky, but wait until we get called then @@ -227,6 +303,11 @@ const ( contactRestored ) +type failure struct { + ID *id.ID + Err error +} + //// // stateStore wraps a kv and stores contact state for the restoration // TODO: Right now, it uses 1 contact-per-key approach, but it might make sense @@ -272,9 +353,10 @@ func (s stateStore) get(id *id.ID) (restoreState, *contact.Contact, error) { // stateStore END func checkRestoreState(IDs []*id.ID, store stateStore) ([]*id.ID, - []*contact.Contact) { + []*contact.Contact, []*id.ID) { var idsToLookup []*id.ID var contactsToReset []*contact.Contact + var contactsRestored []*id.ID for i := range IDs { id := IDs[i] idState, user, err := store.get(id) @@ -289,8 +371,9 @@ func checkRestoreState(IDs []*id.ID, store stateStore) ([]*id.ID, idsToLookup = append(idsToLookup, id) case contactFound: contactsToReset = append(contactsToReset, user) + case contactRestored: + contactsRestored = append(contactsRestored, user.ID) } - // Restored state means we do nothing. } - return idsToLookup, contactsToReset + return idsToLookup, contactsToReset, contactsRestored }