odoo的及时通信核心功能是在bus模块中实现的:http 长轮询 + python thread事件通知 + pg 触发器实现。下面为代码分析:
前端
// 代码来源:odoo/addons/bus/static/src/js/longpolling_bus.js
_poll: function () {
var self = this;
if (!this._isActive) {
return;
}
var now = new Date().getTime();
var options = _.extend({}, this._options, {
bus_inactivity: now - this._getLastPresence(),
});
// 封装请求数据:channels:请求的频道,必填;last:最后一条消息的id,非必填
var data = {channels: this._channels, last: this._lastNotificationID, options: options};
// 虽然长连接的超时时间为60秒,但odoo server的超时时间是50秒
this._pollRpc = this._makePoll(data);
this._pollRpc.then(function (result) {
// 获取到数据或后端超时返回时,处理数据,并发起下次长连接
self._pollRpc = false;
self._onPoll(result);
self._poll();
}).guardedCatch(function (result) {
self._pollRpc = false;
// no error popup if request is interrupted or fails for any reason
result.event.preventDefault();
if (result.message === "XmlHttpRequestError abort") {
// 网络异常,直接发起下次长连接
self._poll();
} else {
// 其它异常,则在10-30秒内,再次发起长连接
self._pollRetryTimeout = setTimeout(self._poll, self.ERROR_RETRY_DELAY + (Math.floor((Math.random()*20)+1)*1000));
}
});
},
/**
* 获取最新消息的长连接
*/
_makePoll: function(data) {
return this._rpc({route: this.POLL_ROUTE, params: data}, {shadow : true, timeout: 60000});
},
后端
# 代码来源 odoo/addons/bus/controllers/main.py
# override to add channels
def _poll(self, dbname, channels, last, options):
# update the user presence
if request.session.uid and 'bus_inactivity' in options:
# 更新在线状态和最后在线时间
request.env['bus.presence'].update(options.get('bus_inactivity'))
request.cr.close()
request._cr = None
# 获取最新消息
return dispatch.poll(dbname, channels, last, options)
@route('/longpolling/poll', type="json", auth="public", cors="*")
def poll(self, channels, last, options=None):
if options is None:
options = {}
if not dispatch:
raise Exception("bus.Bus unavailable")
if [c for c in channels if not isinstance(c, str)]:
raise Exception("bus.Bus only string channels are allowed.")
if request.registry.in_test_mode():
raise exceptions.UserError(_("bus.Bus not available in test mode"))
# 调用第三行的方法
return self._poll(request.db, channels, last, options)
其中,dispatch.poll方法为
# 代码来源 odoo/addons/bus/models/bus.py ImDispatch
# 该方法,是用户发起长连接请求后,创建的线程调用的方法
def poll(self, dbname, channels, last, options=None, timeout=TIMEOUT):
if options is None:
options = {}
# Dont hang ctrl-c for a poll request, we need to bypass private
# attribute access because we dont know before starting the thread that
# it will handle a longpolling request
if not odoo.evented:
current = threading.current_thread()
current._daemonic = True
# rename the thread to avoid tests waiting for a longpolling
current.setName("openerp.longpolling.request.%s" % current.ident)
registry = odoo.registry(dbname)
# 尝试获取最新消息,如果有消息,则直接返回
with registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
notifications = env['bus.bus'].poll(channels, last, options)
# 如果前端传入的peek参数为真,那么,不管有没有消息,都直接返回
if options.get('peek'):
return dict(notifications=notifications, channels=channels)
# or wait for future ones
if not notifications:
# 如果没有新消息,则新建事件,等待被新消息唤醒
if not self.started:
# Lazy start of events listener
self.start()
# 创建事件
event = self.Event()
for channel in channels:
# 加入等待池
self.channels.setdefault(hashable(channel), set()).add(event)
try:
# 挂起当前线程
event.wait(timeout=timeout)
# 线程被唤醒或超时被系统唤醒后,都去查下消息,然后返回
with registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
notifications = env['bus.bus'].poll(channels, last, options)
except Exception:
# timeout
pass
finally:
# gc pointers to event
# 清理等待池
for channel in channels:
channel_events = self.channels.get(hashable(channel))
if channel_events and event in channel_events:
channel_events.remove(event)
return notifications
# 该方法是odoo启动并加载对应的数据库后,就无限循环的方法(主线程)
def loop(self):
""" Dispatch postgres notifications to the relevant polling threads/greenlets """
_logger.info("Bus.loop listen imbus on db postgres")
with odoo.sql_db.db_connect('postgres').cursor() as cr:
conn = cr._cnx
# 监听数据库的触发器
cr.execute("listen imbus")
cr.commit();
while True:
# 持续监听触发器是否已被触发
if select.select([conn], [], [], TIMEOUT) == ([], [], []):
pass
else:
conn.poll()
channels = []
# 获取触发器携带的数据包
while conn.notifies:
channels.extend(json.loads(conn.notifies.pop().payload))
# dispatch to local threads/greenlets
events = set()
for channel in channels:
# 根据数据包中的数据内容,获取等待池中的事件(self.channels是在33-36行设置的)
events.update(self.channels.pop(hashable(channel), set()))
# 唤醒等待池中,挂起的事件(第39行)
for event in events:
event.set()