diff --git a/src/wormhole/db-schemas/v1.sql b/src/wormhole/db-schemas/v1.sql index 0fa51c3..82bdfe9 100644 --- a/src/wormhole/db-schemas/v1.sql +++ b/src/wormhole/db-schemas/v1.sql @@ -18,3 +18,18 @@ CREATE TABLE `messages` `when` INTEGER ); CREATE INDEX `messages_idx` ON `messages` (`appid`, `channelid`); + +CREATE TABLE `usage` +( + `started` INTEGER, -- seconds since epoch, rounded to one day + `result` VARCHAR, -- happy, scary, lonely, errory, pruney + -- "happy": both sides close with mood=happy + -- "scary": any side closes with mood=scary (bad MAC, probably wrong pw) + -- "lonely": any side closes with mood=lonely (no response from 2nd side) + -- "errory": any side closes with mood=errory (other errors) + -- "pruney": channels which get pruned for inactivity + -- "crowded": three or more sides were involved + `total_time` INTEGER, -- seconds from start to closed, or None + `waiting_time` INTEGER -- seconds from start to 2nd side appearing, or None +); +CREATE INDEX `usage_idx` ON `usage` (`started`); diff --git a/src/wormhole/servers/relay_server.py b/src/wormhole/servers/relay_server.py index 4f6fe73..abea91c 100644 --- a/src/wormhole/servers/relay_server.py +++ b/src/wormhole/servers/relay_server.py @@ -274,9 +274,72 @@ class Channel: return True return False + def _store_summary(self, summary): + (started, result, total_time, waiting_time) = summary + self._db.execute("INSERT INTO `usage`" + " (`started`, `result`, `total_time`, `waiting_time`)" + " VALUES (?,?,?,?)", + (started, result, total_time, waiting_time)) + self._db.commit() + + def _summarize(self, messages, delete_time): + all_sides = set([m["side"] for m in messages]) + if len(all_sides) == 0: + log.msg("_summarize was given zero messages") # shouldn't happen + return + + started = min([m["when"] for m in messages]) + # 'total_time' is how long the channel was occupied. That ends now, + # both for channels that got pruned for inactivity, and for channels + # that got pruned because of two DEALLOCATE messages + total_time = delete_time - started + + if len(all_sides) == 1: + return (started, "lonely", total_time, None) + if len(all_sides) > 2: + # TODO: it'll be useful to have more detail here + return (started, "crowded", total_time, None) + + # exactly two sides were involved + A_side = sorted(messages, key=lambda m: m["when"])[0]["side"] + B_side = list(all_sides - set([A_side]))[0] + + # How long did the first side wait until the second side showed up? + first_A = min([m["when"] for m in messages if m["side"] == A_side]) + first_B = min([m["when"] for m in messages if m["side"] == B_side]) + waiting_time = first_B - first_A + + # now, were all sides closed? If not, this is "pruney" + A_deallocs = [m for m in messages + if m["phase"] == DEALLOCATE and m["side"] == A_side] + B_deallocs = [m for m in messages + if m["phase"] == DEALLOCATE and m["side"] == B_side] + if not A_deallocs or not B_deallocs: + return (started, "pruney", total_time, None) + + # ok, both sides closed. figure out the mood + A_mood = A_deallocs[0]["body"] + B_mood = B_deallocs[0]["body"] + mood = "errory" + if A_mood == u"happy" and B_mood == u"happy": + mood = "happy" + if A_mood == u"lonely" or B_mood == u"lonely": + mood = "lonely" + if A_mood == u"errory" or B_mood == u"errory": + mood = "errory" + if A_mood == u"scary" or B_mood == u"scary": + mood = "scary" + return (started, mood, total_time, waiting_time) + def delete_and_summarize(self): - # TODO: summarize usage, write into DB db = self._db + c = self._db.execute("SELECT * FROM `messages`" + " WHERE `appid`=? AND `channelid`=?" + " ORDER BY `when`", + (self._appid, self._channelid)) + messages = c.fetchall() + summary = self._summarize(messages, time.time()) + self._store_summary(summary) db.execute("DELETE FROM `messages`" " WHERE `appid`=? AND `channelid`=?", (self._appid, self._channelid)) diff --git a/src/wormhole/test/test_server.py b/src/wormhole/test/test_server.py index 7728152..fe8a6b2 100644 --- a/src/wormhole/test/test_server.py +++ b/src/wormhole/test/test_server.py @@ -8,6 +8,7 @@ from twisted.internet.threads import deferToThread from twisted.web.client import getPage, Agent, readBody from .. import __version__ from .common import ServerBase +from ..servers import relay_server from ..twisted.eventsource_twisted import EventSource class Reachable(ServerBase, unittest.TestCase): @@ -354,3 +355,61 @@ class OneEventAtATime: def disconnected(self, why): self.disconnected_d.callback((why,)) +class Summary(unittest.TestCase): + def test_summarize(self): + c = relay_server.Channel(None, None, None, None, None) + A = relay_server.ALLOCATE + D = relay_server.DEALLOCATE + + messages = [{"when": 1, "side": "a", "phase": A}] + self.failUnlessEqual(c._summarize(messages, 2), + (1, "lonely", 1, None)) + + messages = [{"when": 1, "side": "a", "phase": A}, + {"when": 2, "side": "a", "phase": D, "body": "lonely"}, + ] + self.failUnlessEqual(c._summarize(messages, 3), + (1, "lonely", 2, None)) + + messages = [{"when": 1, "side": "a", "phase": A}, + {"when": 2, "side": "b", "phase": A}, + {"when": 3, "side": "c", "phase": A}, + ] + self.failUnlessEqual(c._summarize(messages, 4), + (1, "crowded", 3, None)) + + base = [{"when": 1, "side": "a", "phase": A}, + {"when": 2, "side": "a", "phase": "pake", "body": "msg1"}, + {"when": 10, "side": "b", "phase": "pake", "body": "msg2"}, + {"when": 11, "side": "b", "phase": "data", "body": "msg3"}, + {"when": 20, "side": "a", "phase": "data", "body": "msg4"}, + ] + def make_moods(A_mood, B_mood): + return base + [ + {"when": 21, "side": "a", "phase": D, "body": A_mood}, + {"when": 30, "side": "b", "phase": D, "body": B_mood}, + ] + + self.failUnlessEqual(c._summarize(make_moods("happy", "happy"), 41), + (1, "happy", 40, 9)) + + self.failUnlessEqual(c._summarize(make_moods("scary", "happy"), 41), + (1, "scary", 40, 9)) + self.failUnlessEqual(c._summarize(make_moods("happy", "scary"), 41), + (1, "scary", 40, 9)) + + self.failUnlessEqual(c._summarize(make_moods("lonely", "happy"), 41), + (1, "lonely", 40, 9)) + self.failUnlessEqual(c._summarize(make_moods("happy", "lonely"), 41), + (1, "lonely", 40, 9)) + + self.failUnlessEqual(c._summarize(make_moods("errory", "happy"), 41), + (1, "errory", 40, 9)) + self.failUnlessEqual(c._summarize(make_moods("happy", "errory"), 41), + (1, "errory", 40, 9)) + + # scary trumps other moods + self.failUnlessEqual(c._summarize(make_moods("scary", "lonely"), 41), + (1, "scary", 40, 9)) + self.failUnlessEqual(c._summarize(make_moods("scary", "errory"), 41), + (1, "scary", 40, 9))