Merge branch 'transit-usage'

This commit is contained in:
Brian Warner 2015-12-03 20:44:01 -08:00
commit fbec4cef77
7 changed files with 188 additions and 42 deletions

View File

@ -21,14 +21,21 @@ CREATE INDEX `messages_idx` ON `messages` (`appid`, `channelid`);
CREATE TABLE `usage` CREATE TABLE `usage`
( (
`type` VARCHAR, -- "rendezvous" or "transit"
`started` INTEGER, -- seconds since epoch, rounded to one day `started` INTEGER, -- seconds since epoch, rounded to one day
`result` VARCHAR, -- happy, scary, lonely, errory, pruney `result` VARCHAR, -- happy, scary, lonely, errory, pruney
-- "happy": both sides close with mood=happy -- rendezvous moods:
-- "scary": any side closes with mood=scary (bad MAC, probably wrong pw) -- "happy": both sides close with mood=happy
-- "lonely": any side closes with mood=lonely (no response from 2nd side) -- "scary": any side closes with mood=scary (bad MAC, probably wrong pw)
-- "errory": any side closes with mood=errory (other errors) -- "lonely": any side closes with mood=lonely (no response from 2nd side)
-- "pruney": channels which get pruned for inactivity -- "errory": any side closes with mood=errory (other errors)
-- "crowded": three or more sides were involved -- "pruney": channels which get pruned for inactivity
-- "crowded": three or more sides were involved
-- transit moods:
-- "errory": this side have the wrong handshake
-- "lonely": good handshake, but the other side never showed up
-- "happy": both sides gave correct handshake
`total_bytes` INTEGER, -- for transit, total bytes relayed (both directions)
`total_time` INTEGER, -- seconds from start to closed, or None `total_time` INTEGER, -- seconds from start to closed, or None
`waiting_time` INTEGER -- seconds from start to 2nd side appearing, or None `waiting_time` INTEGER -- seconds from start to 2nd side appearing, or None
); );

View File

