move sanity check to server and automatically force full sync

- server will log mismatches so we don't require people to post on the forums
  anymore or manually force a full sync
- if we find problems like missing notes, report that in the sanity check so
  the server can keep track of problems
- when the server detects a conflict it can immediately abort the sync, so a
  subsequent sync will no longer report a conflict
This commit is contained in:
Damien Elmes 2013-01-18 09:11:38 +09:00
parent 634b28388c
commit 8d66578d43
2 changed files with 35 additions and 18 deletions

View file

@ -133,13 +133,16 @@ class Syncer(object):
self.server.applyChunk(chunk=chunk) self.server.applyChunk(chunk=chunk)
if chunk['done']: if chunk['done']:
break break
# step 5: sanity check during beta testing # step 5: sanity check
runHook("sync", "sanity") runHook("sync", "sanity")
c = self.sanityCheck() c = self.sanityCheck()
s = self.server.sanityCheck() ret = self.server.sanityCheck2(client=c)
if c != s: if ret['status'] != "ok":
raise Exception("""\ # roll back and force full sync
Sanity check failed. Please copy and paste the text below:\n%s\n%s""" % (c, s)) self.col.rollback()
self.col.modSchema()
self.col.save()
raise Exception("sanity check failed")
# finalize # finalize
runHook("sync", "finalize") runHook("sync", "finalize")
mod = self.server.finish() mod = self.server.finish()
@ -179,19 +182,22 @@ Sanity check failed. Please copy and paste the text below:\n%s\n%s""" % (c, s))
self.prepareToChunk() self.prepareToChunk()
def sanityCheck(self): def sanityCheck(self):
# some basic checks to ensure the sync went ok. this is slow, so will if self.col.db.scalar("""
# be removed before official release select count() from cards where nid not in (select id from notes)"""):
assert not self.col.db.scalar(""" return "missing notes"
select count() from cards where nid not in (select id from notes)""") if self.col.db.scalar("""
assert not self.col.db.scalar(""" select count() from notes where id not in (select distinct nid from cards)"""):
select count() from notes where id not in (select distinct nid from cards)""") return "missing cards"
for t in "cards", "notes", "revlog", "graves": for t in "cards", "notes", "revlog", "graves":
assert not self.col.db.scalar( if self.col.db.scalar(
"select count() from %s where usn = -1" % t) "select count() from %s where usn = -1" % t):
return "%t had usn = -1" % t
for g in self.col.decks.all(): for g in self.col.decks.all():
assert g['usn'] != -1 if g['usn'] == -1:
return "deck had usn = -1"
for t, usn in self.col.tags.allItems(): for t, usn in self.col.tags.allItems():
assert usn != -1 if usn == -1:
return "tag had usn = -1"
found = False found = False
for m in self.col.models.all(): for m in self.col.models.all():
if self.col.server: if self.col.server:
@ -200,7 +206,8 @@ select count() from notes where id not in (select distinct nid from cards)""")
m['usn'] = 0 m['usn'] = 0
found = True found = True
else: else:
assert m['usn'] != -1 if m['usn'] == -1:
return "model had usn = -1"
if found: if found:
self.col.models.save() self.col.models.save()
self.col.sched.reset() self.col.sched.reset()
@ -218,6 +225,12 @@ select count() from notes where id not in (select distinct nid from cards)""")
len(self.col.decks.allConf()), len(self.col.decks.allConf()),
] ]
def sanityCheck2(self, client):
server = self.sanityCheck()
if client != server:
return dict(status="bad", c=client, s=server)
return dict(status="ok")
def usnLim(self): def usnLim(self):
if self.col.server: if self.col.server:
return "usn >= %d" % self.minUsn return "usn >= %d" % self.minUsn
@ -580,8 +593,8 @@ class RemoteServer(HttpSyncer):
def applyChunk(self, **kw): def applyChunk(self, **kw):
return self._run("applyChunk", kw) return self._run("applyChunk", kw)
def sanityCheck(self, **kw): def sanityCheck2(self, **kw):
return self._run("sanityCheck", kw) return self._run("sanityCheck2", kw)
def finish(self, **kw): def finish(self, **kw):
return self._run("finish", kw) return self._run("finish", kw)

View file

@ -134,6 +134,10 @@ AnkiWeb is too busy at the moment. Please try again in a few minutes.""")
"Antivirus or firewall software is preventing Anki from connecting to the internet.") "Antivirus or firewall software is preventing Anki from connecting to the internet.")
elif "407" in err: elif "407" in err:
return _("Proxy authentication required.") return _("Proxy authentication required.")
elif "sanity check failed" in err:
return _("After syncing, the collection was in an inconsistent \
state. To fix this problem, Anki will force a full sync. Please sync again, and \
choose which side you would like to keep.")
return err return err
def _getUserPass(self): def _getUserPass(self):