You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
217 lines
5.7 KiB
217 lines
5.7 KiB
// Copyright (c) 2022 Proton AG |
|
// |
|
// This file is part of Proton Mail Bridge. |
|
// |
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify |
|
// it under the terms of the GNU General Public License as published by |
|
// the Free Software Foundation, either version 3 of the License, or |
|
// (at your option) any later version. |
|
// |
|
// Proton Mail Bridge is distributed in the hope that it will be useful, |
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
// GNU General Public License for more details. |
|
// |
|
// You should have received a copy of the GNU General Public License |
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. |
|
|
|
package store |
|
|
|
import ( |
|
"sync" |
|
"time" |
|
|
|
"github.com/pkg/errors" |
|
) |
|
|
|
type syncState struct { |
|
lock *sync.RWMutex |
|
store storeSynchronizer |
|
|
|
// finishTime is the time, when the sync was finished for the last time. |
|
// When it's zero, it was never finished or the sync is ongoing. |
|
finishTime int64 |
|
|
|
// idRanges are ID ranges which are used to split work in several workers. |
|
// On the beginning of the sync it will find split IDs which are used to |
|
// create this ranges. If we have 10000 messages and five workers, it will |
|
// find IDs around 2000, 4000, 6000 and 8000 and then first worker will |
|
// sync IDs 0-2000, second 2000-4000 and so on. |
|
idRanges []*syncIDRange |
|
|
|
// idsToBeDeletedMap is map with keys as message IDs. On the beginning |
|
// of the sync, it will load all message IDs in database. During the sync, |
|
// it will delete all messages from the map which were sycned. The rest |
|
// at the end of the sync will be removed as those messages were not synced |
|
// again. We do that because we don't want to remove everything on the |
|
// beginning of the sync to keep client synced. |
|
idsToBeDeletedMap map[string]bool |
|
} |
|
|
|
func newSyncState(store storeSynchronizer, finishTime int64, idRanges []*syncIDRange, idsToBeDeleted []string) *syncState { |
|
idsToBeDeletedMap := map[string]bool{} |
|
for _, id := range idsToBeDeleted { |
|
idsToBeDeletedMap[id] = true |
|
} |
|
|
|
syncState := &syncState{ |
|
lock: &sync.RWMutex{}, |
|
store: store, |
|
|
|
finishTime: finishTime, |
|
idRanges: idRanges, |
|
idsToBeDeletedMap: idsToBeDeletedMap, |
|
} |
|
|
|
for _, idRange := range idRanges { |
|
idRange.syncState = syncState |
|
} |
|
|
|
return syncState |
|
} |
|
|
|
func (s *syncState) save() { |
|
s.lock.Lock() |
|
defer s.lock.Unlock() |
|
|
|
s.store.saveSyncState(s.finishTime, s.idRanges, s.getIDsToBeDeleted()) |
|
} |
|
|
|
// isIncomplete returns whether the sync is in progress (no matter whether |
|
// the sync is running or just not finished by info from database). |
|
func (s *syncState) isIncomplete() bool { |
|
s.lock.Lock() |
|
defer s.lock.Unlock() |
|
|
|
return s.finishTime == 0 && len(s.idRanges) != 0 |
|
} |
|
|
|
// isFinished returns whether the sync was finished. |
|
func (s *syncState) isFinished() bool { |
|
s.lock.Lock() |
|
defer s.lock.Unlock() |
|
|
|
return s.finishTime != 0 |
|
} |
|
|
|
// clearFinishTime sets finish time to zero. |
|
func (s *syncState) clearFinishTime() { |
|
s.lock.Lock() |
|
defer s.save() |
|
defer s.lock.Unlock() |
|
|
|
s.finishTime = 0 |
|
} |
|
|
|
// setFinishTime sets finish time to current time. |
|
func (s *syncState) setFinishTime() { |
|
s.lock.Lock() |
|
defer s.save() |
|
defer s.lock.Unlock() |
|
|
|
s.finishTime = time.Now().UnixNano() |
|
} |
|
|
|
// initIDRanges inits the main full range. Then each range is added |
|
// by `addIDRange`. |
|
func (s *syncState) initIDRanges() { |
|
s.lock.Lock() |
|
defer s.lock.Unlock() |
|
|
|
s.idRanges = []*syncIDRange{{ |
|
syncState: s, |
|
StartID: "", |
|
StopID: "", |
|
}} |
|
} |
|
|
|
// addIDRange sets `splitID` as stopID for last range and adds new one |
|
// starting with `splitID`. |
|
func (s *syncState) addIDRange(splitID string) { |
|
s.lock.Lock() |
|
defer s.lock.Unlock() |
|
|
|
lastGroup := s.idRanges[len(s.idRanges)-1] |
|
lastGroup.StopID = splitID |
|
|
|
s.idRanges = append(s.idRanges, &syncIDRange{ |
|
syncState: s, |
|
StartID: splitID, |
|
StopID: "", |
|
}) |
|
} |
|
|
|
// loadMessageIDsToBeDeleted loads all message IDs from database |
|
// and by default all IDs are meant for deletion. During sync for |
|
// each ID `doNotDeleteMessageID` has to be called to remove that |
|
// message from being deleted by `deleteMessagesToBeDeleted`. |
|
func (s *syncState) loadMessageIDsToBeDeleted() error { |
|
idsToBeDeletedMap := make(map[string]bool) |
|
ids, err := s.store.getAllMessageIDs() |
|
if err != nil { |
|
return err |
|
} |
|
for _, id := range ids { |
|
idsToBeDeletedMap[id] = true |
|
} |
|
|
|
s.lock.Lock() |
|
defer s.save() |
|
defer s.lock.Unlock() |
|
|
|
s.idsToBeDeletedMap = idsToBeDeletedMap |
|
return nil |
|
} |
|
|
|
func (s *syncState) doNotDeleteMessageID(id string) { |
|
s.lock.Lock() |
|
defer s.lock.Unlock() |
|
|
|
delete(s.idsToBeDeletedMap, id) |
|
} |
|
|
|
func (s *syncState) deleteMessagesToBeDeleted() error { |
|
s.lock.Lock() |
|
defer s.lock.Unlock() |
|
|
|
idsToBeDeleted := s.getIDsToBeDeleted() |
|
log.Infof("Deleting %v messages after sync", len(idsToBeDeleted)) |
|
if err := s.store.deleteMessagesEvent(idsToBeDeleted); err != nil { |
|
return errors.Wrap(err, "failed to delete messages") |
|
} |
|
return nil |
|
} |
|
|
|
// getIDsToBeDeleted is helper to convert internal map for easier |
|
// manipulation to array. |
|
func (s *syncState) getIDsToBeDeleted() []string { |
|
keys := []string{} |
|
for key := range s.idsToBeDeletedMap { |
|
keys = append(keys, key) |
|
} |
|
return keys |
|
} |
|
|
|
// syncIDRange holds range which IDs need to be synced. |
|
type syncIDRange struct { |
|
syncState *syncState |
|
StartID string |
|
StopID string |
|
} |
|
|
|
func (r *syncIDRange) setStartID(startID string) { |
|
r.StartID = startID |
|
r.syncState.save() |
|
} |
|
|
|
func (r *syncIDRange) setStopID(stopID string) { |
|
r.StopID = stopID |
|
r.syncState.save() |
|
} |
|
|
|
// isFinished returns syncIDRange is finished when StartID and StopID |
|
// are the same. But it cannot be full range, full range cannot be |
|
// determined in other way than asking API. |
|
func (r *syncIDRange) isFinished() bool { |
|
return r.StartID == r.StopID && r.StartID != "" |
|
}
|
|
|