@ -13,16 +13,44 @@ def abbrev(t):
return "%.1fms" % (t*1e3) return "%.1fms" % (t*1e3)
return "%.1fus" % (t*1e6) return "%.1fus" % (t*1e6)
def abbreviate_space(s, SI=True):
if s is None:
return "-"
if SI:
U = 1000.0
isuffix = "B"
else:
U = 1024.0
isuffix = "iB"
def r(count, suffix):
return "%.2f %s%s" % (count, suffix, isuffix)
if s < 1024: # 1000-1023 get emitted as bytes, even in SI mode
return "%d B" % s
if s < U*U:
return r(s/U, "k")
if s < U*U*U:
return r(s/(U*U), "M")
if s < U*U*U*U:
return r(s/(U*U*U), "G")
if s < U*U*U*U*U:
return r(s/(U*U*U*U), "T")
if s < U*U*U*U*U*U:
return r(s/(U*U*U*U*U), "P")
return r(s/(U*U*U*U*U*U), "E")
def print_event(event): def print_event(event):
started, result, waiting_time, total_time = event event_type, started, result, total_bytes, waiting_time, total_time = event
followthrough = None followthrough = None
if waiting_time and total_time: if waiting_time and total_time:
followthrough = total_time - waiting_time followthrough = total_time - waiting_time
print("%s: %-6s total=%7s wait=%7s ft=%7s" % print(" %16s: total=%7s wait=%7s ft=%7s size=%s (%s)" %
(time.ctime(started), result, ("%s-%s" % (event_type, result),
abbrev(total_time), abbrev(total_time),
abbrev(waiting_time), abbrev(waiting_time),
abbrev(followthrough), abbrev(followthrough),
abbreviate_space(total_bytes),
time.ctime(started),
)) ))
def show_usage(args): def show_usage(args):
@ -30,30 +58,58 @@ def show_usage(args):
raise UsageError("cannot find relay.sqlite, please run from the server directory") raise UsageError("cannot find relay.sqlite, please run from the server directory")
oldest = None oldest = None
newest = None newest = None
counters = defaultdict(int) rendezvous_counters = defaultdict(int)
transit_counters = defaultdict(int)
total_transit_bytes = 0
db = get_db("relay.sqlite") db = get_db("relay.sqlite")
c = db.execute("SELECT * FROM `usage` ORDER BY `started` ASC LIMIT ?", c = db.execute("SELECT * FROM `usage`"
" ORDER BY `started` ASC LIMIT ?",
(args.n,)) (args.n,))
for row in c.fetchall(): for row in c.fetchall():
if row["type"] == u"rendezvous":
counters = rendezvous_counters
elif row["type"] == u"transit":
counters = transit_counters
total_transit_bytes += row["total_bytes"]
else:
continue
counters["total"] += 1 counters["total"] += 1
counters[row["result"]] += 1 counters[row["result"]] += 1
if oldest is None or row["started"] < oldest: if oldest is None or row["started"] < oldest:
oldest = row["started"] oldest = row["started"]
if newest is None or row["started"] > newest: if newest is None or row["started"] > newest:
newest = row["started"] newest = row["started"]
event = (row["started"], row["result"], event = (row["type"], row["started"], row["result"],
row["waiting_time"], row["total_time"]) row["total_bytes"], row["waiting_time"], row["total_time"])
print_event(event) print_event(event)
total = counters["total"] if rendezvous_counters["total"] or transit_counters["total"]:
if total: print("---")
print("(most recent started %s ago)" % abbrev(time.time() - newest)) print("(most recent started %s ago)" % abbrev(time.time() - newest))
if rendezvous_counters["total"]:
print("rendezvous events:")
counters = rendezvous_counters
elapsed = time.time() - oldest elapsed = time.time() - oldest
print("%d events in %s (%.2f per hour)" % (total, abbrev(elapsed), total = counters["total"]
(3600 * total / elapsed))) print(" %d events in %s (%.2f per hour)" % (total, abbrev(elapsed),
print(", ".join(["%s=%d (%d%%)" % (3600 * total / elapsed)))
(k, counters[k], (100.0 * counters[k] / total)) print("", ", ".join(["%s=%d (%d%%)" %
for k in sorted(counters) (k, counters[k], (100.0 * counters[k] / total))
if k != "total"])) for k in sorted(counters)
if k != "total"]))
if transit_counters["total"]:
print("transit events:")
counters = transit_counters
elapsed = time.time() - oldest
total = counters["total"]
print(" %d events in %s (%.2f per hour)" % (total, abbrev(elapsed),
(3600 * total / elapsed)))
rate = total_transit_bytes / elapsed
print(" %s total bytes, %sps" % (abbreviate_space(total_transit_bytes),
abbreviate_space(rate)))
print("", ", ".join(["%s=%d (%d%%)" %
(k, counters[k], (100.0 * counters[k] / total))
for k in sorted(counters)
if k != "total"]))
return 0 return 0
def tail_usage(args): def tail_usage(args):
@ -63,15 +119,20 @@ def tail_usage(args):
# we don't seem to have unique row IDs, so this is an inaccurate and # we don't seem to have unique row IDs, so this is an inaccurate and
# inefficient hack # inefficient hack
seen = set() seen = set()
while True: try:
old = time.time() - 2*60*60 while True:
c = db.execute("SELECT * FROM `usage` WHERE `started` > ?" old = time.time() - 2*60*60
" ORDER BY `started` ASC", (old,)) c = db.execute("SELECT * FROM `usage`"
for row in c.fetchall(): " WHERE `started` > ?"
event = (row["started"], row["result"], " ORDER BY `started` ASC", (old,))
row["waiting_time"], row["total_time"]) for row in c.fetchall():
if event not in seen: event = (row["type"], row["started"], row["result"],
print_event(event) row["total_bytes"], row["waiting_time"],
seen.add(event) row["total_time"])
time.sleep(2) if event not in seen:
print_event(event)
seen.add(event)
time.sleep(2)
except KeyboardInterrupt:
return 0
return 0 return 0

View File

@ -298,9 +298,11 @@ class Channel:
def _store_summary(self, summary): def _store_summary(self, summary):
(started, result, total_time, waiting_time) = summary (started, result, total_time, waiting_time) = summary
self._db.execute("INSERT INTO `usage`" self._db.execute("INSERT INTO `usage`"
" (`started`, `result`, `total_time`, `waiting_time`)" " (`type`, `started`, `result`,"
" VALUES (?,?,?,?)", " `total_time`, `waiting_time`)"
(started, result, total_time, waiting_time)) " VALUES (?,?,?, ?,?)",
(u"rendezvous", started, result,
total_time, waiting_time))
self._db.commit() self._db.commit()
def _summarize(self, messages, delete_time): def _summarize(self, messages, delete_time):

View File

@ -38,7 +38,7 @@ class RelayServer(service.MultiService):
self.relay = Relay(self.db, welcome) # accessible from tests self.relay = Relay(self.db, welcome) # accessible from tests
self.root.putChild(b"wormhole-relay", self.relay) self.root.putChild(b"wormhole-relay", self.relay)
if transitport: if transitport:
self.transit = Transit() self.transit = Transit(self.db)
self.transit.setServiceParent(self) # for the timer self.transit.setServiceParent(self) # for the timer
t = endpoints.serverFromString(reactor, transitport) t = endpoints.serverFromString(reactor, transitport)
self.transport_service = ServerEndpointService(t, self.transit) self.transport_service = ServerEndpointService(t, self.transit)

