From f7717b1de83dd51dd762437d93ed7293abe6bc77 Mon Sep 17 00:00:00 2001 From: HelloZeroNet Date: Fri, 24 Apr 2015 02:36:00 +0200 Subject: [PATCH] rev119, Protection against update flood, Cache webfonts, Publish batching, Task failed holds Peer objects, Remove peer from failed on addTask, Noparallel memory leak fix --- src/Config.py | 2 +- src/Connection/Connection.py | 4 ++ src/Connection/ConnectionServer.py | 1 + src/File/FileRequest.py | 16 ++++- src/Site/Site.py | 2 +- src/Ui/UiRequest.py | 2 +- src/Ui/UiWebsocket.py | 83 ++++++++++++---------- src/Ui/media/img/favicon.psd | Bin 52520 -> 62132 bytes src/Ui/media/img/logo.psd | Bin 0 -> 62132 bytes src/Worker/Worker.py | 2 +- src/Worker/WorkerManager.py | 8 ++- src/util/Event.py | 23 ++++++- src/util/Noparallel.py | 37 ++++++++-- src/util/RateLimit.py | 106 +++++++++++++++++++++++++++++ 14 files changed, 238 insertions(+), 48 deletions(-) create mode 100644 src/Ui/media/img/logo.psd create mode 100644 src/util/RateLimit.py diff --git a/src/Config.py b/src/Config.py index 77f2e33b..627a6d4a 100644 --- a/src/Config.py +++ b/src/Config.py @@ -4,7 +4,7 @@ import ConfigParser class Config(object): def __init__(self): self.version = "0.2.9" - self.rev = 116 + self.rev = 119 self.parser = self.createArguments() argv = sys.argv[:] # Copy command line arguments argv = self.parseConfig(argv) # Add arguments from config file diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 4decdc62..51c616a6 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -117,6 +117,9 @@ class Connection: # Message loop for connection def messageLoop(self, firstchar=None): + if not self.sock: + self.log("Socket error: No socket found") + return False sock = self.sock try: if not firstchar: firstchar = sock.recv(1) @@ -317,4 +320,5 @@ class Connection: # Little cleanup del self.unpacker del self.sock + self.sock = None self.unpacker = None diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 77afa6a6..fdec9348 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -14,6 +14,7 @@ class ConnectionServer: self.port = port self.last_connection_id = 1 # Connection id incrementer self.log = logging.getLogger("ConnServer") + self.port_opened = None self.connections = [] # Connections self.ips = {} # Connection by ip diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index abb80871..a53aa85e 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -2,6 +2,7 @@ import os, msgpack, shutil, gevent, socket, struct, random from cStringIO import StringIO from Debug import Debug from Config import config +from util import RateLimit FILE_BUFF = 1024*512 @@ -14,6 +15,7 @@ class FileRequest: self.req_id = None self.sites = self.server.sites self.log = server.log + self.responded = False # Responded to the request def unpackAddress(self, packed): @@ -21,24 +23,34 @@ class FileRequest: def send(self, msg): - self.connection.send(msg) + if not self.connection.closed: + self.connection.send(msg) def response(self, msg): + if self.responded: + self.log.debug("Req id %s already responded" % self.req_id) + return if not isinstance(msg, dict): # If msg not a dict create a {"body": msg} msg = {"body": msg} msg["cmd"] = "response" msg["to"] = self.req_id + self.responded = True self.send(msg) # Route file requests def route(self, cmd, req_id, params): self.req_id = req_id + if cmd == "getFile": self.actionGetFile(params) elif cmd == "update": - self.actionUpdate(params) + event = "%s update %s %s" % (self.connection.id, params["site"], params["inner_path"]) + if not RateLimit.isAllowed(event): # There was already an updat for this file in the last 10 second + self.response({"ok": "File update queued"}) + RateLimit.callAsync(event, 10, self.actionUpdate, params) # If called more than once within 10 sec only keep the last update + elif cmd == "pex": self.actionPex(params) elif cmd == "ping": diff --git a/src/Site/Site.py b/src/Site/Site.py index 517020c0..c15b306c 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -96,7 +96,6 @@ class Site: # Download all file from content.json - @util.Noparallel(blocking=True) def downloadContent(self, inner_path, download_files=True, peer=None): s = time.time() self.log.debug("Downloading %s..." % inner_path) @@ -223,6 +222,7 @@ class Site: # Update content.json on peers + @util.Noparallel() def publish(self, limit=5, inner_path="content.json"): self.log.info( "Publishing to %s/%s peers..." % (limit, len(self.peers)) ) published = [] # Successfully published (Peer) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index fe6c3ff3..cc8b5e42 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -111,7 +111,7 @@ class UiRequest(object): if self.env["REQUEST_METHOD"] == "OPTIONS": headers.append(("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")) # Allow json access - if (self.env["REQUEST_METHOD"] == "OPTIONS" or not self.isAjaxRequest()) and status == 200 and (content_type == "text/css" or content_type == "application/javascript" or self.env["REQUEST_METHOD"] == "OPTIONS" or content_type.startswith("image")): # Cache Css, Js, Image files for 10min + if (self.env["REQUEST_METHOD"] == "OPTIONS" or not self.isAjaxRequest()) and status == 200 and (content_type == "text/css" or content_type.startswith("application") or self.env["REQUEST_METHOD"] == "OPTIONS" or content_type.startswith("image")): # Cache Css, Js, Image files for 10min headers.append(("Cache-Control", "public, max-age=600")) # Cache 10 min else: # Images, Css, Js headers.append(("Cache-Control", "no-cache, no-store, private, must-revalidate, max-age=0")) # No caching at all diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 7e874d55..83dacaf1 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -2,7 +2,7 @@ import json, gevent, time, sys, hashlib from Config import config from Site import SiteManager from Debug import Debug -from util import QueryJson +from util import QueryJson, RateLimit from Plugin import PluginManager @PluginManager.acceptPlugins @@ -149,21 +149,6 @@ class UiWebsocket(object): func(req["id"], params) - # - Actions - - - # Do callback on response {"cmd": "response", "to": message_id, "result": result} - def actionResponse(self, to, result): - if to in self.waiting_cb: - self.waiting_cb[to](result) # Call callback function - else: - self.log.error("Websocket callback not found: %s, %s" % (to, result)) - - - # Send a simple pong answer - def actionPing(self, to): - self.response(to, "pong") - - # Format site info def formatSiteInfo(self, site, create_user=True): content = site.content_manager.contents.get("content.json") @@ -198,21 +183,6 @@ class UiWebsocket(object): return ret - # Send site details - def actionSiteInfo(self, to, file_status = None): - ret = self.formatSiteInfo(self.site) - if file_status: # Client queries file status - if self.site.storage.isFile(file_status): # File exits, add event done - ret["event"] = ("file_done", file_status) - self.response(to, ret) - - - # Join to an event channel - def actionChannelJoin(self, to, channel): - if channel not in self.channels: - self.channels.append(channel) - - def formatServerInfo(self): return { "ip_external": bool(sys.modules["main"].file_server.port_opened), @@ -228,6 +198,36 @@ class UiWebsocket(object): } + # - Actions - + + # Do callback on response {"cmd": "response", "to": message_id, "result": result} + def actionResponse(self, to, result): + if to in self.waiting_cb: + self.waiting_cb[to](result) # Call callback function + else: + self.log.error("Websocket callback not found: %s, %s" % (to, result)) + + + # Send a simple pong answer + def actionPing(self, to): + self.response(to, "pong") + + + # Send site details + def actionSiteInfo(self, to, file_status = None): + ret = self.formatSiteInfo(self.site) + if file_status: # Client queries file status + if self.site.storage.isFile(file_status): # File exits, add event done + ret["event"] = ("file_done", file_status) + self.response(to, ret) + + + # Join to an event channel + def actionChannelJoin(self, to, channel): + if channel not in self.channels: + self.channels.append(channel) + + # Server variables def actionServerInfo(self, to): ret = self.formatServerInfo() @@ -261,18 +261,28 @@ class UiWebsocket(object): site.saveSettings() site.announce() - published = site.publish(5, inner_path) # Publish to 5 peer + event_name = "publish %s %s" % (site.address, inner_path) + thread = RateLimit.callAsync(event_name, 7, site.publish, 5, inner_path) # Only publish once in 7 second to 5 peers + notification = "linked" not in dir(thread) # Only display notification on first callback + thread.linked = True + thread.link(lambda thread: self.cbSitePublish(to, thread, notification)) # At the end callback with request id and thread + + + # Callback of site publish + def cbSitePublish(self, to, thread, notification=True): + site = self.site + published = thread.value if published>0: # Successfuly published - self.cmd("notification", ["done", "Content published to %s peers." % published, 5000]) + if notification: self.cmd("notification", ["done", "Content published to %s peers." % published, 5000]) self.response(to, "ok") - site.updateWebsocket() # Send updated site data to local websocket clients + if notification: site.updateWebsocket() # Send updated site data to local websocket clients else: if len(site.peers) == 0: - self.cmd("notification", ["info", "No peers found, but your content is ready to access."]) + if notification: self.cmd("notification", ["info", "No peers found, but your content is ready to access."]) self.response(to, "No peers found, but your content is ready to access.") else: - self.cmd("notification", ["error", "Content publish failed."]) + if notification: self.cmd("notification", ["error", "Content publish failed."]) self.response(to, "Content publish failed.") @@ -326,6 +336,7 @@ class UiWebsocket(object): return self.response(to, body) + # - Admin actions - # List all site info diff --git a/src/Ui/media/img/favicon.psd b/src/Ui/media/img/favicon.psd index 1eea4ccf8cec882098b1795cc3d10d31ee5564f8..1cf6f35f3892c8028770669cf6cc5cdec8e8d2f8 100644 GIT binary patch delta 11323 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
m~*)q0&K;0u$sT6 z;C7oz)pomJ%L1zo)Q$MU;1gOU=4(#|3%ijL@rETlVA?USAz_#j^&)CsRV${d=Jld} zM44=F!qj$?exyQ_O;H1;uG^@P8c}wY^_Z%hq(&M}<{27uqB=2f~~AW8?do@Miwl_PGx8$Kp>vuE2q z`L0N+$qKr9y&hs+NLr-qoN<)on(&^=P8MEl=Wo8A=D_^s z{+;j<`$AD6-8GsN=Ljh!)wcc8K0B4yX zgup=t1_=W9IYca?_a?+C8BPMhwE7V|fS45Gah&IJhFuSewga;PgtJ1i9Wp?eorv({ zyl;Sr2v^OGXn+ughd?waKx;=VZSa2J2UYF?JoN=e22X*mhIw&9f_H@<2`%w|bRR&ieHgWNDR}oe$fa%ts$j8EU7+Pksbv)P9PeEpaNs{@g4>52uO?@yA^x@w5k;RR*2MY1=WZd3YP)6 z1LMnTg`ijsza0R)45+X4DEP;ryIO(Zpa3ZWd>O!n06q^)vQPX~F#-xjrVh^->@>1*IB~YZZJDnhZwd0HM)k(0r+e(}6+eGQCEqjzTpjW`|*lFja<(SOFS= z1CJ^IHySj^ufkXfQ!fO$F6$KtcDO{?6G=6k%0MSX=fpr@jz%y?<9UUmxC1CayLwAe z+7m=JC0Gyma|R_0h~7HD$V>&8wvu=Qct)3kDg`b)bwwdmiIW&yr-^LC(G75?>%_61 zI2ov5(D??P5`KRGXYJ#hwNJ%4@l9~zo8ZJlottxeX3+1su()Ih3y2C5IgMqPPm{Lkr3)^Ylf&=|rJY8AZ zN>fX(aPg$t(nM2RR9rkg4r^d&D&#R2Pz}rKX=+&`7f{Vh>S$_7GZ#>OAvHkL#|6}f z)inBm3#iMLG`h?Mlxk@uO$DjAfI8SoQ`z}CmdelWq|rexp41DvXjG(PQBfDopReM~ zbqYY`eJm>PVrbqe4wTSMIrAJMpb2=8a05}O# z3|7gn3(+uvWg}H#C>4h+;acu}~ zzy)$RV-80KM)@qwAH;Di1q2C`qgt|cdK|+srHB+LSHn_83$-*KI+MYO93VBy*0O-( zbf6a+%k>(eYD3hVn6-vE5{1Bs2SFh)N6xrgK1>2(-&PNl=t(KFAi*0_KAOIEz9+fwicRTCnm#Fzo>@ zo>W0yG?fcXcs&P0ph|oM;)!_9B`%)c2XA8D;{vL0EjZa)a54y}=EdNAi^2IIpc;a} zDT5lgfO;I#pm$dIDyTjLFWwAZ3;}g{z`G%!`VY(pzn@S19^y%TDvpbaI1&{QI9(S$ zOFndXoA`}qzcZq^))rCG1TDb&QR0AFax~? z2Gk9Tqk|qgPB5)OSiSIVxSxHDAHgWCc*u{FznK4je*X5%{`~iC&zawQ{`SoN{P%6o incsW<_RRkL_ifJ^a?<#F&flKdpZ~t?xy4iGZ~q5q-%v3C delta 2496 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 F{{hYxQE~tP literal 0 HcmV?d00001 diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py index 553a5d69..0129ed4a 100644 --- a/src/Worker/Worker.py +++ b/src/Worker/Worker.py @@ -61,7 +61,7 @@ class Worker: self.task = None else: # Hash failed self.manager.log.debug("%s: Hash failed: %s, failed peers: %s" % (self.key, task["inner_path"], len(task["failed"]))) - task["failed"].append(self.key) + task["failed"].append(self.peer) self.task = None self.peer.hash_failed += 1 if self.peer.hash_failed >= 3: # Broken peer diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 62fc8d06..783d30f7 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -76,7 +76,7 @@ class WorkerManager: self.tasks.sort(key=self.taskSorter, reverse=True) # Sort tasks by priority and worker numbers for task in self.tasks: # Find a task if task["peers"] and peer not in task["peers"]: continue # This peer not allowed to pick this task - if peer.key in task["failed"]: continue # Peer already tried to solve this, but failed + if peer in task["failed"]: continue # Peer already tried to solve this, but failed return task @@ -145,6 +145,12 @@ class WorkerManager: task["peers"].append(peer) self.log.debug("Added peer %s to %s" % (peer.key, task["inner_path"])) self.startWorkers([peer]) + elif peer and peer in task["failed"]: + task["failed"].remove(peer) # New update arrived, remove the peer from failed peers + self.log.debug("Removed peer %s from failed %s" % (peer.key, task["inner_path"])) + self.startWorkers([peer]) + + if priority: task["priority"] += priority # Boost on priority return task["evt"] diff --git a/src/util/Event.py b/src/util/Event.py index 850e920d..0eab1c63 100644 --- a/src/util/Event.py +++ b/src/util/Event.py @@ -25,9 +25,26 @@ class Event(list): return self -if __name__ == "__main__": + + +def testBenchmark(): def say(pre, text): print "%s Say: %s" % (pre, text) + + import time + s = time.time() + onChanged = Event() + for i in range(1000): + onChanged.once(lambda pre: say(pre, "once"), "once") + print "Created 1000 once in %.3fs" % (time.time()-s) + onChanged("#1") + + + +def testUsage(): + def say(pre, text): + print "%s Say: %s" % (pre, text) + onChanged = Event() onChanged.once(lambda pre: say(pre, "once")) onChanged.once(lambda pre: say(pre, "once")) @@ -37,3 +54,7 @@ if __name__ == "__main__": onChanged("#1") onChanged("#2") onChanged("#3") + + +if __name__ == "__main__": + testBenchmark() diff --git a/src/util/Noparallel.py b/src/util/Noparallel.py index 1af0699f..68521944 100644 --- a/src/util/Noparallel.py +++ b/src/util/Noparallel.py @@ -1,5 +1,6 @@ import gevent, time + class Noparallel(object): # Only allow function running once in same time def __init__(self,blocking=True): self.threads = {} @@ -30,15 +31,21 @@ class Noparallel(object): # Only allow function running once in same time if key in self.threads: del(self.threads[key]) # Allowing it to run again return ret else: # No blocking just return the thread + thread.link(lambda thread: self.cleanup(key, thread)) return thread wrapper.func_name = func.func_name return wrapper + # Cleanup finished threads + def cleanup(self, key, thread): + if key in self.threads: del(self.threads[key]) + + class Test(): @Noparallel() - def count(self): - for i in range(5): + def count(self, num=5): + for i in range(num): print self, i time.sleep(1) return "%s return:%s" % (self, i) @@ -46,8 +53,8 @@ class Test(): class TestNoblock(): @Noparallel(blocking=False) - def count(self): - for i in range(5): + def count(self, num=5): + for i in range(num): print self, i time.sleep(1) return "%s return:%s" % (self, i) @@ -104,11 +111,33 @@ def testNoblocking(): print thread1.value, thread2.value, thread3.value, thread4.value print "Done." + +def testBenchmark(): + import time + def printThreadNum(): + import gc + from greenlet import greenlet + objs = [obj for obj in gc.get_objects() if isinstance(obj, greenlet)] + print "Greenlets: %s" % len(objs) + + printThreadNum() + test = TestNoblock() + s = time.time() + for i in range(3): + gevent.spawn(test.count, i+1) + print "Created in %.3fs" % (time.time()-s) + printThreadNum() + time.sleep(5) + + + if __name__ == "__main__": from gevent import monkey monkey.patch_all() + testBenchmark() print "Testing blocking mode..." testBlocking() print "Testing noblocking mode..." testNoblocking() + print [instance.threads for instance in registry] diff --git a/src/util/RateLimit.py b/src/util/RateLimit.py new file mode 100644 index 00000000..3693f0b3 --- /dev/null +++ b/src/util/RateLimit.py @@ -0,0 +1,106 @@ +import time +import gevent +import logging + +log = logging.getLogger("RateLimit") + +called_db = {} +queue_db = {} + +# Register event as called +# Return: None +def called(event): + called_db[event] = time.time() + + +# Check if calling event is allowed +# Return: True if allowed False if not +def isAllowed(event, allowed_again=10): + last_called = called_db.get(event) + if not last_called: # Its not called before + return True + elif time.time()-last_called >= allowed_again: + del called_db[event] # Delete last call time to save memory + return True + else: + return False + + +def callQueue(event): + func, args, kwargs, thread = queue_db[event] + log.debug("Calling: %s" % event) + del called_db[event] + del queue_db[event] + return func(*args, **kwargs) + + + +# Rate limit and delay function call if needed, If the function called again within the rate limit interval then previous queued call will be dropped +# Return: Immedietly gevent thread +def callAsync(event, allowed_again=10, func=None, *args, **kwargs): + if isAllowed(event): # Not called recently, call it now + called(event) + # print "Calling now" + return gevent.spawn(func, *args, **kwargs) + else: # Called recently, schedule it for later + time_left = allowed_again-max(0, time.time()-called_db[event]) + log.debug("Added to queue (%.2fs left): %s " % (time_left, event)) + if not queue_db.get(event): # Function call not queued yet + thread = gevent.spawn_later(time_left, lambda: callQueue(event)) # Call this function later + queue_db[event] = (func, args, kwargs, thread) + return thread + else: # Function call already queued, just update the parameters + thread = queue_db[event][3] + queue_db[event] = (func, args, kwargs, thread) + return thread + + +# Rate limit and delay function call if needed +# Return: Wait for execution/delay then return value +def call(event, allowed_again=10, func=None, *args, **kwargs): + if isAllowed(event): # Not called recently, call it now + called(event) + # print "Calling now" + return func(*args, **kwargs) + + else: # Called recently, schedule it for later + time_left = max(0, allowed_again-(time.time()-called_db[event])) + # print "Time left: %s" % time_left, args, kwargs + log.debug("Calling sync (%.2fs left): %s" % (time_left, event)) + time.sleep(time_left) + called(event) + back = func(*args, **kwargs) + if event in called_db: + del called_db[event] + return back + + + +if __name__ == "__main__": + from gevent import monkey + monkey.patch_all() + import random + + def publish(inner_path): + print "Publishing %s..." % inner_path + return 1 + + def cb(thread): + print "Value:", thread.value + + print "Testing async spam requests rate limit to 1/sec..." + for i in range(3000): + thread = callAsync("publish content.json", 1, publish, "content.json %s" % i) + time.sleep(float(random.randint(1,20))/100000) + print thread.link(cb) + print "Done" + + time.sleep(2) + + print "Testing sync spam requests rate limit to 1/sec..." + for i in range(5): + call("publish data.json", 1, publish, "data.json %s" % i) + time.sleep(float(random.randint(1,100))/100) + print "Done" + + print called_db, queue_db