625 lines
26 KiB
Python
625 lines
26 KiB
Python
from __future__ import print_function, unicode_literals
|
|
import os, random, base64, collections
|
|
from collections import namedtuple
|
|
from twisted.python import log
|
|
from twisted.application import service
|
|
|
|
def generate_mailbox_id():
|
|
return base64.b32encode(os.urandom(8)).lower().strip(b"=").decode("ascii")
|
|
|
|
class CrowdedError(Exception):
|
|
pass
|
|
|
|
Usage = namedtuple("Usage", ["started", "waiting_time", "total_time", "result"])
|
|
TransitUsage = namedtuple("TransitUsage",
|
|
["started", "waiting_time", "total_time",
|
|
"total_bytes", "result"])
|
|
|
|
SidedMessage = namedtuple("SidedMessage", ["side", "phase", "body",
|
|
"server_rx", "msg_id"])
|
|
|
|
class Mailbox:
|
|
def __init__(self, app, db, app_id, mailbox_id):
|
|
self._app = app
|
|
self._db = db
|
|
self._app_id = app_id
|
|
self._mailbox_id = mailbox_id
|
|
self._listeners = {} # handle -> (send_f, stop_f)
|
|
# "handle" is a hashable object, for deregistration
|
|
# send_f() takes a JSONable object, stop_f() has no args
|
|
|
|
def open(self, side, when):
|
|
# requires caller to db.commit()
|
|
assert isinstance(side, type("")), type(side)
|
|
db = self._db
|
|
|
|
already = db.execute("SELECT * FROM `mailbox_sides`"
|
|
" WHERE `mailbox_id`=? AND `side`=?",
|
|
(self._mailbox_id, side)).fetchone()
|
|
if not already:
|
|
db.execute("INSERT INTO `mailbox_sides`"
|
|
" (`mailbox_id`, `opened`, `side`, `added`)"
|
|
" VALUES(?,?,?,?)",
|
|
(self._mailbox_id, True, side, when))
|
|
self._touch(when)
|
|
db.commit() # XXX: reconcile the need for this with the comment above
|
|
|
|
def _touch(self, when):
|
|
self._db.execute("UPDATE `mailboxes` SET `updated`=? WHERE `id`=?",
|
|
(when, self._mailbox_id))
|
|
|
|
def get_messages(self):
|
|
messages = []
|
|
db = self._db
|
|
for row in db.execute("SELECT * FROM `messages`"
|
|
" WHERE `app_id`=? AND `mailbox_id`=?"
|
|
" ORDER BY `server_rx` ASC",
|
|
(self._app_id, self._mailbox_id)).fetchall():
|
|
sm = SidedMessage(side=row["side"], phase=row["phase"],
|
|
body=row["body"], server_rx=row["server_rx"],
|
|
msg_id=row["msg_id"])
|
|
messages.append(sm)
|
|
return messages
|
|
|
|
def add_listener(self, handle, send_f, stop_f):
|
|
#log.msg("add_listener", self._mailbox_id, handle)
|
|
self._listeners[handle] = (send_f, stop_f)
|
|
#log.msg(" added", len(self._listeners))
|
|
return self.get_messages()
|
|
|
|
def remove_listener(self, handle):
|
|
#log.msg("remove_listener", self._mailbox_id, handle)
|
|
self._listeners.pop(handle, None)
|
|
#log.msg(" removed", len(self._listeners))
|
|
|
|
def has_listeners(self):
|
|
return bool(self._listeners)
|
|
|
|
def broadcast_message(self, sm):
|
|
for (send_f, stop_f) in self._listeners.values():
|
|
send_f(sm)
|
|
|
|
def _add_message(self, sm):
|
|
self._db.execute("INSERT INTO `messages`"
|
|
" (`app_id`, `mailbox_id`, `side`, `phase`, `body`,"
|
|
" `server_rx`, `msg_id`)"
|
|
" VALUES (?,?,?,?,?, ?,?)",
|
|
(self._app_id, self._mailbox_id, sm.side,
|
|
sm.phase, sm.body, sm.server_rx, sm.msg_id))
|
|
self._touch(sm.server_rx)
|
|
self._db.commit()
|
|
|
|
def add_message(self, sm):
|
|
assert isinstance(sm, SidedMessage)
|
|
self._add_message(sm)
|
|
self.broadcast_message(sm)
|
|
|
|
def close(self, side, mood, when):
|
|
assert isinstance(side, type("")), type(side)
|
|
db = self._db
|
|
row = db.execute("SELECT * FROM `mailboxes`"
|
|
" WHERE `app_id`=? AND `id`=?",
|
|
(self._app_id, self._mailbox_id)).fetchone()
|
|
if not row:
|
|
return
|
|
for_nameplate = row["for_nameplate"]
|
|
|
|
row = db.execute("SELECT * FROM `mailbox_sides`"
|
|
" WHERE `mailbox_id`=? AND `side`=?",
|
|
(self._mailbox_id, side)).fetchone()
|
|
if not row:
|
|
return
|
|
db.execute("UPDATE `mailbox_sides` SET `opened`=?, `mood`=?"
|
|
" WHERE `mailbox_id`=? AND `side`=?",
|
|
(False, mood, self._mailbox_id, side))
|
|
db.commit()
|
|
|
|
# are any sides still open?
|
|
side_rows = db.execute("SELECT * FROM `mailbox_sides`"
|
|
" WHERE `mailbox_id`=?",
|
|
(self._mailbox_id,)).fetchall()
|
|
if any([sr["opened"] for sr in side_rows]):
|
|
return
|
|
|
|
# nope. delete and summarize
|
|
db.execute("DELETE FROM `messages` WHERE `mailbox_id`=?",
|
|
(self._mailbox_id,))
|
|
db.execute("DELETE FROM `mailbox_sides` WHERE `mailbox_id`=?",
|
|
(self._mailbox_id,))
|
|
db.execute("DELETE FROM `mailboxes` WHERE `id`=?", (self._mailbox_id,))
|
|
self._app._summarize_mailbox_and_store(for_nameplate, side_rows,
|
|
when, pruned=False)
|
|
db.commit()
|
|
# Shut down any listeners, just in case they're still lingering
|
|
# around.
|
|
for (send_f, stop_f) in self._listeners.values():
|
|
stop_f()
|
|
self._listeners = {}
|
|
self._app.free_mailbox(self._mailbox_id)
|
|
|
|
def _shutdown(self):
|
|
# used at test shutdown to accelerate client disconnects
|
|
for (send_f, stop_f) in self._listeners.values():
|
|
stop_f()
|
|
self._listeners = {}
|
|
|
|
class AppNamespace:
|
|
def __init__(self, db, blur_usage, log_requests, app_id):
|
|
self._db = db
|
|
self._blur_usage = blur_usage
|
|
self._log_requests = log_requests
|
|
self._app_id = app_id
|
|
self._mailboxes = {}
|
|
self._nameplate_counts = collections.defaultdict(int)
|
|
self._mailbox_counts = collections.defaultdict(int)
|
|
|
|
def get_nameplate_ids(self):
|
|
db = self._db
|
|
# TODO: filter this to numeric ids?
|
|
c = db.execute("SELECT DISTINCT `name` FROM `nameplates`"
|
|
" WHERE `app_id`=?", (self._app_id,))
|
|
return set([row["name"] for row in c.fetchall()])
|
|
|
|
def _find_available_nameplate_id(self):
|
|
claimed = self.get_nameplate_ids()
|
|
for size in range(1,4): # stick to 1-999 for now
|
|
available = set()
|
|
for id_int in range(10**(size-1), 10**size):
|
|
id = "%d" % id_int
|
|
if id not in claimed:
|
|
available.add(id)
|
|
if available:
|
|
return random.choice(list(available))
|
|
# ouch, 999 currently claimed. Try random ones for a while.
|
|
for tries in range(1000):
|
|
id_int = random.randrange(1000, 1000*1000)
|
|
id = "%d" % id_int
|
|
if id not in claimed:
|
|
return id
|
|
raise ValueError("unable to find a free nameplate-id")
|
|
|
|
def allocate_nameplate(self, side, when):
|
|
nameplate_id = self._find_available_nameplate_id()
|
|
mailbox_id = self.claim_nameplate(nameplate_id, side, when)
|
|
del mailbox_id # ignored, they'll learn it from claim()
|
|
return nameplate_id
|
|
|
|
def claim_nameplate(self, name, side, when):
|
|
# when we're done:
|
|
# * there will be one row for the nameplate
|
|
# * there will be one 'side' attached to it, with claimed=True
|
|
# * a mailbox id and mailbox row will be created
|
|
# * a mailbox 'side' will be attached, with opened=True
|
|
assert isinstance(name, type("")), type(name)
|
|
assert isinstance(side, type("")), type(side)
|
|
db = self._db
|
|
row = db.execute("SELECT * FROM `nameplates`"
|
|
" WHERE `app_id`=? AND `name`=?",
|
|
(self._app_id, name)).fetchone()
|
|
if not row:
|
|
if self._log_requests:
|
|
log.msg("creating nameplate#%s for app_id %s" %
|
|
(name, self._app_id))
|
|
mailbox_id = generate_mailbox_id()
|
|
self._add_mailbox(mailbox_id, True, side, when) # ensure row exists
|
|
sql = ("INSERT INTO `nameplates`"
|
|
" (`app_id`, `name`, `mailbox_id`)"
|
|
" VALUES(?,?,?)")
|
|
npid = db.execute(sql, (self._app_id, name, mailbox_id)
|
|
).lastrowid
|
|
else:
|
|
npid = row["id"]
|
|
mailbox_id = row["mailbox_id"]
|
|
|
|
row = db.execute("SELECT * FROM `nameplate_sides`"
|
|
" WHERE `nameplates_id`=? AND `side`=?",
|
|
(npid, side)).fetchone()
|
|
if not row:
|
|
db.execute("INSERT INTO `nameplate_sides`"
|
|
" (`nameplates_id`, `claimed`, `side`, `added`)"
|
|
" VALUES(?,?,?,?)",
|
|
(npid, True, side, when))
|
|
db.commit()
|
|
|
|
self.open_mailbox(mailbox_id, side, when) # may raise CrowdedError
|
|
rows = db.execute("SELECT * FROM `nameplate_sides`"
|
|
" WHERE `nameplates_id`=?", (npid,)).fetchall()
|
|
if len(rows) > 2:
|
|
# this line will probably never get hit: any crowding is noticed
|
|
# on mailbox_sides first, inside open_mailbox()
|
|
raise CrowdedError("too many sides have claimed this nameplate")
|
|
return mailbox_id
|
|
|
|
def release_nameplate(self, name, side, when):
|
|
# when we're done:
|
|
# * the 'claimed' flag will be cleared on the nameplate_sides row
|
|
# * if the nameplate is now unused (no claimed sides):
|
|
# * mailbox.nameplate_closed will be populated
|
|
# * the nameplate row will be removed
|
|
# * the nameplate sides will be removed
|
|
assert isinstance(name, type("")), type(name)
|
|
assert isinstance(side, type("")), type(side)
|
|
db = self._db
|
|
np_row = db.execute("SELECT * FROM `nameplates`"
|
|
" WHERE `app_id`=? AND `name`=?",
|
|
(self._app_id, name)).fetchone()
|
|
if not np_row:
|
|
return
|
|
npid = np_row["id"]
|
|
row = db.execute("SELECT * FROM `nameplate_sides`"
|
|
" WHERE `nameplates_id`=? AND `side`=?",
|
|
(npid, side)).fetchone()
|
|
if not row:
|
|
return
|
|
db.execute("UPDATE `nameplate_sides` SET `claimed`=?"
|
|
" WHERE `nameplates_id`=? AND `side`=?",
|
|
(False, npid, side))
|
|
db.commit()
|
|
|
|
# now, are there any remaining claims?
|
|
side_rows = db.execute("SELECT * FROM `nameplate_sides`"
|
|
" WHERE `nameplates_id`=?",
|
|
(npid,)).fetchall()
|
|
claims = [1 for sr in side_rows if sr["claimed"]]
|
|
if claims:
|
|
return
|
|
# delete and summarize
|
|
db.execute("DELETE FROM `nameplate_sides` WHERE `nameplates_id`=?",
|
|
(npid,))
|
|
db.execute("DELETE FROM `nameplates` WHERE `id`=?", (npid,))
|
|
self._summarize_nameplate_and_store(side_rows, when, pruned=False)
|
|
db.commit()
|
|
|
|
def _summarize_nameplate_and_store(self, side_rows, delete_time, pruned):
|
|
# requires caller to db.commit()
|
|
u = self._summarize_nameplate_usage(side_rows, delete_time, pruned)
|
|
self._db.execute("INSERT INTO `nameplate_usage`"
|
|
" (`app_id`,"
|
|
" `started`, `total_time`, `waiting_time`, `result`)"
|
|
" VALUES (?, ?,?,?,?)",
|
|
(self._app_id,
|
|
u.started, u.total_time, u.waiting_time, u.result))
|
|
self._nameplate_counts[u.result] += 1
|
|
|
|
def _summarize_nameplate_usage(self, side_rows, delete_time, pruned):
|
|
times = sorted([row["added"] for row in side_rows])
|
|
started = times[0]
|
|
if self._blur_usage:
|
|
started = self._blur_usage * (started // self._blur_usage)
|
|
waiting_time = None
|
|
if len(times) > 1:
|
|
waiting_time = times[1] - times[0]
|
|
total_time = delete_time - times[0]
|
|
result = "lonely"
|
|
if len(times) == 2:
|
|
result = "happy"
|
|
if pruned:
|
|
result = "pruney"
|
|
if len(times) > 2:
|
|
result = "crowded"
|
|
return Usage(started=started, waiting_time=waiting_time,
|
|
total_time=total_time, result=result)
|
|
|
|
def _add_mailbox(self, mailbox_id, for_nameplate, side, when):
|
|
assert isinstance(mailbox_id, type("")), type(mailbox_id)
|
|
db = self._db
|
|
row = db.execute("SELECT * FROM `mailboxes`"
|
|
" WHERE `app_id`=? AND `id`=?",
|
|
(self._app_id, mailbox_id)).fetchone()
|
|
if not row:
|
|
self._db.execute("INSERT INTO `mailboxes`"
|
|
" (`app_id`, `id`, `for_nameplate`, `updated`)"
|
|
" VALUES(?,?,?,?)",
|
|
(self._app_id, mailbox_id, for_nameplate, when))
|
|
# we don't need a commit here, because mailbox.open() only
|
|
# does SELECT FROM `mailbox_sides`, not from `mailboxes`
|
|
|
|
def open_mailbox(self, mailbox_id, side, when):
|
|
assert isinstance(mailbox_id, type("")), type(mailbox_id)
|
|
self._add_mailbox(mailbox_id, False, side, when) # ensure row exists
|
|
db = self._db
|
|
if not mailbox_id in self._mailboxes: # ensure Mailbox object exists
|
|
if self._log_requests:
|
|
log.msg("spawning #%s for app_id %s" % (mailbox_id,
|
|
self._app_id))
|
|
self._mailboxes[mailbox_id] = Mailbox(self, self._db,
|
|
self._app_id, mailbox_id)
|
|
mailbox = self._mailboxes[mailbox_id]
|
|
|
|
# delegate to mailbox.open() to add a row to mailbox_sides, and
|
|
# update the mailbox.updated timestamp
|
|
mailbox.open(side, when)
|
|
db.commit()
|
|
rows = db.execute("SELECT * FROM `mailbox_sides`"
|
|
" WHERE `mailbox_id`=?",
|
|
(mailbox_id,)).fetchall()
|
|
if len(rows) > 2:
|
|
raise CrowdedError("too many sides have opened this mailbox")
|
|
return mailbox
|
|
|
|
def free_mailbox(self, mailbox_id):
|
|
# called from Mailbox.delete_and_summarize(), which deletes any
|
|
# messages
|
|
|
|
if mailbox_id in self._mailboxes:
|
|
self._mailboxes.pop(mailbox_id)
|
|
#if self._log_requests:
|
|
# log.msg("freed+killed #%s, now have %d DB mailboxes, %d live" %
|
|
# (mailbox_id, len(self.get_claimed()), len(self._mailboxes)))
|
|
|
|
def _summarize_mailbox_and_store(self, for_nameplate, side_rows,
|
|
delete_time, pruned):
|
|
db = self._db
|
|
u = self._summarize_mailbox(side_rows, delete_time, pruned)
|
|
db.execute("INSERT INTO `mailbox_usage`"
|
|
" (`app_id`, `for_nameplate`,"
|
|
" `started`, `total_time`, `waiting_time`, `result`)"
|
|
" VALUES (?,?, ?,?,?,?)",
|
|
(self._app_id, for_nameplate,
|
|
u.started, u.total_time, u.waiting_time, u.result))
|
|
self._mailbox_counts[u.result] += 1
|
|
|
|
def _summarize_mailbox(self, side_rows, delete_time, pruned):
|
|
times = sorted([row["added"] for row in side_rows])
|
|
started = times[0]
|
|
if self._blur_usage:
|
|
started = self._blur_usage * (started // self._blur_usage)
|
|
waiting_time = None
|
|
if len(times) > 1:
|
|
waiting_time = times[1] - times[0]
|
|
total_time = delete_time - times[0]
|
|
|
|
num_sides = len(times)
|
|
if num_sides == 0:
|
|
result = "quiet"
|
|
elif num_sides == 1:
|
|
result = "lonely"
|
|
else:
|
|
result = "happy"
|
|
|
|
# "mood" is only recorded at close()
|
|
moods = [row["mood"] for row in side_rows if row.get("mood")]
|
|
if "lonely" in moods:
|
|
result = "lonely"
|
|
if "errory" in moods:
|
|
result = "errory"
|
|
if "scary" in moods:
|
|
result = "scary"
|
|
if pruned:
|
|
result = "pruney"
|
|
if num_sides > 2:
|
|
result = "crowded"
|
|
|
|
return Usage(started=started, waiting_time=waiting_time,
|
|
total_time=total_time, result=result)
|
|
|
|
def prune(self, now, old):
|
|
# The pruning check runs every 10 minutes, and "old" is defined to be
|
|
# 11 minutes ago (unit tests can use different values). The client is
|
|
# allowed to disconnect for up to 9 minutes without losing the
|
|
# channel (nameplate, mailbox, and messages).
|
|
|
|
# Each time a client does something, the mailbox.updated field is
|
|
# updated with the current timestamp. If a client is subscribed to
|
|
# the mailbox when pruning check runs, the "updated" field is also
|
|
# updated. After that check, if the "updated" field is "old", the
|
|
# channel is deleted.
|
|
|
|
# For now, pruning is logged even if log_requests is False, to debug
|
|
# the pruning process, and since pruning is triggered by a timer
|
|
# instead of by user action. It does reveal which mailboxes were
|
|
# present when the pruning process began, though, so in the log run
|
|
# it should do less logging.
|
|
log.msg(" prune begins (%s)" % self._app_id)
|
|
db = self._db
|
|
modified = False
|
|
|
|
for mailbox in self._mailboxes.values():
|
|
if mailbox.has_listeners():
|
|
log.msg("touch %s because listeners" % mailbox._mailbox_id)
|
|
mailbox._touch(now)
|
|
db.commit() # make sure the updates are visible below
|
|
|
|
new_mailboxes = set()
|
|
old_mailboxes = set()
|
|
for row in db.execute("SELECT * FROM `mailboxes` WHERE `app_id`=?",
|
|
(self._app_id,)).fetchall():
|
|
mailbox_id = row["id"]
|
|
log.msg(" 1: age=%s, old=%s, %s" %
|
|
(now - row["updated"], now - old, mailbox_id))
|
|
if row["updated"] > old:
|
|
new_mailboxes.add(mailbox_id)
|
|
else:
|
|
old_mailboxes.add(mailbox_id)
|
|
log.msg(" 2: mailboxes:", new_mailboxes, old_mailboxes)
|
|
|
|
old_nameplates = set()
|
|
for row in db.execute("SELECT * FROM `nameplates` WHERE `app_id`=?",
|
|
(self._app_id,)).fetchall():
|
|
npid = row["id"]
|
|
mailbox_id = row["mailbox_id"]
|
|
if mailbox_id in old_mailboxes:
|
|
old_nameplates.add(npid)
|
|
log.msg(" 3: old_nameplates", old_nameplates)
|
|
|
|
for npid in old_nameplates:
|
|
log.msg(" deleting nameplate", npid)
|
|
side_rows = db.execute("SELECT * FROM `nameplate_sides`"
|
|
" WHERE `nameplates_id`=?",
|
|
(npid,)).fetchall()
|
|
db.execute("DELETE FROM `nameplate_sides` WHERE `nameplates_id`=?",
|
|
(npid,))
|
|
db.execute("DELETE FROM `nameplates` WHERE `id`=?", (npid,))
|
|
self._summarize_nameplate_and_store(side_rows, now, pruned=True)
|
|
modified = True
|
|
|
|
# delete all messages for old mailboxes
|
|
# delete all old mailboxes
|
|
|
|
for mailbox_id in old_mailboxes:
|
|
log.msg(" deleting mailbox", mailbox_id)
|
|
row = db.execute("SELECT * FROM `mailboxes`"
|
|
" WHERE `id`=?", (mailbox_id,)).fetchone()
|
|
for_nameplate = row["for_nameplate"]
|
|
side_rows = db.execute("SELECT * FROM `mailbox_sides`"
|
|
" WHERE `mailbox_id`=?",
|
|
(mailbox_id,)).fetchall()
|
|
db.execute("DELETE FROM `messages` WHERE `mailbox_id`=?",
|
|
(mailbox_id,))
|
|
db.execute("DELETE FROM `mailbox_sides` WHERE `mailbox_id`=?",
|
|
(mailbox_id,))
|
|
db.execute("DELETE FROM `mailboxes` WHERE `id`=?",
|
|
(mailbox_id,))
|
|
self._summarize_mailbox_and_store(for_nameplate, side_rows,
|
|
now, pruned=True)
|
|
modified = True
|
|
|
|
if modified:
|
|
db.commit()
|
|
log.msg(" prune complete, modified:", modified)
|
|
|
|
def get_counts(self):
|
|
return (self._nameplate_counts, self._mailbox_counts)
|
|
|
|
def _shutdown(self):
|
|
for channel in self._mailboxes.values():
|
|
channel._shutdown()
|
|
|
|
class Rendezvous(service.MultiService):
|
|
def __init__(self, db, welcome, blur_usage):
|
|
service.MultiService.__init__(self)
|
|
self._db = db
|
|
self._welcome = welcome
|
|
self._blur_usage = blur_usage
|
|
log_requests = blur_usage is None
|
|
self._log_requests = log_requests
|
|
self._apps = {}
|
|
|
|
def get_welcome(self):
|
|
return self._welcome
|
|
def get_log_requests(self):
|
|
return self._log_requests
|
|
|
|
def get_app(self, app_id):
|
|
assert isinstance(app_id, type(""))
|
|
if not app_id in self._apps:
|
|
if self._log_requests:
|
|
log.msg("spawning app_id %s" % (app_id,))
|
|
self._apps[app_id] = AppNamespace(self._db,
|
|
self._blur_usage,
|
|
self._log_requests, app_id)
|
|
return self._apps[app_id]
|
|
|
|
def get_all_apps(self):
|
|
apps = set()
|
|
for row in self._db.execute("SELECT DISTINCT `app_id`"
|
|
" FROM `nameplates`").fetchall():
|
|
apps.add(row["app_id"])
|
|
for row in self._db.execute("SELECT DISTINCT `app_id`"
|
|
" FROM `mailboxes`").fetchall():
|
|
apps.add(row["app_id"])
|
|
for row in self._db.execute("SELECT DISTINCT `app_id`"
|
|
" FROM `messages`").fetchall():
|
|
apps.add(row["app_id"])
|
|
return apps
|
|
|
|
def prune_all_apps(self, now, old):
|
|
# As with AppNamespace.prune_old_mailboxes, we log for now.
|
|
log.msg("beginning app prune")
|
|
for app_id in sorted(self.get_all_apps()):
|
|
log.msg(" app prune checking %r" % (app_id,))
|
|
app = self.get_app(app_id)
|
|
app.prune(now, old)
|
|
log.msg("app prune ends, %d apps" % len(self._apps))
|
|
|
|
def get_stats(self):
|
|
stats = {}
|
|
|
|
# current status: expected to be zero most of the time
|
|
c = stats["active"] = {}
|
|
c["apps"] = len(self.get_all_apps())
|
|
def q(query, values=()):
|
|
row = self._db.execute(query, values).fetchone()
|
|
return list(row.values())[0]
|
|
c["nameplates_total"] = q("SELECT COUNT() FROM `nameplates`")
|
|
# TODO: nameplates with only one side (most of them)
|
|
# TODO: nameplates with two sides (very fleeting)
|
|
# TODO: nameplates with three or more sides (crowded, unlikely)
|
|
c["mailboxes_total"] = q("SELECT COUNT() FROM `mailboxes`")
|
|
# TODO: mailboxes with only one side (most of them)
|
|
# TODO: mailboxes with two sides (somewhat fleeting, in-transit)
|
|
# TODO: mailboxes with three or more sides (unlikely)
|
|
c["messages_total"] = q("SELECT COUNT() FROM `messages`")
|
|
|
|
# usage since last reboot
|
|
nameplate_counts = collections.defaultdict(int)
|
|
mailbox_counts = collections.defaultdict(int)
|
|
for app in self._apps.values():
|
|
nc, mc = app.get_counts()
|
|
for result, count in nc.items():
|
|
nameplate_counts[result] += count
|
|
for result, count in mc.items():
|
|
mailbox_counts[result] += count
|
|
urb = stats["since_reboot"] = {}
|
|
urb["nameplate_moods"] = {}
|
|
for result, count in nameplate_counts.items():
|
|
urb["nameplate_moods"][result] = count
|
|
urb["nameplates_total"] = sum(nameplate_counts.values())
|
|
urb["mailbox_moods"] = {}
|
|
for result, count in mailbox_counts.items():
|
|
urb["mailbox_moods"][result] = count
|
|
urb["mailboxes_total"] = sum(mailbox_counts.values())
|
|
|
|
# historical usage (all-time)
|
|
u = stats["all_time"] = {}
|
|
un = u["nameplate_moods"] = {}
|
|
# TODO: there's probably a single SQL query for all this
|
|
un["happy"] = q("SELECT COUNT() FROM `nameplate_usage`"
|
|
" WHERE `result`='happy'")
|
|
un["lonely"] = q("SELECT COUNT() FROM `nameplate_usage`"
|
|
" WHERE `result`='lonely'")
|
|
un["pruney"] = q("SELECT COUNT() FROM `nameplate_usage`"
|
|
" WHERE `result`='pruney'")
|
|
un["crowded"] = q("SELECT COUNT() FROM `nameplate_usage`"
|
|
" WHERE `result`='crowded'")
|
|
u["nameplates_total"] = q("SELECT COUNT() FROM `nameplate_usage`")
|
|
um = u["mailbox_moods"] = {}
|
|
um["happy"] = q("SELECT COUNT() FROM `mailbox_usage`"
|
|
" WHERE `result`='happy'")
|
|
um["scary"] = q("SELECT COUNT() FROM `mailbox_usage`"
|
|
" WHERE `result`='scary'")
|
|
um["lonely"] = q("SELECT COUNT() FROM `mailbox_usage`"
|
|
" WHERE `result`='lonely'")
|
|
um["quiet"] = q("SELECT COUNT() FROM `mailbox_usage`"
|
|
" WHERE `result`='quiet'")
|
|
um["errory"] = q("SELECT COUNT() FROM `mailbox_usage`"
|
|
" WHERE `result`='errory'")
|
|
um["pruney"] = q("SELECT COUNT() FROM `mailbox_usage`"
|
|
" WHERE `result`='pruney'")
|
|
um["crowded"] = q("SELECT COUNT() FROM `mailbox_usage`"
|
|
" WHERE `result`='crowded'")
|
|
u["mailboxes_total"] = q("SELECT COUNT() FROM `mailbox_usage`")
|
|
u["mailboxes_standalone"] = q("SELECT COUNT() FROM `mailbox_usage`"
|
|
" WHERE `for_nameplate`=0")
|
|
|
|
# recent timings (last 100 operations)
|
|
# TODO: median/etc of nameplate.total_time
|
|
# TODO: median/etc of mailbox.waiting_time (should be the same)
|
|
# TODO: median/etc of mailbox.total_time
|
|
|
|
# other
|
|
# TODO: mailboxes without nameplates (needs new DB schema)
|
|
|
|
return stats
|
|
|
|
def stopService(self):
|
|
# This forcibly boots any clients that are still connected, which
|
|
# helps with unit tests that use threads for both clients. One client
|
|
# hits an exception, which terminates the test (and .tearDown calls
|
|
# stopService on the relay), but the other client (in its thread) is
|
|
# still waiting for a message. By killing off all connections, that
|
|
# other client gets an error, and exits promptly.
|
|
for app in self._apps.values():
|
|
app._shutdown()
|
|
return service.MultiService.stopService(self)
|