View File

@ -1,5 +1,5 @@
from __future__ import print_function from __future__ import print_function
import re import re, time
from twisted.python import log from twisted.python import log
from twisted.internet import protocol from twisted.internet import protocol
from twisted.application import service from twisted.application import service
@ -16,8 +16,12 @@ class TransitConnection(protocol.Protocol):
self._token_buffer = b"" self._token_buffer = b""
self._sent_ok = False self._sent_ok = False
self._buddy = None self._buddy = None
self._had_buddy = False
self._total_sent = 0 self._total_sent = 0
def connectionMade(self):
self._started = time.time()
def dataReceived(self, data): def dataReceived(self, data):
if self._sent_ok: if self._sent_ok:
# We are an IPushProducer to our buddy's IConsumer, so they'll # We are an IPushProducer to our buddy's IConsumer, so they'll
@ -29,10 +33,12 @@ class TransitConnection(protocol.Protocol):
self._total_sent += len(data) self._total_sent += len(data)
self._buddy.transport.write(data) self._buddy.transport.write(data)
return return
if self._got_token: # but not yet sent_ok if self._got_token: # but not yet sent_ok
self.transport.write(b"impatient\n") self.transport.write(b"impatient\n")
log.msg("transit impatience failure") log.msg("transit impatience failure")
return self.disconnect() # impatience yields failure return self.disconnect() # impatience yields failure
# else this should be (part of) the token # else this should be (part of) the token
self._token_buffer += data self._token_buffer += data
buf = self._token_buffer buf = self._token_buffer
@ -59,6 +65,7 @@ class TransitConnection(protocol.Protocol):
def buddy_connected(self, them): def buddy_connected(self, them):
self._buddy = them self._buddy = them
self._had_buddy = True
self.transport.write(b"ok\n") self.transport.write(b"ok\n")
self._sent_ok = True self._sent_ok = True
# Connect the two as a producer/consumer pair. We use streaming=True, # Connect the two as a producer/consumer pair. We use streaming=True,
@ -77,11 +84,37 @@ class TransitConnection(protocol.Protocol):
log.msg("connectionLost %r %s" % (self, reason)) log.msg("connectionLost %r %s" % (self, reason))
if self._buddy: if self._buddy:
self._buddy.buddy_disconnected() self._buddy.buddy_disconnected()
self.factory.transitFinished(self, self._total_sent) self.factory.transitFinished(self)
# Record usage. There are four cases:
# * 1: we connected, never had a buddy
# * 2: we connected first, we disconnect before the buddy
# * 3: we connected first, buddy disconnects first
# * 4: buddy connected first, we disconnect before buddy
# * 5: buddy connected first, buddy disconnects first
# whoever disconnects first gets to write the usage record (1,2,4)
finished = time.time()
if not self._had_buddy: # 1
total_time = finished - self._started
self.factory.recordUsage(self._started, u"lonely", 0,
total_time, None)
if self._had_buddy and self._buddy: # 2,4
total_bytes = self._total_sent + self._buddy._total_sent
starts = [self._started, self._buddy._started]
total_time = finished - min(starts)
waiting_time = max(starts) - min(starts)
self.factory.recordUsage(self._started, u"happy", total_bytes,
total_time, waiting_time)
def disconnect(self): def disconnect(self):
self.transport.loseConnection() self.transport.loseConnection()
self.factory.transitFailed(self) self.factory.transitFailed(self)
finished = time.time()
total_time = finished - self._started
self.factory.recordUsage(self._started, u"errory", 0,
total_time, None)
class Transit(protocol.ServerFactory, service.MultiService): class Transit(protocol.ServerFactory, service.MultiService):
# I manage pairs of simultaneous connections to a secondary TCP port, # I manage pairs of simultaneous connections to a secondary TCP port,
@ -110,8 +143,9 @@ class Transit(protocol.ServerFactory, service.MultiService):
MAXTIME = 60*SECONDS MAXTIME = 60*SECONDS
protocol = TransitConnection protocol = TransitConnection
def __init__(self): def __init__(self, db):
service.MultiService.__init__(self) service.MultiService.__init__(self)
self._db = db
self._pending_requests = {} # token -> TransitConnection self._pending_requests = {} # token -> TransitConnection
self._active_connections = set() # TransitConnection self._active_connections = set() # TransitConnection
@ -127,8 +161,20 @@ class Transit(protocol.ServerFactory, service.MultiService):
self._pending_requests[token] = p self._pending_requests[token] = p
log.msg("transit relay 1: %r" % token) log.msg("transit relay 1: %r" % token)
# TODO: timer # TODO: timer
def transitFinished(self, p, total_sent):
log.msg("transitFinished (%dB) %r" % (total_sent, p)) def recordUsage(self, started, result, total_bytes,
total_time, waiting_time):
log.msg("Transit.recordUsage (%dB)" % total_bytes)
self._db.execute("INSERT INTO `usage`"
" (`type`, `started`, `result`, `total_bytes`,"
" `total_time`, `waiting_time`)"
" VALUES (?,?,?,?, ?,?)",
(u"transit", started, result, total_bytes,
total_time, waiting_time))
self._db.commit()
def transitFinished(self, p):
log.msg("transitFinished %r" % (p,))
for token,tc in self._pending_requests.items(): for token,tc in self._pending_requests.items():
if tc is p: if tc is p:
del self._pending_requests[token] del self._pending_requests[token]

