source: trunk/factory/src/factory.py @ 335

Last change on this file since 335 was 328, checked in by sander, 11 years ago

Log jobs without backend

  • Property svn:executable set to *
File size: 6.6 KB
Line 
1#!/usr/bin/env python
2# Officeshots.org - Test your office documents in different applications
3# Copyright (C) 2009 Stichting Lone Wolves
4# Written by Sander Marechal <s.marechal@jejik.com>
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU Affero General Public License as
8# published by the Free Software Foundation, either version 3 of the
9# License, or (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU Affero General Public License for more details.
15#
16# You should have received a copy of the GNU Affero General Public License
17# along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
19"""
20This is the core Factory class and main method of the Officeshots
21factory. Execute it with -h or --help to see the available options
22"""
23
24import os
25import sys
26import time
27import socket
28import logging
29import ConfigParser
30
31from optparse import OptionParser
32from backends import BackendException
33from xmlrpclib import ServerProxy, Error, Fault
34
35LOGLEVELS = {'debug': logging.DEBUG,
36             'info': logging.INFO,
37             'warning': logging.WARNING,
38             'error': logging.ERROR,
39             'critical': logging.CRITICAL}
40
41class Factory:
42        """
43        The core factory class communicates with the Officeshots server and passes
44        requests on to any of the available workers
45        """
46
47        def configure(self, options):
48                self.options = options
49                self.config = ConfigParser.RawConfigParser()
50                self.config.read(os.path.abspath(self.options.config_file))
51
52                # Configure logging
53                if self.options.debug:
54                        logging.basicConfig(format = self.config.get('global', 'log_format'), level = logging.DEBUG)
55                else:
56                        level = LOGLEVELS.get(self.config.get('global', 'log_level'), logging.NOTSET)
57                        try:
58                                logging.basicConfig(
59                                                format = self.config.get('global', 'log_format'),
60                                                filename = self.config.get('global', 'log_file'),
61                                                filemode = 'a',
62                                                level = level
63                                )
64                        except IOError, e:
65                                print "Logfile IO error. Please make sure that the log_file setting is correct in config.ini"
66                                sys.exit(1)
67
68                # Load factory name
69                self.name = self.config.get('global', 'factory_name')
70
71                # Configure the XMLRPC proxy
72                transport_name = self.config.get('global', 'transport')
73                transport = self.load('transports.' + transport_name, 'SSLTransport')
74                if transport is None:
75                        print "Transport %s could not be loaded" % transport_name
76                        sys.exit(1)
77
78                transport = transport(
79                        self.config.get('global', 'tls_key_file'),
80                        self.config.get('global', 'tls_certificate_file')
81                )
82                self.proxy = ServerProxy(self.config.get('global', 'xmlrpc_endpoint'), transport=transport, verbose=self.options.debug)
83
84                # Load all the backends
85                self.backends = []
86                sections = [s.strip() for s in self.config.get('global', 'backends').split(',')]
87
88                for section in sections:
89                        backend_name = self.config.get(section, 'backend')
90                        if backend_name is None:
91                                continue
92
93                        backend = self.load('backends.' + backend_name.lower(), backend_name)
94                        if backend is None:
95                                continue
96
97                        backend = backend(self.options, self.config, section)
98                        try:
99                                backend.initialize()
100                        except BackendException, e:
101                                logging.warning('Error initializing backend %s for %s: ' + str(e), backend_name, section)
102                                continue
103
104                        self.backends.append(backend)
105
106                if len(self.backends) == 0:
107                        logging.critical('No backends could be loaded')
108                        return False
109
110                # Configuration succeeded
111                return True
112
113        def load(self, package, class_name):
114                """
115                A convenience function to import class_name from package
116                """
117                try:
118                        module = __import__(package, globals(), locals(), class_name)
119                except ImportError, e:
120                        logging.warning('Error importing %s from %s. ' + str(e), class_name, package)
121                        return None
122               
123                return getattr(module, class_name)
124
125
126
127        def systemload(self):
128                """
129                Return the average system load
130                """
131                try:
132                        return max(os.getloadavg())
133                except (AttributeError, OSError):
134                        return None
135
136        def loop(self):
137                """
138                A single iteration of the main loop.
139                Return False to terminate the application
140                """
141                # Keep an eye on system load
142                load = self.systemload()
143                maxload = self.config.getfloat('global', 'load_max')
144                if load > maxload:
145                        logging.debug("Systemload %.2f exceeds limit %.2f. Sleeping." % (load, maxload))
146                        time.sleep(60)
147                        return True
148
149                # Poll for a job. Sleep for a minute if there's no work
150                try:
151                        job = self.proxy.jobs.poll(self.name)
152                except socket.error, ex:
153                        logging.warning(ex)
154                        logging.warning("Cannot connect to server. Sleeping.")
155                        time.sleep(60)
156                        return True
157                except Fault, ex:
158                        logging.error("XML-RPC fault. Poll failed. Sleeping.")
159                        time.sleep(60)
160                        return True
161
162                if len(job) == 0:
163                        logging.debug('No jobs found. Sleeping.')
164                        time.sleep(60)
165                        return True
166
167                # We have work. Find a backend to pass it off to
168                for backend in self.backends:
169                        if backend.can_process(job):
170                                try:
171                                        (format, document) = backend.process(job)
172                                except BackendException, ex:
173                                        logging.warning(ex)
174                                        if not ex.recoverable:
175                                                logging.warning('Removing backend')
176                                                self.backends.remove(backend)
177                                                if len(self.backends) == 0:
178                                                        logging.critical('No more active backends.')
179                                                        return False
180                                        return True
181
182                                try:
183                                        self.proxy.jobs.finish(self.name, job['job'], format, document)
184                                except socket.error, ex:
185                                        logging.warning("Cannot connect to server. Job cannot be finished. Sleeping.")
186                                        time.sleep(60)
187                                        return True
188                                except Fault, ex:
189                                        logging.error("XML-RPC fault. Finishing failed. Sleeping.")
190                                        time.sleep(60)
191                                        return True
192
193                                logging.info('Processed job %s', job['job'])
194                                return True
195
196                logging.warning('No suitable backend found for job')
197                logging.debug('Application: %s, version: %s, doctype: %s, format: %s' % (job['application'], job['version'], job['doctype'], job['format']))
198
199                # TODO: Do something smart about that, like deactivating the related worker on the server
200                return True
201
202
203        def run(self):
204                """
205                This is the main execution loop
206                """
207                logging.info('Starting factory server.')
208                while self.loop():
209                        pass
210
211
212
213if __name__ == "__main__":
214        parser = OptionParser(usage='Usage: %prog [options]')
215        parser.add_option('-c', '--config-file', action='store', type='string', dest='config_file',
216                        default='../conf/config.ini', help='Full path to the configuration file to read.')
217        parser.add_option('-d', '--debug', action='store_true', dest='debug',
218                        help='When in debug mode all errors will be written to the console and logging will be set to debug.')
219        (options, args) = parser.parse_args()
220
221        factory = Factory()
222        if factory.configure(options):
223                factory.run()
Note: See TracBrowser for help on using the repository browser.