2015-03-02 08:09:17 +00:00
|
|
|
from __future__ import print_function
|
2016-04-17 23:22:27 +00:00
|
|
|
import time, random
|
2015-02-11 09:05:11 +00:00
|
|
|
from twisted.python import log
|
2015-10-04 22:49:06 +00:00
|
|
|
from twisted.application import service, internet
|
2015-02-11 09:05:11 +00:00
|
|
|
|
2015-02-12 02:13:54 +00:00
|
|
|
SECONDS = 1.0
|
2015-03-03 05:22:56 +00:00
|
|
|
MINUTE = 60*SECONDS
|
|
|
|
HOUR = 60*MINUTE
|
2015-05-05 01:13:14 +00:00
|
|
|
DAY = 24*HOUR
|
2015-02-12 02:13:54 +00:00
|
|
|
MB = 1000*1000
|
|
|
|
|
2015-05-05 01:19:40 +00:00
|
|
|
CHANNEL_EXPIRATION_TIME = 3*DAY
|
2015-10-04 22:49:06 +00:00
|
|
|
EXPIRATION_CHECK_PERIOD = 2*HOUR
|
2015-03-03 05:22:56 +00:00
|
|
|
|
2016-05-13 00:46:15 +00:00
|
|
|
CLAIM = u"_claim"
|
|
|
|
RELEASE = u"_release"
|
2015-11-14 02:22:37 +00:00
|
|
|
|
2016-05-18 07:16:46 +00:00
|
|
|
def get_sides(row):
|
|
|
|
return set([s for s in [row["side1"], row["side2"]] if s])
|
2016-05-19 21:18:49 +00:00
|
|
|
def make_sides(sides):
|
2016-05-18 07:16:46 +00:00
|
|
|
return list(sides) + [None] * (2 - len(sides))
|
|
|
|
def generate_mailbox_id():
|
|
|
|
return base64.b32encode(os.urandom(8)).lower().strip("=")
|
|
|
|
|
|
|
|
# Unlike Channels, these instances are ephemeral, and are created and
|
|
|
|
# destroyed casually.
|
|
|
|
class Nameplate:
|
|
|
|
def __init__(self, app_id, db, id, mailbox_id):
|
|
|
|
self._app_id = app_id
|
|
|
|
self._db = db
|
|
|
|
self._id = id
|
|
|
|
self._mailbox_id = mailbox_id
|
|
|
|
|
|
|
|
def get_id(self):
|
|
|
|
return self._id
|
|
|
|
|
|
|
|
def get_mailbox_id(self):
|
|
|
|
return self._mailbox_id
|
|
|
|
|
|
|
|
def claim(self, side, when):
|
|
|
|
db = self._db
|
|
|
|
sides = get_sides(db.execute("SELECT `side1`, `side2` FROM `nameplates`"
|
|
|
|
" WHERE `app_id`=? AND `id`=?",
|
|
|
|
(self._app_id, self._id)).fetchone())
|
|
|
|
old_sides = len(sides)
|
|
|
|
sides.add(side)
|
|
|
|
if len(sides) > 2:
|
|
|
|
# XXX: crowded: bail
|
|
|
|
pass
|
|
|
|
sides12 = make_sides(sides)
|
|
|
|
db.execute("UPDATE `nameplates` SET `side1`=?, `side2`=?"
|
|
|
|
" WHERE `app_id`=? AND `id`=?",
|
|
|
|
(sides12[0], sides12[1], self._app_id, self._id))
|
|
|
|
if old_sides == 0:
|
|
|
|
db.execute("UPDATE `mailboxes` SET `nameplate_started`=?"
|
|
|
|
" WHERE `app_id`=? AND `id`=?",
|
|
|
|
(when, self._app_id, self._mailbox_id))
|
|
|
|
else:
|
|
|
|
db.execute("UPDATE `mailboxes` SET `nameplate_second`=?"
|
|
|
|
" WHERE `app_id`=? AND `id`=?",
|
|
|
|
(when, self._app_id, self._mailbox_id))
|
|
|
|
db.commit()
|
|
|
|
|
|
|
|
def release(self, side, when):
|
|
|
|
db = self._db
|
|
|
|
sides = get_sides(db.execute("SELECT `side1`, `side2` FROM `nameplates`"
|
|
|
|
" WHERE `app_id`=? AND `id`=?",
|
|
|
|
(self._app_id, self._id)).fetchone())
|
|
|
|
sides.discard(side)
|
|
|
|
sides12 = make_sides(sides)
|
|
|
|
db.execute("UPDATE `nameplates` SET `side1`=?, `side2`=?"
|
|
|
|
" WHERE `app_id`=? AND `id`=?",
|
|
|
|
(sides12[0], sides12[1], self._app_id, self._id))
|
|
|
|
if len(sides) == 0:
|
|
|
|
db.execute("UPDATE `mailboxes` SET `nameplate_closed`=?"
|
|
|
|
" WHERE `app_id`=? AND `id`=?",
|
|
|
|
(when, self._app_id, self._mailbox_id))
|
|
|
|
db.commit()
|
|
|
|
|
|
|
|
class Mailbox:
|
|
|
|
def __init__(self, app, db, blur_usage, log_requests, app_id, channelid):
|
2015-11-14 02:20:47 +00:00
|
|
|
self._app = app
|
|
|
|
self._db = db
|
2015-12-04 05:15:19 +00:00
|
|
|
self._blur_usage = blur_usage
|
2015-12-05 01:35:56 +00:00
|
|
|
self._log_requests = log_requests
|
2016-05-18 07:16:46 +00:00
|
|
|
self._app_id = app_id
|
2015-10-07 00:20:12 +00:00
|
|
|
self._channelid = channelid
|
2016-05-13 00:03:57 +00:00
|
|
|
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
|
2015-05-05 01:13:14 +00:00
|
|
|
|
2016-04-20 08:51:03 +00:00
|
|
|
def get_channelid(self):
|
|
|
|
return self._channelid
|
|
|
|
|
2015-10-07 00:20:12 +00:00
|
|
|
def get_messages(self):
|
|
|
|
messages = []
|
2015-11-14 02:20:47 +00:00
|
|
|
db = self._db
|
2015-10-07 00:20:12 +00:00
|
|
|
for row in db.execute("SELECT * FROM `messages`"
|
2016-05-18 07:16:46 +00:00
|
|
|
" WHERE `app_id`=? AND `channelid`=?"
|
2016-05-06 01:27:01 +00:00
|
|
|
" ORDER BY `server_rx` ASC",
|
2016-05-18 07:16:46 +00:00
|
|
|
(self._app_id, self._channelid)).fetchall():
|
2016-05-13 00:46:15 +00:00
|
|
|
if row["phase"] in (CLAIM, RELEASE):
|
2015-11-14 02:22:37 +00:00
|
|
|
continue
|
2016-05-06 01:34:44 +00:00
|
|
|
messages.append({"phase": row["phase"], "body": row["body"],
|
2016-05-06 01:46:11 +00:00
|
|
|
"server_rx": row["server_rx"], "id": row["msgid"]})
|
2016-03-04 00:53:15 +00:00
|
|
|
return messages
|
2015-10-07 00:20:12 +00:00
|
|
|
|
2016-05-13 00:03:57 +00:00
|
|
|
def add_listener(self, handle, send_f, stop_f):
|
|
|
|
self._listeners[handle] = (send_f, stop_f)
|
2016-05-06 01:21:06 +00:00
|
|
|
return self.get_messages()
|
|
|
|
|
2016-05-13 00:03:57 +00:00
|
|
|
def remove_listener(self, handle):
|
|
|
|
self._listeners.pop(handle)
|
2015-10-07 00:20:12 +00:00
|
|
|
|
2016-05-06 01:46:11 +00:00
|
|
|
def broadcast_message(self, phase, body, server_rx, msgid):
|
2016-05-13 00:03:57 +00:00
|
|
|
for (send_f, stop_f) in self._listeners.values():
|
|
|
|
send_f({"phase": phase, "body": body,
|
|
|
|
"server_rx": server_rx, "id": msgid})
|
2015-10-07 00:20:12 +00:00
|
|
|
|
2016-05-06 01:46:11 +00:00
|
|
|
def _add_message(self, side, phase, body, server_rx, msgid):
|
2015-11-14 02:20:47 +00:00
|
|
|
db = self._db
|
2015-10-07 00:20:12 +00:00
|
|
|
db.execute("INSERT INTO `messages`"
|
2016-05-18 07:16:46 +00:00
|
|
|
" (`app_id`, `channelid`, `side`, `phase`, `body`,"
|
2016-05-06 01:46:11 +00:00
|
|
|
" `server_rx`, `msgid`)"
|
|
|
|
" VALUES (?,?,?,?,?, ?,?)",
|
2016-05-18 07:16:46 +00:00
|
|
|
(self._app_id, self._channelid, side, phase, body,
|
2016-05-06 01:46:11 +00:00
|
|
|
server_rx, msgid))
|
2015-10-07 00:20:12 +00:00
|
|
|
db.commit()
|
2015-11-14 02:22:37 +00:00
|
|
|
|
2016-05-13 00:46:15 +00:00
|
|
|
def claim(self, side):
|
|
|
|
self._add_message(side, CLAIM, None, time.time(), None)
|
2015-11-14 02:22:37 +00:00
|
|
|
|
2016-05-06 01:46:11 +00:00
|
|
|
def add_message(self, side, phase, body, server_rx, msgid):
|
|
|
|
self._add_message(side, phase, body, server_rx, msgid)
|
|
|
|
self.broadcast_message(phase, body, server_rx, msgid)
|
2016-05-06 01:44:56 +00:00
|
|
|
return self.get_messages() # for rendezvous_web.py POST /add
|
2015-10-07 00:20:12 +00:00
|
|
|
|
2016-05-13 00:46:15 +00:00
|
|
|
def release(self, side, mood):
|
|
|
|
self._add_message(side, RELEASE, mood, time.time(), None)
|
2015-11-14 02:22:37 +00:00
|
|
|
db = self._db
|
|
|
|
seen = set([row["side"] for row in
|
|
|
|
db.execute("SELECT `side` FROM `messages`"
|
2016-05-18 07:16:46 +00:00
|
|
|
" WHERE `app_id`=? AND `channelid`=?",
|
|
|
|
(self._app_id, self._channelid))])
|
2015-11-14 02:22:37 +00:00
|
|
|
freed = set([row["side"] for row in
|
|
|
|
db.execute("SELECT `side` FROM `messages`"
|
2016-05-18 07:16:46 +00:00
|
|
|
" WHERE `app_id`=? AND `channelid`=?"
|
2015-11-14 02:22:37 +00:00
|
|
|
" AND `phase`=?",
|
2016-05-18 07:16:46 +00:00
|
|
|
(self._app_id, self._channelid, RELEASE))])
|
2015-11-14 02:22:37 +00:00
|
|
|
if seen - freed:
|
|
|
|
return False
|
|
|
|
self.delete_and_summarize()
|
|
|
|
return True
|
|
|
|
|
|
|
|
def is_idle(self):
|
|
|
|
if self._listeners:
|
|
|
|
return False
|
2016-05-06 01:27:01 +00:00
|
|
|
c = self._db.execute("SELECT `server_rx` FROM `messages`"
|
2016-05-18 07:16:46 +00:00
|
|
|
" WHERE `app_id`=? AND `channelid`=?"
|
2016-05-06 01:27:01 +00:00
|
|
|
" ORDER BY `server_rx` DESC LIMIT 1",
|
2016-05-18 07:16:46 +00:00
|
|
|
(self._app_id, self._channelid))
|
2015-11-14 02:22:37 +00:00
|
|
|
rows = c.fetchall()
|
|
|
|
if not rows:
|
|
|
|
return True
|
|
|
|
old = time.time() - CHANNEL_EXPIRATION_TIME
|
2016-05-06 01:27:01 +00:00
|
|
|
if rows[0]["server_rx"] < old:
|
2015-11-14 02:22:37 +00:00
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2015-11-14 02:36:39 +00:00
|
|
|
def _store_summary(self, summary):
|
|
|
|
(started, result, total_time, waiting_time) = summary
|
2015-12-04 05:15:19 +00:00
|
|
|
if self._blur_usage:
|
|
|
|
started = self._blur_usage * (started // self._blur_usage)
|
2015-11-14 02:36:39 +00:00
|
|
|
self._db.execute("INSERT INTO `usage`"
|
2015-12-04 03:43:20 +00:00
|
|
|
" (`type`, `started`, `result`,"
|
|
|
|
" `total_time`, `waiting_time`)"
|
|
|
|
" VALUES (?,?,?, ?,?)",
|
|
|
|
(u"rendezvous", started, result,
|
|
|
|
total_time, waiting_time))
|
2015-11-14 02:36:39 +00:00
|
|
|
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
|
|
|
|
|
2016-05-06 01:27:01 +00:00
|
|
|
started = min([m["server_rx"] for m in messages])
|
2015-11-14 02:36:39 +00:00
|
|
|
# 'total_time' is how long the channel was occupied. That ends now,
|
|
|
|
# both for channels that got pruned for inactivity, and for channels
|
2016-05-13 00:46:15 +00:00
|
|
|
# that got pruned because of two RELEASE messages
|
2015-11-14 02:36:39 +00:00
|
|
|
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
|
2016-05-06 01:27:01 +00:00
|
|
|
A_side = sorted(messages, key=lambda m: m["server_rx"])[0]["side"]
|
2015-11-14 02:36:39 +00:00
|
|
|
B_side = list(all_sides - set([A_side]))[0]
|
|
|
|
|
|
|
|
# How long did the first side wait until the second side showed up?
|
2016-05-06 01:27:01 +00:00
|
|
|
first_A = min([m["server_rx"] for m in messages if m["side"] == A_side])
|
|
|
|
first_B = min([m["server_rx"] for m in messages if m["side"] == B_side])
|
2015-11-14 02:36:39 +00:00
|
|
|
waiting_time = first_B - first_A
|
|
|
|
|
|
|
|
# now, were all sides closed? If not, this is "pruney"
|
|
|
|
A_deallocs = [m for m in messages
|
2016-05-13 00:46:15 +00:00
|
|
|
if m["phase"] == RELEASE and m["side"] == A_side]
|
2015-11-14 02:36:39 +00:00
|
|
|
B_deallocs = [m for m in messages
|
2016-05-13 00:46:15 +00:00
|
|
|
if m["phase"] == RELEASE and m["side"] == B_side]
|
2015-11-14 02:36:39 +00:00
|
|
|
if not A_deallocs or not B_deallocs:
|
|
|
|
return (started, "pruney", total_time, None)
|
|
|
|
|
|
|
|
# ok, both sides closed. figure out the mood
|
2015-11-15 18:33:17 +00:00
|
|
|
A_mood = A_deallocs[0]["body"] # maybe None
|
|
|
|
B_mood = B_deallocs[0]["body"] # maybe None
|
|
|
|
mood = "quiet"
|
2015-11-14 02:36:39 +00:00
|
|
|
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)
|
|
|
|
|
2015-11-14 02:22:37 +00:00
|
|
|
def delete_and_summarize(self):
|
|
|
|
db = self._db
|
2015-11-14 02:36:39 +00:00
|
|
|
c = self._db.execute("SELECT * FROM `messages`"
|
2016-05-18 07:16:46 +00:00
|
|
|
" WHERE `app_id`=? AND `channelid`=?"
|
2016-05-06 01:27:01 +00:00
|
|
|
" ORDER BY `server_rx`",
|
2016-05-18 07:16:46 +00:00
|
|
|
(self._app_id, self._channelid))
|
2015-11-14 02:36:39 +00:00
|
|
|
messages = c.fetchall()
|
|
|
|
summary = self._summarize(messages, time.time())
|
|
|
|
self._store_summary(summary)
|
2015-11-14 02:22:37 +00:00
|
|
|
db.execute("DELETE FROM `messages`"
|
2016-05-18 07:16:46 +00:00
|
|
|
" WHERE `app_id`=? AND `channelid`=?",
|
|
|
|
(self._app_id, self._channelid))
|
2015-11-14 02:22:37 +00:00
|
|
|
db.commit()
|
|
|
|
|
2016-04-17 23:22:27 +00:00
|
|
|
# Shut down any listeners, just in case they're still lingering
|
|
|
|
# around.
|
2016-05-13 00:03:57 +00:00
|
|
|
for (send_f, stop_f) in self._listeners.values():
|
|
|
|
stop_f()
|
2015-11-14 02:22:37 +00:00
|
|
|
|
|
|
|
self._app.free_channel(self._channelid)
|
|
|
|
|
2016-03-02 00:23:10 +00:00
|
|
|
def _shutdown(self):
|
|
|
|
# used at test shutdown to accelerate client disconnects
|
2016-05-13 00:03:57 +00:00
|
|
|
for (send_f, stop_f) in self._listeners.values():
|
|
|
|
stop_f()
|
2015-11-14 02:22:37 +00:00
|
|
|
|
2015-11-14 02:20:47 +00:00
|
|
|
class AppNamespace:
|
2016-05-18 07:16:46 +00:00
|
|
|
def __init__(self, db, welcome, blur_usage, log_requests, app_id):
|
2015-11-14 02:20:47 +00:00
|
|
|
self._db = db
|
|
|
|
self._welcome = welcome
|
2015-12-04 05:15:19 +00:00
|
|
|
self._blur_usage = blur_usage
|
2015-12-05 01:35:56 +00:00
|
|
|
self._log_requests = log_requests
|
2016-05-18 07:16:46 +00:00
|
|
|
self._app_id = app_id
|
2015-10-07 00:20:12 +00:00
|
|
|
self._channels = {}
|
2015-05-05 01:13:14 +00:00
|
|
|
|
2016-05-18 07:16:46 +00:00
|
|
|
def get_nameplate_ids(self):
|
2015-11-14 02:20:47 +00:00
|
|
|
db = self._db
|
2016-05-18 07:16:46 +00:00
|
|
|
# TODO: filter this to numeric ids?
|
|
|
|
c = db.execute("SELECT DISTINCT `id` FROM `nameplates`"
|
|
|
|
" WHERE `app_id`=?", (self._app_id,))
|
|
|
|
return set([row["id"] for row in c.fetchall()])
|
2015-10-07 00:20:12 +00:00
|
|
|
|
2016-05-19 21:18:49 +00:00
|
|
|
def _find_available_nameplate_id(self):
|
2016-05-18 07:16:46 +00:00
|
|
|
claimed = self.get_nameplate_ids()
|
2015-05-05 01:13:14 +00:00
|
|
|
for size in range(1,4): # stick to 1-999 for now
|
|
|
|
available = set()
|
2016-05-18 07:16:46 +00:00
|
|
|
for id_int in range(10**(size-1), 10**size):
|
|
|
|
id = u"%d" % id_int
|
|
|
|
if id not in claimed:
|
|
|
|
available.add(id)
|
2015-05-05 01:13:14 +00:00
|
|
|
if available:
|
|
|
|
return random.choice(list(available))
|
2016-05-13 00:46:15 +00:00
|
|
|
# ouch, 999 currently claimed. Try random ones for a while.
|
2015-05-05 01:13:14 +00:00
|
|
|
for tries in range(1000):
|
2016-05-18 07:16:46 +00:00
|
|
|
id_int = random.randrange(1000, 1000*1000)
|
|
|
|
id = u"%d" % id_int
|
|
|
|
if id not in claimed:
|
|
|
|
return id
|
|
|
|
raise ValueError("unable to find a free nameplate-id")
|
|
|
|
|
2016-05-19 21:18:49 +00:00
|
|
|
def allocate_nameplate(self, side, when):
|
|
|
|
nameplate_id = self._find_available_nameplate_id()
|
|
|
|
mailbox_id = self.claim_nameplate(self, nameplate_id, side, when)
|
|
|
|
del mailbox_id # ignored, they'll learn it from claim()
|
|
|
|
return nameplate_id
|
|
|
|
|
2016-05-18 07:16:46 +00:00
|
|
|
def _get_mailbox_id(self, nameplate_id):
|
|
|
|
row = self._db.execute("SELECT `mailbox_id` FROM `nameplates`"
|
|
|
|
" WHERE `app_id`=? AND `id`=?",
|
|
|
|
(self._app_id, nameplate_id)).fetchone()
|
|
|
|
return row["mailbox_id"]
|
|
|
|
|
|
|
|
def claim_nameplate(self, nameplate_id, side, when):
|
2016-05-19 21:18:49 +00:00
|
|
|
# when we're done:
|
|
|
|
# * there will be one row for the nameplate
|
|
|
|
# * side1 or side2 will be populated
|
|
|
|
# * started or second will be populated
|
|
|
|
# * a mailbox id will be created, but not a mailbox row
|
|
|
|
# (ids are randomly unique, so we can defer creation until 'open')
|
2016-05-18 07:16:46 +00:00
|
|
|
assert isinstance(nameplate_id, type(u"")), type(nameplate_id)
|
2016-05-19 21:18:49 +00:00
|
|
|
assert isinstance(side, type(u"")), type(side)
|
2016-05-18 07:16:46 +00:00
|
|
|
db = self._db
|
2016-05-19 21:18:49 +00:00
|
|
|
row = db.execute("SELECT * FROM `nameplates`"
|
|
|
|
" WHERE `app_id`=? AND `id`=?",
|
|
|
|
(self._app_id, nameplate_id)).fetchone()
|
|
|
|
if row:
|
|
|
|
mailbox_id = row["mailbox_id"]
|
|
|
|
sides = [row["side1"], row["sides2"]]
|
|
|
|
if side not in sides:
|
|
|
|
if sides[0] and sides[1]:
|
|
|
|
raise XXXERROR("crowded")
|
|
|
|
sides[1] = side
|
|
|
|
db.execute("UPDATE `nameplates` SET "
|
|
|
|
"`side1`=?, `side2`=?, `mailbox_id`=?, `second`=?"
|
|
|
|
" WHERE `app_id`=? AND `id`=?",
|
|
|
|
(sides[0], sides[1], mailbox_id, when,
|
|
|
|
self._app_id, nameplate_id))
|
2016-05-18 07:16:46 +00:00
|
|
|
else:
|
|
|
|
if self._log_requests:
|
|
|
|
log.msg("creating nameplate#%s for app_id %s" %
|
|
|
|
(nameplate_id, self._app_id))
|
2016-05-19 21:18:49 +00:00
|
|
|
mailbox_id = generate_mailbox_id()
|
2016-05-18 07:16:46 +00:00
|
|
|
db.execute("INSERT INTO `nameplates`"
|
2016-05-19 21:18:49 +00:00
|
|
|
" (`app_id`, `id`, `mailbox_id`, `side1`, `started`)"
|
2016-05-18 07:16:46 +00:00
|
|
|
" VALUES(?,?,?,?,?)",
|
2016-05-19 21:18:49 +00:00
|
|
|
(self._app_id, nameplate_id, mailbox_id, side, when))
|
|
|
|
db.commit()
|
|
|
|
return mailbox_id
|
|
|
|
|
|
|
|
def release_nameplate(self, nameplate_id, side, when):
|
|
|
|
# when we're done:
|
|
|
|
# * in the nameplate row, side1 or side2 will be removed
|
|
|
|
# * if the nameplate is now unused:
|
|
|
|
# * mailbox.nameplate_closed will be populated
|
|
|
|
# * the nameplate row will be removed
|
|
|
|
assert isinstance(nameplate_id, type(u"")), type(nameplate_id)
|
|
|
|
assert isinstance(side, type(u"")), type(side)
|
|
|
|
db = self._db
|
|
|
|
row = db.execute("SELECT * FROM `nameplates`"
|
|
|
|
" WHERE `app_id`=? AND `id`=?",
|
|
|
|
(self._app_id, nameplate_id)).fetchone()
|
|
|
|
if not row:
|
|
|
|
return
|
|
|
|
sides = get_sides(row)
|
|
|
|
if side not in sides:
|
|
|
|
return
|
|
|
|
sides.discard(side)
|
|
|
|
if sides:
|
|
|
|
s12 = make_sides(sides)
|
|
|
|
db.execute("UPDATE `nameplates` SET `side1`=?, `side2`=?"
|
|
|
|
" WHERE `app_id`=? AND `id`=?",
|
|
|
|
(s12[0], s12[1], self._app_id, nameplate_id))
|
|
|
|
else:
|
|
|
|
db.execute("DELETE FROM `nameplates`"
|
|
|
|
" WHERE `app_id`=? AND `id`=?",
|
|
|
|
(self._app_id, nameplate_id))
|
|
|
|
self._summarize_nameplate(row)
|
2015-10-04 22:49:06 +00:00
|
|
|
|
2016-05-19 21:18:49 +00:00
|
|
|
def open_mailbox(self, channelid, side):
|
2016-05-13 07:37:53 +00:00
|
|
|
assert isinstance(channelid, type(u"")), type(channelid)
|
2015-11-14 02:22:37 +00:00
|
|
|
channel = self.get_channel(channelid)
|
2016-05-13 00:46:15 +00:00
|
|
|
channel.claim(side)
|
2015-11-14 02:22:37 +00:00
|
|
|
return channel
|
2016-05-19 21:18:49 +00:00
|
|
|
# some of this overlaps with open() on a new mailbox
|
|
|
|
db.execute("INSERT INTO `mailboxes`"
|
|
|
|
" (`app_id`, `id`, `nameplate_started`, `started`)"
|
|
|
|
" VALUES(?,?,?,?)",
|
|
|
|
(self._app_id, mailbox_id, when, when))
|
2015-02-11 09:05:11 +00:00
|
|
|
|
2015-10-07 00:20:12 +00:00
|
|
|
def get_channel(self, channelid):
|
2016-05-13 07:37:53 +00:00
|
|
|
assert isinstance(channelid, type(u""))
|
2015-10-07 00:20:12 +00:00
|
|
|
if not channelid in self._channels:
|
2015-12-05 01:35:56 +00:00
|
|
|
if self._log_requests:
|
2016-05-18 07:16:46 +00:00
|
|
|
log.msg("spawning #%s for app_id %s" % (channelid, self._app_id))
|
2016-05-18 00:35:44 +00:00
|
|
|
self._channels[channelid] = Channel(self, self._db,
|
2015-12-04 05:15:19 +00:00
|
|
|
self._blur_usage,
|
2015-12-05 01:35:56 +00:00
|
|
|
self._log_requests,
|
2016-05-18 07:16:46 +00:00
|
|
|
self._app_id, channelid)
|
2015-10-07 00:20:12 +00:00
|
|
|
return self._channels[channelid]
|
2015-02-11 09:05:11 +00:00
|
|
|
|
2015-11-14 02:22:37 +00:00
|
|
|
def free_channel(self, channelid):
|
|
|
|
# called from Channel.delete_and_summarize(), which deletes any
|
|
|
|
# messages
|
2015-05-05 01:13:14 +00:00
|
|
|
|
2015-10-07 00:20:12 +00:00
|
|
|
if channelid in self._channels:
|
|
|
|
self._channels.pop(channelid)
|
2015-12-05 01:35:56 +00:00
|
|
|
if self._log_requests:
|
2016-05-13 07:37:53 +00:00
|
|
|
log.msg("freed+killed #%s, now have %d DB channels, %d live" %
|
2016-05-13 00:46:15 +00:00
|
|
|
(channelid, len(self.get_claimed()), len(self._channels)))
|
2015-05-05 01:13:14 +00:00
|
|
|
|
|
|
|
def prune_old_channels(self):
|
2015-12-05 01:35:56 +00:00
|
|
|
# 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 channels were
|
|
|
|
# present when the pruning process began, though, so in the log run
|
|
|
|
# it should do less logging.
|
2015-11-14 02:22:37 +00:00
|
|
|
log.msg(" channel prune begins")
|
|
|
|
# a channel is deleted when there are no listeners and there have
|
|
|
|
# been no messages added in CHANNEL_EXPIRATION_TIME seconds
|
2016-05-13 00:46:15 +00:00
|
|
|
channels = set(self.get_claimed()) # these have messages
|
2015-11-14 02:22:37 +00:00
|
|
|
channels.update(self._channels) # these might have listeners
|
|
|
|
for channelid in channels:
|
|
|
|
log.msg(" channel prune checking %d" % channelid)
|
|
|
|
channel = self.get_channel(channelid)
|
|
|
|
if channel.is_idle():
|
|
|
|
log.msg(" channel prune expiring %d" % channelid)
|
|
|
|
channel.delete_and_summarize() # calls self.free_channel
|
|
|
|
log.msg(" channel prune done, %r left" % (self._channels.keys(),))
|
2015-10-07 00:20:12 +00:00
|
|
|
return bool(self._channels)
|
|
|
|
|
2016-03-02 00:23:10 +00:00
|
|
|
def _shutdown(self):
|
|
|
|
for channel in self._channels.values():
|
|
|
|
channel._shutdown()
|
|
|
|
|
2016-04-17 21:41:12 +00:00
|
|
|
class Rendezvous(service.MultiService):
|
2015-12-04 05:15:19 +00:00
|
|
|
def __init__(self, db, welcome, blur_usage):
|
2015-10-07 00:20:12 +00:00
|
|
|
service.MultiService.__init__(self)
|
2015-11-14 02:20:47 +00:00
|
|
|
self._db = db
|
|
|
|
self._welcome = welcome
|
2015-12-04 05:15:19 +00:00
|
|
|
self._blur_usage = blur_usage
|
2015-12-05 01:35:56 +00:00
|
|
|
log_requests = blur_usage is None
|
|
|
|
self._log_requests = log_requests
|
2015-10-07 00:20:12 +00:00
|
|
|
self._apps = {}
|
|
|
|
t = internet.TimerService(EXPIRATION_CHECK_PERIOD, self.prune)
|
|
|
|
t.setServiceParent(self)
|
2016-04-17 21:41:12 +00:00
|
|
|
|
|
|
|
def get_welcome(self):
|
|
|
|
return self._welcome
|
|
|
|
def get_log_requests(self):
|
|
|
|
return self._log_requests
|
2015-11-11 05:02:44 +00:00
|
|
|
|
2016-05-18 07:16:46 +00:00
|
|
|
def get_app(self, app_id):
|
|
|
|
assert isinstance(app_id, type(u""))
|
|
|
|
if not app_id in self._apps:
|
2015-12-05 01:35:56 +00:00
|
|
|
if self._log_requests:
|
2016-05-18 07:16:46 +00:00
|
|
|
log.msg("spawning app_id %s" % (app_id,))
|
|
|
|
self._apps[app_id] = AppNamespace(self._db, self._welcome,
|
2015-12-05 01:35:56 +00:00
|
|
|
self._blur_usage,
|
2016-05-18 07:16:46 +00:00
|
|
|
self._log_requests, app_id)
|
|
|
|
return self._apps[app_id]
|
2015-02-11 09:05:11 +00:00
|
|
|
|
2015-10-07 00:20:12 +00:00
|
|
|
def prune(self):
|
2015-12-05 01:35:56 +00:00
|
|
|
# As with AppNamespace.prune_old_channels, we log for now.
|
2015-11-14 02:22:37 +00:00
|
|
|
log.msg("beginning app prune")
|
2016-05-18 07:16:46 +00:00
|
|
|
c = self._db.execute("SELECT DISTINCT `app_id` FROM `messages`")
|
|
|
|
apps = set([row["app_id"] for row in c.fetchall()]) # these have messages
|
2015-11-14 02:22:37 +00:00
|
|
|
apps.update(self._apps) # these might have listeners
|
2016-05-18 07:16:46 +00:00
|
|
|
for app_id in apps:
|
|
|
|
log.msg(" app prune checking %r" % (app_id,))
|
|
|
|
still_active = self.get_app(app_id).prune_old_channels()
|
2015-10-07 00:20:12 +00:00
|
|
|
if not still_active:
|
2016-05-18 07:16:46 +00:00
|
|
|
log.msg("prune pops app %r" % (app_id,))
|
|
|
|
self._apps.pop(app_id)
|
2015-11-14 02:22:37 +00:00
|
|
|
log.msg("app prune ends, %d remaining apps" % len(self._apps))
|
2016-03-02 00:23:10 +00:00
|
|
|
|
|
|
|
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)
|