View File

@ -15,6 +15,7 @@ class ServerBase:
__version__) __version__)
s.setServiceParent(self.sp) s.setServiceParent(self.sp)
self._relay_server = s.relay self._relay_server = s.relay
self._transit_server = s.transit
self.relayurl = u"http://127.0.0.1:%d/wormhole-relay/" % relayport self.relayurl = u"http://127.0.0.1:%d/wormhole-relay/" % relayport
self.transit = u"tcp:127.0.0.1:%d" % transitport self.transit = u"tcp:127.0.0.1:%d" % transitport
d.addCallback(_got_ports) d.addCallback(_got_ports)

View File

@ -6,7 +6,9 @@ from twisted.internet.threads import deferToThread
from ..blocking.transcribe import (Wormhole, UsageError, ChannelManager, from ..blocking.transcribe import (Wormhole, UsageError, ChannelManager,
WrongPasswordError) WrongPasswordError)
from ..blocking.eventsource import EventSourceFollower from ..blocking.eventsource import EventSourceFollower
from ..blocking.transit import TransitSender, TransitReceiver from ..blocking.transit import (TransitSender, TransitReceiver,
build_sender_handshake,
build_receiver_handshake)
from .common import ServerBase from .common import ServerBase
APPID = u"appid" APPID = u"appid"
@ -461,6 +463,8 @@ class Transit(_DoBothMixin, ServerBase, unittest.TestCase):
r.add_their_direct_hints([]) r.add_their_direct_hints([])
r.add_their_relay_hints([]) r.add_their_relay_hints([])
# it'd be nice to factor this chunk out with 'yield from', but that
# didn't appear until python-3.3, and isn't in py2 at all.
(sp, rp) = yield self.doBoth([s.connect], [r.connect]) (sp, rp) = yield self.doBoth([s.connect], [r.connect])
yield deferToThread(sp.send_record, b"01234") yield deferToThread(sp.send_record, b"01234")
rec = yield deferToThread(rp.receive_record) rec = yield deferToThread(rp.receive_record)
@ -508,3 +512,28 @@ class Transit(_DoBothMixin, ServerBase, unittest.TestCase):
self.assertEqual(rec, b"01234") self.assertEqual(rec, b"01234")
yield deferToThread(sp.close) yield deferToThread(sp.close)
yield deferToThread(rp.close) yield deferToThread(rp.close)
# TODO: this may be racy if we don't poll the server to make sure
# it's witnessed the first connection closing before querying the DB
#import time
#yield deferToThread(time.sleep, 1)
# check the transit relay's DB, make sure it counted the bytes
db = self._transit_server._db
c = db.execute("SELECT * FROM `usage` WHERE `type`=?", (u"transit",))
rows = c.fetchall()
self.assertEqual(len(rows), 1)
row = rows[0]
self.assertEqual(row["result"], u"happy")
# Sender first writes relay_handshake and waits for OK, but that's
# not counted by the transit server. Then sender writes
# sender_handshake and waits for receiver_handshake. Then sender
# writes GO and the body. Body is length-prefixed SecretBox, so
# includes 4-byte length, 24-byte nonce, and 16-byte MAC.
sender_count = (len(build_sender_handshake(b""))+
len(b"go\n")+
4+24+len(b"01234")+16)
# Receiver first writes relay_handshake and waits for OK, but that's
# not counted. Then receiver writes receiver_handshake and waits for
# sender_handshake+GO.
receiver_count = len(build_receiver_handshake(b""))
self.assertEqual(row["total_bytes"], sender_count+receiver_count)