diff --git a/docs/ref/settings.rst b/docs/ref/settings.rst index 77b998b3c..ae67516d7 100644 --- a/docs/ref/settings.rst +++ b/docs/ref/settings.rst @@ -81,98 +81,6 @@ Default: ``True`` Whether to split HTTP cache storage in several dirs for performance. -.. setting:: CLUSTER_LOGDIR - -CLUSTER_LOGDIR --------------- - -Default: ``''`` (empty string) - -The directory to use for cluster logging. - -.. setting:: CLUSTER_MASTER_CACHEFILE - -CLUSTER_MASTER_CACHEFILE ------------------------- - -Default: ``''`` - -The file to use for storing the state of the cluster master, before shotting -down. And also used for restoring the state on start up. If not set, state -won't be persisted. - -.. setting:: CLUSTER_MASTER_ENABLED - -CLUSTER_MASTER_ENABLED ------------------------- - -Default: ``False`` - -A boolean which specifies whether to enabled the cluster master. - -.. setting:: CLUSTER_MASTER_NODES - -CLUSTER_MASTER_NODES --------------------- - -Default: ``{}`` - -A dict which defines the nodes of the cluster. The keys are the node/worker -names and the values are the worker URLs. - -Example:: - - CLUSTER_MASTER_NODES = { - 'local': 'localhost:8789', - 'remote': 'someworker.example.com:8789', - } - -.. setting:: CLUSTER_MASTER_POLL_INTERVAL - -CLUSTER_MASTER_POLL_INTERVAL ----------------------------- - -Default: ``60`` - -The amount of time (in secs) that the master should wait before polling the -workers. - -.. setting:: CLUSTER_MASTER_PORT - -CLUSTER_MASTER_PORT -------------------- - -Default: ``8790`` - -The port where the cluster master will listen. - -.. setting:: CLUSTER_WORKER_ENABLED - -CLUSTER_WORKER_ENABLED ------------------------- - -Default: ``False`` - -A boolean which specifies whether to enabled the cluster master. - -.. setting:: CLUSTER_WORKER_MAXPROC - -CLUSTER_WORKER_MAXPROC ------------------------- - -Default: ``4`` - -The maximum number of process that the cluster worker will be allowed to spawn. - -.. setting:: CLUSTER_WORKER_PORT - -CLUSTER_WORKER_PORT -------------------- - -Default: ``8789`` - -The port where the cluster worker will listen. - .. setting:: COMMANDS_MODULE COMMANDS_MODULE diff --git a/scrapy/conf/default_settings.py b/scrapy/conf/default_settings.py index 1162eb094..ac0113ef3 100644 --- a/scrapy/conf/default_settings.py +++ b/scrapy/conf/default_settings.py @@ -23,18 +23,6 @@ BOT_VERSION = '1.0' CLOSEDOMAIN_TIMEOUT = 0 CLOSEDOMAIN_ITEMPASSED = 0 -CLUSTER_LOGDIR = '' - -CLUSTER_MASTER_PORT = 8790 -CLUSTER_MASTER_ENABLED = 0 -CLUSTER_MASTER_POLL_INTERVAL = 60 -CLUSTER_MASTER_NODES = {} -CLUSTER_MASTER_STATEFILE = "" - -CLUSTER_WORKER_ENABLED = 0 -CLUSTER_WORKER_MAXPROC = 4 -CLUSTER_WORKER_PORT = 8789 - COMMANDS_MODULE = '' COMMANDS_SETTINGS_MODULE = '' @@ -199,6 +187,3 @@ WEBCONSOLE_ENABLED = True WEBCONSOLE_PORT = 6080 WEBCONSOLE_LOGFILE = None -# this setting is used by the cluster master to pass additional settings to -# workers at connection time -GLOBAL_CLUSTER_SETTINGS = [] diff --git a/scrapy/contrib/cluster/__init__.py b/scrapy/contrib/cluster/__init__.py deleted file mode 100644 index 2c6267ad8..000000000 --- a/scrapy/contrib/cluster/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from scrapy.contrib.cluster.worker.manager import ClusterWorker -from scrapy.contrib.cluster.master.web import ClusterMasterWeb -from scrapy.contrib.cluster.crawler.manager import ClusterCrawler diff --git a/scrapy/contrib/cluster/crawler/__init__.py b/scrapy/contrib/cluster/crawler/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/scrapy/contrib/cluster/crawler/manager.py b/scrapy/contrib/cluster/crawler/manager.py deleted file mode 100644 index 097206855..000000000 --- a/scrapy/contrib/cluster/crawler/manager.py +++ /dev/null @@ -1,41 +0,0 @@ -import os - -from twisted.spread import pb -from twisted.internet import reactor - -from scrapy.conf import settings -from scrapy import log -from scrapy.core.manager import scrapymanager -from scrapy.core.exceptions import NotConfigured - -class ClusterCrawlerBroker(pb.Referenceable): - """ClusterCrawlerBroker is the class that's used for communication between - the cluster worker and the crawling proces""" - - def __init__(self, crawler, remote): - self.__remote = remote - self.__crawler = crawler - deferred = self.__remote.callRemote("register_crawler", os.getpid(), self) - deferred.addCallbacks(callback=lambda x: None, errback=lambda reason: log.msg(reason, log.ERROR)) - - def remote_stop(self): - scrapymanager.stop() - -class ClusterCrawler(object): - """ClusterCrawler is an extension that instances a ClusterCrawlerBroker - which is used to control a crawling process from the cluster worker. It - also registers that broker to the local cluster worker""" - - def __init__(self): - if not settings.getbool('CLUSTER_CRAWLER_ENABLED'): - raise NotConfigured - - self.worker = None - - factory = pb.PBClientFactory() - reactor.connectTCP("localhost", settings.getint('CLUSTER_WORKER_PORT'), factory) - d = factory.getRootObject() - def _set_worker(obj): - self.worker = ClusterCrawlerBroker(self, obj) - d.addCallbacks(callback=_set_worker, errback=lambda reason: log.msg(reason, log.ERROR)) - diff --git a/scrapy/contrib/cluster/hooks/__init__.py b/scrapy/contrib/cluster/hooks/__init__.py deleted file mode 100644 index a66f00650..000000000 --- a/scrapy/contrib/cluster/hooks/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -""" -This module contains pre-run hooks that can be attached to scrapy workers. - -Pre-run hooks must be callable objects (ie. functions) which implement this -interface: - -pre_hook(domain, spider_settings) - -domain is the domain to be scraped -spider_settings is the settings to use to scrape it - -Values returned from the pre-run hooks will be ignored. -""" diff --git a/scrapy/contrib/cluster/hooks/svn.py b/scrapy/contrib/cluster/hooks/svn.py deleted file mode 100644 index b3f7a6d6c..000000000 --- a/scrapy/contrib/cluster/hooks/svn.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -This module contains hooks for updating code via svn. Useful for running before -starting to crawl a domain, for example to update spider code -""" - -import pysvn - -from scrapy.conf import settings -from scrapy import log - -SVN_DIR = settings['SVN_DIR'] -SVN_USER = settings['SVN_USER'] -SVN_PASS = settings['SVN_PASS'] - -def svnup(domain, spider_settings): - c = pysvn.Client() - c.callback_get_login = lambda x,y,z: (True, SVN_USER, SVN_PASS, False) - try: - r = c.update(SVN_DIR) - log.msg("ClusterWorker: SVN code updated to revision %s (triggered by domain %s)" % \ - (r[0].number, domain), level=log.DEBUG) - except pysvn.ClientError, e: - log.msg("ClusterWorker: unable to update svn code - %s" % e, level=log.WARNING) diff --git a/scrapy/contrib/cluster/master/__init__.py b/scrapy/contrib/cluster/master/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/scrapy/contrib/cluster/master/manager.py b/scrapy/contrib/cluster/master/manager.py deleted file mode 100644 index 6166fb421..000000000 --- a/scrapy/contrib/cluster/master/manager.py +++ /dev/null @@ -1,390 +0,0 @@ -from __future__ import with_statement - -import datetime -import cPickle as pickle - -from scrapy.xlib.pydispatch import dispatcher -from twisted.spread import pb -from twisted.internet import reactor - -from scrapy.core import signals -from scrapy import log -from scrapy.core.engine import scrapyengine -from scrapy.core.exceptions import NotConfigured -from scrapy.contrib.cluster.worker.manager import ResponseCode -from scrapy.conf import settings - -def my_import(name): - mod = __import__(name) - components = name.split('.') - for comp in components[1:]: - mod = getattr(mod, comp) - return mod - -class ClusterNodeBroker(pb.Referenceable): - - def __init__(self, worker, name, master): - self.unsafeTracebacks = True - self._worker = worker - self.alive = False - self.name = name - self.master = master - self.available = True - try: - deferred = self._worker.callRemote("set_master", self) - except pb.DeadReferenceError: - self._set_status(None) - log.msg("ClusterMaster: Lost connection to node %s." % self.name, log.ERROR) - else: - def _eb(failure): - self._logfailure("Error while setting master to worker node", failure) - deferred.addCallbacks(callback=self._set_status, errback=_eb) - - def status_as_dict(self, verbosity=1): - if verbosity == 0: - return - status = {"alive": self.alive} - if self.alive: - if verbosity == 1: - # dont show spider settings - status["running"] = [] - for proc in self.running: - proccopy = proc.copy() - del proccopy["settings"] - status["running"].append(proccopy) - elif verbosity == 2: - status["running"] = self.running - status["maxproc"] = self.maxproc - status["freeslots"] = self.maxproc - len(self.running) - status["available"] = self.available - status["starttime"] = self.starttime - status["timestamp"] = self.timestamp - status["loadavg"] = self.loadavg - return status - - def update_status(self): - """Update status from this worker. This is called periodically.""" - try: - deferred = self._worker.callRemote("status") - except pb.DeadReferenceError: - self._set_status(None) - log.msg("ClusterMaster: Lost connection to worker=%s." % self.name, log.ERROR) - else: - def _eb(failure): - self._logfailure("Error while updating status", failure) - deferred.addCallbacks(callback=self._set_status, errback=_eb) - - def stop(self, domain): - try: - deferred = self._worker.callRemote("stop", domain) - except pb.DeadReferenceError: - self._set_status(None) - log.msg("ClusterMaster: Lost connection to worker=%s." % self.name, log.ERROR) - else: - def _eb(failure): - self._logfailure("Error while stopping domain=%s" % domain, failure) - deferred.addCallbacks(callback=self._set_status, errback=_eb) - - def run(self, domain_info): - """Run the given domain. - - domain_info is a dict of keys: - domain - the domain to run - settings - the settings to use - priority - the priority to use - """ - - domain = domain_info['domain'] - priority = domain_info['priority'] - spider_settings = domain_info['settings'] - dsettings = self.master.compute_final_spider_settings(domain, spider_settings) - - def _run_errback(failure): - self._logfailure("Error while running domain=%s" % domain, failure) - self.master.loading.remove(domain) - newprio = priority - 1 # increase priority for reschedule - self.master.reschedule([domain], spider_settings, newprio, - reason="error while try to run it") - - def _run_callback(status): - if status['callresponse'][0] == ResponseCode.NO_FREE_SLOT: - log.msg("ClusterMaster: No available slots at worker=%s when trying to run domain=%s" - % (self.name, domain), log.WARNING) - self.master.loading.remove(domain) - newprio = priority - 1 # increase priority for rerunning asap - self.master.reschedule([domain], spider_settings, newprio, - reason="no available slots at worker=%s" % self.name) - elif status['callresponse'][0] == ResponseCode.DOMAIN_ALREADY_RUNNING: - log.msg("ClusterMaster: Already running domain=%s at worker=%s" % - (domain, self.name), log.WARNING) - self.master.loading.remove(domain) - self.master.reschedule([domain], spider_settings, priority, - reason="domain already running at worker=%s" % self.name) - - try: - log.msg("ClusterMaster: Running domain=%s at worker=%s" % (domain, self.name), log.DEBUG) - deferred = self._worker.callRemote("run", domain, dsettings) - except pb.DeadReferenceError: - self._set_status(None) - log.msg("ClusterMaster: Lost connection to worker=%s." % self.name, log.ERROR) - else: - deferred.addCallbacks(callback=_run_callback, errback=_run_errback) - - def remote_update(self, worker_status, domain, domain_status): - """Called remotely form worker when domains finish to update status""" - self._set_status(worker_status) - stats = self.master.statistics - dstats = stats["domains"] - - if domain in self.master.loading and domain_status == "running": - self.master.loading.remove(domain) - dstats["running"].add(domain) - elif domain_status in ("done", "terminated"): - dstats["running"].remove(domain) - dstats["scraped"][domain] = dstats["scraped"].get(domain, 0) + 1 - stats["scraped_count"] = stats.get("scraped_count", 0) + 1 - if domain in dstats["lost"]: - dstats["lost"].remove(domain) - - log.msg("ClusterMaster: Changed status to <%s> for domain=%s at worker=%s" % - (domain_status, domain, self.name)) - - def _logfailure(self, msg, failure): - log.msg("ClusterMaster: %s (worker=%s)\n%s" % (msg, self.name, failure), log.ERROR) - - def _set_status(self, status): - if not status: - self.alive = False - else: - self.alive = True - self.running = status['running'] - self.maxproc = status['maxproc'] - self.starttime = status['starttime'] - self.timestamp = status['timestamp'] - self.loadavg = status['loadavg'] - self.logdir = status['logdir'] - free_slots = self.maxproc - len(self.running) - - # load domains by one, so to mix up better the domain loading between nodes. - # The next one in the same node will be loaded - # when there is no loading domain or in the next status update. - # This way also we load the nodes softly - if self.available and free_slots > 0 and self.master.pending: - pending = self.master.pending.pop(0) - # If domain already running in some node, reschedule with same - # priority (so it will be run later) - if pending['domain'] in self.master.running or pending['domain'] in self.master.loading: - self.master.reschedule([pending['domain']], pending['settings'], - pending['priority'], reason="domain already running in other worker") - else: - self.run(pending) - self.master.loading.append(pending['domain']) - - -class ScrapyPBClientFactory(pb.PBClientFactory): - - noisy = False - - def __init__(self, master, nodename): - pb.PBClientFactory.__init__(self) - self.unsafeTracebacks = True - self.master = master - self.nodename = nodename - - def clientConnectionLost(self, *args, **kargs): - pb.PBClientFactory.clientConnectionLost(self, *args, **kargs) - self.master.remove_node(self.nodename) - log.msg("ClusterMaster: Lost connection to worker=%s. Node removed" % self.nodename) - - -class ClusterMaster(object): - - def __init__(self): - - if not settings.getbool('CLUSTER_MASTER_ENABLED'): - raise NotConfigured - - self.statefile = settings['CLUSTER_MASTER_STATEFILE'] - if not self.statefile: - raise NotConfigured("ClusterMaster: Missing CLUSTER_MASTER_STATEFILE setting") - - # import groups settings - if settings.getbool('GROUPSETTINGS_ENABLED'): - self.get_spider_groupsettings = my_import(settings["GROUPSETTINGS_MODULE"]).get_spider_groupsettings - else: - self.get_spider_groupsettings = lambda x: {} - - # load pending domains - try: - statefile = open(self.statefile, "r") - self.pending = pickle.load(statefile) - log.msg("ClusterMaster: Loaded state from %s" % self.statefile) - except IOError: - self.pending = [] - - self.loading = [] - self.nodes = {} - self.nodesconf = settings.get('CLUSTER_MASTER_NODES', {}) - self.start_time = datetime.datetime.utcnow() - - # for more info about statistics see self.update_nodes() and ClusterNodeBroker.remote_update() - self.statistics = {"domains": {"running": set(), "scraped": {}, "lost_count": {}, "lost": set()}, "scraped_count": 0 } - self.global_settings = {} - - # load cluster global settings - for sname in settings.getlist('GLOBAL_CLUSTER_SETTINGS'): - self.global_settings[sname] = settings[sname] - - dispatcher.connect(self._engine_started, signal=signals.engine_started) - dispatcher.connect(self._engine_stopped, signal=signals.engine_stopped) - - def load_nodes(self): - """Loads nodes listed in CLUSTER_MASTER_NODES setting""" - for name, hostport in self.nodesconf.iteritems(): - self.load_node(name, hostport) - - def load_node(self, name, hostport): - """Creates the remote reference for a worker node""" - server, port = hostport.split(":") - port = int(port) - log.msg("ClusterMaster: Connecting to worker=%s (%s)..." % (name, hostport)) - factory = ScrapyPBClientFactory(self, name) - try: - reactor.connectTCP(server, port, factory) - except Exception, err: - log.msg("ClusterMaster: Could not connect to worker=%s (%s): %s" % - (name, hostport, err), log.ERROR) - else: - def _eb(failure): - log.msg("ClusterMaster: Could not connect to worker=%s (%s): %s" % - (name, hostport, failure.value), log.ERROR) - - d = factory.getRootObject() - d.addCallbacks(callback=lambda obj: self.add_node(obj, name), errback=_eb) - - def update_nodes(self): - """Update worker nodes statistics""" - for name, hostport in self.nodesconf.iteritems(): - if name in self.nodes and self.nodes[name].alive: - self.nodes[name].update_status() - else: - self.load_node(name, hostport) - - dstats = self.statistics["domains"] - real_running = set(self.running.keys()) - lost = dstats["running"].difference(real_running) - for domain in lost: - dstats["lost_count"][domain] = dstats["lost_count"].get(domain, 0) + 1 - dstats["lost"] = dstats["lost"].union(lost) - - def add_node(self, cworker, name): - """Add node given its node""" - node = ClusterNodeBroker(cworker, name, self) - self.nodes[name] = node - log.msg("ClusterMaster: Added worker=%s" % name) - - def remove_node(self, nodename): - del self.nodes[nodename] - - def disable_node(self, name): - self.nodes[name].available = False - - def enable_node(self, name): - self.nodes[name].available = True - - def _schedule(self, domains, spider_settings=None, priority=20): - """Private method which performs the schedule of the given domains, - with the given priority. Used for both scheduling and rescheduling.""" - insert_pos = len([p for p in self.pending if p['priority'] <= priority]) - for domain in domains: - pd = self.get_first_pending(domain) - if pd: # domain already pending, so just change priority if new is higher - if priority < pd['priority']: - self.pending.remove(pd) - pd['priority'] = priority - self.pending.insert(insert_pos, pd) - else: - self.pending.insert(insert_pos, - {'domain': domain, 'settings': spider_settings, 'priority': priority}) - - def compute_final_spider_settings(self, domain, spider_settings=None): - """Return merged dictionary with final settings to run spider""" - final = dict(self.get_spider_groupsettings(domain) or {}) - final.update(self.global_settings) - final.update(spider_settings or {}) - return final - - def schedule(self, domains, spider_settings=None, priority=20): - """Schedule the given domains, with the given priority""" - self._schedule(domains, spider_settings, priority) - log.msg("clustermaster: Scheduled domains=%s with priority=%s" % - (','.join(domains), priority), log.DEBUG) - - def reschedule(self, domains, spider_settings=None, priority=20, reason=None): - """Reschedule the given domains, with the given priority""" - self._schedule(domains, spider_settings, priority) - log.msg("clustermaster: Rescheduled domains=%s with priority=%s reason='%s'" % - (','.join(domains), priority, reason), log.DEBUG) - - - def stop(self, domains): - """Stop the given domains""" - to_stop = {} - for domain in domains: - node = self.running.get(domain, None) - if node: - if node.name not in to_stop: - to_stop[node.name] = [] - to_stop[node.name].append(domain) - - for nodename, domains in to_stop.iteritems(): - for domain in domains: - self.nodes[nodename].stop(domain) - - def remove(self, domains): - """Remove all scheduled instances of the given domains (if they haven't - started yet). Otherwise use stop() to stop running domains""" - - self.pending = [p for p in self.pending if p['domain'] not in domains] - - def discard(self, domains): - """Stop and remove all running and pending instances of the given - domains""" - self.remove(domains) - self.stop(domains) - - @property - def running(self): - """Return dict of running domains as domain -> node""" - d = {} - for node in self.nodes.itervalues(): - for proc in node.running: - d[proc['domain']] = node - return d - - def get_first_pending(self, domain): - """Return first pending instance of a given domain""" - for p in self.pending: - if domain == p['domain']: - return p - - def get_pending(self, verbosity=1): - if verbosity == 1: - pending = [] - for p in self.pending: - pp = p.copy() - del pp["settings"] - pending.append(pp) - return pending - elif verbosity == 2: - return self.pending - return - - def _engine_started(self): - self.load_nodes() - scrapyengine.addtask(self.update_nodes, settings.getint('CLUSTER_MASTER_POLL_INTERVAL', 60)) - - def _engine_stopped(self): - with open(self.statefile, "w") as f: - pickle.dump(self.pending, f) - log.msg("ClusterMaster: Saved state in %s" % self.statefile) diff --git a/scrapy/contrib/cluster/master/web.py b/scrapy/contrib/cluster/master/web.py deleted file mode 100644 index 24114d442..000000000 --- a/scrapy/contrib/cluster/master/web.py +++ /dev/null @@ -1,239 +0,0 @@ -import datetime - -from scrapy.xlib.pydispatch import dispatcher - -from scrapy.spider import spiders -from scrapy.management.web import banner, webconsole_discover_module -from scrapy.contrib.cluster.master.manager import ClusterMaster -from scrapy.utils.serialization import serialize - -class ClusterMasterWeb(ClusterMaster): - webconsole_id = 'cluster_master' - webconsole_name = 'Cluster master' - - def __init__(self): - ClusterMaster.__init__(self) - - dispatcher.connect(self.webconsole_discover_module, signal=webconsole_discover_module) - - def webconsole_render(self, wc_request): - changes = "" - if wc_request.path == '/cluster_master/nodes/': - return self.render_nodes(wc_request) - elif wc_request.path == '/cluster_master/domains/': - return self.render_domains(wc_request) - elif wc_request.path == '/cluster_master/ws/': - return self.webconsole_control(wc_request, ws=True) - elif wc_request.args: - changes = self.webconsole_control(wc_request) - - s = self.render_header() - - s += "
| Name | Available | Running | Load.avg | |
|---|---|---|---|---|
| %s | %s | %s | %d/%d | %s |