odoo web机制

一、wsgi实现

    1.1 application

	路径:odoo.service.wsgi_server.application
	实现代码:
	def application(environ, start_response):
	    # FIXME: is checking for the presence of HTTP_X_FORWARDED_HOST really useful?
	    #        we're ignoring the user configuration, and that means we won't
	    #        support the standardised Forwarded header once werkzeug supports
	    #        it
	    if config['proxy_mode'] and 'HTTP_X_FORWARDED_HOST' in environ:
	        return ProxyFix(application_unproxied)(environ, start_response)
	    else:
	        return application_unproxied(environ, start_response)

    1.2 server

	负责启动wsgi,加载application()。odoo由三个类定义server,分别是ThreadedServer、
	PreforkServer、GeventServer,通过调用全局方法start来实现对application的启动,该方法路径
	odoo.service.server
def start(preload=None, stop=False):
    """ Start the odoo http server and cron processor.
    """
    global server

    load_server_wide_modules()
    odoo.service.wsgi_server._patch_xmlrpc_marshaller()
	#  
    if odoo.evented:
    	#  GeventServer启动app
        server = GeventServer(odoo.service.wsgi_server.application)
    elif config['workers']:
        if config['test_enable'] or config['test_file']:
            _logger.warning("Unit testing in workers mode could fail; use --workers 0.")
		#  PreforkServer启动app
        server = PreforkServer(odoo.service.wsgi_server.application)

        # Workaround for Python issue24291, fixed in 3.6 (see Python issue26721)
        if sys.version_info[:2] == (3,5):
            # turn on buffering also for wfile, to avoid partial writes (Default buffer = 8k)
            werkzeug.serving.WSGIRequestHandler.wbufsize = -1
    else:
    	# ThreadedServer启动app
        server = ThreadedServer(odoo.service.wsgi_server.application)

    watcher = None
    if 'reload' in config['dev_mode'] and not odoo.evented:
        if inotify:
            watcher = FSWatcherInotify()
            watcher.start()
        elif watchdog:
            watcher = FSWatcherWatchdog()
            watcher.start()
        else:
            if os.name == 'posix' and platform.system() != 'Darwin':
                module = 'inotify'
            else:
                module = 'watchdog'
            _logger.warning("'%s' module not installed. Code autoreload feature is disabled", module)
    if 'werkzeug' in config['dev_mode']:
        server.app = DebuggedApplication(server.app, evalex=True)

    rc = server.run(preload, stop)

    if watcher:
        watcher.stop()
    # like the legend of the phoenix, all ends with beginnings
    if getattr(odoo, 'phoenix', False):
        _reexec()

    return rc if rc else 0

二、三种服务启动方式

    2.1 gevent模式

	   当启动参数配置为gevent时,odoo采用gevent模式启动,参考1.2的start方法
	   伪代码:
if len(sys.argv) > 1 and sys.argv[1] == 'gevent':
	sys.argv.remove('gevent')
    import gevent.monkey
    import psycopg2
    from gevent.socket import wait_read, wait_write
    gevent.monkey.patch_all()
    pass
    #  设置event参数标志位
    evented = True
      gevent服务的功能:监听一个longpolling_port端口,实现longpolling长连接

    2.2 multiprocessing模式

     当配置设置了worker参数且不为0时,odoo采用该模式启动。 worker数采用"Rule of thumb"为(#cpu*2+1)
	
	 开启的服务有HTTP、Cron、Longpolling,以进程的方式启动服务
    def worker_spawn(self, klass, workers_registry):
        self.generation += 1
        worker = klass(self)
        pid = os.fork()
        if pid != 0:
            worker.pid = pid
            self.workers[pid] = worker
            workers_registry[pid] = worker
            return worker
        else:
            worker.run()
            sys.exit(0)
    def long_polling_spawn(self):
	    nargs = stripped_sys_argv()
	    cmd = [sys.executable, sys.argv[0], 'gevent'] + nargs[1:]
	    popen = subprocess.Popen(cmd)
	    self.long_polling_pid = popen.pid
    def process_spawn(self):
        if config['http_enable']:
            while len(self.workers_http) < self.population:
            	#  根据worker参数(population)创建子进程
                self.worker_spawn(WorkerHTTP, self.workers_http)
            	#  创建long_polling
            if not self.long_polling_pid:
                self.long_polling_spawn()
        while len(self.workers_cron) < config['max_cron_threads']:
        		#  创建cron进程
            self.worker_spawn(WorkerCron, self.workers_cron)
     LiveChat worker自动开启,并且监听"longpolling port",该端口与"normal http port"不同,
      在配置文件中可设置。客户端只能连接"normal http port",并不能连接到"longpolling port"。
      
      解决方案:使用代理服务器,如Nginx、Apache等,转发请求,凡是url中带有/longpolling/则
      转发至"longpolling port",其它则转发至"normal http port"

    2.3 multithreading模式

	  当以上两种模式都没有设置时,默认的饿启动模式。开启HTTP、Cron服务。均以线程方式启动服务
    def http_thread(self):
        def app(e, s):
            return self.app(e, s)
        self.httpd = ThreadedWSGIServerReloadable(self.interface, self.port, app)
        self.httpd.serve_forever()

    def http_spawn(self):
        t = threading.Thread(target=self.http_thread, name="odoo.service.httpd")
        t.setDaemon(True)
        t.start()

	    def cron_thread(self, number):
        from odoo.addons.base.models.ir_cron import ir_cron
        while True:
            time.sleep(SLEEP_INTERVAL + number)     # Steve Reich timing style
            registries = odoo.modules.registry.Registry.registries
            _logger.debug('cron%d polling for jobs', number)
            for db_name, registry in registries.items():
                if registry.ready:
                    thread = threading.currentThread()
                    thread.start_time = time.time()
                    try:
                        ir_cron._acquire_job(db_name)
                    except Exception:
                        _logger.warning('cron%d encountered an Exception:', number, exc_info=True)
                    thread.start_time = None

    def cron_spawn(self):
        """ Start the above runner function in a daemon thread.

        The thread is a typical daemon thread: it will never quit and must be
        terminated when the main process exits - with no consequence (the processing
        threads it spawns are not marked daemon).

        """
        # Force call to strptime just before starting the cron thread
        # to prevent time.strptime AttributeError within the thread.
        # See: http://bugs.python.org/issue7980
        datetime.datetime.strptime('2012-01-01', '%Y-%m-%d')
        #  根据配置的max_cron_threads参数,创建cron线程
        for i in range(odoo.tools.config['max_cron_threads']):
            def target():
                self.cron_thread(i)
            t = threading.Thread(target=target, name="odoo.service.cron.cron%d" % i)
            t.setDaemon(True)
            t.type = 'cron'
            t.start()
            _logger.debug("cron%d started!" % i)

三、服务启动过程

	 第一步:运行启动文件odoo-bin
	 第二步:加载系统默认参数
	 	导入Python包时运行其__init__文件,最终执行到文件odoo/tools/config.py,其中初始化类configmanager
class configmanager(object):
    def __init__(self, fname=None):
        """Constructor.

        :param fname: a shortcut allowing to instantiate :class:`configmanager`
                      from Python code without resorting to environment
                      variable
        """
        # Options not exposed on the command line. Command line options will be added
        # from optparse's parser.
        self.options = {
            'admin_passwd': 'admin',
            'csv_internal_sep': ',',
            'publisher_warranty_url': 'http://services.openerp.com/publisher-warranty/',
            'reportgz': False,
            'root_path': None,
        }

        # Not exposed in the configuration file.
        self.blacklist_for_save = set([
            'publisher_warranty_url', 'load_language', 'root_path',
            'init', 'save', 'config', 'update', 'stop_after_init', 'dev_mode', 'shell_interface'
        ])

        # dictionary mapping option destination (keys in self.options) to MyOptions.
        self.casts = {}

        self.misc = {}
        self.config_file = fname

        self._LOGLEVELS = dict([
            (getattr(loglevels, 'LOG_%s' % x), getattr(logging, x))
            for x in ('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'NOTSET')
        ])
        pass
	 第三步: 调用odoo/cli/command.py下main()方法
	 	     加载"--addons-path"参数
    第四步:调用odoo/cli/server.py  mian方法
    	   确认数据库,是否是多进程启动,csv字段大小设置,调用方法odoo.service.server.start
   第五步:执行odoo.service.server.start
   			根据参数配置决定服务的启动方式

四、HTTP请求

          以odoo.service.server.ThreadedServer为例

          第一步:

	 请求通过server到达http_thread,即http线程,该线程调用application
    def application(environ, start_response):
	    if config['proxy_mode'] and 'HTTP_X_FORWARDED_HOST' in environ:
	        return werkzeug.contrib.fixers.ProxyFix(application_unproxied)(environ, start_response)
	    else:
	        return application_unproxied(environ, start_response)
    def http_thread(self):
        def app(e, s):
            #  调用application
            return self.app(e, s)
        self.httpd = ThreadedWSGIServerReloadable(self.interface, self.port, app)
        self.httpd.serve_forever()

    def http_spawn(self):
        t = threading.Thread(target=self.http_thread, name="odoo.service.httpd")
        t.setDaemon(True)
        t.start()

          第二步:

	 application经过处理函数application_unproxied,到达
	 "Root WSGI application for the OpenERP Web Client ",odoo.http.Root实现__call__方法,
	 使application_unproxied返回的Root实例可调用,最终实现对odoo.http.Root.dispatch调用,
	 dispatch方法分发处理,调用odoo.api.ir.http._dispatch(),该方法请求参数处理、session处理
    #  Root.dispatch,截取部分代码
    def dispatch(self, environ, start_response):
        """
        Performs the actual WSGI dispatching for the application.
        """
        try:
          	。。。。。。。。。。。。
			#  
            explicit_session = self.setup_session(httprequest)
            self.setup_db(httprequest)
            self.setup_lang(httprequest)

            request = self.get_request(httprequest)

            def _dispatch_nodb():
                try:
                    func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
                except werkzeug.exceptions.HTTPException as e:
                    return request._handle_exception(e)
                request.set_handler(func, arguments, "none")
                result = request.dispatch()
                return result

            with request:
                db = request.session.db
                if db:
                    try:
                        odoo.registry(db).check_signaling()
                        with odoo.tools.mute_logger('odoo.sql_db'):
                            ir_http = request.registry['ir.http']
                    except (AttributeError, psycopg2.OperationalError, psycopg2.ProgrammingError):
                        # psycopg2 error or attribute error while constructing
                        # the registry. That means either
                        # - the database probably does not exists anymore
                        # - the database is corrupted
                        # - the database version doesnt match the server version
                        # Log the user out and fall back to nodb
                        request.session.logout()
                        # If requesting /web this will loop
                        if request.httprequest.path == '/web':
                            result = werkzeug.utils.redirect('/web/database/selector')
                        else:
                            result = _dispatch_nodb()
                    else:
                        result = ir_http._dispatch()
                else:
                    result = _dispatch_nodb()

                response = self.get_response(httprequest, result, explicit_session)
            return response(environ, start_response)

版权声明:本文为weixin_47681417原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。